module tlang.compiler.core;

import gogga;
import std.conv : to;
import tlang.compiler.lexer.core;
import tlang.compiler.lexer.kinds.basic : BasicLexer;
import std.stdio : File;
import tlang.compiler.parsing.core;
import tlang.compiler.symbols.check;
import tlang.compiler.symbols.data;
import tlang.compiler.typecheck.core;
import tlang.compiler.typecheck.exceptions;
import core.stdc.stdlib;
import tlang.compiler.codegen.emit.core;
import tlang.compiler.codegen.emit.dgen;
import misc.exceptions : TError;
import tlang.compiler.codegen.mapper.core : SymbolMapper;
import tlang.compiler.codegen.mapper.hashmapper : HashMapper;
import tlang.compiler.codegen.mapper.lebanese : LebaneseMapper;
import std.string : cmp;
import tlang.compiler.configuration : CompilerConfiguration, ConfigEntry;

// TODO: Add configentry unittests

/** 
 * The sub-error type of `CompilerException`
 */
public enum CompilerError
{
    /** 
     * Occurs when tokens are requested but
     * the tokenization process has not yet
     * occurred
     */
    LEX_NOT_PERFORMED,

    /** 
     * Occurs if the tokenization process resulted
     * in zero tokens being produced
     */
    NO_TOKENS,

    /** 
     * Occurs if typechecking is performed
     * but no module was produced due to
     * parsing not yet being performed
     */
    PARSE_NOT_YET_PERFORMED,

    /** 
     * Occurs if emit was called but no
     * code queue wa sproduced due to
     * typechecking not being performed
     * yet
     */
    TYPECHECK_NOT_YET_PERFORMED,

    /** 
     * Occurs on a configuration error
     */
    CONFIG_ERROR,

    /** 
     * Occurs if a configuration key cannot
     * be found
     */
    CONFIG_KEY_NOT_FOUND,
    
    /** 
     * Occurs when the type of the configuration
     * key requested does not match its actual type
     */
    CONFIG_TYPE_ERROR,

    /** 
     * Occurs when a duplicate configuration key
     * entry is attempted to be created
     */
    CONFIG_DUPLICATE_ENTRY
}

public final class CompilerException : TError
{
    private CompilerError errType;

    this(CompilerError errType, string msg = "")
    {
        super("CompilerError("~to!(string)(errType)~")"~(msg.length ? ": "~msg : ""));
        this.errType = errType;
    }
}

public class Compiler
{
    /* The input source code */
    private string inputSource;

    /* The lexer */
    private LexerInterface lexer;

    /* The lexed tokens */
    private Token[] tokens;

    /* The parser */
    private Parser parser;
    private Module modulle;

    /* The typechecker/code generator */
    private TypeChecker typeChecker;

    /* The chosen code emitter to use */
    private CodeEmitter emitter;
    private File emitOutFile;

    /* The configuration */
    private CompilerConfiguration config;


    public CompilerConfiguration getConfig()
    {
        return config;
    }

    /** 
     * Create a new compiler instance to compile the given
     * source code
     * Params:
     *   sourceCode = the source code to compile
     */
    this(string sourceCode, File emitOutFile)
    {
        this.inputSource = sourceCode;
        this.emitOutFile = emitOutFile;

        /* Get the default config */
        this.config = CompilerConfiguration.defaultConfig();
    }

    /* Setup the lexer and begin lexing */
    public void doLex()
    {
        /* Setup the lexer and begin lexing */
        this.lexer = new BasicLexer(inputSource);
        (cast(BasicLexer)(this.lexer)).performLex();

        this.tokens = this.lexer.getTokens();
    }

    public Token[] getTokens()
    {
        if(this.lexer is null)
        {
            throw new CompilerException(CompilerError.LEX_NOT_PERFORMED);
        }

        return tokens;
    }

    /* Spawn a new parser with the provided tokens */
    public void doParse()
    {
        Token[] lexedTokens = getTokens();

        if(lexedTokens.length == 0)
        {
            throw new CompilerException(CompilerError.NO_TOKENS);
        }
        else
        {
            /* Spawn a new parser with the provided tokens */
            this.parser = new Parser(lexer);

            modulle = parser.parse();
        }
    }

    public Module getModule()
    {
        return modulle;
    }

    /** 
     * Spawn a new typechecker/codegenerator on the module
     * and perform type checking and code generation
     */
    public void doTypeCheck()
    {
        if(this.parser is null)
        {
            throw new CompilerException(CompilerError.PARSE_NOT_YET_PERFORMED);
        }

        this.typeChecker = new TypeChecker(modulle, config);

        /* Perform typechecking/codegen */
        this.typeChecker.beginCheck();
    }

    /* Perform code emitting */
    public void doEmit()
    {
        if(typeChecker is null)
        {
            throw new CompilerException(CompilerError.TYPECHECK_NOT_YET_PERFORMED);
        }

        if(!config.hasConfig("emit:mapper"))
        {
            throw new CompilerException(CompilerError.CONFIG_ERROR, "Missing a symbol mapper");
        }
        
        SymbolMapper mapper;
        string mapperType = config.getConfig("emit:mapper").getText();

        if(cmp(mapperType, "hashmapper") == 0)
        {
            mapper = new HashMapper(typeChecker);
        }
        else if(cmp(mapperType, "lebanese") == 0)
        {
            mapper = new LebaneseMapper(typeChecker);
        }
        else
        {
            throw new CompilerException(CompilerError.CONFIG_ERROR, "Invalid mapper type '"~mapperType~"'");
        }

        this.emitter = new DCodeEmitter(typeChecker, emitOutFile, config, mapper);
        emitter.emit(); // Emit the code
        emitOutFile.close(); // Flush (perform the write() syscall)
        emitter.finalize(); // Call CC on the file containing generated C code
    }

    public void compile()
    {
        /* Setup the lexer, perform the tokenization and obtain the tokens */
        doLex();

        /* Setup the parser with the provided tokens and perform parsing */
        doParse();

        /* Spawn a new typechecker/codegenerator on the module and perform type checking */
        doTypeCheck();

        /* Perform code emitting */
        doEmit();
    }
}

/** 
 * Performs compilation of the provided module(s)
 *
 * Params:
 *   sourceFiles = The module(s) to perform compilation with
 */
void beginCompilation(string[] sourceFiles)
{
    /* TODO: Begin compilation process, take in data here */
    gprintln("Compiling files "~to!(string)(sourceFiles)~" ...");

    foreach(string sourceFile; sourceFiles)
    {
        /* Read in the source code */
        // TODO: THis below code is used so many times, for heavens-sake please make a helper function for it
        gprintln("Reading source file '"~sourceFile~"' ...");
        File sourceFileFile;
        sourceFileFile.open(sourceFile); /* TODO: Error handling with ANY file I/O */
        ulong fileSize = sourceFileFile.size();
        byte[] fileBytes;
        fileBytes.length = fileSize;
        fileBytes = sourceFileFile.rawRead(fileBytes);
        sourceFileFile.close();

        /* The file to output to */
        File outFile;
        outFile.open("tlangout.c", "w");

        /* Create a new compiler */
        Compiler compiler = new Compiler(cast(string)fileBytes, outFile);
    
        /* Perform the compilation */
        compiler.compile();
    }
}

unittest
{
    // TODO: Add tests here for our `simple_<x>.t` tests or put them in DGen, I think here is better
    // FIXME: Crashes and I think because too fast or actually bad state? Maybe something is not being
    // cleared, I believe this may be what is happening
    // ... see issue #88
    // ... UPDATE: It seems to be any unit test..... mhhhh.
    // string[] testFiles = ["source/tlang/testing/simple_while.t"
    //                     ];

    //                     // "source/tlang/testing/simple_functions.t",
    //                     // "source/tlang/testing/simple_while.t",
    //                     // "source/tlang/testing/simple_for_loops.t",
    //                     // "source/tlang/testing/simple_cast.t",
    //                     // "source/tlang/testing/simple_conditionals.t",
    //                     // "source/tlang/testing/nested_conditionals.t",
    //                     // "source/tlang/testing/simple_discard.t"
    // foreach(string testFile; testFiles)
    // {
    //     beginCompilation([testFile]);
    // }
}

