package json

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/hashicorp/go-cty/cty"
)

// ImpliedType returns the cty Type implied by the structure of the given
// JSON-compliant buffer. This function implements the default type mapping
// behavior used when decoding arbitrary JSON without explicit cty Type
// information.
//
// The rules are as follows:
//
// JSON strings, numbers and bools map to their equivalent primitive type in
// cty.
//
// JSON objects map to cty object types, with the attributes defined by the
// object keys and the types of their values.
//
// JSON arrays map to cty tuple types, with the elements defined by the
// types of the array members.
//
// Any nulls are typed as DynamicPseudoType, so callers of this function
// must be prepared to deal with this. Callers that do not wish to deal with
// dynamic typing should not use this function and should instead describe
// their required types explicitly with a cty.Type instance when decoding.
//
// Any JSON syntax errors will be returned as an error, and the type will
// be the invalid value cty.NilType.
func ImpliedType(buf []byte) (cty.Type, error) {
	r := bytes.NewReader(buf)
	dec := json.NewDecoder(r)
	dec.UseNumber()

	ty, err := impliedType(dec)
	if err != nil {
		return cty.NilType, err
	}

	if dec.More() {
		return cty.NilType, fmt.Errorf("extraneous data after JSON object")
	}

	return ty, nil
}

func impliedType(dec *json.Decoder) (cty.Type, error) {
	tok, err := dec.Token()
	if err != nil {
		return cty.NilType, err
	}

	return impliedTypeForTok(tok, dec)
}

func impliedTypeForTok(tok json.Token, dec *json.Decoder) (cty.Type, error) {
	if tok == nil {
		return cty.DynamicPseudoType, nil
	}

	switch ttok := tok.(type) {
	case bool:
		return cty.Bool, nil

	case json.Number:
		return cty.Number, nil

	case string:
		return cty.String, nil

	case json.Delim:

		switch rune(ttok) {
		case '{':
			return impliedObjectType(dec)
		case '[':
			return impliedTupleType(dec)
		default:
			return cty.NilType, fmt.Errorf("unexpected token %q", ttok)
		}

	default:
		return cty.NilType, fmt.Errorf("unsupported JSON token %#v", tok)
	}
}

func impliedObjectType(dec *json.Decoder) (cty.Type, error) {
	// By the time we get in here, we've already consumed the { delimiter
	// and so our next token should be the first object key.

	var atys map[string]cty.Type

	for {
		// Read the object key first
		tok, err := dec.Token()
		if err != nil {
			return cty.NilType, err
		}

		if ttok, ok := tok.(json.Delim); ok {
			if rune(ttok) != '}' {
				return cty.NilType, fmt.Errorf("unexpected delimiter %q", ttok)
			}
			break
		}

		key, ok := tok.(string)
		if !ok {
			return cty.NilType, fmt.Errorf("expected string but found %T", tok)
		}

		// Now read the value
		tok, err = dec.Token()
		if err != nil {
			return cty.NilType, err
		}

		aty, err := impliedTypeForTok(tok, dec)
		if err != nil {
			return cty.NilType, err
		}

		if atys == nil {
			atys = make(map[string]cty.Type)
		}
		atys[key] = aty
	}

	if len(atys) == 0 {
		return cty.EmptyObject, nil
	}

	return cty.Object(atys), nil
}

func impliedTupleType(dec *json.Decoder) (cty.Type, error) {
	// By the time we get in here, we've already consumed the [ delimiter
	// and so our next token should be the first value.

	var etys []cty.Type

	for {
		tok, err := dec.Token()
		if err != nil {
			return cty.NilType, err
		}

		if ttok, ok := tok.(json.Delim); ok {
			if rune(ttok) == ']' {
				break
			}
		}

		ety, err := impliedTypeForTok(tok, dec)
		if err != nil {
			return cty.NilType, err
		}
		etys = append(etys, ety)
	}

	if len(etys) == 0 {
		return cty.EmptyTuple, nil
	}

	return cty.Tuple(etys), nil
}
