package compiler

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"strconv"
	"unicode"
)

// We have some function naming conventions.
//
// For terminals:
//   scanX     takes buf and position, returns new position (and maybe a value)
//   peekX     takes *parser, returns bool or string
//   consumeX  takes *parser and maybe a required literal, maybe returns value
//             also updates the parser position
//
// For nonterminals:
//   parseX    takes *parser, returns AST node, updates parser position

type parser struct {
	buf []byte
	pos int
}

func (p *parser) errorf(format string, args ...interface{}) {
	panic(parserErr{buf: p.buf, offset: p.pos, format: format, args: args})
}

// parse is the main entry point to the parser
func parse(buf []byte) (contracts []*Contract, err error) {
	defer func() {
		if val := recover(); val != nil {
			if e, ok := val.(parserErr); ok {
				err = e
			} else {
				panic(val)
			}
		}
	}()
	p := &parser{buf: buf}
	contracts = parseContracts(p)
	return
}

// parse functions

func parseContracts(p *parser) []*Contract {
	var result []*Contract
	if pos := scanKeyword(p.buf, p.pos, "contract"); pos < 0 {
		p.errorf("expected contract")
	}

	for peekKeyword(p) == "contract" {
		contract := parseContract(p)
		result = append(result, contract)
	}
	return result
}

// contract name(p1, p2: t1, p3: t2) locks value { ... }
func parseContract(p *parser) *Contract {
	consumeKeyword(p, "contract")
	name := consumeIdentifier(p)
	params := parseParams(p)
	// locks amount of asset
	consumeKeyword(p, "locks")
	value := ValueInfo{}
	value.Amount = consumeIdentifier(p)
	consumeKeyword(p, "of")
	value.Asset = consumeIdentifier(p)
	consumeTok(p, "{")
	clauses := parseClauses(p)
	consumeTok(p, "}")
	return &Contract{Name: name, Params: params, Clauses: clauses, Value: value}
}

// (p1, p2: t1, p3: t2)
func parseParams(p *parser) []*Param {
	var params []*Param
	consumeTok(p, "(")
	first := true
	for !peekTok(p, ")") {
		if first {
			first = false
		} else {
			consumeTok(p, ",")
		}
		pt := parseParamsType(p)
		params = append(params, pt...)
	}
	consumeTok(p, ")")
	return params
}

func parseClauses(p *parser) []*Clause {
	var clauses []*Clause
	for !peekTok(p, "}") {
		c := parseClause(p)
		clauses = append(clauses, c)
	}
	return clauses
}

func parseParamsType(p *parser) []*Param {
	firstName := consumeIdentifier(p)
	params := []*Param{&Param{Name: firstName}}
	for peekTok(p, ",") {
		consumeTok(p, ",")
		name := consumeIdentifier(p)
		params = append(params, &Param{Name: name})
	}
	consumeTok(p, ":")
	typ := consumeIdentifier(p)
	for _, parm := range params {
		if tdesc, ok := types[typ]; ok {
			parm.Type = tdesc
		} else {
			p.errorf("unknown type %s", typ)
		}
	}
	return params
}

func parseClause(p *parser) *Clause {
	var c Clause
	consumeKeyword(p, "clause")
	c.Name = consumeIdentifier(p)
	c.Params = parseParams(p)
	consumeTok(p, "{")
	c.statements = parseStatements(p)
	consumeTok(p, "}")
	return &c
}

func parseStatements(p *parser) []statement {
	var statements []statement
	for !peekTok(p, "}") {
		s := parseStatement(p)
		statements = append(statements, s)
	}
	return statements
}

func parseStatement(p *parser) statement {
	switch peekKeyword(p) {
	case "if":
		return parseIfStmt(p)
	case "define":
		return parseDefineStmt(p)
	case "assign":
		return parseAssignStmt(p)
	case "verify":
		return parseVerifyStmt(p)
	case "lock":
		return parseLockStmt(p)
	case "unlock":
		return parseUnlockStmt(p)
	}
	panic(parseErr(p.buf, p.pos, "unknown keyword \"%s\"", peekKeyword(p)))
}

func parseIfStmt(p *parser) *ifStatement {
	consumeKeyword(p, "if")
	condition := parseExpr(p)
	body := &IfStatmentBody{}
	consumeTok(p, "{")
	body.trueBody = parseStatements(p)
	consumeTok(p, "}")
	if peekKeyword(p) == "else" {
		consumeKeyword(p, "else")
		consumeTok(p, "{")
		body.falseBody = parseStatements(p)
		consumeTok(p, "}")
	}
	return &ifStatement{condition: condition, body: body}
}

func parseDefineStmt(p *parser) *defineStatement {
	defineStat := &defineStatement{}
	consumeKeyword(p, "define")
	param := &Param{}
	param.Name = consumeIdentifier(p)
	consumeTok(p, ":")
	variableType := consumeIdentifier(p)
	if tdesc, ok := types[variableType]; ok {
		param.Type = tdesc
	} else {
		p.errorf("unknown type %s", variableType)
	}
	defineStat.variable = param
	if peekTok(p, "=") {
		consumeTok(p, "=")
		defineStat.expr = parseExpr(p)
	}
	return defineStat
}

func parseAssignStmt(p *parser) *assignStatement {
	consumeKeyword(p, "assign")
	varName := consumeIdentifier(p)
	consumeTok(p, "=")
	expr := parseExpr(p)
	return &assignStatement{variable: &Param{Name: varName}, expr: expr}
}

func parseVerifyStmt(p *parser) *verifyStatement {
	consumeKeyword(p, "verify")
	expr := parseExpr(p)
	return &verifyStatement{expr: expr}
}

func parseLockStmt(p *parser) *lockStatement {
	consumeKeyword(p, "lock")
	lockedAmount := parseExpr(p)
	consumeKeyword(p, "of")
	lockedAsset := parseExpr(p)
	consumeKeyword(p, "with")
	program := parseExpr(p)
	return &lockStatement{lockedAmount: lockedAmount, lockedAsset: lockedAsset, program: program}
}

func parseUnlockStmt(p *parser) *unlockStatement {
	consumeKeyword(p, "unlock")
	unlockedAmount := parseExpr(p)
	consumeKeyword(p, "of")
	unlockedAsset := parseExpr(p)
	return &unlockStatement{unlockedAmount: unlockedAmount, unlockedAsset: unlockedAsset}
}

func parseExpr(p *parser) expression {
	// Uses the precedence-climbing algorithm
	// <https://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method>
	expr := parseUnaryExpr(p)
	expr2, pos := parseExprCont(p, expr, 0)
	if pos < 0 {
		p.errorf("expected expression")
	}
	p.pos = pos
	return expr2
}

func parseUnaryExpr(p *parser) expression {
	op, pos := scanUnaryOp(p.buf, p.pos)
	if pos < 0 {
		return parseExpr2(p)
	}
	p.pos = pos
	expr := parseUnaryExpr(p)
	return &unaryExpr{op: op, expr: expr}
}

func parseExprCont(p *parser, lhs expression, minPrecedence int) (expression, int) {
	for {
		op, pos := scanBinaryOp(p.buf, p.pos)
		if pos < 0 || op.precedence < minPrecedence {
			break
		}
		p.pos = pos

		rhs := parseUnaryExpr(p)

		for {
			op2, pos2 := scanBinaryOp(p.buf, p.pos)
			if pos2 < 0 || op2.precedence <= op.precedence {
				break
			}
			rhs, p.pos = parseExprCont(p, rhs, op2.precedence)
			if p.pos < 0 {
				return nil, -1 // or is this an error?
			}
		}
		lhs = &binaryExpr{left: lhs, right: rhs, op: op}
	}
	return lhs, p.pos
}

func parseExpr2(p *parser) expression {
	if expr, pos := scanLiteralExpr(p.buf, p.pos); pos >= 0 {
		p.pos = pos
		return expr
	}
	return parseExpr3(p)
}

func parseExpr3(p *parser) expression {
	e := parseExpr4(p)
	if peekTok(p, "(") {
		args := parseArgs(p)
		return &callExpr{fn: e, args: args}
	}
	return e
}

func parseExpr4(p *parser) expression {
	if peekTok(p, "(") {
		consumeTok(p, "(")
		e := parseExpr(p)
		consumeTok(p, ")")
		return e
	}
	if peekTok(p, "[") {
		var elts []expression
		consumeTok(p, "[")
		first := true
		for !peekTok(p, "]") {
			if first {
				first = false
			} else {
				consumeTok(p, ",")
			}
			e := parseExpr(p)
			elts = append(elts, e)
		}
		consumeTok(p, "]")
		return listExpr(elts)
	}
	name := consumeIdentifier(p)
	return varRef(name)
}

func parseArgs(p *parser) []expression {
	var exprs []expression
	consumeTok(p, "(")
	first := true
	for !peekTok(p, ")") {
		if first {
			first = false
		} else {
			consumeTok(p, ",")
		}
		e := parseExpr(p)
		exprs = append(exprs, e)
	}
	consumeTok(p, ")")
	return exprs
}

// peek functions

func peekKeyword(p *parser) string {
	name, _ := scanIdentifier(p.buf, p.pos)
	return name
}

func peekTok(p *parser, token string) bool {
	pos := scanTok(p.buf, p.pos, token)
	return pos >= 0
}

// consume functions

var keywords = []string{
	"contract", "clause", "verify", "locks", "of",
	"lock", "with", "unlock", "if", "else",
	"define", "assign", "true", "false",
}

func consumeKeyword(p *parser, keyword string) {
	pos := scanKeyword(p.buf, p.pos, keyword)
	if pos < 0 {
		p.errorf("expected keyword %s", keyword)
	}
	p.pos = pos
}

func consumeIdentifier(p *parser) string {
	name, pos := scanIdentifier(p.buf, p.pos)
	if pos < 0 {
		p.errorf("expected identifier")
	}
	p.pos = pos
	return name
}

func consumeTok(p *parser, token string) {
	pos := scanTok(p.buf, p.pos, token)
	if pos < 0 {
		p.errorf("expected %s token", token)
	}
	p.pos = pos
}

// scan functions

func scanUnaryOp(buf []byte, offset int) (*unaryOp, int) {
	// Maximum munch. Make sure "-3" scans as ("-3"), not ("-", "3").
	if _, pos := scanIntLiteral(buf, offset); pos >= 0 {
		return nil, -1
	}
	for _, op := range unaryOps {
		newOffset := scanTok(buf, offset, op.op)
		if newOffset >= 0 {
			return &op, newOffset
		}
	}
	return nil, -1
}

func scanBinaryOp(buf []byte, offset int) (*binaryOp, int) {
	offset = skipWsAndComments(buf, offset)
	var (
		found     *binaryOp
		newOffset = -1
	)
	for i, op := range binaryOps {
		offset2 := scanTok(buf, offset, op.op)
		if offset2 >= 0 {
			if found == nil || len(op.op) > len(found.op) {
				found = &binaryOps[i]
				newOffset = offset2
			}
		}
	}
	return found, newOffset
}

// TODO(bobg): boolean literals?
func scanLiteralExpr(buf []byte, offset int) (expression, int) {
	offset = skipWsAndComments(buf, offset)
	intliteral, newOffset := scanIntLiteral(buf, offset)
	if newOffset >= 0 {
		return intliteral, newOffset
	}
	strliteral, newOffset := scanStrLiteral(buf, offset)
	if newOffset >= 0 {
		return strliteral, newOffset
	}
	bytesliteral, newOffset := scanBytesLiteral(buf, offset) // 0x6c249a...
	if newOffset >= 0 {
		return bytesliteral, newOffset
	}
	booleanLiteral, newOffset := scanBoolLiteral(buf, offset) // true or false
	if newOffset >= 0 {
		return booleanLiteral, newOffset
	}
	return nil, -1
}

func scanIdentifier(buf []byte, offset int) (string, int) {
	offset = skipWsAndComments(buf, offset)
	i := offset
	for ; i < len(buf) && isIDChar(buf[i], i == offset); i++ {
	}
	if i == offset {
		return "", -1
	}
	return string(buf[offset:i]), i
}

func scanTok(buf []byte, offset int, s string) int {
	offset = skipWsAndComments(buf, offset)
	prefix := []byte(s)
	if bytes.HasPrefix(buf[offset:], prefix) {
		return offset + len(prefix)
	}
	return -1
}

func scanKeyword(buf []byte, offset int, keyword string) int {
	id, newOffset := scanIdentifier(buf, offset)
	if newOffset < 0 {
		return -1
	}
	if id != keyword {
		return -1
	}
	return newOffset
}

func scanIntLiteral(buf []byte, offset int) (integerLiteral, int) {
	offset = skipWsAndComments(buf, offset)
	start := offset
	if offset < len(buf) && buf[offset] == '-' {
		offset++
	}
	i := offset
	for ; i < len(buf) && unicode.IsDigit(rune(buf[i])); i++ {
		// the literal is BytesLiteral when it starts with 0x/0X
		if buf[i] == '0' && i < len(buf)-1 && (buf[i+1] == 'x' || buf[i+1] == 'X') {
			return 0, -1
		}
	}
	if i > offset {
		n, err := strconv.ParseInt(string(buf[start:i]), 10, 64)
		if err != nil {
			return 0, -1
		}
		return integerLiteral(n), i
	}
	return 0, -1
}

func scanStrLiteral(buf []byte, offset int) (bytesLiteral, int) {
	offset = skipWsAndComments(buf, offset)
	if offset >= len(buf) || buf[offset] != '\'' {
		return bytesLiteral{}, -1
	}
	var byteBuf bytesLiteral
	for i := offset + 1; i < len(buf); i++ {
		if buf[i] == '\'' {
			return byteBuf, i + 1
		}
		if buf[i] == '\\' && i < len(buf)-1 {
			if c, ok := scanEscape(buf[i+1]); ok {
				byteBuf = append(byteBuf, c)
				i++
				continue
			}
		}
		byteBuf = append(byteBuf, buf[i])
	}
	panic(parseErr(buf, offset, "unterminated string literal"))
}

func scanBytesLiteral(buf []byte, offset int) (bytesLiteral, int) {
	offset = skipWsAndComments(buf, offset)
	if offset+4 >= len(buf) {
		return nil, -1
	}
	if buf[offset] != '0' || (buf[offset+1] != 'x' && buf[offset+1] != 'X') {
		return nil, -1
	}
	if !isHexDigit(buf[offset+2]) || !isHexDigit(buf[offset+3]) {
		return nil, -1
	}
	i := offset + 4
	for ; i < len(buf); i += 2 {
		if i == len(buf)-1 {
			panic(parseErr(buf, offset, "odd number of digits in hex literal"))
		}
		if !isHexDigit(buf[i]) {
			break
		}
		if !isHexDigit(buf[i+1]) {
			panic(parseErr(buf, offset, "odd number of digits in hex literal"))
		}
	}
	decoded := make([]byte, hex.DecodedLen(i-(offset+2)))
	_, err := hex.Decode(decoded, buf[offset+2:i])
	if err != nil {
		return bytesLiteral{}, -1
	}
	return bytesLiteral(decoded), i
}

func scanBoolLiteral(buf []byte, offset int) (booleanLiteral, int) {
	offset = skipWsAndComments(buf, offset)
	if offset >= len(buf) {
		return false, -1
	}

	newOffset := scanKeyword(buf, offset, "true")
	if newOffset < 0 {
		if newOffset = scanKeyword(buf, offset, "false"); newOffset < 0 {
			return false, -1
		}
		return false, newOffset
	}
	return true, newOffset
}

func skipWsAndComments(buf []byte, offset int) int {
	var inComment bool
	for ; offset < len(buf); offset++ {
		c := buf[offset]
		if inComment {
			if c == '\n' {
				inComment = false
			}
		} else {
			if c == '/' && offset < len(buf)-1 && buf[offset+1] == '/' {
				inComment = true
				offset++ // skip two chars instead of one
			} else if !unicode.IsSpace(rune(c)) {
				break
			}
		}
	}
	return offset
}

func isHexDigit(b byte) bool {
	return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
}

func isIDChar(c byte, initial bool) bool {
	if c >= 'a' && c <= 'z' {
		return true
	}
	if c >= 'A' && c <= 'Z' {
		return true
	}
	if c == '_' {
		return true
	}
	if initial {
		return false
	}
	return unicode.IsDigit(rune(c))
}

type parserErr struct {
	buf    []byte
	offset int
	format string
	args   []interface{}
}

func parseErr(buf []byte, offset int, format string, args ...interface{}) error {
	return parserErr{buf: buf, offset: offset, format: format, args: args}
}

func (p parserErr) Error() string {
	// Lines start at 1, columns start at 0, like nature intended.
	line := 1
	col := 0
	for i := 0; i < p.offset; i++ {
		if p.buf[i] == '\n' {
			line++
			col = 0
		} else {
			col++
		}
	}
	args := []interface{}{line, col}
	args = append(args, p.args...)
	return fmt.Sprintf("line %d, col %d: "+p.format, args...)
}

func scanEscape(c byte) (byte, bool) {
	escapeFlag := true
	switch c {
	case '\'', '"', '\\':
	case 'b':
		c = '\b'
	case 'f':
		c = '\f'
	case 'n':
		c = '\n'
	case 'r':
		c = '\r'
	case 't':
		c = '\t'
	case 'v':
		c = '\v'
	default:
		escapeFlag = false
	}
	return c, escapeFlag
}
