package yaml

import (
	"errors"
	"fmt"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

func (c *Converter) unmarshal(src []byte, ty cty.Type) (cty.Value, error) {
	p := &yaml_parser_t{}
	if !yaml_parser_initialize(p) {
		return cty.NilVal, errors.New("failed to initialize YAML parser")
	}
	if len(src) == 0 {
		src = []byte{'\n'}
	}

	an := &valueAnalysis{
		anchorsPending: map[string]int{},
		anchorVals:     map[string]cty.Value{},
	}

	yaml_parser_set_input_string(p, src)

	var evt yaml_event_t
	if !yaml_parser_parse(p, &evt) {
		return cty.NilVal, parserError(p)
	}
	if evt.typ != yaml_STREAM_START_EVENT {
		return cty.NilVal, parseEventErrorf(&evt, "missing stream start token")
	}
	if !yaml_parser_parse(p, &evt) {
		return cty.NilVal, parserError(p)
	}
	if evt.typ != yaml_DOCUMENT_START_EVENT {
		return cty.NilVal, parseEventErrorf(&evt, "missing start of document")
	}

	v, err := c.unmarshalParse(an, p)
	if err != nil {
		return cty.NilVal, err
	}

	if !yaml_parser_parse(p, &evt) {
		return cty.NilVal, parserError(p)
	}
	if evt.typ == yaml_DOCUMENT_START_EVENT {
		return cty.NilVal, parseEventErrorf(&evt, "only a single document is allowed")
	}
	if evt.typ != yaml_DOCUMENT_END_EVENT {
		return cty.NilVal, parseEventErrorf(&evt, "unexpected extra content (%s) after value", evt.typ.String())
	}
	if !yaml_parser_parse(p, &evt) {
		return cty.NilVal, parserError(p)
	}
	if evt.typ != yaml_STREAM_END_EVENT {
		return cty.NilVal, parseEventErrorf(&evt, "unexpected extra content after value")
	}

	return convert.Convert(v, ty)
}

func (c *Converter) unmarshalParse(an *valueAnalysis, p *yaml_parser_t) (cty.Value, error) {
	var evt yaml_event_t
	if !yaml_parser_parse(p, &evt) {
		return cty.NilVal, parserError(p)
	}
	return c.unmarshalParseRemainder(an, &evt, p)
}

func (c *Converter) unmarshalParseRemainder(an *valueAnalysis, evt *yaml_event_t, p *yaml_parser_t) (cty.Value, error) {
	switch evt.typ {
	case yaml_SCALAR_EVENT:
		return c.unmarshalScalar(an, evt, p)
	case yaml_ALIAS_EVENT:
		return c.unmarshalAlias(an, evt, p)
	case yaml_MAPPING_START_EVENT:
		return c.unmarshalMapping(an, evt, p)
	case yaml_SEQUENCE_START_EVENT:
		return c.unmarshalSequence(an, evt, p)
	case yaml_DOCUMENT_START_EVENT:
		return cty.NilVal, parseEventErrorf(evt, "only a single document is allowed")
	case yaml_STREAM_END_EVENT:
		// Decoding an empty buffer, probably
		return cty.NilVal, parseEventErrorf(evt, "expecting value but found end of stream")
	default:
		// Should never happen; the above should be comprehensive
		return cty.NilVal, parseEventErrorf(evt, "unexpected parser event %s", evt.typ.String())
	}
}

func (c *Converter) unmarshalScalar(an *valueAnalysis, evt *yaml_event_t, p *yaml_parser_t) (cty.Value, error) {
	src := evt.value
	tag := string(evt.tag)
	anchor := string(evt.anchor)

	if len(anchor) > 0 {
		an.beginAnchor(anchor)
	}

	val, err := c.resolveScalar(tag, string(src))
	if err != nil {
		return cty.NilVal, parseEventErrorWrap(evt, err)
	}

	if val.RawEquals(mergeMappingVal) {
		// In any context other than a mapping key, this is just a plain string
		val = cty.StringVal("<<")
	}

	if len(anchor) > 0 {
		an.completeAnchor(anchor, val)
	}
	return val, nil
}

func (c *Converter) unmarshalMapping(an *valueAnalysis, evt *yaml_event_t, p *yaml_parser_t) (cty.Value, error) {
	tag := string(evt.tag)
	anchor := string(evt.anchor)

	if tag != "" && tag != yaml_MAP_TAG {
		return cty.NilVal, parseEventErrorf(evt, "can't interpret mapping as %s", tag)
	}

	if anchor != "" {
		an.beginAnchor(anchor)
	}

	vals := make(map[string]cty.Value)
	for {
		var nextEvt yaml_event_t
		if !yaml_parser_parse(p, &nextEvt) {
			return cty.NilVal, parserError(p)
		}
		if nextEvt.typ == yaml_MAPPING_END_EVENT {
			v := cty.ObjectVal(vals)
			if anchor != "" {
				an.completeAnchor(anchor, v)
			}
			return v, nil
		}

		if nextEvt.typ != yaml_SCALAR_EVENT {
			return cty.NilVal, parseEventErrorf(&nextEvt, "only strings are allowed as mapping keys")
		}
		keyVal, err := c.resolveScalar(string(nextEvt.tag), string(nextEvt.value))
		if err != nil {
			return cty.NilVal, err
		}
		if keyVal.RawEquals(mergeMappingVal) {
			// Merging the value (which must be a mapping) into our mapping,
			// then.
			val, err := c.unmarshalParse(an, p)
			if err != nil {
				return cty.NilVal, err
			}
			ty := val.Type()
			if !(ty.IsObjectType() || ty.IsMapType()) {
				return cty.NilVal, parseEventErrorf(&nextEvt, "cannot merge %s into mapping", ty.FriendlyName())
			}
			for it := val.ElementIterator(); it.Next(); {
				k, v := it.Element()
				vals[k.AsString()] = v
			}
			continue
		}
		if keyValStr, err := convert.Convert(keyVal, cty.String); err == nil {
			keyVal = keyValStr
		} else {
			return cty.NilVal, parseEventErrorf(&nextEvt, "only strings are allowed as mapping keys")
		}
		if keyVal.IsNull() {
			return cty.NilVal, parseEventErrorf(&nextEvt, "mapping key cannot be null")
		}
		if !keyVal.IsKnown() {
			return cty.NilVal, parseEventErrorf(&nextEvt, "mapping key must be known")
		}
		val, err := c.unmarshalParse(an, p)
		if err != nil {
			return cty.NilVal, err
		}

		vals[keyVal.AsString()] = val
	}
}

func (c *Converter) unmarshalSequence(an *valueAnalysis, evt *yaml_event_t, p *yaml_parser_t) (cty.Value, error) {
	tag := string(evt.tag)
	anchor := string(evt.anchor)

	if tag != "" && tag != yaml_SEQ_TAG {
		return cty.NilVal, parseEventErrorf(evt, "can't interpret sequence as %s", tag)
	}

	if anchor != "" {
		an.beginAnchor(anchor)
	}

	var vals []cty.Value
	for {
		var nextEvt yaml_event_t
		if !yaml_parser_parse(p, &nextEvt) {
			return cty.NilVal, parserError(p)
		}
		if nextEvt.typ == yaml_SEQUENCE_END_EVENT {
			ty := cty.TupleVal(vals)
			if anchor != "" {
				an.completeAnchor(anchor, ty)
			}
			return ty, nil
		}

		val, err := c.unmarshalParseRemainder(an, &nextEvt, p)
		if err != nil {
			return cty.NilVal, err
		}

		vals = append(vals, val)
	}
}

func (c *Converter) unmarshalAlias(an *valueAnalysis, evt *yaml_event_t, p *yaml_parser_t) (cty.Value, error) {
	v, err := an.anchorVal(string(evt.anchor))
	if err != nil {
		err = parseEventErrorWrap(evt, err)
	}
	return v, err
}

type valueAnalysis struct {
	anchorsPending map[string]int
	anchorVals     map[string]cty.Value
}

func (an *valueAnalysis) beginAnchor(name string) {
	an.anchorsPending[name]++
}

func (an *valueAnalysis) completeAnchor(name string, v cty.Value) {
	an.anchorsPending[name]--
	if an.anchorsPending[name] == 0 {
		delete(an.anchorsPending, name)
	}
	an.anchorVals[name] = v
}

func (an *valueAnalysis) anchorVal(name string) (cty.Value, error) {
	if _, pending := an.anchorsPending[name]; pending {
		// YAML normally allows self-referencing structures, but cty cannot
		// represent them (it requires all structures to be finite) so we
		// must fail here.
		return cty.NilVal, fmt.Errorf("cannot refer to anchor %q from inside its own definition", name)
	}
	ty, ok := an.anchorVals[name]
	if !ok {
		return cty.NilVal, fmt.Errorf("reference to undefined anchor %q", name)
	}
	return ty, nil
}
