/* -*- c-file-style: "linux";indent-tabs-mode:t -*- */
/* Copyright (C) 2016 Jeremiah Orians
 * Copyright (C) 2017 Jan Nieuwenhuizen <janneke@gnu.org>
 * This file is part of mescc-tools.
 *
 * mescc-tools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * mescc-tools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with mescc-tools.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>

//CONSTANT max_string 4096
#define max_string 4096
//CONSTANT MACRO 1
#define MACRO 1
//CONSTANT STR 2
#define STR 2
//CONSTANT NEWLINE 3
#define NEWLINE 3

//CONSTANT TRUE 1
#define TRUE 1
//CONSTANT FALSE 0
#define FALSE 0

// CONSTANT KNIGHT 0
#define KNIGHT 0
// CONSTANT X86 1
#define X86 1
// CONSTANT AMD64 2
#define AMD64 2
// CONSTANT ARMV7L 40
#define ARMV7L 40
// CONSTANT AARM64 80
#define AARM64 80


/* Imported functions */
char* numerate_number(int a);
int hex2char(int c);
int in_set(int c, char* s);
int match(char* a, char* b);
int numerate_string(char *a);
int string_length(char* a);
void file_print(char* s, FILE* f);
void require(int bool, char* error);


/* Globals */
FILE* source_file;
FILE* destination_file;
int BigEndian;
int BigBitEndian;
int ByteMode;
int Architecture;
int linenumber;

void line_error(char* filename, int linenumber)
{
	file_print(filename, stderr);
	file_print(":", stderr);
	file_print(numerate_number(linenumber), stderr);
	file_print(" :", stderr);
}

struct Token
{
	struct Token* next;
	int type;
	char* Text;
	char* Expression;
	char* filename;
	int linenumber;
};

struct Token* newToken(char* filename, int linenumber)
{
	struct Token* p;

	p = calloc (1, sizeof (struct Token));
	require(NULL != p, "Exhusted available memory\n");

	p->filename = filename;
	p->linenumber = linenumber;

	return p;
}

struct Token* reverse_list(struct Token* head)
{
	struct Token* root = NULL;
	while(NULL != head)
	{
		struct Token* next = head->next;
		head->next = root;
		root = head;
		head = next;
	}
	return root;
}

void purge_lineComment()
{
	int c = fgetc(source_file);
	while(!in_set(c, "\n\r"))
	{
		if(EOF == c) break;
		c = fgetc(source_file);
	}
}

struct Token* append_newline(struct Token* head, char* filename)
{
	linenumber = linenumber + 1;
	if(NULL == head) return NULL;
	if(NEWLINE == head->type)
	{/* Don't waste whitespace*/
		return head;
	}

	struct Token* lf = newToken(filename, linenumber);
	lf->type = NEWLINE;
	lf->next = head;
	lf->Text = "\n";
	lf->Expression = lf->Text;
	return lf;
}


struct Token* store_atom(struct Token* head, char c, char* filename)
{
	char* store = calloc(max_string + 1, sizeof(char));
	require(NULL != store, "Exhusted available memory\n");
	int ch = c;
	int i = 0;
	do
	{
		store[i] = ch;
		ch = fgetc(source_file);
		i = i + 1;
		require(i < max_string, "storing atom of size larger than max_string\n");
		if(EOF == ch) break;
	} while (!in_set(ch, "\t\n "));

	head->Text = store;
	if('\n' == ch)
	{
		return append_newline(head, filename);
	}
	return head;
}

char* store_string(char c, char* filename)
{
	char* store = calloc(max_string + 1, sizeof(char));
	require(NULL != store, "Exhusted available memory\n");

	int ch = c;
	int i = 0;
	do
	{
		store[i] = ch;
		i = i + 1;
		if('\n' == ch) linenumber = linenumber + 1;
		ch = fgetc(source_file);
		require(EOF != ch, "Unmatched \"!\n");

		if(max_string == i)
		{
			line_error(filename, linenumber);
			file_print("String: ", stderr);
			file_print(store, stderr);
			file_print(" exceeds max string size\n", stderr);
			exit(EXIT_FAILURE);
		}
	} while(ch != c);

	return store;
}

struct Token* Tokenize_Line(struct Token* head, char* filename)
{
	int c;
	struct Token* p;
	linenumber = 1;

	do
	{
restart:
		c = fgetc(source_file);

		if(in_set(c, ";#"))
		{
			purge_lineComment();
			head = append_newline(head, filename);
			goto restart;
		}

		if(in_set(c, "\t "))
		{
			goto restart;
		}

		if('\n' == c)
		{
			head = append_newline(head, filename);
			goto restart;
		}

		if(EOF == c)
		{
			head = append_newline(head, filename);
			goto done;
		}

		p = newToken(filename, linenumber);
		p->next = head;
		if(in_set(c, "'\""))
		{
			p->Text = store_string(c, filename);
			p->type = STR;
		}
		else
		{
			p = store_atom(p, c, filename);
		}

		head = p;
	} while(TRUE);
done:
	return head;
}

void setExpression(struct Token* p, char *c, char *Exp)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		/* Leave macros alone */
		if(MACRO == i->type)
		{
			if(match(i->Text, c))
			{
				line_error(i->filename, i->linenumber);
				file_print("Multiple definitions for macro ", stderr);
				file_print(c, stderr);
				file_print("\n", stderr);
				exit(EXIT_FAILURE);
			}
			continue;
		}
		else if(match(i->Text, c))
		{ /* Only if there is an exact match replace */
			i->Expression = Exp;
		}
	}
}

void identify_macros(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if(match(i->Text, "DEFINE"))
		{
			i->type = MACRO;
			require(NULL != i->next, "Macro name must exist\n");
			i->Text = i->next->Text;
			require(NULL != i->next->next, "Macro value must exist\n");
			if(STR == i->next->next->type)
			{
				i->Expression = i->next->next->Text + 1;
			}
			else
			{
				i->Expression = i->next->next->Text;
			}
			i->next = i->next->next->next;
		}
	}
}

void line_macro(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if(MACRO == i->type)
		{
			setExpression(i->next, i->Text, i->Expression);
		}
	}
}

void hexify_string(struct Token* p)
{
	char* table = "0123456789ABCDEF";
	int i = string_length(p->Text);

	char* d = calloc(((((i >> 2) + 1) << 3) + 1), sizeof(char));
	require(NULL != d, "Exhusted available memory\n");
	p->Expression = d;
	char* S = p->Text;

	if(KNIGHT == Architecture)
	{
		i = ((((i - 1) >> 2) + 1) << 3);
		while( 0 < i)
		{
			i = i - 1;
			d[i] = '0';
		}
	}

	while( 0 != S[0])
	{
		S = S + 1;
		d[0] = table[S[0] >> 4];
		d[1] = table[S[0] & 0xF];
		d = d + 2;
	}
}

void process_string(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if(STR == i->type)
		{
			if('\'' == i->Text[0])
			{
				i->Expression = i->Text + 1;
			}
			else if('"' == i->Text[0])
			{
				hexify_string(i);
			}
		}
	}
}

char* pad_nulls(int size, char* nil)
{
	if(0 == size) return nil;
	require(size > 0, "negative null padding not possible\n");
	size = size * 2;

	char* s = calloc(size + 1, sizeof(char));
	require(NULL != s, "Exhusted available memory\n");

	int i = 0;
	while(i < size)
	{
		s[i] = '0';
		i = i + 1;
	}

	return s;
}

void preserve_other(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if((NULL == i->Expression) && !(i->type & MACRO))
		{
			char c = i->Text[0];

			if(in_set(c, "!@$~%&:^"))
			{
				i->Expression = i->Text;
			}
			else if('<' == c)
			{
				i->Expression = pad_nulls(numerate_string(i->Text + 1), i->Text);
			}
			else
			{
				line_error(i->filename, i->linenumber);
				file_print("Received invalid other; ", stderr);
				file_print(i->Text, stderr);
				file_print("\n", stderr);
				exit(EXIT_FAILURE);
			}
		}
	}
}

void bound_values(int displacement, int number_of_bytes, int low, int high)
{
	if((high < displacement) || (displacement < low))
	{
		file_print("A displacement of ", stderr);
		file_print(numerate_number(displacement), stderr);
		file_print(" does not fit in ", stderr);
		file_print(numerate_number(number_of_bytes), stderr);
		file_print(" bytes\n", stderr);
		exit(EXIT_FAILURE);
	}
}

void range_check(int displacement, int number_of_bytes)
{
	if(4 == number_of_bytes) return;
	else if(3 == number_of_bytes)
	{
		bound_values(displacement, number_of_bytes, -8388608, 16777216);
		return;
	}
	else if(2 == number_of_bytes)
	{
		bound_values(displacement, number_of_bytes, -32768, 65535);
		return;
	}
	else if(1 == number_of_bytes)
	{
		bound_values(displacement, number_of_bytes, -128, 255);
		return;
	}

	file_print("Received an invalid number of bytes in range_check\n", stderr);
	exit(EXIT_FAILURE);
}

void reverseBitOrder(char* c)
{
	if(NULL == c) return;
	if(0 == c[1]) return;
	int hold = c[0];

	if(16 == ByteMode)
	{
		c[0] = c[1];
		c[1] = hold;
		reverseBitOrder(c+2);
	}
	else if(8 == ByteMode)
	{
		c[0] = c[2];
		c[2] = hold;
		reverseBitOrder(c+3);
	}
	else if(2 == ByteMode)
	{
		c[0] = c[7];
		c[7] = hold;
		hold = c[1];
		c[1] = c[6];
		c[6] = hold;
		hold = c[2];
		c[2] = c[5];
		c[5] = hold;
		hold = c[3];
		c[3] = c[4];
		c[4] = hold;
		reverseBitOrder(c+8);
	}
}

void LittleEndian(char* start)
{
	char* end = start;
	char* c = start;
	while(0 != end[0]) end = end + 1;
	int hold;
	for(end = end - 1; start < end; start = start + 1)
	{
		hold = start[0];
		start[0] = end[0];
		end[0] = hold;
		end = end - 1;
	}

	if(BigBitEndian) reverseBitOrder(c);
}

int stringify(char* s, int digits, int divisor, int value, int shift)
{
	int i = value;
	if(digits > 1)
	{
		i = stringify(s+1, (digits - 1), divisor, value, shift);
	}
	s[0] = hex2char(i & (divisor - 1));
	return (i >> shift);
}

char* express_number(int value, char c)
{
	char* ch = calloc(42, sizeof(char));
	require(NULL != ch, "Exhusted available memory\n");
	int size;
	int number_of_bytes;
	int shift;
	if('!' == c)
	{
		number_of_bytes = 1;
		value = value & 0xFF;
	}
	else if('@' == c)
	{
		number_of_bytes = 2;
		value = value & 0xFFFF;
	}
	else if('~' == c)
	{
		number_of_bytes = 3;
		value = value & 0xFFFFFF;
	}
	else if('%' == c)
	{
		number_of_bytes = 4;
		value = value & 0xFFFFFFFF;
	}
	else
	{
		file_print("Given symbol ", stderr);
		fputc(c, stderr);
		file_print(" to express immediate value ", stderr);
		file_print(numerate_number(value), stderr);
		fputc('\n', stderr);
		exit(EXIT_FAILURE);
	}

	range_check(value, number_of_bytes);

	if(16 == ByteMode)
	{
		size = number_of_bytes * 2;
		shift = 4;
	}
	else if(8 == ByteMode)
	{
		size = number_of_bytes * 3;
		shift = 3;
	}
	else if(2 == ByteMode)
	{
		size = number_of_bytes * 8;
		shift = 1;
	}
	else
	{
		file_print("Got invalid ByteMode in express_number\n", stderr);
		exit(EXIT_FAILURE);
	}

	stringify(ch, size, ByteMode, value, shift);

	if(!BigEndian) LittleEndian(ch);
	else if(!BigBitEndian) reverseBitOrder(ch);
	return ch;
}

void eval_immediates(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if(MACRO == i->type) continue;
		else if(NEWLINE == i->type) continue;
		else if('<' == i->Text[0]) continue;
		else if(NULL == i->Expression)
		{
			int value;
			if((X86 == Architecture) || (AMD64 == Architecture) || (ARMV7L == Architecture) || (AARM64 == Architecture))
			{
				value = numerate_string(i->Text + 1);
				if(('0' == i->Text[1]) || (0 != value))
				{
					i->Expression = express_number(value, i->Text[0]);
				}
			}
			else if(KNIGHT == Architecture)
			{
				value = numerate_string(i->Text);
				if(('0' == i->Text[0]) || (0 != value))
				{
					i->Expression = express_number(value, '@');
				}
			}
			else
			{
				file_print("Unknown architecture received in eval_immediates\n", stderr);
				exit(EXIT_FAILURE);
			}
		}
	}
}

void print_hex(struct Token* p)
{
	struct Token* i;
	for(i = p; NULL != i; i = i->next)
	{
		if(NEWLINE == i->type)
		{
			if(NULL == i->next) fputc('\n', destination_file);
			else if((NEWLINE != i->next->type) && (MACRO != i->next->type)) fputc('\n', destination_file);
		}
		else if(i->type != MACRO)
		{
			file_print(i->Expression, destination_file);
			if(NEWLINE != i->next->type) fputc(' ', destination_file);
		}
	}
}

/* Standard C main program */
int main(int argc, char **argv)
{
	BigEndian = TRUE;
	struct Token* head = NULL;
	Architecture = KNIGHT;
	destination_file = stdout;
	BigBitEndian = TRUE;
	ByteMode = 16;
	char* filename;
	char* arch;

	int option_index = 1;
	while(option_index <= argc)
	{
		if(NULL == argv[option_index])
		{
			option_index = option_index + 1;
		}
		else if(match(argv[option_index], "--BigEndian"))
		{
			BigEndian = TRUE;
			option_index = option_index + 1;
		}
		else if(match(argv[option_index], "--LittleEndian"))
		{
			BigEndian = FALSE;
			option_index = option_index + 1;
		}
		else if(match(argv[option_index], "-A") || match(argv[option_index], "--architecture"))
		{
			arch = argv[option_index + 1];
			if(match("knight-native", arch) || match("knight-posix", arch)) Architecture = KNIGHT;
			else if(match("x86", arch)) Architecture = X86;
			else if(match("amd64", arch)) Architecture = AMD64;
			else if(match("armv7l", arch)) Architecture = ARMV7L;
			else if(match("aarch64", arch)) Architecture = AARM64;
			else
			{
				file_print("Unknown architecture: ", stderr);
				file_print(arch, stderr);
				file_print(" know values are: knight-native, knight-posix, x86, amd64, armv7l and aarch64", stderr);
				exit(EXIT_FAILURE);
			}
			option_index = option_index + 2;
		}
		else if(match(argv[option_index], "-b") || match(argv[option_index], "--binary"))
		{
			ByteMode = 2;
			option_index = option_index + 1;
		}
		else if(match(argv[option_index], "-h") || match(argv[option_index], "--help"))
		{
			file_print("Usage: ", stderr);
			file_print(argv[0], stderr);
			file_print(" -f FILENAME1 {-f FILENAME2} (--BigEndian|--LittleEndian) ", stderr);
			file_print("[--architecture name]\nArchitectures: knight-native, knight-posix, x86, amd64 and armv7\n", stderr);
			file_print("To leverage octal or binary output: --octal, --binary\n", stderr);
			exit(EXIT_SUCCESS);
		}
		else if(match(argv[option_index], "-f") || match(argv[option_index], "--file"))
		{
			filename = argv[option_index + 1];
			source_file = fopen(filename, "r");

			if(NULL == source_file)
			{
				file_print("The file: ", stderr);
				file_print(argv[option_index + 1], stderr);
				file_print(" can not be opened!\n", stderr);
				exit(EXIT_FAILURE);
			}

			head = Tokenize_Line(head, filename);
			option_index = option_index + 2;
		}
		else if(match(argv[option_index], "-o") || match(argv[option_index], "--output"))
		{
			destination_file = fopen(argv[option_index + 1], "w");

			if(NULL == destination_file)
			{
				file_print("The file: ", stderr);
				file_print(argv[option_index + 1], stderr);
				file_print(" can not be opened!\n", stderr);
				exit(EXIT_FAILURE);
			}
			option_index = option_index + 2;
		}
		else if(match(argv[option_index], "-O") || match(argv[option_index], "--octal"))
		{
			ByteMode = 8;
			option_index = option_index + 1;
		}
		else if(match(argv[option_index], "-V") || match(argv[option_index], "--version"))
		{
			file_print("M1 0.7.0\n", stdout);
			exit(EXIT_SUCCESS);
		}
		else
		{
			file_print("Unknown option\n", stderr);
			exit(EXIT_FAILURE);
		}
	}

	if(NULL == head)
	{
		file_print("Either no input files were given or they were empty\n", stderr);
		exit(EXIT_FAILURE);
	}

	head = reverse_list(head);
	identify_macros(head);
	line_macro(head);
	process_string(head);
	eval_immediates(head);
	preserve_other(head);
	print_hex(head);

	return EXIT_SUCCESS;
}
