# SPDX-FileCopyrightText: 2022-present Didier Malenfant
#
# SPDX-License-Identifier: MIT

import os
import shutil
import stat

from typing import List
from pathlib import Path

from .boxfile import Boxfile
from .dependency import Dependency
from .paths import Paths
from .utils import Utils


class Files:
    """Methods to generate and test various files for toyboxpy."""

    @classmethod
    def findMakefileIn(cls, folder: str, repo_name: str) -> str:
        paths_found = Utils.lookInFolderFor(folder, repo_name + '.mk')
        paths_found += Utils.lookInFolderFor(folder, os.path.join('Makefile'))
        paths_found += Utils.lookInFolderFor(folder, os.path.join('Makefile.mk'))

        potential_names = [repo_name + '.mk',
                           'Makefile',
                           'Makefile.mk',
                           'makefile',
                           'makefile.mk']
        for name in paths_found:
            if name in potential_names:
                return name

        return None

    @classmethod
    def findIncludeFileIn(cls, folder: str, repo_name: str) -> str:
        paths_found = Utils.lookInFolderFor(folder, os.path.join(repo_name, '*.h'))

        potential_names = [os.path.join(repo_name, repo_name + '.h'),
                           os.path.join(repo_name, 'include.h')]

        for path in paths_found:
            if path in potential_names:
                return path

        return None

    @classmethod
    def findLuacheckFileIn(cls, folder: str) -> str:
        paths_found = Utils.lookInFolderFor(folder, os.path.join('luacheck', '*.lua'))

        potential_names = [os.path.join('luacheck', 'luacheck.lua'),
                           os.path.join('luacheck', 'Luacheck.lua')]
        for path in paths_found:
            if path in potential_names:
                return os.path.join(folder, path)

        return None

    @classmethod
    def findLuaIncludeFileIn(cls, folder: str, repo_name: str, maybe_additional_path: str) -> str:
        if maybe_additional_path is not None:
            filename = maybe_additional_path + '.lua'
            paths_found = Utils.lookInFolderFor(folder, filename)
            if len(paths_found) != 0:
                return maybe_additional_path

            print('Warning: Could not find file \'' + filename + '\' to import in \'' + repo_name + '\'.')

        paths_found = Utils.lookInFolderFor(folder, os.path.join('**', 'import.lua'))
        paths_found += Utils.lookInFolderFor(folder, os.path.join('**', repo_name + '.lua'))

        correct_names = [repo_name,
                         'import',
                         os.path.join('Source', repo_name),
                         os.path.join('Source', 'import'),
                         os.path.join('source', repo_name),
                         os.path.join('source', 'import')]

        for path_found in paths_found:
            for correct_name in correct_names:
                path_without_extension = path_found[:-4]
                if path_without_extension == correct_name:
                    return path_without_extension

        return None

    @classmethod
    def generateLuaIncludeFile(cls, dependencies: List[Dependency]):
        lua_includes = []

        for dep in dependencies:
            dep_folder = dep.toyboxFolder()

            maybe_lua_include_path = Boxfile(dep_folder, empty_if_does_not_exist=True).maybeLuaImportFile()
            if maybe_lua_include_path is not None and maybe_lua_include_path.endswith('.lua'):
                maybe_lua_include_path = maybe_lua_include_path[:-4]

            lua_include_path = Files.findLuaIncludeFileIn(dep_folder, dep.url.repo_name, maybe_lua_include_path)

            if lua_include_path is not None:
                lua_includes.append(os.path.join(dep.subFolder(), lua_include_path))

        if len(lua_includes) == 0:
            return

        with open(os.path.join(Paths.toyboxesFolder(), 'toyboxes.lua'), 'w') as out_file:
            out_file.write('--\n')
            out_file.write('--  toyboxes.lua - include file auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('--\n')
            out_file.write('\n')

            for lua_include in lua_includes:
                # -- We need to always use forward slashes, even on Windows, because pdc expects them
                out_file.write('import \'' + lua_include.replace('\\', '/') + '.lua\'\n')

            out_file.close()

    @classmethod
    def generateMakefile(cls, dependencies: List[Dependency]):
        makefiles = []

        for dep in dependencies:
            dep_folder = dep.toyboxFolder()

            makefile_path = Boxfile(dep_folder, empty_if_does_not_exist=True).maybeMakefile()
            if makefile_path is None:
                makefile_path = Files.findMakefileIn(dep_folder, dep.url.repo_name)

            if makefile_path is not None:
                makefiles.append([os.path.join(dep.subFolder(), makefile_path), dep.url.repo_name.upper()])

        if len(makefiles) == 0:
            return

        with open(os.path.join(Paths.toyboxesFolder(), 'toyboxes.mk'), 'w') as out_file:
            out_file.write('#\n')
            out_file.write('#  toyboxes.mk - include file auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('#\n')
            out_file.write('\n')
            out_file.write('_RELATIVE_FILE_PATH := $(lastword $(MAKEFILE_LIST))\n')
            out_file.write('_RELATIVE_DIR := $(subst /$(notdir $(_RELATIVE_FILE_PATH)),,$(_RELATIVE_FILE_PATH))\n')
            out_file.write('\n')
            out_file.write('uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))\n')
            out_file.write('UINCDIR := $(call uniq, $(UINCDIR) $(_RELATIVE_DIR))\n')
            out_file.write('\n')

            for makefile in makefiles:
                out_file.write(makefile[1] + '_MAKEFILE := $(_RELATIVE_DIR)/' + makefile[0] + '\n')

            out_file.write('\n')

            for makefile in makefiles:
                out_file.write('include $(' + makefile[1] + '_MAKEFILE)\n')

            out_file.close()

        if not os.path.exists(os.path.join(Paths.boxfileFolder(), 'makefile')) and not os.path.exists(os.path.join(Paths.boxfileFolder(), 'Makefile')):
            print("You have installed a C toybox but you don't seem to have a Makefile setup.\nUse 'toybox setupMakefile' to get started.")

    @classmethod
    def generateIncludeFile(cls, dependencies: List[Dependency]):
        include_files = []

        for dep in dependencies:
            dep_folder = dep.toyboxFolder()

            include_file_path = Boxfile(dep_folder, empty_if_does_not_exist=True).maybeIncludeHeader()
            if include_file_path is None:
                include_file_path = Files.findIncludeFileIn(dep_folder, dep.url.repo_name)

            if include_file_path is not None:
                include_files.append([os.path.join(dep.subFolder(), include_file_path), dep.url.repo_name])

        if len(include_files) == 0:
            return

        with open(os.path.join(Paths.toyboxesFolder(), 'toyboxes.h'), 'w') as out_file:
            out_file.write('//\n')
            out_file.write('//  toyboxes.h - include file auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('//\n')
            out_file.write('\n')

            for include in include_files:
                out_file.write('#include "' + include[0] + '"\n')

            out_file.write('\n')

            prefix = ''
            out_file.write('#define REGISTER_TOYBOXES(pd)')
            for include in include_files:
                out_file.write(prefix + '   register_' + include[1] + '(pd);')
                prefix = ' \\\n                             '

            out_file.write('\n')

            out_file.close()

    @classmethod
    def generateLuacheckFile(cls, dependencies: List[Dependency]):
        luacheck_results = []
        boxfile_folder_len = len(Paths.boxfileFolder())

        for dep in dependencies:
            dep_folder = dep.toyboxFolder()
            luacheck_file_path = Files.findLuacheckFileIn(dep_folder)
            if luacheck_file_path is not None:
                version = dep.resolveVersion()
                if version.isLocal():
                    local_folder = version.original_version
                else:
                    local_folder = None

                luacheck_results.append([luacheck_file_path[boxfile_folder_len + 1:-4],
                                        luacheck_file_path,
                                        dep_folder[boxfile_folder_len + 1:],
                                        local_folder])

        if len(luacheck_results) == 0:
            return

        with open(os.path.join(Paths.toyboxesFolder(), 'luacheck.lua'), 'w') as out_file:
            out_file.write('--\n')
            out_file.write('--  Luacheck.lua - include file auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('--\n')
            out_file.write('\n')
            out_file.write('local toyboxes = {}\n')
            out_file.write('toyboxes.read_globals = {}\n')
            out_file.write('\n')
            out_file.write('local function mergeTables(t1, t2)\n')
            out_file.write('    for k, v in pairs(t2) do\n')
            out_file.write('        if type(v) == \'table\' then\n')
            out_file.write('            t1[k] = mergeTables(t1[k] or { }, t2[k])\n')
            out_file.write('        else\n')
            out_file.write('            assert(t1[k] == nil, "Found duplicate entries for \'"..k.."\' while merging luacheck files.")\n')
            out_file.write('\n')
            out_file.write('            t1[k] = v\n')
            out_file.write('        end\n')
            out_file.write('    end\n')
            out_file.write('\n')
            out_file.write('    return t1\n')
            out_file.write('end\n')
            out_file.write('\n')
            out_file.write('local function luacheckMerge(other)\n')
            out_file.write('    if other.globals ~= nil then\n')
            out_file.write('        mergeTables(toyboxes.read_globals, other.globals)\n')
            out_file.write('    end\n')
            out_file.write('\n')
            out_file.write('    if other.read_globals ~= nil then\n')
            out_file.write('        mergeTables(toyboxes.read_globals, other.read_globals)\n')
            out_file.write('    end\n')
            out_file.write('end\n')
            out_file.write('\n')

            out_file.write('-- Add globals from the toyboxes as read-only globals.\n')
            for luacheck_result in luacheck_results:
                out_file.write('luacheckMerge(require "' + luacheck_result[0] + '")\n')

            out_file.write('\n')
            out_file.write('return function(stds, files)\n')
            out_file.write('    stds.toyboxes = toyboxes\n')

            out_file.write('\n')
            out_file.write('    -- Also add globals for files from the toyboxes themselves as the project intended.\n')

            add_newline = False

            toybox_index = 1
            for luacheck_result in luacheck_results:
                if add_newline:
                    out_file.write('\n')
                else:
                    add_newline = True

                out_file.write('    stds.toybox_' + str(toybox_index) + ' = require "' + luacheck_result[0] + '"\n')
                out_file.write('    files["' + luacheck_result[2] + '"].std="+toybox_' + str(toybox_index) + '"\n')

                local_folder = luacheck_result[3]
                if local_folder is not None:
                    out_file.write('    files["' + local_folder + '"].std="+toybox_' + str(toybox_index) + '"\n')

                toybox_index += 1

            out_file.write('end\n')

    @classmethod
    def generateReadMeFileIn(cls, folder: str):
        with open(os.path.join(folder, 'README.md'), 'w') as out_file:
            out_file.write('# ' + os.path.basename(folder) + '\n')
            out_file.write('\n')
            out_file.write('This folder contains files auto-generated and managed by [**toybox.py**](https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('\n')
            out_file.write('**!!! DO NOT MODIFY OR PLACE ANYTHING IN THIS FOLDER !!!**\n')
            out_file.write('\n')
            out_file.write('Please [install](https://code.malenfant.net/didier/toybox.py#installation) **toybox.py** in order to modify or update the content of this folder.\n')

    @classmethod
    def generatePreCommitFile(cls):
        if not os.path.exists('.git'):
            return

        pre_commit_file_path = Paths.preCommitFilePath()
        if os.path.exists(pre_commit_file_path):
            shutil.move(pre_commit_file_path, Paths.preCommitFilePathBackup())
        else:
            with open(Paths.preCommitFileNoBackupPath(), 'w') as out_file:
                out_file.write('#!/bin/sh\n')
                out_file.write('#\n')
                out_file.write('# This file is generated by toybox.py to remember there was no backup to be restored.\n')
                out_file.write('\n')

        with open(pre_commit_file_path, 'w') as out_file:
            out_file.write('#!/bin/sh\n')
            out_file.write('#\n')
            out_file.write('# This file is generated by toybox.py to prevent committing local toyboxes.\n')
            out_file.write('\n')
            out_file.write('echo "You shouldn\'t commit when you have \'local\' toyboxes in your repo."\n')
            out_file.write('exit 1\n')

        if os.path.exists(pre_commit_file_path):
            os.chmod(pre_commit_file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)

    @classmethod
    def restorePreCommitFileIfAny(cls):
        if not os.path.exists('.git'):
            return

        pre_commit_file_path = Paths.preCommitFilePath()
        pre_commit_file_backup_path = Paths.preCommitFileBackupPath()
        pre_commit_file_no_backup_path = Paths.preCommitFileNoBackupPath()

        if os.path.exists(pre_commit_file_no_backup_path):
            os.remove(pre_commit_file_no_backup_path)
            os.remove(pre_commit_file_path)
        elif os.path.exists(pre_commit_file_backup_path):
            os.remove(pre_commit_file_path)
            shutil.move(pre_commit_file_backup_path, pre_commit_file_path)

    @classmethod
    def generateMakefileSetup(cls):
        extension_main_file = Paths.extensionMainFile()
        extension_makefile = Paths.extensionMakefile()
        project_makefile = Paths.projectMakefile()

        if os.path.exists(extension_main_file):
            raise RuntimeError('File ' + extension_main_file + ' already exists.')

        if os.path.exists(extension_makefile):
            raise RuntimeError('File ' + extension_makefile + ' already exists.')

        if os.path.exists(project_makefile):
            raise RuntimeError('File ' + project_makefile + ' already exists.')

        extension_folder = Path(extension_makefile).parent
        if not os.path.exists(extension_folder):
            os.makedirs(extension_folder)

        with open(extension_main_file, 'w') as out_file:
            out_file.write('// -- This file was auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('\n')
            out_file.write('#include "toyboxes/toyboxes.h"\n')
            out_file.write('\n')
            out_file.write('#include "pd_api.h"\n')
            out_file.write('\n')
            out_file.write('#ifdef _WINDLL\n')
            out_file.write('    #define DllExport __declspec(dllexport)\n')
            out_file.write('#else\n')
            out_file.write('    #define DllExport\n')
            out_file.write('#endif\n')
            out_file.write('\n')
            out_file.write('DllExport int eventHandler(PlaydateAPI* playdate, PDSystemEvent event, uint32_t arg)\n')
            out_file.write('{\n')
            out_file.write('    if(event == kEventInitLua) {\n')
            out_file.write('        REGISTER_TOYBOXES(playdate);\n')
            out_file.write('    }\n')
            out_file.write('\n')
            out_file.write('    return 0;\n')
            out_file.write('}\n')

        with open(extension_makefile, 'w') as out_file:
            out_file.write('# -- This file was auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('\n')
            out_file.write('# -- Find out more about where this file is relative to the Makefile including it\n')
            out_file.write('RELATIVE_FILE_PATH := $(lastword $(MAKEFILE_LIST))\n')
            out_file.write('RELATIVE_DIR := $(subst /$(notdir $(RELATIVE_FILE_PATH)),,$(RELATIVE_FILE_PATH))\n')
            out_file.write('RELATIVE_PARENT_DIR := $(subst /$(notdir $(RELATIVE_DIR)),,$(RELATIVE_DIR))\n')
            out_file.write('\n')
            out_file.write('# -- Add us as an include search folder only if it is not already there\n')
            out_file.write('uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))\n')
            out_file.write('UINCDIR := $(call uniq, $(UINCDIR) $(RELATIVE_PARENT_DIR))\n')
            out_file.write('\n')
            out_file.write('# -- Add our source files\n')
            out_file.write('SRC := $(SRC)\\\n')
            out_file.write('       $(RELATIVE_DIR)/main.c\n')

        with open(project_makefile, 'w') as out_file:
            out_file.write('# -- This file was auto-generated by toybox.py (https://code.malenfant.net/didier/toybox.py).\n')
            out_file.write('\n')
            out_file.write('HEAP_SIZE      = 8388208\n')
            out_file.write('STACK_SIZE     = 61800\n')
            out_file.write('\n')
            out_file.write('# Locate the SDK\n')
            out_file.write('SDK = ${PLAYDATE_SDK_PATH}\n')
            out_file.write('ifeq ($(SDK),)\n')
            out_file.write('    SDK = $(shell egrep \'^\\s*SDKRoot\' ~/.Playdate/config | head -n 1 | cut -c9-)\n')
            out_file.write('endif\n')
            out_file.write('\n')
            out_file.write('ifeq ($(SDK),)\n')
            out_file.write('    $(error SDK path not found; Make sure it is installed or set ENV value PLAYDATE_SDK_PATH)\n')
            out_file.write('endif\n')
            out_file.write('\n')
            out_file.write('GAME=$(notdir $(CURDIR))\n')
            out_file.write('PRODUCT = $(GAME).pdx\n')
            out_file.write('SIM=Playdate Simulator\n')
            out_file.write('\n')
            out_file.write('# Include sub makefiles\n')
            out_file.write('include toyboxes/toyboxes.mk\n')
            out_file.write('include extension/extension.mk\n')
            out_file.write('\n')
            out_file.write('include $(SDK)/C_API/buildsupport/common.mk\n')
            out_file.write('\n')
            out_file.write('# -- Make sure we compile a universal binary for M1 and Intel macs\n')
            out_file.write('DYLIB_FLAGS+=-arch x86_64 -arch arm64\n')

    @classmethod
    def createAppUpdateCheckFile(cls):
        file_path = Paths.appUpdateCheckFile()

        os.makedirs(Path(file_path).parent, exist_ok=True)

        if os.path.exists(file_path):
            os.remove(file_path)

        with open(file_path, 'w') as out_file:
            out_file.write('check')
