"""
Various utilities for interacting with Visual Studio on Windows.
"""
import json
import os
import re
import subprocess
from typing import Any, Optional, Callable

import bpy

import lnx.log as log
import lnx.make
import lnx.make_state as state
import lnx.utils

if lnx.is_reload(__name__):
    log = lnx.reload_module(log)
    lnx.make = lnx.reload_module(lnx.make)
    state = lnx.reload_module(state)
    lnx.utils = lnx.reload_module(lnx.utils)
else:
    lnx.enable_reload(__name__)


# VS versions supported by khamake. Keep in mind that this list is also
# used for the wrd.lnx_project_win_list_vs EnumProperty!
supported_versions = [
    ('10', '2010', 'Visual Studio 2010 (version 10)'),
    ('11', '2012', 'Visual Studio 2012 (version 11)'),
    ('12', '2013', 'Visual Studio 2013 (version 12)'),
    ('14', '2015', 'Visual Studio 2015 (version 14)'),
    ('15', '2017', 'Visual Studio 2017 (version 15)'),
    ('16', '2019', 'Visual Studio 2019 (version 16)'),
    ('17', '2022', 'Visual Studio 2022 (version 17)')
]

# version_major to --visualstudio parameter
version_to_khamake_id = {
    '10': 'vs2010',
    '11': 'vs2012',
    '12': 'vs2013',
    '14': 'vs2015',
    '15': 'vs2017',
    '16': 'vs2019',
    '17': 'vs2022',
}

# VS versions found with fetch_installed_vs()
_installed_versions = []

_REGEX_SLN_MIN_VERSION = re.compile(r'MinimumVisualStudioVersion\s*=\s*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)')


def is_version_installed(version_major: str) -> bool:
    return any(v['version_major'] == version_major for v in _installed_versions)


def get_installed_version(version_major: str, re_fetch=False) -> Optional[dict[str, str]]:
    for installed_version in _installed_versions:
        if installed_version['version_major'] == version_major:
            return installed_version

    # No installation was found. If re_fetch is True, fetch and try again
    # (the user may not have fetched installations before for example)
    if re_fetch:
        if not fetch_installed_vs():
            return None
        return get_installed_version(version_major, False)

    return None


def get_supported_version(version_major: str) -> Optional[dict[str, str]]:
    for version in supported_versions:
        if version[0] == version_major:
            return {
                'version_major': version[0],
                'year': version[1],
                'name': version[2]
            }
    return None


def fetch_installed_vs(silent=False) -> bool:
    global _installed_versions

    data_instances = _vswhere_get_instances(silent)
    if data_instances is None:
        return False

    items = []

    for inst in data_instances:
        name = _vswhere_get_display_name(inst)
        versions = _vswhere_get_version(inst)
        path = _vswhere_get_path(inst)

        if name is None or versions is None or path is None:
            if not silent:
                log.warn(
                    f'Found a Visual Studio installation with incomplete information, skipping\n'
                    f'    ({name=}, {versions=}, {path=})'
                )
            continue

        items.append({
            'version_major': versions[0],
            'version_full': versions[1],
            'version_full_ints': versions[2],
            'name': name,
            'path': path
        })

    # Store in descending order
    items.sort(key=lambda x: x['version_major'], reverse=True)

    _installed_versions = items
    return True


def open_project_in_vs(version_major: str, version_min_full: Optional[str] = None) -> bool:
    installation = get_installed_version(version_major, re_fetch=True)
    if installation is None:
        if version_min_full is not None:
            # Try whether other installed versions are supported, versions
            # are already sorted in descending order
            for installed_version in _installed_versions:
                if (installed_version['version_full_ints'] >= version_full_to_ints(version_min_full)
                        and int(installed_version['version_major']) < int(version_major)):
                    installation = installed_version
                    break

        # Still nothing found, warn for version_major
        if installation is None:
            vs_info = get_supported_version(version_major)
            log.warn(f'Could not open project in Visual Studio, {vs_info["name"]} was not found.')
            return False

    sln_path = get_sln_path()
    devenv_path = os.path.join(installation['path'], 'Common7', 'IDE', 'devenv.exe')
    cmd = ['start', devenv_path, sln_path]

    try:
        subprocess.check_call(cmd, shell=True)
    except subprocess.CalledProcessError as e:
        log.warn_called_process_error(e)
        return False

    return True


def enable_vsvars_env(version_major: str, done: Callable[[], None]) -> bool:
    installation = get_installed_version(version_major, re_fetch=True)
    if installation is None:
        vs_info = get_supported_version(version_major)
        log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.')
        return False

    wrd = bpy.data.worlds['Lnx']
    arch_bits = '64' if wrd.lnx_project_win_build_arch == 'x64' else '32'
    vcvars_path = os.path.join(installation['path'], 'VC', 'Auxiliary', 'Build', 'vcvars' + arch_bits + '.bat')

    if not os.path.isfile(vcvars_path):
        log.error(
            'Could not compile project in Visual Studio\n'
            f'    File "{vcvars_path}" not found. Please verify that {installation["name"]} was installed correctly.'
        )
        return False

    state.proc_publish_build = lnx.make.run_proc(vcvars_path, done)
    return True


def compile_in_vs(version_major: str, done: Callable[[], None]) -> bool:
    installation = get_installed_version(version_major, re_fetch=True)
    if installation is None:
        vs_info = get_supported_version(version_major)
        log.error(f'Could not compile project in Visual Studio, {vs_info["name"]} was not found.')
        return False

    wrd = bpy.data.worlds['Lnx']

    msbuild_path = os.path.join(installation['path'], 'MSBuild', 'Current', 'Bin', 'MSBuild.exe')
    if not os.path.isfile(msbuild_path):
        log.error(
            'Could not compile project in Visual Studio\n'
            f'    File "{msbuild_path}" not found. Please verify that {installation["name"]} was installed correctly.'
        )
        return False

    projectfile_path = get_vcxproj_path()

    cmd = [msbuild_path, projectfile_path]

    # Arguments
    platform = 'x64' if wrd.lnx_project_win_build_arch == 'x64' else 'win32'
    log_param = wrd.lnx_project_win_build_log
    if log_param == 'WarningsAndErrorsOnly':
        log_param = 'WarningsOnly;ErrorsOnly'

    cmd.extend([
        '-m:' + str(wrd.lnx_project_win_build_cpu),
        '-clp:' + log_param,
        '/p:Configuration=' + wrd.lnx_project_win_build_mode,
        '/p:Platform=' + platform
    ])

    print('\nCompiling the project ' + projectfile_path)
    state.proc_publish_build = lnx.make.run_proc(cmd, done)
    state.redraw_ui = True
    return True


def _vswhere_get_display_name(instance_data: dict[str, Any]) -> Optional[str]:
    name_raw = instance_data.get('displayName', None)
    if name_raw is None:
        return None
    return lnx.utils.safestr(name_raw).replace('_', ' ').strip()


def _vswhere_get_version(instance_data: dict[str, Any]) -> Optional[tuple[str, str, tuple[int, ...]]]:
    version_raw = instance_data.get('installationVersion', None)
    if version_raw is None:
        return None

    version_full = version_raw.strip()
    version_full_ints = version_full_to_ints(version_full)
    version_major = version_full.split('.')[0]
    return version_major, version_full, version_full_ints


def _vswhere_get_path(instance_data: dict[str, Any]) -> Optional[str]:
    return instance_data.get('installationPath', None)


def _vswhere_get_instances(silent=False) -> Optional[list[dict[str, Any]]]:
    # vswhere.exe only exists at that location since VS2017 v15.2, for
    # earlier versions we'd need to package vswhere with Leenkx
    exe_path = os.path.join(os.environ["ProgramFiles(x86)"], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
    command = [exe_path, '-format', 'json', '-utf8']

    try:
        result = subprocess.check_output(command)
    except subprocess.CalledProcessError as e:
        # Do not silence this warning, if this exception is caught there
        # likely is an issue in the command above
        log.warn_called_process_error(e)
        return None
    except FileNotFoundError as e:
        if not silent:
            log.warn(f'Could not open file "{exe_path}", make sure the file exists (errno {e.errno}).')
        return None

    result = json.loads(result.decode('utf-8'))
    return result


def version_full_to_ints(version_full: str) -> tuple[int, ...]:
    return tuple(int(i) for i in version_full.split('.'))


def get_project_path() -> str:
    return os.path.join(lnx.utils.get_fp_build(), 'windows-hl-build')


def get_project_name():
    wrd = bpy.data.worlds['Lnx']
    return lnx.utils.safesrc(wrd.lnx_project_name + '-' + wrd.lnx_project_version)


def get_sln_path() -> str:
    project_path = get_project_path()
    project_name = get_project_name()
    return os.path.join(project_path, project_name + '.sln')


def get_vcxproj_path() -> str:
    project_name = get_project_name()
    project_path = get_project_path()
    return os.path.join(project_path, project_name + '.vcxproj')


def fetch_project_version() -> tuple[Optional[str], Optional[str], Optional[str]]:
    version_major = None
    version_min_full = None

    try:
        # References:
        # https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2022#file-header
        # https://github.com/Kode/kmake/blob/a104a89b55218054ceed761d5bc75d6e5cd60573/kmake/src/Exporters/VisualStudioExporter.ts#L188-L225
        with open(get_sln_path(), 'r') as file:
            for linenum, line in enumerate(file):
                line = line.strip()

                if linenum == 1:
                    if line == '# Visual Studio Version 17':
                        version_major = 17
                    elif line == '# Visual Studio Version 16':
                        version_major = 16
                    elif line == '# Visual Studio 15':
                        version_major = 15
                    elif line == '# Visual Studio 14':
                        version_major = 14
                    elif line == '# Visual Studio 2013':
                        version_major = 12
                    elif line == '# Visual Studio 2012':
                        version_major = 11
                    elif line == '# Visual Studio 2010':
                        version_major = 10
                    else:
                        log.warn(f'Could not parse Visual Studio version. Invalid major version, parsed {line}')
                        return None, None, 'err_invalid_version_major'

                elif linenum == 3 and version_major >= 12:
                    match = _REGEX_SLN_MIN_VERSION.match(line)
                    if match:
                        version_min_full = match.group(1)
                        break

                    log.warn(f'Could not parse Visual Studio version. Invalid full version, parsed {line}')
                    return None, None, 'err_invalid_version_full'

    except FileNotFoundError:
        return None, None, 'err_file_not_found'

    return str(version_major), version_min_full, None