#include "tokens.hpp"

#include <cctype>
#include <cstdio>
#include <iostream>

const char *tokenNames[_TOK_ANY + 1] = {
	"assignment operator",
	"start of block",
	"end of block",
	"start of list",
	"end of list",
	"number",
	"name",
	"string",
	"reference",
	"register",
	"start of parameters",
	"parameter seperator",
	"end of parameters",
	"plus sign",
	"minus sign",
	"increment operator",
	"decrement operator",
	"comparison operator",
	"less than operator",
	"greater than operator",
	"less/equal operator",
	"greater/equal operator",
	"any token"
};

TokenReader::TokenReader(const char *filepath)
	: filename(filepath), linenum(1)
{
	FILE *fp = fopen(filepath, "r");
	if (fp == nullptr) {
		std::cerr << "Unable to open file " << filepath << std::endl;
		buffer = nullptr;
		read = nullptr;
		end = nullptr;
		return;
	}

	fseek(fp, 0, SEEK_END);
	size_t length = ftell(fp);
	buffer = new char[length+1];
	fseek(fp, 0, SEEK_SET);
	length = fread(buffer, 1, length, fp);
	buffer[length] = '\0';

	read = buffer;
	end = read + length;

	fclose(fp);
}

TokenReader::~TokenReader()
{
	delete[] buffer;
}

token TokenReader::ReadToken(TokenType type)
{
	TokenType foundType = _TOK_ANY;
	FilePos start;
	std::string contents;

	while (foundType == _TOK_ANY) {
		start = GetFilePos();
		int c = ReadChar();
		if (c == EOF) {
			PrintErrorMsg(start, ERR_LVL_WARNING, "Unexpected end of file while looking for %s", tokenNames[type]);
			exit(1);
		}

		switch (c) {
		// Skip over space characters
		case ' ':
		case '\t':
		case '\n':
		case '\v':
		case '\f':
		case '\r':
			continue;
		// Skip over lines with comments
		case '#':
			SkipUntil("\n");
			continue;
		// Simple, single-char tokens
		case '{':
			foundType = TOK_BLOCK_BEGIN;
			continue;
		case '}':
			foundType = TOK_BLOCK_END;
			continue;
		case '[':
			foundType = TOK_LIST_BEGIN;
			continue;
		case ']':
			foundType = TOK_LIST_END;
			continue;
		case '(':
			foundType = TOK_PARAMS_BEGIN;
			continue;
		case ')':
			foundType = TOK_PARAMS_END;
			continue;
		case ',':
			foundType = TOK_PARAMS_SEPARATOR;
			continue;
		// Multi-char constant tokens
		case '=':
			if (PeekChar() == '=') {
				ReadChar();
				foundType = TOK_COMPARE;
			} else {
				foundType = TOK_ASSIGN;
			}
			continue;
		case '+':
			if (PeekChar() == '=') {
				ReadChar();
				foundType = TOK_PLUSEQU;
			} else {
				foundType = TOK_PLUS;
			}
			continue;
		case '-':
			if (PeekChar() == '=') {
				ReadChar();
				foundType = TOK_MINUSEQU;
				continue;
			} else if (!isdigit(PeekChar())) {
				foundType = TOK_MINUS;
				continue;
			}
			// Fall through to the end and let the chars be processed as a number
			break;
		case '<':
			if (PeekChar() == '=') {
				ReadChar();
				foundType = TOK_LESSEQU;
			} else {
				foundType = TOK_LESSTHAN;
			}
			continue;
		case '>':
			if (PeekChar() == '=') {
				ReadChar();
				foundType = TOK_GREATEREQU;
			} else {
				foundType = TOK_GREATERTHAN;
			}
			continue;
		// String types
		case '@':
			foundType = TOK_REFERENCE;
			while (isalnum(PeekChar()) || PeekChar() == '_')
				contents += (char)ReadChar();
			continue;
		case '$':
			foundType = TOK_REGISTER;
			while (isalnum(PeekChar()) || PeekChar() == '_')
				contents += (char)ReadChar();
			continue;
		case '"':
			foundType = TOK_STRING;
			while ((c = ReadChar()) != '"') {
				if (c == EOF) {
					PrintErrorMsg(start, ERR_LVL_ERROR, "String is missing end quote");
					exit(1);
				}
				contents += (char)c;
			}
			continue;
		}

		// Char didn't match any special character, it must be either a number or a string
		if (isdigit(c) || c == '-') {
			foundType = TOK_NUMBER;
			contents += (char)c;
			if (c == '0' && PeekChar() == 'x') {
				// Hexadecimal number
				contents += ReadChar();
				while (isdigit(PeekChar()) || (PeekChar() >= 'a' && PeekChar() <= 'f') || (PeekChar() >= 'A' && PeekChar() <= 'F'))
					contents += (char)ReadChar();
			} else {
				// Regular base 10 number
				while (isdigit(PeekChar()) || PeekChar() == '.')
					contents += (char)ReadChar();
			}
		} else {
			foundType = TOK_NAME;
			contents += (char)c;
			while (isalnum(PeekChar()) || PeekChar() == '_' || PeekChar() == '+' || PeekChar() == '-') // Allow + and - so that flag lists work
				contents += (char)ReadChar();
		}
	}

	if (type != _TOK_ANY && foundType != type) {
		PrintErrorMsg(start, ERR_LVL_ERROR, "Expected %s, but found %s",
			tokenNames[type], contents.empty() ? tokenNames[foundType] : contents.c_str());
		exit(1);
	}

	token result;
	result.type = foundType;
	result.contents = contents;
	result.filepos = start;
	return result;
}

token TokenReader::PeekToken()
{
	char *savedRead = read;
	int savedLinenum = linenum;

	token tok = ReadToken();

	read = savedRead;
	linenum = savedLinenum;

	return tok;
}

int TokenReader::ReadChar()
{
	if (read == end)
		return EOF;
	char c = *read;
	read++;
	if (PeekChar() == '\n')
		linenum++;
	return c;
}

int TokenReader::PeekChar(size_t ahead) const
{
	if ((read + ahead) >= end)
		return EOF;
	return *(read + ahead);
}

void TokenReader::SkipUntil(const char *chars)
{
	while (read != end) {
		for (const char *cc = chars; *cc != '\0'; cc++) {
			if (*read == *cc)
				return;
		}
		ReadChar();
	}
}

size_t TokenReader::FindNext(const char *chars) const
{
	int c;
	size_t offs = 0;
	do {
		c = PeekChar(offs);
		for (const char *cc = chars; *cc != '\0'; cc++) {
			if (c == *cc)
				return offs;
		}
		offs++;
	} while (c != EOF);
	return offs;
}

bool TokenReader::EndOfFile() const
{
	int c;
	size_t offs = 0;
	c = PeekChar(offs);
	while (c != EOF) {
		if (!isspace(c))
			return false;
		c = PeekChar(++offs);
		// Ignore lines with comments
		if (c == '#')
			while (c != '\n')
			{
				c = PeekChar(++offs);
				if (c == EOF)
					return true;
			}
	}
	return true;
}

