package jsonx

import (
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/antonmedv/fx/internal/utils"
)

type jsonParser struct {
	data           []byte
	end            int
	lastChar       byte
	lineNumber     uint
	sourceTail     *ring
	depth          uint8
	skipFirstIdent bool
}

func Parse(data []byte) (head *Node, err error) {
	p := &jsonParser{
		data:       data,
		lineNumber: 1,
		sourceTail: &ring{},
	}
	defer func() {
		if r := recover(); r != nil {
			err = p.errorSnippet(fmt.Sprintf("%v", r))
		}
	}()
	p.next()
	var next *Node
	for p.lastChar != 0 {
		value := p.parseValue()
		if head == nil {
			head = value
			next = head
		} else {
			value.Index = -1
			next.adjacent(value)
			next = value
		}
	}
	return
}

func (p *jsonParser) next() {
	if p.end < len(p.data) {
		p.lastChar = p.data[p.end]
		p.end++
	} else {
		p.lastChar = 0
	}
	if p.lastChar == '\n' {
		p.lineNumber++
	}
	p.sourceTail.writeByte(p.lastChar)
}

func (p *jsonParser) parseValue() *Node {
	p.skipWhitespace()

	var l *Node
	switch p.lastChar {
	case '"':
		l = p.parseString()
	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
		l = p.parseNumber()
	case '{':
		l = p.parseObject()
	case '[':
		l = p.parseArray()
	case 't':
		l = p.parseKeyword("true")
	case 'f':
		l = p.parseKeyword("false")
	case 'n':
		l = p.parseKeyword("null")
	default:
		panic(fmt.Sprintf("Unexpected character %q", p.lastChar))
	}

	p.skipWhitespace()
	return l
}

func (p *jsonParser) parseString() *Node {
	str := &Node{Depth: p.depth}
	start := p.end - 1
	p.next()
	escaped := false
	for {
		if escaped {
			switch p.lastChar {
			case 'u':
				var unicode string
				for i := 0; i < 4; i++ {
					p.next()
					if !utils.IsHexDigit(p.lastChar) {
						panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar))
					}
					unicode += string(p.lastChar)
				}
				_, err := strconv.ParseInt(unicode, 16, 32)
				if err != nil {
					panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s'", unicode))
				}
			case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
			default:
				panic(fmt.Sprintf("Invalid escape sequence '\\%c'", p.lastChar))
			}
			escaped = false
		} else if p.lastChar == '\\' {
			escaped = true
		} else if p.lastChar == '"' {
			break
		} else if p.lastChar == 0 {
			panic("Unexpected end of input in string")
		} else if p.lastChar < 0x1F {
			panic(fmt.Sprintf("Invalid character %q in string", p.lastChar))
		}
		p.next()
	}

	str.Value = p.data[start:p.end]
	p.next()
	return str
}

func (p *jsonParser) parseNumber() *Node {
	num := &Node{Depth: p.depth}
	start := p.end - 1

	// Handle negative numbers
	if p.lastChar == '-' {
		p.next()
		if !utils.IsDigit(p.lastChar) {
			panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
		}
	}

	// Leading zero
	if p.lastChar == '0' {
		p.next()
	} else {
		for utils.IsDigit(p.lastChar) {
			p.next()
		}
	}

	// Decimal portion
	if p.lastChar == '.' {
		p.next()
		if !utils.IsDigit(p.lastChar) {
			panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
		}
		for utils.IsDigit(p.lastChar) {
			p.next()
		}
	}

	// Exponent
	if p.lastChar == 'e' || p.lastChar == 'E' {
		p.next()
		if p.lastChar == '+' || p.lastChar == '-' {
			p.next()
		}
		if !utils.IsDigit(p.lastChar) {
			panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
		}
		for utils.IsDigit(p.lastChar) {
			p.next()
		}
	}

	num.Value = p.data[start : p.end-1]
	return num
}

func (p *jsonParser) parseObject() *Node {
	object := &Node{Depth: p.depth}
	object.Value = []byte{'{'}

	p.next()
	p.skipWhitespace()

	// Empty object
	if p.lastChar == '}' {
		object.Value = append(object.Value, '}')
		p.next()
		return object
	}

	for {
		// Expecting a key which should be a string
		if p.lastChar != '"' {
			panic(fmt.Sprintf("Expected object key to be a string, got %q", p.lastChar))
		}

		p.depth++
		key := p.parseString()
		key.Key, key.Value = key.Value, nil
		object.Size += 1
		key.directParent = object

		p.skipWhitespace()

		// Expecting colon after key
		if p.lastChar != ':' {
			panic(fmt.Sprintf("Expected colon after object key, got %q", p.lastChar))
		}

		p.next()

		p.skipFirstIdent = true
		value := p.parseValue()
		p.depth--

		key.Value = value.Value
		key.Size = value.Size
		key.Next = value.Next
		if key.Next != nil {
			key.Next.Prev = key
		}
		key.End = value.End
		value.indirectParent = key
		object.append(key)

		p.skipWhitespace()

		if p.lastChar == ',' {
			object.End.Comma = true
			p.next()
			p.skipWhitespace()
			if p.lastChar == '}' {
				object.End.Comma = false
			} else {
				continue
			}
		}

		if p.lastChar == '}' {
			closeBracket := &Node{Depth: p.depth}
			closeBracket.Value = []byte{'}'}
			closeBracket.directParent = object
			closeBracket.Index = -1
			object.append(closeBracket)
			p.next()
			return object
		}

		panic(fmt.Sprintf("Unexpected character %q in object", p.lastChar))
	}
}

func (p *jsonParser) parseArray() *Node {
	arr := &Node{Depth: p.depth}
	arr.Value = []byte{'['}

	p.next()
	p.skipWhitespace()

	if p.lastChar == ']' {
		arr.Value = append(arr.Value, ']')
		p.next()
		return arr
	}

	for i := 0; ; i++ {
		p.depth++
		value := p.parseValue()
		value.directParent = arr
		arr.Size += 1
		value.Index = i
		p.depth--

		arr.append(value)
		p.skipWhitespace()

		if p.lastChar == ',' {
			arr.End.Comma = true
			p.next()
			p.skipWhitespace()
			if p.lastChar == ']' {
				arr.End.Comma = false
			} else {
				continue
			}
		}

		if p.lastChar == ']' {
			closeBracket := &Node{Depth: p.depth}
			closeBracket.Value = []byte{']'}
			closeBracket.directParent = arr
			closeBracket.Index = -1
			arr.append(closeBracket)
			p.next()
			return arr
		}

		panic(fmt.Sprintf("Invalid character %q in array", p.lastChar))
	}
}

func (p *jsonParser) parseKeyword(name string) *Node {
	for i := 1; i < len(name); i++ {
		p.next()
		if p.lastChar != name[i] {
			panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
		}
	}
	p.next()

	nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0
	if nextCharIsSpecial {
		keyword := &Node{Depth: p.depth}
		keyword.Value = []byte(name)
		return keyword
	}

	panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
}

func isWhitespace(ch byte) bool {
	return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}

func (p *jsonParser) skipWhitespace() {
	for {
		switch p.lastChar {
		case ' ', '\t', '\n', '\r':
			p.next()
		case '/':
			p.skipComment()
		default:
			return
		}
	}
}

func (p *jsonParser) skipComment() {
	p.next()
	switch p.lastChar {
	case '/':
		for p.lastChar != '\n' && p.lastChar != 0 {
			p.next()
		}
	case '*':
		for {
			p.next()
			if p.lastChar == '*' {
				p.next()
				if p.lastChar == '/' {
					p.next()
					return
				}
			}
			if p.lastChar == 0 {
				panic("Unexpected end of input in comment")
			}
		}
	default:
		panic(fmt.Sprintf("Invalid comment: '/%c'", p.lastChar))
	}
}

func (p *jsonParser) errorSnippet(message string) error {
	lines := strings.Split(p.sourceTail.string(), "\n")
	lastLineLen := utf8.RuneCountInString(lines[len(lines)-1])
	pointer := strings.Repeat(".", lastLineLen-1) + "^"
	lines = append(lines, pointer)

	paddedLines := make([]string, len(lines))
	for i, line := range lines {
		paddedLines[i] = "   " + line
	}

	return fmt.Errorf(
		"%s on line %d.\n\n%s\n\n",
		message,
		p.lineNumber,
		strings.Join(paddedLines, "\n"),
	)
}
