/**
 *  Copyright 2014 Paul Querna
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package ffjsoninception

import (
	"fmt"
	"reflect"

	"github.com/pquerna/ffjson/shared"
)

func typeInInception(ic *Inception, typ reflect.Type, f shared.Feature) bool {
	for _, v := range ic.objs {
		if v.Typ == typ {
			return v.Options.HasFeature(f)
		}
		if typ.Kind() == reflect.Ptr {
			if v.Typ == typ.Elem() {
				return v.Options.HasFeature(f)
			}
		}
	}

	return false
}

func getOmitEmpty(ic *Inception, sf *StructField) string {
	ptname := "j." + sf.Name
	if sf.Pointer {
		ptname = "*" + ptname
		return "if true {\n"
	}
	switch sf.Typ.Kind() {

	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
		return "if len(" + ptname + ") != 0 {" + "\n"

	case reflect.Int,
		reflect.Int8,
		reflect.Int16,
		reflect.Int32,
		reflect.Int64,
		reflect.Uint,
		reflect.Uint8,
		reflect.Uint16,
		reflect.Uint32,
		reflect.Uint64,
		reflect.Uintptr,
		reflect.Float32,
		reflect.Float64:
		return "if " + ptname + " != 0 {" + "\n"

	case reflect.Bool:
		return "if " + ptname + " != false {" + "\n"

	case reflect.Interface, reflect.Ptr:
		return "if " + ptname + " != nil {" + "\n"

	default:
		// TODO(pquerna): fix types
		return "if true {" + "\n"
	}
}

func getMapValue(ic *Inception, name string, typ reflect.Type, ptr bool, forceString bool) string {
	var out = ""

	if typ.Key().Kind() != reflect.String {
		out += fmt.Sprintf("/* Falling back. type=%v kind=%v */\n", typ, typ.Kind())
		out += ic.q.Flush()
		out += "err = buf.Encode(" + name + ")" + "\n"
		out += "if err != nil {" + "\n"
		out += "  return err" + "\n"
		out += "}" + "\n"
		return out
	}

	var elemKind reflect.Kind
	elemKind = typ.Elem().Kind()

	switch elemKind {
	case reflect.String,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
		reflect.Float32,
		reflect.Float64,
		reflect.Bool:

		ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true

		out += "if " + name + " == nil  {" + "\n"
		ic.q.Write("null")
		out += ic.q.GetQueued()
		ic.q.DeleteLast()
		out += "} else {" + "\n"
		out += ic.q.WriteFlush("{ ")
		out += "  for key, value := range " + name + " {" + "\n"
		out += "    fflib.WriteJsonString(buf, key)" + "\n"
		out += "    buf.WriteString(`:`)" + "\n"
		out += getGetInnerValue(ic, "value", typ.Elem(), false, forceString)
		out += "    buf.WriteByte(',')" + "\n"
		out += "  }" + "\n"
		out += "buf.Rewind(1)" + "\n"
		out += ic.q.WriteFlush("}")
		out += "}" + "\n"

	default:
		out += ic.q.Flush()
		out += fmt.Sprintf("/* Falling back. type=%v kind=%v */\n", typ, typ.Kind())
		out += "err = buf.Encode(" + name + ")" + "\n"
		out += "if err != nil {" + "\n"
		out += "  return err" + "\n"
		out += "}" + "\n"
	}
	return out
}

func getGetInnerValue(ic *Inception, name string, typ reflect.Type, ptr bool, forceString bool) string {
	var out = ""

	// Flush if not bool or maps
	if typ.Kind() != reflect.Bool && typ.Kind() != reflect.Map && typ.Kind() != reflect.Struct {
		out += ic.q.Flush()
	}

	if typ.Implements(marshalerFasterType) ||
		reflect.PtrTo(typ).Implements(marshalerFasterType) ||
		typeInInception(ic, typ, shared.MustEncoder) ||
		typ.Implements(marshalerType) ||
		reflect.PtrTo(typ).Implements(marshalerType) {

		out += ic.q.Flush()
		out += tplStr(encodeTpl["handleMarshaler"], handleMarshaler{
			IC:             ic,
			Name:           name,
			Typ:            typ,
			Ptr:            reflect.Ptr,
			MarshalJSONBuf: typ.Implements(marshalerFasterType) || reflect.PtrTo(typ).Implements(marshalerFasterType) || typeInInception(ic, typ, shared.MustEncoder),
			Marshaler:      typ.Implements(marshalerType) || reflect.PtrTo(typ).Implements(marshalerType),
		})
		return out
	}

	ptname := name
	if ptr {
		ptname = "*" + name
	}

	switch typ.Kind() {
	case reflect.Int,
		reflect.Int8,
		reflect.Int16,
		reflect.Int32,
		reflect.Int64:
		ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true
		out += "fflib.FormatBits2(buf, uint64(" + ptname + "), 10, " + ptname + " < 0)" + "\n"
	case reflect.Uint,
		reflect.Uint8,
		reflect.Uint16,
		reflect.Uint32,
		reflect.Uint64,
		reflect.Uintptr:
		ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true
		out += "fflib.FormatBits2(buf, uint64(" + ptname + "), 10, false)" + "\n"
	case reflect.Float32:
		ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true
		out += "fflib.AppendFloat(buf, float64(" + ptname + "), 'g', -1, 32)" + "\n"
	case reflect.Float64:
		ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true
		out += "fflib.AppendFloat(buf, float64(" + ptname + "), 'g', -1, 64)" + "\n"
	case reflect.Array,
		reflect.Slice:

		// Arrays cannot be nil
		if typ.Kind() != reflect.Array {
			out += "if " + name + "!= nil {" + "\n"
		}
		// Array and slice values encode as JSON arrays, except that
		// []byte encodes as a base64-encoded string, and a nil slice
		// encodes as the null JSON object.
		if typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
			ic.OutputImports[`"encoding/base64"`] = true

			out += "buf.WriteString(`\"`)" + "\n"
			out += `{` + "\n"
			out += `enc := base64.NewEncoder(base64.StdEncoding, buf)` + "\n"
			if typ.Elem().Name() != "byte" {
				ic.OutputImports[`"reflect"`] = true
				out += `enc.Write(reflect.Indirect(reflect.ValueOf(` + ptname + `)).Bytes())` + "\n"

			} else {
				out += `enc.Write(` + ptname + `)` + "\n"
			}
			out += `enc.Close()` + "\n"
			out += `}` + "\n"
			out += "buf.WriteString(`\"`)" + "\n"
		} else {
			out += "buf.WriteString(`[`)" + "\n"
			out += "for i, v := range " + ptname + "{" + "\n"
			out += "if i != 0 {" + "\n"
			out += "buf.WriteString(`,`)" + "\n"
			out += "}" + "\n"
			out += getGetInnerValue(ic, "v", typ.Elem(), false, false)
			out += "}" + "\n"
			out += "buf.WriteString(`]`)" + "\n"
		}
		if typ.Kind() != reflect.Array {
			out += "} else {" + "\n"
			out += "buf.WriteString(`null`)" + "\n"
			out += "}" + "\n"
		}
	case reflect.String:
		// Is it a json.Number?
		if typ.PkgPath() == "encoding/json" && typ.Name() == "Number" {
			// Fall back to json package to rely on the valid number check.
			// See: https://github.com/golang/go/blob/92cd6e3af9f423ab4d8ac78f24e7fd81c31a8ce6/src/encoding/json/encode.go#L550
			out += fmt.Sprintf("/* json.Number */\n")
			out += "err = buf.Encode(" + name + ")" + "\n"
			out += "if err != nil {" + "\n"
			out += "  return err" + "\n"
			out += "}" + "\n"
		} else {
			ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true
			if forceString {
				// Forcestring on strings does double-escaping of the entire value.
				// We create a temporary buffer, encode to that an re-encode it.
				out += "{" + "\n"
				out += "tmpbuf := fflib.Buffer{}" + "\n"
				out += "tmpbuf.Grow(len(" + ptname + ") + 16)" + "\n"
				out += "fflib.WriteJsonString(&tmpbuf, string(" + ptname + "))" + "\n"
				out += "fflib.WriteJsonString(buf, string( tmpbuf.Bytes() " + `))` + "\n"
				out += "}" + "\n"
			} else {
				out += "fflib.WriteJsonString(buf, string(" + ptname + "))" + "\n"
			}
		}
	case reflect.Ptr:
		out += "if " + name + "!= nil {" + "\n"
		switch typ.Elem().Kind() {
		case reflect.Struct:
			out += getGetInnerValue(ic, name, typ.Elem(), false, false)
		default:
			out += getGetInnerValue(ic, "*"+name, typ.Elem(), false, false)
		}
		out += "} else {" + "\n"
		out += "buf.WriteString(`null`)" + "\n"
		out += "}" + "\n"
	case reflect.Bool:
		out += "if " + ptname + " {" + "\n"
		ic.q.Write("true")
		out += ic.q.GetQueued()
		out += "} else {" + "\n"
		// Delete 'true'
		ic.q.DeleteLast()
		out += ic.q.WriteFlush("false")
		out += "}" + "\n"
	case reflect.Interface:
		out += fmt.Sprintf("/* Interface types must use runtime reflection. type=%v kind=%v */\n", typ, typ.Kind())
		out += "err = buf.Encode(" + name + ")" + "\n"
		out += "if err != nil {" + "\n"
		out += "  return err" + "\n"
		out += "}" + "\n"
	case reflect.Map:
		out += getMapValue(ic, ptname, typ, ptr, forceString)
	case reflect.Struct:
		if typ.Name() == "" {
			ic.q.Write("{")
			ic.q.Write(" ")
			out += fmt.Sprintf("/* Inline struct. type=%v kind=%v */\n", typ, typ.Kind())
			newV := reflect.Indirect(reflect.New(typ)).Interface()
			fields := extractFields(newV)

			// Output all fields
			for _, field := range fields {
				// Adjust field name
				field.Name = name + "." + field.Name
				out += getField(ic, field, "")
			}

			if lastConditional(fields) {
				out += ic.q.Flush()
				out += `buf.Rewind(1)` + "\n"
			} else {
				ic.q.DeleteLast()
			}
			out += ic.q.WriteFlush("}")
		} else {
			out += fmt.Sprintf("/* Struct fall back. type=%v kind=%v */\n", typ, typ.Kind())
			out += ic.q.Flush()
			if ptr {
				out += "err = buf.Encode(" + name + ")" + "\n"
			} else {
				// We send pointer to avoid copying entire struct
				out += "err = buf.Encode(&" + name + ")" + "\n"
			}
			out += "if err != nil {" + "\n"
			out += "  return err" + "\n"
			out += "}" + "\n"
		}
	default:
		out += fmt.Sprintf("/* Falling back. type=%v kind=%v */\n", typ, typ.Kind())
		out += "err = buf.Encode(" + name + ")" + "\n"
		out += "if err != nil {" + "\n"
		out += "  return err" + "\n"
		out += "}" + "\n"
	}

	return out
}

func getValue(ic *Inception, sf *StructField, prefix string) string {
	closequote := false
	if sf.ForceString {
		switch sf.Typ.Kind() {
		case reflect.Int,
			reflect.Int8,
			reflect.Int16,
			reflect.Int32,
			reflect.Int64,
			reflect.Uint,
			reflect.Uint8,
			reflect.Uint16,
			reflect.Uint32,
			reflect.Uint64,
			reflect.Uintptr,
			reflect.Float32,
			reflect.Float64,
			reflect.Bool:
			ic.q.Write(`"`)
			closequote = true
		}
	}
	out := getGetInnerValue(ic, prefix+sf.Name, sf.Typ, sf.Pointer, sf.ForceString)
	if closequote {
		if sf.Pointer {
			out += ic.q.WriteFlush(`"`)
		} else {
			ic.q.Write(`"`)
		}
	}

	return out
}

func p2(v uint32) uint32 {
	v--
	v |= v >> 1
	v |= v >> 2
	v |= v >> 4
	v |= v >> 8
	v |= v >> 16
	v++
	return v
}

func getTypeSize(t reflect.Type) uint32 {
	switch t.Kind() {
	case reflect.String:
		// TODO: consider runtime analysis.
		return 32
	case reflect.Array, reflect.Map, reflect.Slice:
		// TODO: consider runtime analysis.
		return 4 * getTypeSize(t.Elem())
	case reflect.Int,
		reflect.Int8,
		reflect.Int16,
		reflect.Int32,
		reflect.Uint,
		reflect.Uint8,
		reflect.Uint16,
		reflect.Uint32:
		return 8
	case reflect.Int64,
		reflect.Uint64,
		reflect.Uintptr:
		return 16
	case reflect.Float32,
		reflect.Float64:
		return 16
	case reflect.Bool:
		return 4
	case reflect.Ptr:
		return getTypeSize(t.Elem())
	default:
		return 16
	}
}

func getTotalSize(si *StructInfo) uint32 {
	rv := uint32(si.Typ.Size())
	for _, f := range si.Fields {
		rv += getTypeSize(f.Typ)
	}
	return rv
}

func getBufGrowSize(si *StructInfo) uint32 {

	// TOOD(pquerna): automatically calc a better grow size based on history
	// of a struct.
	return p2(getTotalSize(si))
}

func isIntish(t reflect.Type) bool {
	if t.Kind() >= reflect.Int && t.Kind() <= reflect.Uintptr {
		return true
	}
	if t.Kind() == reflect.Array || t.Kind() == reflect.Slice || t.Kind() == reflect.Ptr {
		if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Uint8 {
			// base64 special case.
			return false
		} else {
			return isIntish(t.Elem())
		}
	}
	return false
}

func getField(ic *Inception, f *StructField, prefix string) string {
	out := ""
	if f.OmitEmpty {
		out += ic.q.Flush()
		if f.Pointer {
			out += "if " + prefix + f.Name + " != nil {" + "\n"
		}
		out += getOmitEmpty(ic, f)
	}

	if f.Pointer && !f.OmitEmpty {
		// Pointer values encode as the value pointed to. A nil pointer encodes as the null JSON object.
		out += "if " + prefix + f.Name + " != nil {" + "\n"
	}

	// JsonName is already escaped and quoted.
	// getInnervalue should flush
	ic.q.Write(f.JsonName + ":")
	// We save a copy in case we need it
	t := ic.q

	out += getValue(ic, f, prefix)
	ic.q.Write(",")

	if f.Pointer && !f.OmitEmpty {
		out += "} else {" + "\n"
		out += t.WriteFlush("null")
		out += "}" + "\n"
	}

	if f.OmitEmpty {
		out += ic.q.Flush()
		if f.Pointer {
			out += "}" + "\n"
		}
		out += "}" + "\n"
	}
	return out
}

// We check if the last field is conditional.
func lastConditional(fields []*StructField) bool {
	if len(fields) > 0 {
		f := fields[len(fields)-1]
		return f.OmitEmpty
	}
	return false
}

func CreateMarshalJSON(ic *Inception, si *StructInfo) error {
	conditionalWrites := lastConditional(si.Fields)
	out := ""

	out += "// MarshalJSON marshal bytes to json - template\n"
	out += `func (j *` + si.Name + `) MarshalJSON() ([]byte, error) {` + "\n"
	out += `var buf fflib.Buffer` + "\n"

	out += `if j == nil {` + "\n"
	out += `  buf.WriteString("null")` + "\n"
	out += "  return buf.Bytes(), nil" + "\n"
	out += `}` + "\n"

	out += `err := j.MarshalJSONBuf(&buf)` + "\n"
	out += `if err != nil {` + "\n"
	out += "  return nil, err" + "\n"
	out += `}` + "\n"
	out += `return buf.Bytes(), nil` + "\n"
	out += `}` + "\n"

	out += "// MarshalJSONBuf marshal buff to json - template\n"
	out += `func (j *` + si.Name + `) MarshalJSONBuf(buf fflib.EncodingBuffer) (error) {` + "\n"
	out += `  if j == nil {` + "\n"
	out += `    buf.WriteString("null")` + "\n"
	out += "    return nil" + "\n"
	out += `  }` + "\n"

	out += `var err error` + "\n"
	out += `var obj []byte` + "\n"
	out += `_ = obj` + "\n"
	out += `_ = err` + "\n"

	ic.q.Write("{")

	// The extra space is inserted here.
	// If nothing is written to the field this will be deleted
	// instead of the last comma.
	if conditionalWrites || len(si.Fields) == 0 {
		ic.q.Write(" ")
	}

	for _, f := range si.Fields {
		out += getField(ic, f, "j.")
	}

	// Handling the last comma is tricky.
	// If the last field has omitempty, conditionalWrites is set.
	// If something has been written, we delete the last comma,
	// by backing up the buffer, otherwise it will delete a space.
	if conditionalWrites {
		out += ic.q.Flush()
		out += `buf.Rewind(1)` + "\n"
	} else {
		ic.q.DeleteLast()
	}

	out += ic.q.WriteFlush("}")
	out += `return nil` + "\n"
	out += `}` + "\n"
	ic.OutputFuncs = append(ic.OutputFuncs, out)
	return nil
}
