"""Collection of tasks. Run using `doit <task>`."""

from pathlib import Path
import subprocess
from functools import cache
from gitignore_parser import parse_gitignore
from typing import Dict, Optional, List
from enum import Enum
from shutil import rmtree, copy, copytree, make_archive
import platform
import tomllib

root_dir = Path(__file__).parent
rhubarb_dir = root_dir / 'rhubarb'
rhubarb_build_dir = rhubarb_dir / 'build'
extras_dir = root_dir / 'extras'


def task_format():
    """Format source files"""

    files_by_formatters = get_files_by_formatters()
    for formatter, files in files_by_formatters.items():
        yield {
            'name': formatter.value,
            'actions': [(format, [files, formatter])],
            'file_dep': files,
        }


def task_check_formatted():
    """Fails unless source files are formatted"""

    files_by_formatters = get_files_by_formatters()
    for formatter, files in files_by_formatters.items():
        yield {
            'basename': 'check-formatted',
            'name': formatter.value,
            'actions': [(format, [files, formatter], {'check_only': True})],
        }


class Formatter(Enum):
    """A source code formatter."""

    CLANG_FORMAT = 'clang-format'
    GERSEMI = 'gersemi'
    PRETTIER = 'prettier'
    RUFF = 'ruff'


def format(files: List[Path], formatter: Formatter, *, check_only: bool = False):
    match formatter:
        case Formatter.CLANG_FORMAT:
            # Pass relative paths to avoid exceeding the maximum command line length
            relative_paths = [file.relative_to(root_dir) for file in files]
            subprocess.run(
                ['clang-format', '--dry-run' if check_only else '-i', '--Werror', *relative_paths],
                cwd=root_dir,
                check=True,
            )
        case Formatter.GERSEMI:
            subprocess.run(['gersemi', '--check' if check_only else '-i', *files], check=True)
        case Formatter.PRETTIER:
            subprocess.run(
                [
                    *['deno', 'run', '-A', 'npm:prettier@3.4.2'],
                    *['--check' if check_only else '--write', '--log-level', 'warn', *files],
                ],
                check=True,
            )
        case Formatter.RUFF:
            subprocess.run(
                ['ruff', '--quiet', 'format', *(['--check'] if check_only else []), *files],
                check=True,
            )
        case _:
            raise ValueError(f'Unknown formatter: {formatter}')


def task_configure_rhubarb():
    """Configure CMake for the Rhubarb binary"""

    def configure_rhubarb():
        ensure_dir(rhubarb_build_dir)
        subprocess.run(['cmake', '..'], cwd=rhubarb_build_dir, check=True)

    return {'basename': 'configure-rhubarb', 'actions': [configure_rhubarb]}


def task_build_rhubarb():
    """Build the Rhubarb binary"""

    def build_rhubarb():
        subprocess.run(
            ['cmake', '--build', '.', '--config', 'Release'], cwd=rhubarb_build_dir, check=True
        )

    return {'basename': 'build-rhubarb', 'actions': [build_rhubarb]}


def task_build_spine():
    """Build Rhubarb for Spine"""

    def build_spine():
        onWindows = platform.system() == 'Windows'
        subprocess.run(
            ['gradlew.bat' if onWindows else './gradlew', 'build'],
            cwd=extras_dir / 'esoteric-software-spine',
            check=True,
            shell=onWindows,
        )

    return {'basename': 'build-spine', 'actions': [build_spine]}


def task_package():
    """Package all artifacts into an archive file"""

    with open(root_dir / 'app-info.toml', 'rb') as file:
        appInfo = tomllib.load(file)

    os_name = 'macOS' if platform.system() == 'Darwin' else platform.system()
    file_name = f"{appInfo['appName'].replace(' ', '-')}-{appInfo['appVersion']}-{os_name}"

    artifacts_dir = ensure_empty_dir(root_dir / 'artifacts')
    tree_dir = ensure_dir(artifacts_dir.joinpath(file_name))

    def collect_artifacts():
        # Misc. files
        copy(root_dir / 'README.adoc', tree_dir)
        copy(root_dir / 'LICENSE.md', tree_dir)
        copy(root_dir / 'CHANGELOG.md', tree_dir)
        copytree(root_dir / 'img', tree_dir / 'img')

        # Rhubarb
        subprocess.run(
            ['cmake', '--install', '.', '--prefix', tree_dir], cwd=rhubarb_build_dir, check=True
        )

        # Adobe After Effects script
        src = extras_dir / 'adobe-after-effects'
        dst_extras_dir = ensure_dir(tree_dir / 'extras')
        dst = ensure_dir(dst_extras_dir / 'adobe-after-effects')
        copy(src / 'README.adoc', dst)
        copy(src / 'Rhubarb Lip Sync.jsx', dst)

        # Rhubarb for Spine
        src = extras_dir / 'esoteric-software-spine'
        dst = ensure_dir(dst_extras_dir / 'esoteric-software-spine')
        copy(src / 'README.adoc', dst)
        for file in (src / 'build' / 'libs').iterdir():
            copy(file, dst)

        # Magix Vegas
        src = extras_dir / 'magix-vegas'
        dst = ensure_dir(dst_extras_dir / 'magix-vegas')
        copy(src / 'README.adoc', dst)
        copy(src / 'Debug Rhubarb.cs', dst)
        copy(src / 'Debug Rhubarb.cs.config', dst)
        copy(src / 'Import Rhubarb.cs', dst)
        copy(src / 'Import Rhubarb.cs.config', dst)

    def pack_artifacts():
        zip_base_name = tree_dir
        format = 'gztar' if platform.system() == 'Linux' else 'zip'
        make_archive(zip_base_name, format, tree_dir)

    return {
        'actions': [collect_artifacts, pack_artifacts],
        'task_dep': ['build-rhubarb', 'build-spine'],
    }


@cache
def get_files_by_formatters() -> Dict[Formatter, List[Path]]:
    """Returns a dict with all formattable code files grouped by formatter."""

    is_gitignored = parse_gitignore(root_dir / '.gitignore')

    def is_hidden(path: Path):
        return path.name.startswith('.')

    def is_third_party(path: Path):
        return path.name == 'lib' or path.name == 'gradle'

    result = {formatter: [] for formatter in Formatter}

    def visit(dir: Path):
        for path in dir.iterdir():
            if is_gitignored(path) or is_hidden(path) or is_third_party(path):
                continue

            if path.is_file():
                formatter = get_formatter(path)
                if formatter is not None:
                    result[formatter].append(path)
            else:
                visit(path)

    visit(root_dir)
    return result


def get_formatter(path: Path) -> Optional[Formatter]:
    """Returns the formatter to use for the given code file, if any."""

    match path.suffix.lower():
        case '.c' | '.cpp' | '.h':
            return Formatter.CLANG_FORMAT
        case '.cmake':
            return Formatter.GERSEMI
        case _ if path.name.lower() == 'cmakelists.txt':
            return Formatter.GERSEMI
        case '.js' | '.jsx' | '.ts':
            return Formatter.PRETTIER
        case '.py':
            return Formatter.RUFF


def ensure_dir(dir: Path) -> Path:
    """Makes sure the given directory exists."""

    if not dir.exists():
        dir.mkdir()
    return dir


def ensure_empty_dir(dir: Path) -> Path:
    """Makes sure the given directory exists and is empty."""

    if dir.exists():
        rmtree(dir)
    return ensure_dir(dir)