#!/usr/bin/env python3

import fnmatch
import hashlib
import json
import os
import shutil
import sys
from pathlib import Path


BASE_DIR = Path(
    "/opt/rpgmultiplayer/updates/chaapaai"
)

SOURCE_DIR = BASE_DIR / "source"
VERSIONS_DIR = BASE_DIR / "versions"
LATEST_FILE = BASE_DIR / "latest.json"
EXCLUDE_FILE = SOURCE_DIR / "update_exclude.txt"

# The current launcher build is supplied separately from source/.
LAUNCHER_SOURCE_DIR = BASE_DIR / "release_launcher"

LAUNCHER_EXE = "Chaapaai_Launcher.exe"
LAUNCHER_UPDATER_EXE = "Chaapaai_LauncherUpdater.exe"

EXCLUDED_DIRS = {
    ".git",
    "__pycache__",
}

LOCAL_ONLY_FILES = {
    "manifest.json",
    "update_exclude.txt",
}


def load_exclude_patterns():

    patterns = []

    if not EXCLUDE_FILE.exists():
        return patterns

    with EXCLUDE_FILE.open(
        "r",
        encoding="utf-8-sig"
    ) as f:

        for raw_line in f:

            line = raw_line.strip()

            if not line:
                continue

            if line.startswith("#"):
                continue

            line = line.replace("\\", "/")

            if line.startswith("./"):
                line = line[2:]

            if line:
                patterns.append(line)

    return patterns


def is_excluded(relative_path, patterns):

    path_string = relative_path.as_posix()
    filename = relative_path.name

    for pattern in patterns:

        if fnmatch.fnmatch(
            path_string.lower(),
            pattern.lower()
        ):
            return True

        if fnmatch.fnmatch(
            filename.lower(),
            pattern.lower()
        ):
            return True

    return False


def sha256_file(path):

    digest = hashlib.sha256()

    with path.open("rb") as f:

        while True:

            chunk = f.read(
                1024 * 1024
            )

            if not chunk:
                break

            digest.update(chunk)

    return digest.hexdigest()


def collect_game_files(version):

    patterns = load_exclude_patterns()

    files = {}
    excluded = []

    for root, dirs, filenames in os.walk(
        SOURCE_DIR
    ):

        dirs[:] = [
            d
            for d in dirs
            if d not in EXCLUDED_DIRS
        ]

        for filename in filenames:

            if filename in LOCAL_ONLY_FILES:
                continue

            full_path = Path(root) / filename

            relative_path = (
                full_path.relative_to(
                    SOURCE_DIR
                )
            )

            if is_excluded(
                relative_path,
                patterns
            ):

                excluded.append(
                    relative_path.as_posix()
                )

                continue

            relative_string = (
                relative_path.as_posix()
            )

            # version.txt is release metadata for the source/release.
            # It is not installed on the client anymore.
            if relative_string == "version.txt":
                continue

            files[relative_string] = {
                "size": full_path.stat().st_size,
                "sha256": sha256_file(full_path)
            }

    return files, excluded


def copy_game_files(
    release_dir,
    files
):

    for relative_string in files:

        source = (
            SOURCE_DIR /
            relative_string
        )

        destination = (
            release_dir /
            "files" /
            relative_string
        )

        destination.parent.mkdir(
            parents=True,
            exist_ok=True
        )

        shutil.copy2(
            source,
            destination
        )


def write_manifest(
    release_dir,
    version,
    files
):

    manifest = {
        "project": "chaapaai",
        "version": version,
        "file_count": len(files),
        "files": files
    }

    manifest_path = (
        release_dir /
        "manifest.json"
    )

    manifest_path.write_text(
        json.dumps(
            manifest,
            indent=2,
            ensure_ascii=True
        ),
        encoding="utf-8"
    )

    return manifest_path


def copy_launcher_files(
    release_dir
):

    launcher_dir = (
        release_dir /
        "launcher"
    )

    launcher_dir.mkdir(
        parents=True,
        exist_ok=True
    )

    launcher_source = (
        LAUNCHER_SOURCE_DIR /
        LAUNCHER_EXE
    )

    updater_source = (
        LAUNCHER_SOURCE_DIR /
        LAUNCHER_UPDATER_EXE
    )

    if not launcher_source.exists():
        raise RuntimeError(
            "Launcher-EXE fehlt:\n" +
            str(launcher_source)
        )

    if not updater_source.exists():
        raise RuntimeError(
            "Launcher-Updater-EXE fehlt:\n" +
            str(updater_source)
        )

    launcher_destination = (
        launcher_dir /
        LAUNCHER_EXE
    )

    updater_destination = (
        launcher_dir /
        LAUNCHER_UPDATER_EXE
    )

    shutil.copy2(
        launcher_source,
        launcher_destination
    )

    shutil.copy2(
        updater_source,
        updater_destination
    )

    return (
        launcher_destination,
        updater_destination
    )


def write_latest(
    version
):

    latest = {
        "project": "chaapaai",
        "version": version,
        "manifest":
            f"versions/{version}/manifest.json",
        "files":
            f"versions/{version}/files/",
        "launcher":
            f"versions/{version}/launcher/{LAUNCHER_EXE}",
        "launcher_updater":
            f"versions/{version}/launcher/{LAUNCHER_UPDATER_EXE}"
    }

    LATEST_FILE.write_text(
        json.dumps(
            latest,
            indent=2,
            ensure_ascii=True
        ),
        encoding="utf-8"
    )


def main():

    if len(sys.argv) != 2:

        print("Usage:")
        print(
            "  python3 publish_release_with_launcher.py 1.2.0"
        )
        sys.exit(1)

    version = sys.argv[1].strip()

    if not version:
        print("ERROR: Version fehlt.")
        sys.exit(1)

    if not SOURCE_DIR.exists():
        print("ERROR: Source directory not found:")
        print(SOURCE_DIR)
        sys.exit(1)

    if not LAUNCHER_SOURCE_DIR.exists():
        print("ERROR: release_launcher directory not found:")
        print(LAUNCHER_SOURCE_DIR)
        sys.exit(1)

    release_dir = (
        VERSIONS_DIR /
        version
    )

    if release_dir.exists():

        print(
            "ERROR: Release existiert bereits:"
        )

        print(release_dir)
        print("Es wurde nichts veraendert.")
        sys.exit(1)

    print()
    print(
        "=============================================="
    )
    print(
        "Chaapa'ai Release Publisher"
    )
    print(
        "Version: " + version
    )
    print(
        "=============================================="
    )
    print()

    game_files, excluded = (
        collect_game_files(
            version
        )
    )

    print(
        "Source-Dateien: " +
        str(len(game_files))
    )

    print(
        "Ausgeschlossen: " +
        str(len(excluded))
    )

    print(
        "Launcher-Quelle: " +
        str(LAUNCHER_SOURCE_DIR)
    )

    print()

    release_dir.mkdir(
        parents=True,
        exist_ok=False
    )

    try:

        print(
            "Kopiere Spieldateien..."
        )

        copy_game_files(
            release_dir,
            game_files
        )

        print(
            "Erstelle Manifest..."
        )

        manifest_path = write_manifest(
            release_dir,
            version,
            game_files
        )

        print(
            "Kopiere Launcher-Dateien..."
        )

        launcher_path, updater_path = (
            copy_launcher_files(
                release_dir
            )
        )

        print(
            "Aktualisiere latest.json..."
        )

        write_latest(
            version
        )

    except Exception:

        print()
        print(
            "FEHLER: Release konnte nicht "
            "vollstaendig erstellt werden."
        )

        print(
            "Entferne unvollstaendiges Release..."
        )

        try:
            shutil.rmtree(
                release_dir
            )
        except Exception:
            pass

        raise

    print()
    print(
        "=============================================="
    )

    print(
        "Release erfolgreich erstellt."
    )

    print(
        "Version: " + version
    )

    print(
        "Spieldateien: " +
        str(len(game_files))
    )

    print()

    print(
        "Manifest:"
    )

    print(
        manifest_path
    )

    print()

    print(
        "Launcher:"
    )

    print(
        launcher_path
    )

    print()

    print(
        "Launcher-Updater:"
    )

    print(
        updater_path
    )

    print()

    print(
        "latest.json wurde auf " +
        version +
        " gesetzt."
    )

    print(
        "=============================================="
    )

    print()


if __name__ == "__main__":
    main()
