/*

SPDX-Copyright: Copyright (c) Christian Zangl, Capital One Services, LLC
SPDX-License-Identifier: MIT
Copyright 2016, 2017 Christian Zangl, Capital One Services, LLC

See below for original copyright information.
*/
package hjson

import (
	"encoding/json"

	"bytes"
	"errors"
	"fmt"
	"github.com/hjson/hjson-go"
	"math"
	"reflect"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

func Unmarshal(data []byte, v interface{}) error {
	var container interface{}
	err := hjson.Unmarshal(data, &container)
	if err != nil {
		return err
	}
	data, err = json.Marshal(container)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, &v)
}

// The code below is copied and modified from the github.com/hjson/hjson-go project.
// The existing code (as of hash 94a8f0bea3436e0721b2e28b85df411d75a399fa, released on 25-Sept-2016)
// does not handle encoding of structs. The code below does.
// I intend to contribute this back to the project, but that will take time.
// Until then, this copy of the code is working well for our needs.

// Copyright notice for original code:
/*
MIT License

Copyright (c) 2016 Christian Zangl

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// EncoderOptions defines options for encoding to Hjson.
type EncoderOptions struct {
	// End of line, should be either \n or \r\n
	Eol string
	// Place braces on the same line
	BracesSameLine bool
	// Emit braces at the root level
	EmitRootBraces bool
	// Always place string in quotes
	QuoteAlways bool
	// Indent string
	IndentBy string
	// Allow the -0 value (unlike ES6)
	AllowMinusZero bool
	// Encode unknown values as 'null'
	UnknownAsNull bool
	// Skip empty structs
	OmitEmptyStructs bool
}

// DefaultOptions returns the default encoding options.
func DefaultOptions() EncoderOptions {
	opt := EncoderOptions{}
	opt.Eol = "\n"
	opt.BracesSameLine = false
	opt.EmitRootBraces = true
	opt.QuoteAlways = false
	opt.IndentBy = "  "
	opt.AllowMinusZero = false
	opt.UnknownAsNull = false
	opt.OmitEmptyStructs = false
	return opt
}

type hjsonEncoder struct {
	bytes.Buffer // output
	EncoderOptions
	indent int
}

var needsEscape, needsQuotes, needsEscapeML, startsWithKeyword, needsEscapeName *regexp.Regexp

func init() {
	// needsEscape tests if the string can be written without escapes
	needsEscape = regexp.MustCompile(`[\\\"\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
	// needsQuotes tests if the string can be written as a quoteless string (includes needsEscape but without \\ and \")
	needsQuotes = regexp.MustCompile(`^\s|^"|^'''|^#|^/\*|^//|^\{|^\}|^\[|^\]|^:|^,|\s$|[\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
	// needsEscapeML tests if the string can be written as a multiline string (includes needsEscape but without \n, \r, \\ and \")
	needsEscapeML = regexp.MustCompile(`'''|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
	// starts with a keyword and optionally is followed by a comment
	startsWithKeyword = regexp.MustCompile(`^(true|false|null)\s*((,|\]|\}|#|//|/\*).*)?$`)
	needsEscapeName = regexp.MustCompile(`[,\{\[\}\]\s:#"]|//|/\*|'''`)
}

var meta = map[byte][]byte{
	// table of character substitutions
	'\b': []byte("\\b"),
	'\t': []byte("\\t"),
	'\n': []byte("\\n"),
	'\f': []byte("\\f"),
	'\r': []byte("\\r"),
	'"':  []byte("\\\""),
	'\\': []byte("\\\\"),
}

func (e *hjsonEncoder) quoteReplace(text string) string {
	return string(needsEscape.ReplaceAllFunc([]byte(text), func(a []byte) []byte {
		c := meta[a[0]]
		if c != nil {
			return c
		}
		return []byte(fmt.Sprintf("\\u%04x", c))
	}))
}

func (e *hjsonEncoder) quote(value string, separator string, isRootObject bool) {

	// Check if we can insert this string without quotes
	// see hjson syntax (must not parse as true, false, null or number)

	if len(value) == 0 {
		e.WriteString(separator + `""`)
	} else if e.QuoteAlways ||
		needsQuotes.MatchString(value) ||
		startsWithNumber([]byte(value)) ||
		startsWithKeyword.MatchString(value) {

		// If the string contains no control characters, no quote characters, and no
		// backslash characters, then we can safely slap some quotes around it.
		// Otherwise we first check if the string can be expressed in multiline
		// format or we must replace the offending characters with safe escape
		// sequences.

		if !needsEscape.MatchString(value) {
			e.WriteString(separator + `"` + value + `"`)
		} else if !needsEscapeML.MatchString(value) && !isRootObject {
			e.mlString(value, separator)
		} else {
			e.WriteString(separator + `"` + e.quoteReplace(value) + `"`)
		}
	} else {
		// return without quotes
		e.WriteString(separator + value)
	}
}

func (e *hjsonEncoder) mlString(value string, separator string) {
	// wrap the string into the ''' (multiline) format

	a := strings.Split(strings.Replace(value, "\r", "", -1), "\n")

	if len(a) == 1 {
		// The string contains only a single line. We still use the multiline
		// format as it avoids escaping the \ character (e.g. when used in a
		// regex).
		e.WriteString(separator + "'''")
		e.WriteString(a[0])
	} else {
		e.writeIndent(e.indent + 1)
		e.WriteString("'''")
		for _, v := range a {
			indent := e.indent + 1
			if len(v) == 0 {
				indent = 0
			}
			e.writeIndent(indent)
			e.WriteString(v)
		}
		e.writeIndent(e.indent + 1)
	}
	e.WriteString("'''")
}

func (e *hjsonEncoder) quoteName(name string) string {
	if len(name) == 0 {
		return `""`
	}

	// Check if we can insert this name without quotes

	if needsEscapeName.MatchString(name) {
		if needsEscape.MatchString(name) {
			name = e.quoteReplace(name)
		}
		return `"` + name + `"`
	}
	// without quotes
	return name
}

type sortAlpha []reflect.Value

func (s sortAlpha) Len() int {
	return len(s)
}
func (s sortAlpha) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s sortAlpha) Less(i, j int) bool {
	return s[i].String() < s[j].String()
}

func (e *hjsonEncoder) writeIndent(indent int) {
	e.WriteString(e.Eol)
	for i := 0; i < indent; i++ {
		e.WriteString(e.IndentBy)
	}
}

func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, isRootObject bool) error {

	// Produce a string from value.

	//first check if value implements Marshaler, and use it
	m := reflect.TypeOf((*json.Marshaler)(nil)).Elem()

	kind := value.Kind()

	for kind == reflect.Interface || kind == reflect.Ptr {
		if value.IsNil() {
			e.WriteString(separator)
			e.WriteString("null")
			return nil
		}
		if value.Type().Implements(m) {
			b, err := value.Interface().(json.Marshaler).MarshalJSON()
			if err != nil {
				return err
			}
			e.WriteString(separator)
			e.WriteString(string(b))
			return nil
		}
		value = value.Elem()
		kind = value.Kind()
	}

	if value.Type().Implements(m) {
		b, err := value.Interface().(json.Marshaler).MarshalJSON()
		if err != nil {
			return err
		}
		e.WriteString(separator)
		e.WriteString(string(b))
		return nil
	}

	switch kind {
	case reflect.String:
		e.quote(value.String(), separator, isRootObject)

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		e.WriteString(separator)
		e.WriteString(strconv.FormatInt(value.Int(), 10))

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
		reflect.Uintptr:
		e.WriteString(separator)
		e.WriteString(strconv.FormatUint(value.Uint(), 10))

	case reflect.Float32, reflect.Float64:
		// JSON numbers must be finite. Encode non-finite numbers as null.
		e.WriteString(separator)
		number := value.Float()
		if math.IsInf(number, 0) || math.IsNaN(number) {
			e.WriteString("null")
		} else if !e.AllowMinusZero && number == -0 {
			e.WriteString("0")
		} else {
			// find shortest representation ('G' does not work)
			val := strconv.FormatFloat(number, 'f', -1, 64)
			exp := strconv.FormatFloat(number, 'E', -1, 64)
			if len(exp) < len(val) {
				val = strings.ToLower(exp)
			}
			e.WriteString(val)
		}

	case reflect.Bool:
		e.WriteString(separator)
		if value.Bool() {
			e.WriteString("true")
		} else {
			e.WriteString("false")
		}

	case reflect.Slice, reflect.Array:

		len := value.Len()
		if len == 0 {
			e.WriteString(separator)
			e.WriteString("[]")
			break
		}

		indent1 := e.indent
		e.indent++

		if !noIndent && !e.BracesSameLine {
			e.writeIndent(indent1)
		} else {
			e.WriteString(separator)
		}
		e.WriteString("[")

		// Join all of the element texts together, separated with newlines
		for i := 0; i < len; i++ {
			e.writeIndent(e.indent)
			if err := e.str(value.Index(i), true, "", false); err != nil {
				return err
			}
		}

		e.writeIndent(indent1)
		e.WriteString("]")

		e.indent = indent1

	case reflect.Map:

		len := value.Len()
		if len == 0 {
			e.WriteString(separator)
			e.WriteString("{}")
			break
		}

		showBraces := !isRootObject || e.EmitRootBraces
		indent1 := e.indent
		if showBraces {
			e.indent++
		}

		if showBraces {
			if !noIndent && !e.BracesSameLine {
				e.writeIndent(indent1)
			} else {
				e.WriteString(separator)
			}
			e.WriteString("{")
		}

		keys := value.MapKeys()
		sort.Sort(sortAlpha(keys))

		// Join all of the member texts together, separated with newlines
		for i := 0; i < len; i++ {
			if i > 0 || showBraces {
				e.writeIndent(e.indent)
			}
			e.WriteString(e.quoteName(keys[i].String()))
			e.WriteString(":")
			if err := e.str(value.MapIndex(keys[i]), false, " ", false); err != nil {
				return err
			}
		}

		if showBraces {
			e.writeIndent(indent1)
			e.WriteString("}")
		}

		e.indent = indent1

	case reflect.Struct:

		l := value.NumField()
		if l == 0 {
			e.WriteString(separator)
			e.WriteString("{}")
			break
		}

		showBraces := !isRootObject || e.EmitRootBraces
		indent1 := e.indent
		if showBraces {
			e.indent++
		}

		if showBraces {
			if !noIndent && !e.BracesSameLine {
				e.writeIndent(indent1)
			} else {
				e.WriteString(separator)
			}
			e.WriteString("{")
		}

		// Join all of the member texts together, separated with newlines
		for i := 0; i < l; i++ {
			curStructField := value.Type().Field(i)
			curField := value.Field(i)

			name := curStructField.Name
			jsonTag, ok := curStructField.Tag.Lookup("json")
			omitEmpty := false
			if ok {
				splits := strings.Split(jsonTag, ",")
				if splits[0] == "-" {
					continue
				}
				name = splits[0]
				if len(splits) > 1 {
					omitEmpty = (splits[1] == "omitempty")
				}
			}
			if omitEmpty && isEmptyValue(curField, e.OmitEmptyStructs) {
				continue
			}
			if i > 0 || showBraces {
				e.writeIndent(e.indent)
			}
			e.WriteString(e.quoteName(name))
			e.WriteString(":")
			if err := e.str(curField, false, " ", false); err != nil {
				return err
			}
		}

		if showBraces {
			e.writeIndent(indent1)
			e.WriteString("}")
		}

		e.indent = indent1

	default:
		if e.UnknownAsNull {
			// Use null as a placeholder for non-JSON values.
			e.WriteString("null")
		} else {
			return errors.New("Unsupported type " + value.Type().String())
		}
	}
	return nil
}

func isEmptyValue(v reflect.Value, omitEmptyStructs bool) bool {
	switch v.Kind() {
	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
		return v.Len() == 0
	case reflect.Bool:
		return !v.Bool()
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return v.Int() == 0
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return v.Uint() == 0
	case reflect.Float32, reflect.Float64:
		return v.Float() == 0
	case reflect.Interface, reflect.Ptr:
		return v.IsNil()
	case reflect.Struct:
		if omitEmptyStructs {
			//recursively check all the fields
			l := v.NumField()
			for i := 0; i < l; i++ {
				if !isEmptyValue(v.Field(i), omitEmptyStructs) {
					return false
				}
			}
			return true
		}
	}
	return false
}

// Marshal returns the Hjson encoding of v using
// default options.
//
// See MarshalWithOptions.
//
func Marshal(v interface{}) ([]byte, error) {
	return MarshalWithOptions(v, DefaultOptions())
}

// MarshalWithOptions returns the Hjson encoding of v.
//
// Marshal traverses the value v recursively.
//
// Boolean values encode as JSON booleans.
//
// Floating point, integer, and Number values encode as JSON numbers.
//
// String values encode as Hjson strings (quoteless, multiline or
// JSON).
//
// Array and slice values encode as JSON arrays.
//
// Map values encode as JSON objects. The map's key type must be a
// string. The map keys are sorted and used as JSON object keys.
//
// Pointer values encode as the value pointed to.
// A nil pointer encodes as the null JSON value.
//
// Interface values encode as the value contained in the interface.
// A nil interface value encodes as the null JSON value.
//
// JSON cannot represent cyclic data structures and Marshal does not
// handle them. Passing cyclic structures to Marshal will result in
// an infinite recursion.
//
func MarshalWithOptions(v interface{}, options EncoderOptions) ([]byte, error) {
	e := &hjsonEncoder{}
	e.indent = 0
	e.Eol = options.Eol
	e.BracesSameLine = options.BracesSameLine
	e.EmitRootBraces = options.EmitRootBraces
	e.QuoteAlways = options.QuoteAlways
	e.IndentBy = options.IndentBy
	e.OmitEmptyStructs = options.OmitEmptyStructs

	err := e.str(reflect.ValueOf(v), true, "", true)
	if err != nil {
		return nil, err
	}
	return e.Bytes(), nil
}

type parseNumber struct {
	data []byte
	at   int  // The index of the current character
	ch   byte // The current character
}

func (p *parseNumber) next() bool {
	// get the next character.
	len := len(p.data)
	if p.at < len {
		p.ch = p.data[p.at]
		p.at++
		return true
	}
	if p.at == len {
		p.at++
		p.ch = 0
	}
	return false
}

func startsWithNumber(text []byte) bool {
	if _, err := tryParseNumber(text, true); err == nil {
		return true
	}
	return false
}

func tryParseNumber(text []byte, stopAtNext bool) (float64, error) {
	// Parse a number value.

	p := parseNumber{text, 0, ' '}
	leadingZeros := 0
	testLeading := true
	p.next()
	if p.ch == '-' {
		p.next()
	}
	for p.ch >= '0' && p.ch <= '9' {
		if testLeading {
			if p.ch == '0' {
				leadingZeros++
			} else {
				testLeading = false
			}
		}
		p.next()
	}
	if testLeading {
		leadingZeros--
	} // single 0 is allowed
	if p.ch == '.' {
		for p.next() && p.ch >= '0' && p.ch <= '9' {
		}
	}
	if p.ch == 'e' || p.ch == 'E' {
		p.next()
		if p.ch == '-' || p.ch == '+' {
			p.next()
		}
		for p.ch >= '0' && p.ch <= '9' {
			p.next()
		}
	}

	end := p.at

	// skip white/to (newline)
	for p.ch > 0 && p.ch <= ' ' {
		p.next()
	}

	if stopAtNext {
		// end scan if we find a punctuator character like ,}] or a comment
		if p.ch == ',' || p.ch == '}' || p.ch == ']' ||
			p.ch == '#' || p.ch == '/' && (p.data[p.at] == '/' || p.data[p.at] == '*') {
			p.ch = 0
		}
	}

	if p.ch > 0 || leadingZeros != 0 {
		return 0, errors.New("Invalid number")
	}
	number, err := strconv.ParseFloat(string(p.data[0:end-1]), 64)
	if err != nil {
		return 0, err
	}
	if math.IsInf(number, 0) || math.IsNaN(number) {
		return 0, errors.New("Invalid number")
	}
	return number, nil
}
