/* Copyright (c) 2017 Jeffrey Massung
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source distribution.
 */

package chip8

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

// Type for scanned tokens.
type tokenType uint

// Lexical assembly tokens.
const (
	TOKEN_END tokenType = iota
	TOKEN_CHAR
	TOKEN_LABEL
	TOKEN_ID
	TOKEN_INSTRUCTION
	TOKEN_OPERAND
	TOKEN_V
	TOKEN_R
	TOKEN_I
	TOKEN_EFFECTIVE_ADDRESS
	TOKEN_F
	TOKEN_HF
	TOKEN_K
	TOKEN_DT
	TOKEN_ST
	TOKEN_LIT
	TOKEN_TEXT
	TOKEN_BREAK
	TOKEN_ASSERT
	TOKEN_EQU
	TOKEN_VAR
	TOKEN_HERE
	TOKEN_SUPER
	TOKEN_EXTENDED
	TOKEN_ASCII
)

// A parsed, lexical token.
type token struct {
	typ tokenType

	// tokens can have an optional value associated with them
	val interface{}
}

// CHIP-8 assembler token scanner.
type tokenScanner struct {
	bytes []byte

	// scan position
	pos int
}

// Helper function.
func (t token) debug() {
	fmt.Fprintf(os.Stderr, "%#v\n", t)
}

// Reads the next token from a scanner. Returns the token.
func (s *tokenScanner) scanToken() token {
	for len(s.bytes) > s.pos && s.bytes[s.pos] < 33 {
		s.pos++
	}

	// if at the end, return a comment token
	if len(s.bytes) <= s.pos {
		return token{typ: TOKEN_END, val: ""}
	}

	// get the next character
	c := s.bytes[s.pos]

	// get the next character
	switch {
	case c == ';':
		return s.scanToEnd()
	case c == '[':
		return s.scanEffectiveAddress()
	case c == ',':
		return s.scanOperand()
	case c == '#':
		return s.scanHexLit()
	case c == '%':
		return s.scanBinLit()
	case c == '-':
		return s.scanDecLit()
	case c >= '0' && c <= '9':
		return s.scanDecLit()
	case c >= 'A' && c <= 'Z':
		return s.scanIdentifier()
	case c == '"' || c == '\'' || c == '`':
		return s.scanString(c)
	}

	return s.scanChar()
}

// Scan a single character.
func (s *tokenScanner) scanChar() token {
	i := s.pos

	// advance the scan pos
	s.pos += 1

	// some characters are special tokens
	switch s.bytes[i] {
	case '*':
		return token{typ: TOKEN_HERE}
	}

	return token{typ: TOKEN_CHAR, val: s.bytes[i]}
}

// Scan to the end of the input and return.
func (s *tokenScanner) scanToEnd() token {
	text := string(s.bytes[s.pos:])

	// skip to the end
	s.pos = len(s.bytes)

	// a hard-coded token
	return token{typ: TOKEN_END, val: strings.TrimSpace(text)}
}

// Scan a comma-separated operand token.
func (s *tokenScanner) scanOperand() token {
	s.pos += 1

	// scan the next token as the operand
	t := s.scanToken()

	// make sure there was an operand
	if t.typ == TOKEN_END {
		panic("expected operand")
	}

	return token{typ: TOKEN_OPERAND, val: t}
}

// Scan a list of comma-separated tokens.
func (s *tokenScanner) scanOperands() []token {
	tokens := make([]token, 0, 3)

	// is this the end of the operand list?
	for t := s.scanToken(); t.typ != TOKEN_END; {
		tokens = append(tokens, t)

		// get another token, are we at the end?
		if t = s.scanToken(); t.typ != TOKEN_OPERAND {
			if t.typ == TOKEN_END {
				break
			}

			panic("unexpected token")
		}

		// expand the operand
		t = t.val.(token)
	}

	return tokens
}

// Scan the effective address of a register (I).
func (s *tokenScanner) scanEffectiveAddress() token {
	s.pos += 1

	// scan the next token to take the effective address
	if t := s.scanToken(); t.typ != TOKEN_I {
		panic("illegal indirection")
	}

	// terminate with closing bracket
	if t := s.scanToken(); t.typ != TOKEN_CHAR || t.val.(byte) != ']' {
		panic("illegal effective address")
	}

	return token{typ: TOKEN_EFFECTIVE_ADDRESS}
}

// Scan an identifier: instruction, register, or label reference.
func (s *tokenScanner) scanIdentifier() token {
	i := s.pos

	// advance to the first non-identifier character
	for ; s.pos < len(s.bytes); s.pos++ {
		c := s.bytes[s.pos]

		// validate identifier characters
		if (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_' {
			break
		}
	}

	// extract the label
	id := string(s.bytes[i:s.pos])

	// determine whether the label is an instruction, register or reference
	switch id {
	case "V0":
		return token{typ: TOKEN_V, val: 0}
	case "V1":
		return token{typ: TOKEN_V, val: 1}
	case "V2":
		return token{typ: TOKEN_V, val: 2}
	case "V3":
		return token{typ: TOKEN_V, val: 3}
	case "V4":
		return token{typ: TOKEN_V, val: 4}
	case "V5":
		return token{typ: TOKEN_V, val: 5}
	case "V6":
		return token{typ: TOKEN_V, val: 6}
	case "V7":
		return token{typ: TOKEN_V, val: 7}
	case "V8":
		return token{typ: TOKEN_V, val: 8}
	case "V9":
		return token{typ: TOKEN_V, val: 9}
	case "VA":
		return token{typ: TOKEN_V, val: 10}
	case "VB":
		return token{typ: TOKEN_V, val: 11}
	case "VC":
		return token{typ: TOKEN_V, val: 12}
	case "VD":
		return token{typ: TOKEN_V, val: 13}
	case "VE":
		return token{typ: TOKEN_V, val: 14}
	case "VF":
		return token{typ: TOKEN_V, val: 15}
	case "R":
		return token{typ: TOKEN_R}
	case "I":
		return token{typ: TOKEN_I}
	case "F":
		return token{typ: TOKEN_F}
	case "HF":
		return token{typ: TOKEN_HF}
	case "K":
		return token{typ: TOKEN_K}
	case "A":
		return token{typ: TOKEN_ASCII}
	case "D", "DT":
		return token{typ: TOKEN_DT}
	case "S", "ST":
		return token{typ: TOKEN_ST}
	case "CLS", "RET", "EXIT", "LOW", "HIGH", "SCU", "SCD", "SCR", "SCL", "SYS", "JP", "CALL", "SE", "SNE", "SGT", "SLT", "SKP", "SKNP", "LD", "OR", "AND", "XOR", "ADD", "SUB", "SUBN", "MUL", "DIV", "SHR", "SHL", "BCD", "RND", "DRW":
		return token{typ: TOKEN_INSTRUCTION, val: id}
	case "ASCII", "BYTE", "WORD", "ALIGN", "PAD":
		return token{typ: TOKEN_INSTRUCTION, val: id}
	case "BREAK":
		return token{typ: TOKEN_BREAK}
	case "ASSERT":
		return token{typ: TOKEN_ASSERT}
	case "EQU":
		return token{typ: TOKEN_EQU}
	case "VAR":
		return token{typ: TOKEN_VAR}
	case "SUPER":
		return token{typ: TOKEN_SUPER}
	case "EXTENDED":
		return token{typ: TOKEN_EXTENDED}
	}

	if i == 0 {
		return token{typ: TOKEN_LABEL, val: id}
	} else {
		return token{typ: TOKEN_ID, val: id}
	}
}

// Scan a decimal literal.
func (s *tokenScanner) scanDecLit() token {
	i := s.pos

	// skip a unary minus negation
	if s.bytes[i] == '-' {
		s.pos += 1
	}

	// find the first non-numeric character
	for ; s.pos < len(s.bytes); s.pos += 1 {
		if strings.IndexByte("0123456789", s.bytes[s.pos]) < 0 {
			break
		}
	}

	// convert the hex value to a signed number
	if n, err := strconv.ParseInt(string(s.bytes[i:s.pos]), 10, 32); err == nil {
		return token{typ: TOKEN_LIT, val: int(n)}
	}

	panic(fmt.Errorf("illegal decimal value: %s", string(s.bytes[i:s.pos])))
}

// Scan a hexadecimal literal.
func (s *tokenScanner) scanHexLit() token {
	i := s.pos

	// find the first non-hex character
	for s.pos += 1; s.pos < len(s.bytes); s.pos += 1 {
		if strings.IndexByte("0123456789ABCDEF", s.bytes[s.pos]) < 0 {
			break
		}
	}

	// convert the hex value to an unsigned number
	if n, err := strconv.ParseInt(string(s.bytes[i+1:s.pos]), 16, 32); err == nil {
		return token{typ: TOKEN_LIT, val: int(n)}
	}

	panic(fmt.Errorf("illegal hex value: #%s", string(s.bytes[i:s.pos])))
}

// Scan a binary literal.
func (s *tokenScanner) scanBinLit() token {
	i := s.pos

	// find the first non-binary character
	for s.pos += 1; s.pos < len(s.bytes); s.pos += 1 {
		if strings.IndexByte(".01", s.bytes[s.pos]) < 0 {
			break
		}
	}

	// replace all '.' with '0'
	v := strings.Replace(string(s.bytes[i+1:s.pos]), ".", "0", -1)

	// convert the hex value to an unsigned number
	if n, err := strconv.ParseInt(v, 2, 32); err == nil {
		return token{typ: TOKEN_LIT, val: int(n)}
	}

	panic(fmt.Errorf("illegal binary value: $%s", string(s.bytes[i:s.pos])))
}

// Scan a quoted string.
func (s *tokenScanner) scanString(term byte) token {
	s.pos += 1

	// store starting position
	i := s.pos

	// find the terminating quotation
	for s.pos < len(s.bytes) && s.bytes[s.pos] != term {
		s.pos++
	}

	// advance past the terminator
	s.pos += 1

	return token{typ: TOKEN_TEXT, val: string(s.bytes[i : s.pos-1])}
}
