#!/usr/bin/env python3

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


# ============================================================
# Einstellungen
# ============================================================

PROJECT_ROOT = Path(
    "/opt/rpgmultiplayer/updates/chaapaai/source"
)

VERSION = (
    sys.argv[1]
    if len(sys.argv) > 1
    else "1.0.0"
)

OUTPUT_DIR = Path(
    "/opt/rpgmultiplayer/updates/chaapaai/versions"
) / VERSION

OUTPUT_FILE = (
    OUTPUT_DIR /
    "manifest.json"
)

EXCLUDE_FILE = (
    PROJECT_ROOT /
    "update_exclude.txt"
)


# ============================================================
# Ausgeschlossene Ordner
# ============================================================

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


# ============================================================
# Dateien, die grundsaetzlich nicht ins Manifest gehoeren
# ============================================================

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


# ============================================================
# Schutzmuster aus update_exclude.txt laden
#
# Beispiele:
#
# Test.*
# serverpasswort
# serverpasswort.*
# config/server.*
# *.key
# ============================================================

def load_exclude_patterns():

    patterns = []


    if not EXCLUDE_FILE.exists():

        print(
            "Keine update_exclude.txt gefunden."
        )

        return patterns


    try:

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

            for raw_line in f:

                line = raw_line.strip()


                # Leerzeilen ignorieren

                if not line:

                    continue


                # Kommentare ignorieren

                if line.startswith("#"):

                    continue


                # Einheitliche Pfadschreibweise

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


                # Fuehrenden ./ entfernen

                if line.startswith("./"):

                    line = line[2:]


                if line:

                    patterns.append(
                        line
                    )


    except UnicodeDecodeError:

        print(
            "ERROR: update_exclude.txt ist "
            "nicht als UTF-8 gespeichert."
        )

        sys.exit(1)


    return patterns


# ============================================================
# Pruefen, ob Datei ausgeschlossen ist
# ============================================================

def is_excluded(
    relative_path,
    filename,
    patterns
):

    path_string = (
        str(relative_path)
        .replace(
            "\\",
            "/"
        )
    )


    # --------------------------------------------------------
    # Exakte / Wildcard-Pruefung
    # --------------------------------------------------------

    for pattern in patterns:

        pattern = (
            pattern
            .replace(
                "\\",
                "/"
            )
            .strip()
        )


        if not pattern:

            continue


        # ----------------------------------------------------
        # 1. Vollstaendiger relativer Pfad
        #
        # Beispiel:
        # config/server.*
        # ----------------------------------------------------

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

            return True


        # ----------------------------------------------------
        # 2. Nur Dateiname pruefen
        #
        # Beispiel:
        # Test.*
        #
        # Dadurch wird auch:
        # Data/Test.rxdata
        # Graphics/Test.png
        #
        # erkannt.
        # ----------------------------------------------------

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

            return True


    return False


# ============================================================
# SHA-256 berechnen
# ============================================================

def calculate_sha256(
    path
):

    sha256 = hashlib.sha256()


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

        while True:

            chunk = f.read(
                1024 * 1024
            )


            if not chunk:

                break


            sha256.update(
                chunk
            )


    return sha256.hexdigest()


# ============================================================
# Manifest erstellen
# ============================================================

def create_manifest():

    if not PROJECT_ROOT.exists():

        print(
            "ERROR: Source directory not found:"
        )

        print(
            PROJECT_ROOT
        )

        sys.exit(1)


    exclude_patterns = (
        load_exclude_patterns()
    )


    files = {}


    excluded_files = []


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

    print(
        "Chaapaai Manifest Generator"
    )

    print(
        "Version: " +
        VERSION
    )

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

    print()


    print(
        "Source:"
    )

    print(
        PROJECT_ROOT
    )

    print()


    print(
        "Exclude patterns:"
    )


    if exclude_patterns:

        for pattern in exclude_patterns:

            print(
                "  " +
                pattern
            )

    else:

        print(
            "  keine"
        )


    print()


    # ========================================================
    # Dateien durchsuchen
    # ========================================================

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

        # Ausgeschlossene Ordner entfernen

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


        for filename in filenames:


            # ------------------------------------------------
            # Feste Ausschlussdateien
            # ------------------------------------------------

            if filename in EXCLUDED_FILES:

                continue


            full_path = (
                Path(root) /
                filename
            )


            relative_path = (
                full_path.relative_to(
                    PROJECT_ROOT
                )
            )


            relative_string = (
                str(relative_path)
                .replace(
                    "\\",
                    "/"
                )
            )


            # ------------------------------------------------
            # update_exclude.txt / Wildcards
            # ------------------------------------------------

            if is_excluded(
                relative_path,
                filename,
                exclude_patterns
            ):

                excluded_files.append(
                    relative_string
                )

                print(
                    "[EXCLUDED] " +
                    relative_string
                )

                continue


            # ------------------------------------------------
            # Hash und Dateigroesse
            # ------------------------------------------------

            try:

                file_size = (
                    full_path.stat().st_size
                )


                file_hash = (
                    calculate_sha256(
                        full_path
                    )
                )


            except Exception as e:

                print()

                print(
                    "ERROR processing:"
                )

                print(
                    relative_string
                )

                print(
                    str(e)
                )

                sys.exit(1)


            # ------------------------------------------------
            # Manifest-Eintrag
            # ------------------------------------------------

            files[
                relative_string
            ] = {

                "size":
                    file_size,

                "sha256":
                    file_hash

            }


            print(
                relative_string
            )


    # ========================================================
    # Ausgabeordner
    # ========================================================

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


    # ========================================================
    # Manifest
    # ========================================================

    manifest = {

        "project":
            "chaapaai",

        "version":
            VERSION,

        "file_count":
            len(files),

        "files":
            files

    }


    # ========================================================
    # Schreiben
    # ========================================================

    with OUTPUT_FILE.open(
        "w",
        encoding="utf-8"
    ) as f:

        json.dump(
            manifest,
            f,
            indent=2,
            ensure_ascii=True
        )


    # ========================================================
    # Ergebnis
    # ========================================================

    print()

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

    print(
        "Manifest created."
    )

    print(
        "Files: " +
        str(
            len(files)
        )
    )

    print(
        "Excluded: " +
        str(
            len(excluded_files)
        )
    )

    print()

    print(
        "Output:"
    )

    print(
        OUTPUT_FILE
    )

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

    print()


# ============================================================
# Start
# ============================================================

if __name__ == "__main__":

    create_manifest()