package jsoninfo

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

// MarshalStrictStruct function:
//   * Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag.
//   * Correctly handles StrictStruct semantics.
func MarshalStrictStruct(value StrictStruct) ([]byte, error) {
	encoder := NewObjectEncoder()
	if err := value.EncodeWith(encoder, value); err != nil {
		return nil, err
	}
	return encoder.Bytes()
}

type ObjectEncoder struct {
	result map[string]json.RawMessage
}

func NewObjectEncoder() *ObjectEncoder {
	return &ObjectEncoder{
		result: make(map[string]json.RawMessage, 8),
	}
}

// Bytes returns the result of encoding.
func (encoder *ObjectEncoder) Bytes() ([]byte, error) {
	return json.Marshal(encoder.result)
}

// EncodeExtension adds a key/value to the current JSON object.
func (encoder *ObjectEncoder) EncodeExtension(key string, value interface{}) error {
	data, err := json.Marshal(value)
	if err != nil {
		return err
	}
	encoder.result[key] = data
	return nil
}

// EncodeExtensionMap adds all properties to the result.
func (encoder *ObjectEncoder) EncodeExtensionMap(value map[string]json.RawMessage) error {
	if value != nil {
		result := encoder.result
		for k, v := range value {
			result[k] = v
		}
	}
	return nil
}

func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) error {
	reflection := reflect.ValueOf(value)

	// Follow "encoding/json" semantics
	if reflection.Kind() != reflect.Ptr {
		// Panic because this is a clear programming error
		panic(fmt.Errorf("Value %s is not a pointer", reflection.Type().String()))
	}
	if reflection.IsNil() {
		// Panic because this is a clear programming error
		panic(fmt.Errorf("Value %s is nil", reflection.Type().String()))
	}

	// Take the element
	reflection = reflection.Elem()

	// Obtain typeInfo
	typeInfo := GetTypeInfo(reflection.Type())

	// Declare result
	result := encoder.result

	// Supported fields
iteration:
	for _, field := range typeInfo.Fields {
		// Fields without JSON tag are ignored
		if !field.HasJSONTag {
			continue
		}

		// Marshal
		fieldValue := reflection.FieldByIndex(field.Index)
		if v, ok := fieldValue.Interface().(json.Marshaler); ok {
			if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
				if field.JSONOmitEmpty {
					continue iteration
				}
				result[field.JSONName] = []byte("null")
				continue
			}
			fieldData, err := v.MarshalJSON()
			if err != nil {
				return err
			}
			result[field.JSONName] = fieldData
			continue
		}
		switch fieldValue.Kind() {
		case reflect.Ptr, reflect.Interface:
			if fieldValue.IsNil() {
				if field.JSONOmitEmpty {
					continue iteration
				}
				result[field.JSONName] = []byte("null")
				continue
			}
		case reflect.Struct:
		case reflect.Map:
			if field.JSONOmitEmpty && (fieldValue.IsNil() || fieldValue.Len() == 0) {
				continue iteration
			}
		case reflect.Slice:
			if field.JSONOmitEmpty && fieldValue.Len() == 0 {
				continue iteration
			}
		case reflect.Bool:
			x := fieldValue.Bool()
			if field.JSONOmitEmpty && !x {
				continue iteration
			}
			s := "false"
			if x {
				s = "true"
			}
			result[field.JSONName] = []byte(s)
			continue iteration
		case reflect.Int64, reflect.Int, reflect.Int32:
			if field.JSONOmitEmpty && fieldValue.Int() == 0 {
				continue iteration
			}
		case reflect.Uint64, reflect.Uint, reflect.Uint32:
			if field.JSONOmitEmpty && fieldValue.Uint() == 0 {
				continue iteration
			}
		case reflect.Float64:
			if field.JSONOmitEmpty && fieldValue.Float() == 0.0 {
				continue iteration
			}
		case reflect.String:
			if field.JSONOmitEmpty && len(fieldValue.String()) == 0 {
				continue iteration
			}
		default:
			panic(fmt.Errorf("Field '%s' has unsupported type %s", field.JSONName, field.Type.String()))
		}

		// No special treament is needed
		// Use plain old "encoding/json".Marshal
		fieldData, err := json.Marshal(fieldValue.Addr().Interface())
		if err != nil {
			return err
		}
		result[field.JSONName] = fieldData
	}

	return nil
}
