import os
import sys
import argparse
import shutil
import tempfile
import json
import subprocess
import threading

base_path = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, base_path)

project_path = os.path.abspath(os.path.join(base_path, '..', '..'))
docs_path = os.path.join(project_path, 'docs')
sys.path.insert(0, docs_path)

import create_fake_lib_c  # NOQA
import pycparser_monkeypatch  # NOQA
import pycparser  # NOQA

DEVELOP = False


class STDOut:
    def __init__(self):
        self._stdout = sys.stdout
        sys.__stdout__ = self
        sys.stdout = self

    def write(self, data):
        pass

    def __getattr__(self, item):
        if item in self.__dict__:
            return self.__dict__[item]

        return getattr(self._stdout, item)

    def reset(self):
        sys.stdout = self._stdout


temp_directory = tempfile.mkdtemp(suffix='.lvgl_json')


def run(output_path, lvgl_config_path, output_to_stdout, target_header, filter_private, *compiler_args):
    # stdout = STDOut()

    pycparser_monkeypatch.FILTER_PRIVATE = filter_private

    # The thread is to provide an indication that things are being processed.
    # There are long periods where nothing gets output to the screen and this
    # is to let the user know that it is still working.
    if not output_to_stdout:
        event = threading.Event()

        def _do():
            while not event.is_set():
                event.wait(1)
                sys.stdout.write('.')
                sys.stdout.flush()

            print()

        t = threading.Thread(target=_do)
        t.daemon = True
        t.start()

    lvgl_path = project_path
    lvgl_src_path = os.path.join(lvgl_path, 'src')
    temp_lvgl = os.path.join(temp_directory, 'lvgl')
    target_header_base_name = (
        os.path.splitext(os.path.split(target_header)[-1])[0]
    )

    try:
        os.mkdir(temp_lvgl)
        shutil.copytree(lvgl_src_path, os.path.join(temp_lvgl, 'src'))
        shutil.copyfile(os.path.join(lvgl_path, 'lvgl.h'), os.path.join(temp_lvgl, 'lvgl.h'))

        pp_file = os.path.join(temp_directory, target_header_base_name + '.pp')

        if lvgl_config_path is None:
            lvgl_config_path = os.path.join(lvgl_path, 'lv_conf_template.h')

            with open(lvgl_config_path, 'rb') as f:
                data = f.read().decode('utf-8').split('\n')

            for i, line in enumerate(data):
                if line.startswith('#if 0'):
                    data[i] = '#if 1'
                else:
                    for item in (
                        'LV_USE_LOG',
                        'LV_USE_OBJ_ID',
                        'LV_USE_OBJ_ID_BUILTIN',
                        'LV_USE_FLOAT',
                        'LV_USE_BIDI',
                        'LV_USE_LODEPNG',
                        'LV_USE_LIBPNG',
                        'LV_USE_BMP',
                        'LV_USE_TJPGD',
                        'LV_USE_LIBJPEG_TURBO',
                        'LV_USE_GIF',
                        'LV_BIN_DECODER_RAM_LOAD',
                        'LV_USE_RLE',
                        'LV_USE_QRCODE',
                        'LV_USE_BARCODE',
                        'LV_USE_TINY_TTF',
                        'LV_USE_GRIDNAV',
                        'LV_USE_FRAGMENT',
                        'LV_USE_IMGFONT',
                        'LV_USE_SNAPSHOT',
                        'LV_USE_FREETYPE'
                    ):
                        if line.startswith(f'#define {item} '):
                            data[i] = f'#define {item} 1'
                            break

            with open(os.path.join(temp_directory, 'lv_conf.h'), 'wb') as f:
                f.write('\n'.join(data).encode('utf-8'))
        else:
            src = lvgl_config_path
            dst = os.path.join(temp_directory, 'lv_conf.h')
            shutil.copyfile(src, dst)

        include_dirs = [temp_directory, project_path]

        if sys.platform.startswith('win'):
            import get_sdl2

            try:
                import pyMSVC  # NOQA
            except ImportError:
                sys.stderr.write(
                    '\nThe pyMSVC library is missing, '
                    'please run "pip install pyMSVC" to install it.\n'
                )
                sys.stderr.flush()
                sys.exit(-500)

            env = pyMSVC.setup_environment()  # NOQA
            cpp_cmd = ['cl', '/std:c11', '/nologo', '/P']
            output_pp = f'/Fi"{pp_file}"'
            sdl2_include, _ = get_sdl2.get_sdl2(temp_directory)
            include_dirs.append(sdl2_include)
            include_path_env_key = 'INCLUDE'

        elif sys.platform.startswith('darwin'):
            include_path_env_key = 'C_INCLUDE_PATH'
            cpp_cmd = [
                'clang', '-std=c11', '-E', '-DINT32_MIN=0x80000000',
            ]
            output_pp = f' >> "{pp_file}"'
        else:
            include_path_env_key = 'C_INCLUDE_PATH'
            cpp_cmd = [
                'gcc', '-std=c11', '-E', '-Wno-incompatible-pointer-types',
            ]
            output_pp = f' >> "{pp_file}"'

        fake_libc_path = create_fake_lib_c.run(temp_directory)

        if include_path_env_key not in os.environ:
            os.environ[include_path_env_key] = ''

        os.environ[include_path_env_key] = (
            f'{fake_libc_path}{os.pathsep}{os.environ[include_path_env_key]}'
        )

        if 'PATH' not in os.environ:
            os.environ['PATH'] = ''

        os.environ['PATH'] = (
            f'{fake_libc_path}{os.pathsep}{os.environ["PATH"]}'
        )

        cpp_cmd.extend(compiler_args)
        cpp_cmd.extend([
            '-DLV_LVGL_H_INCLUDE_SIMPLE',
            '-DLV_CONF_INCLUDE_SIMPLE',
            '-DLV_USE_DEV_VERSION'
        ])

        cpp_cmd.extend(['-DPYCPARSER', f'"-I{fake_libc_path}"'])
        cpp_cmd.extend([f'"-I{item}"' for item in include_dirs])
        cpp_cmd.append(f'"{target_header}"')

        if sys.platform.startswith('win'):
            cpp_cmd.insert(len(cpp_cmd) - 2, output_pp)
        else:
            cpp_cmd.append(output_pp)

        cpp_cmd = ' '.join(cpp_cmd)

        p = subprocess.Popen(
            cpp_cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=os.environ,
            shell=True
        )
        out, err = p.communicate()
        exit_code = p.returncode

        if not os.path.exists(pp_file):
            sys.stdout.write(out.decode('utf-8').strip() + '\n')
            sys.stdout.write('EXIT CODE: ' + str(exit_code) + '\n')
            sys.stderr.write(err.decode('utf-8').strip() + '\n')
            sys.stdout.flush()
            sys.stderr.flush()

            raise RuntimeError('Unknown Failure')

        with open(pp_file, 'r') as f:
            pp_data = f.read()

        cparser = pycparser.CParser()
        ast = cparser.parse(pp_data, target_header)

        ast.setup_docs(temp_directory)

        if not output_to_stdout and output_path is None:
            # stdout.reset()

            if not DEVELOP:
                shutil.rmtree(temp_directory)

            return ast

        elif output_to_stdout:
            # stdout.reset()
            print(json.dumps(ast.to_dict(), indent=4))
        else:
            if not os.path.exists(output_path):
                os.makedirs(output_path)

            output_path = os.path.join(output_path, target_header_base_name + '.json')

            with open(output_path, 'w') as f:
                f.write(json.dumps(ast.to_dict(), indent=4))

            # stdout.reset()

        if not output_to_stdout:
            event.set()  # NOQA
            t.join()  # NOQA
    except Exception as err:
        if not output_to_stdout:
            event.set()  # NOQA
            t.join()  # NOQA

        print()
        try:
            print(cpp_cmd)  # NOQA
            print()
        except:  # NOQA
            pass

        for key, value in os.environ.items():
            print(key + ':', value)

        print()
        import traceback

        traceback.print_exc()
        print()

        exceptions = [
            ArithmeticError,
            AssertionError,
            AttributeError,
            EOFError,
            FloatingPointError,
            GeneratorExit,
            ImportError,
            IndentationError,
            IndexError,
            KeyError,
            KeyboardInterrupt,
            LookupError,
            MemoryError,
            NameError,
            NotImplementedError,
            OverflowError,
            ReferenceError,
            RuntimeError,
            StopIteration,
            SyntaxError,
            TabError,
            SystemExit,
            TypeError,
            UnboundLocalError,
            UnicodeError,
            UnicodeEncodeError,
            UnicodeDecodeError,
            UnicodeTranslateError,
            ValueError,
            ZeroDivisionError,
            SystemError
        ]

        if isinstance(err, OSError):
            error = err.errno
        else:
            if type(err) in exceptions:
                error = ~exceptions.index(type(err))
            else:
                error = -100
    else:
        error = 0

    if DEVELOP:
        print('temporary file path:', temp_directory)
    else:
        shutil.rmtree(temp_directory)

    sys.exit(error)


if __name__ == '__main__':
    parser = argparse.ArgumentParser('-')
    parser.add_argument(
        '--output-path',
        dest='output_path',
        help=(
            'output directory for JSON file. If one is not '
            'supplied then it will be output stdout'
        ),
        action='store',
        default=None
    )
    parser.add_argument(
        '--lvgl-config',
        dest='lv_conf',
        help=(
            'path to lv_conf.h (including file name), if this is not set then '
            'a config file will be generated that has everything turned on.'
        ),
        action='store',
        default=None
    )
    parser.add_argument(
        '--develop',
        dest='develop',
        help='this option leaves the temporary folder in place.',
        action='store_true',
    )
    parser.add_argument(
        "--target-header",
        dest="target_header",
        help=(
            "path to a custom header file. When using this to supply a custom"
            "header file you MUST insure that any LVGL includes are done so "
            "they are relitive to the LVGL repository root folder.\n\n"
            '#include "src/lvgl_private.h"\n\n'
            "If you have includes to header files that are not LVGL then you "
            "will need to add the include locations for those header files "
            "when running this script. It is done using the same manner that "
            "is used when calling a C compiler\n\n"
            "You need to provide the absolute path to the header file when "
            "using this feature."
        ),
        action="store",
        default=os.path.join(temp_directory, "lvgl", "lvgl.h")
    )
    parser.add_argument(
        '--filter-private',
        dest='filter_private',
        help='Internal Use',
        action='store_true',
    )

    args, extra_args = parser.parse_known_args()

    DEVELOP = args.develop

    run(args.output_path, args.lv_conf, args.output_path is None, args.target_header, args.filter_private, *extra_args)
