# Copyright (C) 2022 b5327157
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

import itertools
import os
import pathlib
import shutil
import tempfile

from contextlib import contextmanager
from dataclasses import dataclass
from typing import Iterator, List, Optional, Sequence

import nox

nox.needs_version = ">= 2022.1.7"

nox.options.default_venv_backend = "venv"
nox.options.error_on_external_run = True
nox.options.error_on_missing_interpreters = True
nox.options.sessions = ["lint"]


@nox.session(python="3.10")
def lint(session: nox.Session) -> None:
    session.install("black[colorama] >= 22.3.0, < 23")
    session.install("flake8 >= 4.0.1, < 5")
    session.install("flake8-deprecated >= 1.3, < 2")
    session.install("isort[colors] >= 5.10.1, < 6")
    session.install("mypy >= 0.941, < 1")
    session.install("nox >= 2022.1.7, < 2023")
    session.install("pep8-naming >= 0.12.1, < 1")

    session.install(".")

    posargs_paths = session.posargs
    fix = False
    if posargs_paths and posargs_paths[0] == "--fix":
        posargs_paths = posargs_paths[1:]
        fix = True

    paths = ["no_vtf", "noxfile.py"]
    if posargs_paths:
        paths = posargs_paths

    if not fix:
        session.run("mypy", "--pretty", "--show-error-context", *paths)
        session.run("flake8", *paths)
        session.run("isort", "--check", "--diff", *paths)
        session.run("black", "--check", "--diff", *paths)
    else:
        session.run("isort", *paths)
        session.run("black", *paths)


@nox.session(python="3.10")
def package(session: nox.Session) -> None:
    session.install("build >= 0.7.0, < 1")

    session.run("python", "-m", "build", silent=True)

    path_dist = pathlib.Path("dist")
    path_sdist = next(path_dist.glob("*.tar.gz"))
    path_wheel = next(path_dist.glob("*.whl"))

    # run even with the --no-install flag
    session.run("pip", "install", str(path_wheel), silent=True)

    executable = ["python", "-m", "no_vtf"]
    session.run(*executable, "--version")

    with SanityTest.with_archived_samples(session, executable) as sanity_test:
        sanity_test.readback()

    if len(session.posargs) >= 1:
        shutil.copy2(path_sdist, session.posargs[0])

    if len(session.posargs) >= 2:
        shutil.copy2(path_wheel, session.posargs[1])


@nox.session(python="3.10")
def freeze(session: nox.Session) -> None:
    session.install("pyinstaller >= 4.9, < 5")

    # installing in editable mode will ensure the _version.py file is generated by setuptools_scm
    # run even with the --no-install flag
    session.run("pip", "install", "-e", ".", silent=True)

    session.run("imageio_download_bin", "--package-dir", silent=True)

    pyinstaller_datas: list[tuple[str, str]] = []
    pyinstaller_args: list[str] = []

    pyinstaller_args.extend(["--log-level", "WARN"])
    pyinstaller_args.extend(["--name", "no_vtf"])
    pyinstaller_args.extend(["--icon", "resources/pyinstaller/empty.ico"])

    if os.name == "posix":
        pyinstaller_datas.append(("resources/pyinstaller/no_vtf.desktop", "."))

    # https://github.com/rsalmei/alive-progress/issues/123
    pyinstaller_args.extend(["--collect-data", "grapheme"])

    # https://github.com/imageio/imageio/issues/690#issuecomment-974605991
    # plugin list: imageio/docs/conf.py
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.bsdf"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.dicom"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.feisem"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.ffmpeg"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.fits"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.freeimage"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.gdal"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.lytro"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.npz"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.pillow_legacy"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.simpleitk"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.spe"])
    pyinstaller_args.extend(["--hidden-import", "imageio.plugins.swf"])

    # bundle plugin binary dependencies downloaded by imageio_download_bin
    pyinstaller_args.extend(["--collect-binaries", "imageio"])

    pyinstaller_args.append("--noconfirm")
    pyinstaller_args.append("--clean")

    for pyinstaller_data in pyinstaller_datas:
        pyinstaller_args.extend(["--add-data", os.pathsep.join(pyinstaller_data)])

    session.run("pyinstaller", *pyinstaller_args, "no_vtf/__main__.py")
    shutil.rmtree("dist/no_vtf/imageio/resources/images", ignore_errors=True)

    executable = "dist/no_vtf/no_vtf"
    paths = ["."]
    nox.command.run([executable, "--version"], paths=paths, external=True)

    with SanityTest.with_archived_samples(session, [executable], paths) as sanity_test:
        sanity_test.readback()

    if not session.posargs:
        return

    archive_path = session.posargs[0]
    archive_base_name: str
    archive_format: str
    if (archive_base_name := archive_path.removesuffix(".tar.xz")) != archive_path:
        archive_format = "xztar"
    elif (archive_base_name := archive_path.removesuffix(".zip")) != archive_path:
        archive_format = "zip"
    else:
        raise RuntimeError("Unsupported archive format")

    shutil.make_archive(archive_base_name, archive_format, root_dir="dist", base_dir="no_vtf")


@nox.session(python="3.10")
def publish(session: nox.Session) -> None:
    if not session.posargs:
        session.error("Path to API token file was not provided")

    session.install("twine >= 4.0.0, < 5")

    dist = pathlib.Path("dist")
    dist_files = [path for path in dist.iterdir() if path.is_file()]
    dist_args = [str(path) for path in dist_files]

    session.run("twine", "check", "--strict", *dist_args)

    upload_args = []
    upload_args.append("--non-interactive")
    upload_args.append("--disable-progress-bar")
    upload_args.extend(dist_args)

    env = session.env.copy()
    env["TWINE_USERNAME"] = "__token__"
    env["TWINE_PASSWORD"] = pathlib.Path(session.posargs[0]).read_text().strip()

    nox.command.run(
        ["twine", "upload", *upload_args],
        env=env,
        paths=session.bin_paths,
        external=True,
    )


@nox.session(python="3.10")
def write_reference(session: nox.Session) -> None:
    if not session.posargs:
        session.error("Path to test samples was not provided")

    # run even with the --no-install flag
    session.run("pip", "install", ".", silent=True)

    executable = ["python", "-m", "no_vtf"]

    base_path = pathlib.Path(session.posargs[0])
    input = base_path / "input"
    output = base_path / "output"

    sanity_test = SanityTest(session=session, executable=executable, input=input, output=output)
    sanity_test.write()


@dataclass(frozen=True, kw_only=True)
class SanityTest:
    session: nox.Session
    executable: Sequence[str]
    paths: Optional[List[str]] = None

    input: pathlib.Path
    output: pathlib.Path

    @classmethod
    @contextmanager
    def with_archived_samples(
        cls, session: nox.Session, executable: Sequence[str], paths: Optional[List[str]] = None
    ) -> Iterator[SanityTest]:
        with samples_archive_view() as samples_directory:
            input = samples_directory / "input"
            output = samples_directory / "output"
            sanity_test = cls(
                session=session, executable=executable, paths=paths, input=input, output=output
            )
            yield sanity_test

    def write(self) -> None:
        args: list[str] = []
        args.append("--always-write")
        args.append("--no-compress")

        self._run_combinations(args)

    def readback(self) -> None:
        args: list[str] = []
        args.append("--no-write")
        args.append("--readback")

        self._run_combinations(args)

    def _run_combinations(self, args: Sequence[str]) -> None:
        mipmaps = [[], ["--mipmaps"]]
        separate_channels = [[], ["--separate-channels"]]

        combinations = [mipmaps, separate_channels]
        for combination in itertools.product(*combinations):
            combination_flattened = itertools.chain.from_iterable(combination)
            self._run_formats([*args, *combination_flattened])

    def _run_formats(self, args: Sequence[str]) -> None:
        args = list(args)

        ldr_formats = ["raw", "png", "tga", "tiff"]
        for ldr_format in ldr_formats:
            self._run([*args, "--ldr-format", ldr_format, "--hdr-format", "skip"])

        hdr_formats = ["raw", "exr", "tiff"]
        for hdr_format in hdr_formats:
            self._run([*args, "--ldr-format", "skip", "--hdr-format", hdr_format])

    def _run(self, args: Sequence[str]) -> None:
        args = list(args)

        args.append("--no-progress")

        args.append(str(self.input))
        args.extend(["--output-dir", str(self.output)])

        paths = self.paths or self.session.bin_paths
        nox.command.run([*self.executable, *args], env=self.session.env, paths=paths, external=True)


@contextmanager
def archive_view(archive: pathlib.Path) -> Iterator[pathlib.Path]:
    with archives_view([archive]) as view_directory:
        yield view_directory


@contextmanager
def archives_view(archives: Sequence[pathlib.Path]) -> Iterator[pathlib.Path]:
    with tempfile.TemporaryDirectory() as view_directory:
        for archive in archives:
            shutil.unpack_archive(archive, view_directory)
        yield pathlib.Path(view_directory)


@contextmanager
def samples_archive_view() -> Iterator[pathlib.Path]:
    samples_path = pathlib.Path("resources/test/samples")
    sample_archives = [path for path in samples_path.iterdir() if path.is_file()]
    with archives_view(sample_archives) as samples_directory:
        yield samples_directory
