// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package json

import (
	"encoding/json"
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"
)

func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
	tokens := scan(buf, pos{Filename: filename, Pos: start})
	p := newPeeker(tokens)
	node, diags := parseValue(p)
	if len(diags) == 0 && p.Peek().Type != tokenEOF {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Extraneous data after value",
			Detail:   "Extra characters appear after the JSON value.",
			Subject:  p.Peek().Range.Ptr(),
		})
	}
	return node, diags
}

func parseExpression(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
	tokens := scan(buf, pos{Filename: filename, Pos: start})
	p := newPeeker(tokens)
	node, diags := parseValue(p)
	if len(diags) == 0 && p.Peek().Type != tokenEOF {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Extraneous data after value",
			Detail:   "Extra characters appear after the JSON value.",
			Subject:  p.Peek().Range.Ptr(),
		})
	}
	return node, diags
}

func parseValue(p *peeker) (node, hcl.Diagnostics) {
	tok := p.Peek()

	wrapInvalid := func(n node, diags hcl.Diagnostics) (node, hcl.Diagnostics) {
		if n != nil {
			return n, diags
		}
		return invalidVal{tok.Range}, diags
	}

	switch tok.Type {
	case tokenBraceO:
		return wrapInvalid(parseObject(p))
	case tokenBrackO:
		return wrapInvalid(parseArray(p))
	case tokenNumber:
		return wrapInvalid(parseNumber(p))
	case tokenString:
		return wrapInvalid(parseString(p))
	case tokenKeyword:
		return wrapInvalid(parseKeyword(p))
	case tokenBraceC:
		return wrapInvalid(nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Missing JSON value",
				Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
				Subject:  &tok.Range,
			},
		})
	case tokenBrackC:
		return wrapInvalid(nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Missing array element value",
				Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
				Subject:  &tok.Range,
			},
		})
	case tokenEOF:
		return wrapInvalid(nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Missing value",
				Detail:   "The JSON data ends prematurely.",
				Subject:  &tok.Range,
			},
		})
	default:
		return wrapInvalid(nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid start of value",
				Detail:   "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.",
				Subject:  &tok.Range,
			},
		})
	}
}

func parseObject(p *peeker) (node, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	open := p.Read()
	attrs := []*objectAttr{}

	// recover is used to shift the peeker to what seems to be the end of
	// our object, so that when we encounter an error we leave the peeker
	// at a reasonable point in the token stream to continue parsing.
	recover := func(tok token) {
		open := 1
		for {
			switch tok.Type {
			case tokenBraceO:
				open++
			case tokenBraceC:
				open--
				if open <= 1 {
					return
				}
			case tokenEOF:
				// Ran out of source before we were able to recover,
				// so we'll bail here and let the caller deal with it.
				return
			}
			tok = p.Read()
		}
	}

Token:
	for {
		if p.Peek().Type == tokenBraceC {
			break Token
		}

		keyNode, keyDiags := parseValue(p)
		diags = diags.Extend(keyDiags)
		if keyNode == nil {
			return nil, diags
		}

		keyStrNode, ok := keyNode.(*stringVal)
		if !ok {
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid object property name",
				Detail:   "A JSON object property name must be a string",
				Subject:  keyNode.StartRange().Ptr(),
			})
		}

		key := keyStrNode.Value

		colon := p.Read()
		if colon.Type != tokenColon {
			recover(colon)

			if colon.Type == tokenBraceC || colon.Type == tokenComma {
				// Catch common mistake of using braces instead of brackets
				// for an object.
				return nil, diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Missing object value",
					Detail:   "A JSON object attribute must have a value, introduced by a colon.",
					Subject:  &colon.Range,
				})
			}

			if colon.Type == tokenEquals {
				// Possible confusion with native HCL syntax.
				return nil, diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Missing property value colon",
					Detail:   "JSON uses a colon as its name/value delimiter, not an equals sign.",
					Subject:  &colon.Range,
				})
			}

			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Missing property value colon",
				Detail:   "A colon must appear between an object property's name and its value.",
				Subject:  &colon.Range,
			})
		}

		valNode, valDiags := parseValue(p)
		diags = diags.Extend(valDiags)
		if valNode == nil {
			return nil, diags
		}

		attrs = append(attrs, &objectAttr{
			Name:      key,
			Value:     valNode,
			NameRange: keyStrNode.SrcRange,
		})

		switch p.Peek().Type {
		case tokenComma:
			comma := p.Read()
			if p.Peek().Type == tokenBraceC {
				// Special error message for this common mistake
				return nil, diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Trailing comma in object",
					Detail:   "JSON does not permit a trailing comma after the final property in an object.",
					Subject:  &comma.Range,
				})
			}
			continue Token
		case tokenEOF:
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unclosed object",
				Detail:   "No closing brace was found for this JSON object.",
				Subject:  &open.Range,
			})
		case tokenBrackC:
			// Consume the bracket anyway, so that we don't return with the peeker
			// at a strange place.
			p.Read()
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Mismatched braces",
				Detail:   "A JSON object must be closed with a brace, not a bracket.",
				Subject:  p.Peek().Range.Ptr(),
			})
		case tokenBraceC:
			break Token
		default:
			recover(p.Read())
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Missing attribute seperator comma",
				Detail:   "A comma must appear between each property definition in an object.",
				Subject:  p.Peek().Range.Ptr(),
			})
		}

	}

	close := p.Read()
	return &objectVal{
		Attrs:      attrs,
		SrcRange:   hcl.RangeBetween(open.Range, close.Range),
		OpenRange:  open.Range,
		CloseRange: close.Range,
	}, diags
}

func parseArray(p *peeker) (node, hcl.Diagnostics) {
	var diags hcl.Diagnostics

	open := p.Read()
	vals := []node{}

	// recover is used to shift the peeker to what seems to be the end of
	// our array, so that when we encounter an error we leave the peeker
	// at a reasonable point in the token stream to continue parsing.
	recover := func(tok token) {
		open := 1
		for {
			switch tok.Type {
			case tokenBrackO:
				open++
			case tokenBrackC:
				open--
				if open <= 1 {
					return
				}
			case tokenEOF:
				// Ran out of source before we were able to recover,
				// so we'll bail here and let the caller deal with it.
				return
			}
			tok = p.Read()
		}
	}

Token:
	for {
		if p.Peek().Type == tokenBrackC {
			break Token
		}

		valNode, valDiags := parseValue(p)
		diags = diags.Extend(valDiags)
		if valNode == nil {
			return nil, diags
		}

		vals = append(vals, valNode)

		switch p.Peek().Type {
		case tokenComma:
			comma := p.Read()
			if p.Peek().Type == tokenBrackC {
				// Special error message for this common mistake
				return nil, diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  "Trailing comma in array",
					Detail:   "JSON does not permit a trailing comma after the final value in an array.",
					Subject:  &comma.Range,
				})
			}
			continue Token
		case tokenColon:
			recover(p.Read())
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid array value",
				Detail:   "A colon is not used to introduce values in a JSON array.",
				Subject:  p.Peek().Range.Ptr(),
			})
		case tokenEOF:
			recover(p.Read())
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Unclosed object",
				Detail:   "No closing bracket was found for this JSON array.",
				Subject:  &open.Range,
			})
		case tokenBraceC:
			recover(p.Read())
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Mismatched brackets",
				Detail:   "A JSON array must be closed with a bracket, not a brace.",
				Subject:  p.Peek().Range.Ptr(),
			})
		case tokenBrackC:
			break Token
		default:
			recover(p.Read())
			return nil, diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Missing attribute seperator comma",
				Detail:   "A comma must appear between each value in an array.",
				Subject:  p.Peek().Range.Ptr(),
			})
		}

	}

	close := p.Read()
	return &arrayVal{
		Values:    vals,
		SrcRange:  hcl.RangeBetween(open.Range, close.Range),
		OpenRange: open.Range,
	}, diags
}

func parseNumber(p *peeker) (node, hcl.Diagnostics) {
	tok := p.Read()

	// Use encoding/json to validate the number syntax.
	// TODO: Do this more directly to produce better diagnostics.
	var num json.Number
	err := json.Unmarshal(tok.Bytes, &num)
	if err != nil {
		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid JSON number",
				Detail:   "There is a syntax error in the given JSON number.",
				Subject:  &tok.Range,
			},
		}
	}

	// We want to guarantee that we parse numbers the same way as cty (and thus
	// native syntax HCL) would here, so we'll use the cty parser even though
	// in most other cases we don't actually introduce cty concepts until
	// decoding time. We'll unwrap the parsed float immediately afterwards, so
	// the cty value is just a temporary helper.
	nv, err := cty.ParseNumberVal(string(num))
	if err != nil {
		// Should never happen if above passed, since JSON numbers are a subset
		// of what cty can parse...
		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid JSON number",
				Detail:   "There is a syntax error in the given JSON number.",
				Subject:  &tok.Range,
			},
		}
	}

	return &numberVal{
		Value:    nv.AsBigFloat(),
		SrcRange: tok.Range,
	}, nil
}

func parseString(p *peeker) (node, hcl.Diagnostics) {
	tok := p.Read()
	var str string
	err := json.Unmarshal(tok.Bytes, &str)

	if err != nil {
		var errRange hcl.Range
		if serr, ok := err.(*json.SyntaxError); ok {
			errOfs := serr.Offset
			errPos := tok.Range.Start
			errPos.Byte += int(errOfs)

			// TODO: Use the byte offset to properly count unicode
			// characters for the column, and mark the whole of the
			// character that was wrong as part of our range.
			errPos.Column += int(errOfs)

			errEndPos := errPos
			errEndPos.Byte++
			errEndPos.Column++

			errRange = hcl.Range{
				Filename: tok.Range.Filename,
				Start:    errPos,
				End:      errEndPos,
			}
		} else {
			errRange = tok.Range
		}

		var contextRange *hcl.Range
		if errRange != tok.Range {
			contextRange = &tok.Range
		}

		// FIXME: Eventually we should parse strings directly here so
		// we can produce a more useful error message in the face fo things
		// such as invalid escapes, etc.
		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid JSON string",
				Detail:   "There is a syntax error in the given JSON string.",
				Subject:  &errRange,
				Context:  contextRange,
			},
		}
	}

	return &stringVal{
		Value:    str,
		SrcRange: tok.Range,
	}, nil
}

func parseKeyword(p *peeker) (node, hcl.Diagnostics) {
	tok := p.Read()
	s := string(tok.Bytes)

	switch s {
	case "true":
		return &booleanVal{
			Value:    true,
			SrcRange: tok.Range,
		}, nil
	case "false":
		return &booleanVal{
			Value:    false,
			SrcRange: tok.Range,
		}, nil
	case "null":
		return &nullVal{
			SrcRange: tok.Range,
		}, nil
	case "undefined", "NaN", "Infinity":
		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid JSON keyword",
				Detail:   fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s),
				Subject:  &tok.Range,
			},
		}
	default:
		var dym string
		if suggest := keywordSuggestion(s); suggest != "" {
			dym = fmt.Sprintf(" Did you mean %q?", suggest)
		}

		return nil, hcl.Diagnostics{
			{
				Severity: hcl.DiagError,
				Summary:  "Invalid JSON keyword",
				Detail:   fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym),
				Subject:  &tok.Range,
			},
		}
	}
}
