#include <ctype.h>
#include <unistd.h>

#include "config.h"
#include "pac_common.h"
#include "pac_decl.h"
#include "pac_exception.h"
#include "pac_exttype.h"
#include "pac_id.h"
#include "pac_output.h"
#include "pac_parse.h"
#include "pac_type.h"
#include "pac_utils.h"

extern int yydebug;
extern int yyparse();
extern void switch_to_file(FILE* fp_input);
string input_filename;

bool FLAGS_pac_debug = false;
bool FLAGS_quiet = false;
string FLAGS_output_directory;
vector<string> FLAGS_include_directories;

Output* header_output = nullptr;
Output* source_output = nullptr;

void add_to_include_directories(string dirs) {
    unsigned int dir_begin = 0, dir_end;
    while ( dir_begin < dirs.length() ) {
        for ( dir_end = dir_begin; dir_end < dirs.length(); ++dir_end )
            if ( dirs[dir_end] == ':' )
                break;

        string dir = dirs.substr(dir_begin, dir_end - dir_begin);

        // Add a trailing '/' if necessary
        if ( dir.length() > 0 && *(dir.end() - 1) != '/' )
            dir += '/';

        FLAGS_include_directories.push_back(std::move(dir));
        dir_begin = dir_end + 1;
    }
}

void pac_init() {
    init_builtin_identifiers();
    Type::init();
}

void insert_comments(Output* out, const char* source_filename) {
    out->println("// This file is automatically generated from %s.\n", source_filename);
}

void insert_basictype_defs(Output* out) {
    out->println("#ifndef pac_type_defs");
    out->println("#define pac_type_defs");
    out->println("");
    out->println("typedef char int8;");
    out->println("typedef short int16;");
    out->println("typedef long int32;");
    out->println("typedef long long int64;");

    out->println("typedef unsigned char uint8;");
    out->println("typedef unsigned short uint16;");
    out->println("typedef unsigned long uint32;");
    out->println("typedef unsigned long long uint64;");

    out->println("");
    out->println("#endif /* pac_type_defs */");
    out->println("");
}

void insert_byteorder_macros(Output* out) {
    out->println("#define FixByteOrder16(x)	(byteorder == HOST_BYTEORDER ? (x) : pac_swap16(x))");
    out->println("#define FixByteOrder32(x)	(byteorder == HOST_BYTEORDER ? (x) : pac_swap32(x))");
    out->println("#define FixByteOrder64(x)	(byteorder == HOST_BYTEORDER ? (x) : pac_swap64(x))");
    out->println("");
}

const char* to_id(const char* s) {
    static char t[1024];
    int i;
    for ( i = 0; s[i] && i < (int)sizeof(t) - 1; ++i )
        t[i] = isalnum(s[i]) ? s[i] : '_';
    if ( isdigit(t[0]) )
        t[0] = '_';
    t[i] = '\0';
    return t;
}

int compile(const char* filename) {
    FILE* fp_input = fopen(filename, "r");
    if ( ! fp_input ) {
        string tmp = strfmt("Error in opening %s", filename);
        perror(tmp.c_str());
        return -1;
    }
    input_filename = filename;

    string basename;

    if ( ! FLAGS_output_directory.empty() ) {
        // Strip leading directories of filename
        const char* last_slash = strrchr(filename, '/');
        if ( last_slash )
            basename = last_slash + 1;
        else
            basename = filename;
        basename = FLAGS_output_directory + "/" + basename;
    }
    else
        basename = filename;

    // If the file name ends with ".pac"
    if ( basename.length() > 4 && basename.substr(basename.length() - 4) == ".pac" ) {
        basename = basename.substr(0, basename.length() - 4);
    }

    basename += "_pac";

    DEBUG_MSG("Output file: %s.{h,cc}\n", basename.c_str());

    int ret = 0;

    try {
        switch_to_file(fp_input);
        if ( yyparse() )
            return 1;

        Output out_h(strfmt("%s.h", basename.c_str()));
        Output out_cc(strfmt("%s.cc", basename.c_str()));

        header_output = &out_h;
        source_output = &out_cc;

        insert_comments(&out_h, filename);
        insert_comments(&out_cc, filename);

        const char* filename_id = to_id(filename);

        out_h.println("#ifndef %s_h", filename_id);
        out_h.println("#define %s_h", filename_id);
        out_h.println("");
        out_h.println("#include <vector>");
        out_h.println("");
        out_h.println("#include \"binpac.h\"");
        out_h.println("");

        out_cc.println("");
        out_cc.println("#ifdef __clang__");
        out_cc.println("#pragma clang diagnostic ignored \"-Wparentheses-equality\"");
        out_cc.println("#endif");
        out_cc.println("");

        out_cc.println("#include \"%s.h\"\n", basename.c_str());

        Decl::ProcessDecls(&out_h, &out_cc);

        out_h.println("#endif /* %s_h */", filename_id);
    } catch ( OutputException& e ) {
        fprintf(stderr, "Error in compiling %s: %s\n", filename, e.errmsg());
        ret = 1;
    } catch ( Exception& e ) {
        fprintf(stderr, "%s\n", e.msg());
        exit(1);
    }

    header_output = nullptr;
    source_output = nullptr;
    input_filename = "";
    fclose(fp_input);

    return ret;
}

void usage() {
#ifdef BINPAC_VERSION
    fprintf(stderr, "binpac version %s\n", BINPAC_VERSION);
#endif
    fprintf(stderr, "usage: binpac [options] <pac files>\n");
    fprintf(stderr, "     <pac files>           | pac-language input files\n");
    fprintf(stderr, "     -d <dir>              | use given directory for compiler output\n");
    fprintf(stderr, "     -D                    | enable debugging output\n");
    fprintf(stderr, "     -q                    | stay quiet\n");
    fprintf(stderr, "     -h                    | show command line help\n");
    fprintf(stderr, "     -I <dir>              | include <dir> in input file search path\n");
    exit(1);
}

// GCC uses __SANITIZE_ADDRESS__, Clang uses __has_feature
#if defined(__SANITIZE_ADDRESS__)
#define USING_ASAN
#endif

#if defined(__has_feature)
#if __has_feature(address_sanitizer)
#define USING_ASAN
#endif
#endif

// FreeBSD doesn't support LeakSanitizer
#if defined(USING_ASAN) && ! defined(__FreeBSD__)
#include <sanitizer/lsan_interface.h>
#define BINPAC_LSAN_DISABLE() __lsan_disable()
#else
#define BINPAC_LSAN_DISABLE()
#endif

int main(int argc, char* argv[]) {
    // We generally do not care at all if binpac is leaking and other
    // projects that use it, like Zeek, only have their build tripped up
    // by the default behavior of LSAN to treat leaks as errors.
    BINPAC_LSAN_DISABLE();

#ifdef HAVE_MALLOC_OPTIONS
    extern char* malloc_options;
#endif
    int o;
    while ( (o = getopt(argc, argv, "DqI:d:h")) != -1 ) {
        switch ( o ) {
            case 'D': yydebug = 1; FLAGS_pac_debug = true;
#ifdef HAVE_MALLOC_OPTIONS
                malloc_options = "A";
#endif
                break;

            case 'q': FLAGS_quiet = true; break;

            case 'I':
                // Add to FLAGS_include_directories
                add_to_include_directories(optarg);
                break;

            case 'd': FLAGS_output_directory = optarg; break;

            case 'h': usage(); break;
        }
    }

    // Strip the trailing '/'s
    while ( ! FLAGS_output_directory.empty() && *(FLAGS_output_directory.end() - 1) == '/' ) {
        FLAGS_output_directory.erase(FLAGS_output_directory.end() - 1);
    }

    // Add the current directory to FLAGS_include_directories
    add_to_include_directories(".");

    pac_init();

    argc -= optind;
    argv += optind;
    if ( argc == 0 )
        compile("-");

    int ret = 0;
    for ( int i = 0; i < argc; ++i )
        if ( compile(argv[i]) )
            ret = 1;

    return ret;
}
