package goyaml

import (
	"math"
	"strconv"
	"strings"
)

// TODO: merge, timestamps, base 60 floats, omap.

type resolveMapItem struct {
	value interface{}
	tag   string
}

var resolveTable = make([]byte, 256)
var resolveMap = make(map[string]resolveMapItem)

func init() {
	t := resolveTable
	t[int('+')] = 'S' // Sign
	t[int('-')] = 'S'
	for _, c := range "0123456789" {
		t[int(c)] = 'D' // Digit
	}
	for _, c := range "yYnNtTfFoO~" {
		t[int(c)] = 'M' // In map
	}
	t[int('.')] = '.' // Float (potentially in map)
	t[int('<')] = '<' // Merge

	var resolveMapList = []struct {
		v   interface{}
		tag string
		l   []string
	}{
		{true, "!!bool", []string{"y", "Y", "yes", "Yes", "YES"}},
		{true, "!!bool", []string{"true", "True", "TRUE"}},
		{true, "!!bool", []string{"on", "On", "ON"}},
		{false, "!!bool", []string{"n", "N", "no", "No", "NO"}},
		{false, "!!bool", []string{"false", "False", "FALSE"}},
		{false, "!!bool", []string{"off", "Off", "OFF"}},
		{nil, "!!null", []string{"~", "null", "Null", "NULL"}},
		{math.NaN(), "!!float", []string{".nan", ".NaN", ".NAN"}},
		{math.Inf(+1), "!!float", []string{".inf", ".Inf", ".INF"}},
		{math.Inf(+1), "!!float", []string{"+.inf", "+.Inf", "+.INF"}},
		{math.Inf(-1), "!!float", []string{"-.inf", "-.Inf", "-.INF"}},
	}

	m := resolveMap
	for _, item := range resolveMapList {
		for _, s := range item.l {
			m[s] = resolveMapItem{item.v, item.tag}
		}
	}
}

const longTagPrefix = "tag:yaml.org,2002:"

func shortTag(tag string) string {
	if strings.HasPrefix(tag, longTagPrefix) {
		return "!!" + tag[len(longTagPrefix):]
	}
	return tag
}

func resolvableTag(tag string) bool {
	switch tag {
	case "", "!!str", "!!bool", "!!int", "!!float", "!!null":
		return true
	}
	return false
}

func resolve(tag string, in string) (rtag string, out interface{}) {
	tag = shortTag(tag)
	if !resolvableTag(tag) {
		return tag, in
	}

	defer func() {
		if tag != "" && tag != rtag {
			panic("Can't decode " + rtag + " '" + in + "' as a " + tag)
		}
	}()

	if in == "" {
		return "!!null", nil
	}

	c := resolveTable[in[0]]
	if c == 0 {
		// It's a string for sure. Nothing to do.
		return "!!str", in
	}

	// Handle things we can lookup in a map.
	if item, ok := resolveMap[in]; ok {
		return item.tag, item.value
	}

	switch c {
	case 'M':
		// We've already checked the map above.

	case '.':
		// Not in the map, so maybe a normal float.
		floatv, err := strconv.ParseFloat(in, 64)
		if err == nil {
			return "!!float", floatv
		}
	// XXX Handle base 60 floats here (WTF!)

	case 'D', 'S':
		// Int, float, or timestamp.
		for i := 0; i != len(in); i++ {
			if in[i] == '_' {
				in = strings.Replace(in, "_", "", -1)
				break
			}
		}
		intv, err := strconv.ParseInt(in, 0, 64)
		if err == nil {
			if intv == int64(int(intv)) {
				return "!!int", int(intv)
			} else {
				return "!!int", intv
			}
		}
		floatv, err := strconv.ParseFloat(in, 64)
		if err == nil {
			return "!!float", floatv
		}
		if strings.HasPrefix(in, "0b") {
			intv, err := strconv.ParseInt(in[2:], 2, 64)
			if err == nil {
				return "!!int", int(intv)
			}
		} else if strings.HasPrefix(in, "-0b") {
			intv, err := strconv.ParseInt(in[3:], 2, 64)
			if err == nil {
				return "!!int", -int(intv)
			}
		}
	// XXX Handle timestamps here.

	case '<':
		// XXX Handle merge (<<) here.

	default:
		panic("resolveTable item not yet handled: " +
			string([]byte{c}) + " (with " + in + ")")
	}
	return "!!str", in
}
