package parser

import (
	"fmt"
	"sort"

	"github.com/robertkrimen/otto/file"
	"github.com/robertkrimen/otto/token"
)

const (
	errUnexpectedToken      = "Unexpected token %v"
	errUnexpectedEndOfInput = "Unexpected end of input"
)

//    UnexpectedNumber:  'Unexpected number',
//    UnexpectedString:  'Unexpected string',
//    UnexpectedIdentifier:  'Unexpected identifier',
//    UnexpectedReserved:  'Unexpected reserved word',
//    NewlineAfterThrow:  'Illegal newline after throw',
//    InvalidRegExp: 'Invalid regular expression',
//    UnterminatedRegExp:  'Invalid regular expression: missing /',
//    InvalidLHSInAssignment:  'invalid left-hand side in assignment',
//    InvalidLHSInForIn:  'Invalid left-hand side in for-in',
//    MultipleDefaultsInSwitch: 'More than one default clause in switch statement',
//    NoCatchOrFinally:  'Missing catch or finally after try',
//    UnknownLabel: 'Undefined label \'%0\'',
//    Redeclaration: '%0 \'%1\' has already been declared',
//    IllegalContinue: 'Illegal continue statement',
//    IllegalBreak: 'Illegal break statement',
//    IllegalReturn: 'Illegal return statement',
//    StrictModeWith:  'Strict mode code may not include a with statement',
//    StrictCatchVariable:  'Catch variable may not be eval or arguments in strict mode',
//    StrictVarName:  'Variable name may not be eval or arguments in strict mode',
//    StrictParamName:  'Parameter name eval or arguments is not allowed in strict mode',
//    StrictParamDupe: 'Strict mode function may not have duplicate parameter names',
//    StrictFunctionName:  'Function name may not be eval or arguments in strict mode',
//    StrictOctalLiteral:  'Octal literals are not allowed in strict mode.',
//    StrictDelete:  'Delete of an unqualified identifier in strict mode.',
//    StrictDuplicateProperty:  'Duplicate data property in object literal not allowed in strict mode',
//    AccessorDataProperty:  'Object literal may not have data and accessor property with the same name',
//    AccessorGetSet:  'Object literal may not have multiple get/set accessors with the same name',
//    StrictLHSAssignment:  'Assignment to eval or arguments is not allowed in strict mode',
//    StrictLHSPostfix:  'Postfix increment/decrement may not have eval or arguments operand in strict mode',
//    StrictLHSPrefix:  'Prefix increment/decrement may not have eval or arguments operand in strict mode',
//    StrictReservedWord:  'Use of future reserved word in strict mode'

// A SyntaxError is a description of an ECMAScript syntax error.

// An Error represents a parsing error. It includes the position where the error occurred and a message/description.
type Error struct {
	Position file.Position
	Message  string
}

// FIXME Should this be "SyntaxError"?

func (e Error) Error() string {
	filename := e.Position.Filename
	if filename == "" {
		filename = "(anonymous)"
	}
	return fmt.Sprintf("%s: Line %d:%d %s",
		filename,
		e.Position.Line,
		e.Position.Column,
		e.Message,
	)
}

func (p *parser) error(place interface{}, msg string, msgValues ...interface{}) {
	var idx file.Idx
	switch place := place.(type) {
	case int:
		idx = p.idxOf(place)
	case file.Idx:
		if place == 0 {
			idx = p.idxOf(p.chrOffset)
		} else {
			idx = place
		}
	default:
		panic(fmt.Errorf("error(%T, ...)", place))
	}

	position := p.position(idx)
	msg = fmt.Sprintf(msg, msgValues...)
	p.errors.Add(position, msg)
}

func (p *parser) errorUnexpected(idx file.Idx, chr rune) {
	if chr == -1 {
		p.error(idx, errUnexpectedEndOfInput)
		return
	}
	p.error(idx, errUnexpectedToken, token.ILLEGAL)
}

func (p *parser) errorUnexpectedToken(tkn token.Token) {
	if tkn == token.EOF {
		p.error(file.Idx(0), errUnexpectedEndOfInput)
		return
	}
	value := tkn.String()
	switch tkn {
	case token.BOOLEAN, token.NULL:
		p.error(p.idx, errUnexpectedToken, p.literal)
	case token.IDENTIFIER:
		p.error(p.idx, "Unexpected identifier")
	case token.KEYWORD:
		// TODO Might be a future reserved word
		p.error(p.idx, "Unexpected reserved word")
	case token.NUMBER:
		p.error(p.idx, "Unexpected number")
	case token.STRING:
		p.error(p.idx, "Unexpected string")
	default:
		p.error(p.idx, errUnexpectedToken, value)
	}
}

// ErrorList is a list of *Errors.
type ErrorList []*Error //nolint: errname

// Add adds an Error with given position and message to an ErrorList.
func (el *ErrorList) Add(position file.Position, msg string) {
	*el = append(*el, &Error{position, msg})
}

// Reset resets an ErrorList to no errors.
func (el *ErrorList) Reset() {
	*el = (*el)[0:0]
}

// Len implement sort.Interface.
func (el *ErrorList) Len() int {
	return len(*el)
}

// Swap implement sort.Interface.
func (el *ErrorList) Swap(i, j int) {
	(*el)[i], (*el)[j] = (*el)[j], (*el)[i]
}

// Less implement sort.Interface.
func (el *ErrorList) Less(i, j int) bool {
	x := (*el)[i].Position
	y := (*el)[j].Position
	if x.Filename < y.Filename {
		return true
	}
	if x.Filename == y.Filename {
		if x.Line < y.Line {
			return true
		}
		if x.Line == y.Line {
			return x.Column < y.Column
		}
	}
	return false
}

// Sort sorts el.
func (el *ErrorList) Sort() {
	sort.Sort(el)
}

// Error implements the Error interface.
func (el *ErrorList) Error() string {
	switch len(*el) {
	case 0:
		return "no errors"
	case 1:
		return (*el)[0].Error()
	default:
		return fmt.Sprintf("%s (and %d more errors)", (*el)[0].Error(), len(*el)-1)
	}
}

// Err returns an error equivalent to this ErrorList.
// If the list is empty, Err returns nil.
func (el *ErrorList) Err() error {
	if len(*el) == 0 {
		return nil
	}
	return el
}
