package parser

import (
	"fmt"

	"github.com/sblinch/kdl-go/document"
	"github.com/sblinch/kdl-go/internal/tokenizer"
)

type Parser struct {
}

// New creates a new Parser
func New() *Parser {
	return &Parser{}
}

// NewContext creates a new ParseContext
func (p *Parser) NewContext() *ParseContext {
	return p.NewContextOptions(defaultParseContextOptions)
}

func (p *Parser) NewContextOptions(opts ParseContextOptions) *ParseContext {
	return &ParseContext{
		doc:  document.New(),
		opts: opts,
	}
}

var eofToken = tokenizer.Token{
	ID:   tokenizer.EOF,
	Data: []byte{},
}

func (p *Parser) tokenContext(context []tokenizer.Token, idx int) string {
	if len(context) == 0 {
		return ""
	}
	start := idx - 5
	if start < 0 {
		start = 0
	}
	end := start + 10
	if end > len(context) {
		end = len(context)
	}
	b := make([]byte, 0, 128)
	b = append(b, "..."...)
	caretOffset := 3
	for i := start; i < end; i++ {
		for _, c := range context[i].Data {
			if c == '\t' || c == '\r' || c == '\n' {
				c = ' '
			}
			b = append(b, c)
		}
		if i < idx {
			caretOffset += len(context[i].Data)
		}
	}
	b = append(b, "..."...)
	b = append(b, '\n')
	for i := 0; i < caretOffset; i++ {
		b = append(b, ' ')
	}
	b = append(b, '^')
	return string(b)
}

func (p *Parser) annotatedError(err error, t tokenizer.Token, context []tokenizer.Token, contextIndex int) error {
	return fmt.Errorf("parse failed: %w at line %d, column %d:\n%s", err, t.Line, t.Column, p.tokenContext(context, contextIndex))
}

// Parse parses a single token (usually generated by tokenizer.Scanner) into the given context, and returns a non-nil
// error on failure
func (p *Parser) parse(c *ParseContext, token tokenizer.Token) error {
	if c.continuation {
		switch token.ID {
		case tokenizer.Newline, tokenizer.Whitespace, tokenizer.SingleLineComment, tokenizer.MultiLineComment:
			// continue waiting for the first token of the next (continued) line
		default:
			// this is the first token of the next (continued) line
			c.continuation = false
		}
	}

	validTransitions := stateTransitions[c.state]

	handler, ok := validTransitions[token.ID]
	if !ok {
		// if there's no handler for this token's specific ID, check for a handler for the class of token instead
		tokenClasses := token.ID.Classes()
		for _, tokenClass := range tokenClasses {
			if handler, ok = validTransitions[tokenClass]; ok {
				break
			}
		}
	}

	var err error
	if handler == nil {
		err = fmt.Errorf("unexpected %s in state %s", token.ID, c.state)
	} else {
		err = handler(c, token)
	}

	return err
}

func (p *Parser) Parse(c *ParseContext, token tokenizer.Token) error {
	c.recent.Add(token)
	if err := p.parse(c, token); err != nil {
		tokens := c.recent.Get()
		return p.annotatedError(err, token, tokens, len(tokens)-1)
	}

	return nil
}

// ParseAll parses a slice of tokens and returns the resulting Document, or a non-nil error on failure
func (p *Parser) ParseAll(tokens []tokenizer.Token) (*document.Document, error) {
	c := p.NewContext()
	return p.ParseAllContext(c, tokens)
}

// ParseAllContext parses a slice of tokens using the given context and returns the resulting Document, or a non-nil
// error on failure
func (p *Parser) ParseAllContext(c *ParseContext, tokens []tokenizer.Token) (*document.Document, error) {
	for i, t := range tokens {
		if err := p.Parse(c, t); err != nil {
			return nil, p.annotatedError(err, t, tokens, i)
		}
	}

	eof := eofToken

	if len(tokens) > 0 {
		lastToken := tokens[len(tokens)-1]
		eof.Line = lastToken.Line
		eof.Column = lastToken.Column + len(lastToken.Data)
	}

	if err := p.Parse(c, eof); err != nil {
		return nil, p.annotatedError(err, eof, tokens, len(tokens)-1)
	}

	return c.doc, nil
}
