// Copyright © 2019 Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

package tmc

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
)

// JSONMarshaler encodes and decodes JSON and is the default used in this
// codec.
var JSONMarshaler = new(jsonMarshaler)

// New creates a new Coded with some optional options.
func New(opts ...Option) (*Codec, error) {
	c := &Codec{
		typeSep:               "|",
		marshaler:             JSONMarshaler,
		typeAdapters:          DefaultTypeAdapters,
		typeAdaptersMap:       make(map[reflect.Type]Adapter),
		typeAdaptersStringMap: make(map[string]Adapter),
	}

	for _, opt := range opts {
		if err := opt(c); err != nil {
			return c, err
		}
	}

	for _, w := range c.typeAdapters {
		tp := w.Type()
		c.typeAdaptersMap[tp] = w
		c.typeAdaptersStringMap[tp.String()] = w
	}

	return c, nil
}

// Option configures the Codec.
type Option func(c *Codec) error

// WithTypeSep sets the separator to use before the type information encoded in
// the key field. Default is "|".
func WithTypeSep(sep string) func(c *Codec) error {
	return func(c *Codec) error {
		if sep == "" {
			return errors.New("separator cannot be empty")
		}
		c.typeSep = sep
		return nil
	}
}

// WithMarshalUnmarshaler sets the MarshalUnmarshaler to use.
// Default is JSONMarshaler.
func WithMarshalUnmarshaler(marshaler MarshalUnmarshaler) func(c *Codec) error {
	return func(c *Codec) error {
		c.marshaler = marshaler
		return nil
	}
}

// WithTypeAdapters sets the type adapters to use. Note that if more than one
// adapter exists for the same type, the last one will win. This means that
// if you want to use the default adapters, but override some of them, you
// can do:
//
//	adapters := append(typedmapcodec.DefaultTypeAdapters, mycustomAdapters ...)
//	codec := typedmapcodec.New(WithTypeAdapters(adapters))
func WithTypeAdapters(typeAdapters []Adapter) func(c *Codec) error {
	return func(c *Codec) error {
		c.typeAdapters = typeAdapters
		return nil
	}
}

// Codec provides methods to marshal and unmarshal a Go map while preserving
// type information.
type Codec struct {
	typeSep               string
	marshaler             MarshalUnmarshaler
	typeAdapters          []Adapter
	typeAdaptersMap       map[reflect.Type]Adapter
	typeAdaptersStringMap map[string]Adapter
}

// Marshal accepts a Go map and marshals it to the configured marshaler
// anntated with type information.
func (c *Codec) Marshal(v any) ([]byte, error) {
	m, err := c.toTypedMap(v)
	if err != nil {
		return nil, err
	}
	return c.marshaler.Marshal(m)
}

// Unmarshal unmarshals the given data to the given Go map, using
// any annotated type information found to preserve the type information
// stored in Marshal.
func (c *Codec) Unmarshal(data []byte, v any) error {
	if err := c.marshaler.Unmarshal(data, v); err != nil {
		return err
	}
	_, err := c.fromTypedMap(v)
	return err
}

func (c *Codec) newKey(key reflect.Value, a Adapter) reflect.Value {
	return reflect.ValueOf(fmt.Sprintf("%s%s%s", key, c.typeSep, a.Type()))
}

func (c *Codec) fromTypedMap(mi any) (reflect.Value, error) {
	m := reflect.ValueOf(mi)
	if m.Kind() == reflect.Ptr {
		m = m.Elem()
	}

	if m.Kind() != reflect.Map {
		return reflect.Value{}, errors.New("must be a Map")
	}

	keyKind := m.Type().Key().Kind()
	if keyKind == reflect.Interface {
		// We only support string keys.
		// YAML creates map[interface {}]interface {}, so try to convert it.
		var err error
		m, err = c.toStringMap(m)
		if err != nil {
			return reflect.Value{}, err
		}
	}

	for _, key := range m.MapKeys() {

		v := indirectInterface(m.MapIndex(key))

		var (
			keyStr   = key.String()
			keyPlain string
			keyType  string
		)

		sepIdx := strings.LastIndex(keyStr, c.typeSep)

		if sepIdx != -1 {
			keyPlain = keyStr[:sepIdx]
			keyType = keyStr[sepIdx+len(c.typeSep):]
		}

		adapter, found := c.typeAdaptersStringMap[keyType]

		if !found {
			if v.Kind() == reflect.Map {
				var err error
				v, err = c.fromTypedMap(v.Interface())
				if err != nil {
					return reflect.Value{}, err
				}
				m.SetMapIndex(key, v)
			}
			continue
		}

		switch v.Kind() {
		case reflect.Map:
			mm := reflect.MakeMap(reflect.MapOf(stringType, adapter.Type()))
			for _, key := range v.MapKeys() {
				vv := indirectInterface(v.MapIndex(key))
				nv, err := adapter.FromString(vv.String())
				if err != nil {
					return reflect.Value{}, err
				}
				mm.SetMapIndex(indirectInterface(key), reflect.ValueOf(nv))
			}
			m.SetMapIndex(reflect.ValueOf(keyPlain), mm)
		case reflect.Slice:
			slice := reflect.MakeSlice(reflect.SliceOf(adapter.Type()), v.Len(), v.Cap())
			for i := 0; i < v.Len(); i++ {
				vv := indirectInterface(v.Index(i))
				nv, err := adapter.FromString(vv.String())
				if err != nil {
					return reflect.Value{}, err
				}
				slice.Index(i).Set(reflect.ValueOf(nv))
			}

			m.SetMapIndex(reflect.ValueOf(keyPlain), slice)
		default:
			nv, err := adapter.FromString(v.String())
			if err != nil {
				return reflect.Value{}, err
			}
			m.SetMapIndex(reflect.ValueOf(keyPlain), reflect.ValueOf(nv))
		}

		m.SetMapIndex(key, reflect.Value{})

	}

	return m, nil
}

var (
	interfaceMapType   = reflect.TypeOf(make(map[string]any))
	interfaceSliceType = reflect.TypeOf([]any{})
	stringType         = reflect.TypeFor[string]()
)

func (c *Codec) toTypedMap(mi any) (any, error) {
	mv := reflect.ValueOf(mi)

	if mv.Kind() != reflect.Map || mv.Type().Key().Kind() != reflect.String {
		return nil, errors.New("must provide a map with string keys")
	}

	m := reflect.MakeMap(interfaceMapType)

	for _, key := range mv.MapKeys() {
		v := indirectInterface(mv.MapIndex(key))

		switch v.Kind() {

		case reflect.Map:

			if wrapper, found := c.typeAdaptersMap[v.Type().Elem()]; found {
				mm := reflect.MakeMap(interfaceMapType)
				for _, key := range v.MapKeys() {
					mm.SetMapIndex(key, reflect.ValueOf(wrapper.Wrap(v.MapIndex(key).Interface())))
				}
				m.SetMapIndex(c.newKey(key, wrapper), mm)
			} else {
				nested, err := c.toTypedMap(v.Interface())
				if err != nil {
					return nil, err
				}
				m.SetMapIndex(key, reflect.ValueOf(nested))
			}
			continue
		case reflect.Slice:
			if adapter, found := c.typeAdaptersMap[v.Type().Elem()]; found {
				slice := reflect.MakeSlice(interfaceSliceType, v.Len(), v.Cap())
				for i := 0; i < v.Len(); i++ {
					slice.Index(i).Set(reflect.ValueOf(adapter.Wrap(v.Index(i).Interface())))
				}
				m.SetMapIndex(c.newKey(key, adapter), slice)
				continue
			}
		}

		if adapter, found := c.typeAdaptersMap[v.Type()]; found {
			m.SetMapIndex(c.newKey(key, adapter), reflect.ValueOf(adapter.Wrap(v.Interface())))
		} else {
			m.SetMapIndex(key, v)
		}
	}

	return m.Interface(), nil
}

func (c *Codec) toStringMap(mi reflect.Value) (reflect.Value, error) {
	elemType := mi.Type().Elem()
	m := reflect.MakeMap(reflect.MapOf(stringType, elemType))
	for _, key := range mi.MapKeys() {
		key = indirectInterface(key)
		if key.Kind() != reflect.String {
			return reflect.Value{}, errors.New("this library supports only string keys in maps")
		}
		vv := mi.MapIndex(key)
		m.SetMapIndex(reflect.ValueOf(key.String()), vv)
	}

	return m, nil
}

// MarshalUnmarshaler is the interface that must be implemented if you want to
// add support for more than JSON to this codec.
type MarshalUnmarshaler interface {
	Marshal(v any) ([]byte, error)
	Unmarshal(b []byte, v any) error
}

type jsonMarshaler int

func (jsonMarshaler) Marshal(v any) ([]byte, error) {
	return json.Marshal(v)
}

func (jsonMarshaler) Unmarshal(b []byte, v any) error {
	return json.Unmarshal(b, v)
}

// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
func indirectInterface(v reflect.Value) reflect.Value {
	if v.Kind() != reflect.Interface {
		return v
	}
	if v.IsNil() {
		return reflect.Value{}
	}
	return v.Elem()
}
