#  Copyright 2022 Eugene Gershnik
#
#  Use of this source code is governed by a BSD-style
#  license that can be found in the LICENSE file or at
#  https://github.com/gershnik/argum/blob/master/LICENSE
#
import hashlib
import re
from pathlib import Path
from typing import Dict, List


line_comment_re = re.compile(r'\s*//.*')
sys_include_re = re.compile(r'\s*#\s*include\s+<([^>]+)>\s*')
user_include_re = re.compile(r'\s*#\s*include\s+"([^"]+)"\s*')

special_handling = {
    'unistd.h': '#if !defined(_WIN32) && __has_include(<unistd.h>)\n    #include <unistd.h>\n#endif',
    'sys/ioctl.h': '#if !defined(_WIN32) && __has_include(<sys/ioctl.h>)\n    #include <sys/ioctl.h>\n#endif',
    'termios.h': '#if !defined(_WIN32) && __has_include(<termios.h>)\n    #include <termios.h>\n#endif',
    'io.h': '#ifdef _WIN32\n    #include <io.h>\n#endif',
    'Windows.h': 
'''#ifdef _WIN32
    #ifndef NOMINMAX
        #define NOMINMAX
    #endif
    #ifndef WIN32_LEAN_AND_MEAN
        #define WIN32_LEAN_AND_MEAN
    #endif
    #include <Windows.h>
    #ifdef min
        #undef min
    #endif
    #ifdef max
        #undef max
    #endif
#endif''',
}

def getReplacement(header: str):
    return special_handling.get(header, f"#include <{header}>")


def processHeader(dir: Path, path: Path, sys_includes: List[str], processed_headers:Dict[str, bool], strip_initial_comment: bool):

    ret = ""
    initial_comment = strip_initial_comment
    with open(path, "r") as headerfile:
        for line in headerfile:
            m = line_comment_re.match(line)
            if m and initial_comment:
                continue
            initial_comment = False
            m = sys_include_re.match(line)
            if m:
                sys_includes.append(m.group(1))
                continue
            m = user_include_re.match(line)
            if m:
                name = m.group(1)
                if not processed_headers.get(name):
                    new_path = (dir / name).absolute()
                    ret += processHeader(new_path.parent, new_path, sys_includes, processed_headers, strip_initial_comment=True)
                    processed_headers[name] = True
            else:
                ret += line
    return ret


def combineHeaders(dir: Path, template: Path, output: Path):
    sys_includes = []
    processed_headers = {}

    text = processHeader(dir, template, sys_includes, processed_headers, strip_initial_comment=False)

    sys_includes = list(set(sys_includes))
    sys_includes.sort(key = lambda x: x.lower())
    sys_includes_text = ""
    sys_c_includes_text = ""
    for sys_include in sys_includes:
        repl = "\n" + getReplacement(sys_include)
        sys_includes_text += (repl)
        if sys_include.endswith('.h'):
            sys_c_includes_text += (repl)

    text = text.replace("##SYS_INCLUDES##", sys_includes_text)
    text = text.replace("##SYS_C_INCLUDES##", sys_c_includes_text)
    text = text.replace("##NAME##", output.name.replace('.', '_').replace('-', '_').upper())
    text += '\n'

    digest = hashlib.md5(text.encode("utf-8")).hexdigest()

    if output.exists():
        existing_digest = hashlib.md5(output.read_text(encoding='utf-8').encode('utf-8')).hexdigest()
        if existing_digest == digest:
            return

    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(text, encoding='utf-8')

def amalgamate():
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--dir', '-d', default=str(Path.cwd()))
    parser.add_argument('template')
    parser.add_argument('output')
    args = parser.parse_args()

    combineHeaders(Path(args.dir), Path(args.template), Path(args.output))

if __name__ == "__main__":
	amalgamate()