package phpx

import (
	"encoding/json"
	"math"
	"strconv"
	"strings"

	"github.com/tkw1536/pkglib/collection"
)

// Marshal marshals data as a PHP expression, so that it can be safely used inside a php expession.
//
// Typically data is marshaled using [json.Marshal] and decoded in PHP using 'json_decode'.
// Special cases may exist for specific datatypes.
func Marshal(data any) (string, error) {
	switch d := data.(type) {
	case string:
		return MarshalString(d), nil
	}

	bytes, err := json.Marshal(data)
	if err != nil {
		return "", err
	}

	return "json_decode(" + MarshalString(string(bytes)) + ")", nil
}

// MarshalJSON marshals a json-value safely as an expression to be used as a php string.
//
// A json value is one returned by calling [json.Unmarshal] on a value of type any.
// These are then marshaled by the appropriate function:
//
// - a nil is turned into [PHPNil]
// - a bool is passed to [MarshalBool]
// - a float64 is passed to [MarshalFloat]
// - a string is passed to [MarshalString]
// - an []any is passed to [MarshalSlice]
// - an map[string]any is passed to [MarshalMap]
//
// All marshaling attempts to minify the length of the returned string, meaning compact encodings
// are prefered over length ones.
//
// If a value is none of these types, an empty string is returned.
// No valid value ever returns the empty string
func MarshalJSON(v any) string {
	switch v := v.(type) {
	case nil:
		return PHPNil
	case bool:
		return MarshalBool(v)
	case float64:
		return MarshalFloat(v)
	case string:
		return MarshalString(v)
	case []any:
		return MarshalSlice(v)
	case map[string]any:
		return MarshalMap(v)
	}
	return ""
}

const (
	// PHPNil represents the equivalent of a nil value in php
	PHPNil = "null"

	phpTrue  = "!0"
	phpFalse = "!1"

	phpNaN              = "NAN"
	phpPositiveInfinity = "INF"
	phpNegativeInfinity = "-INF"
)

// MarshalBool marshals b as a boolean to be used in php code.
// This corresponds to the strings "true" or "false".
func MarshalBool(b bool) string {
	if b {
		return phpTrue
	}
	return phpFalse
}

// MarshalFloat marshals a floating point number or integer
func MarshalFloat(f float64) string {
	// if we actually have an integer, return it!
	if i := int64(f); f == float64(i) {
		return MarshalInt(i)
	}

	// special cases
	if math.IsNaN(f) {
		return phpNaN
	}
	if math.IsInf(f, 1) {
		return phpPositiveInfinity
	}
	if math.IsInf(f, -1) {
		return phpNegativeInfinity
	}

	// all other cases
	return strconv.FormatFloat(f, 'E', -1, 64)
}

// MarshalInt marshals an integer as a string to be used inside a php literal
func MarshalInt(i int64) string {
	return strconv.FormatInt(i, 10)
}

var stringReplacer = strings.NewReplacer("'", "\\'", "\\", "\\\\")

// MarshalString marshals s as a php string that can be used safely as a PHP expression.
func MarshalString(s string) string {
	// See [https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.single]
	// we just escape
	return "'" + stringReplacer.Replace(s) + "'"
}

func MarshalSlice(slice []any) string {
	var builder strings.Builder

	builder.WriteRune('[')
	{
		for _, v := range slice {
			builder.WriteString(MarshalJSON(v))
			builder.WriteRune(',')
		}
	}
	builder.WriteRune(']')

	return builder.String()
}

func MarshalMap(m map[string]any) string {
	var builder strings.Builder

	builder.WriteString("array(")
	collection.IterateSorted(m, func(k string, v any) bool {
		builder.WriteString(MarshalString(k))
		builder.WriteString("=>")
		builder.WriteString(MarshalJSON(v))
		builder.WriteString(",")
		return true
	})
	builder.WriteString(")")

	return builder.String()
}
