#!/usr/bin/env python3
"""Generate the exact Magic120Cell sticker-permutation table.

The extractor compiles a temporary, headless compatibility copy of the
MIT-licensed roice3/Magic120Cell geometry/state engine. It assigns a unique
identity to each of the 7,560 sticker slots, applies every cell/sticker move,
and records only the changed destination/source pairs.

Copyright (c) 2016 Roice Nelson
Magic120Cell source license: MIT
"""

from __future__ import annotations

import argparse
import base64
import hashlib
import json
import shutil
import struct
import subprocess
import tempfile
import zlib
from pathlib import Path


CELL_COUNT = 120
STICKERS_PER_CELL = 63
TWIST_STICKER_COUNT = 62
STATE_SIZE = CELL_COUNT * STICKERS_PER_CELL
MOVE_COUNT = CELL_COUNT * TWIST_STICKER_COUNT
RAW_MAGIC = b"HTM120P1"


COMPAT_STDAFX = r"""
#pragma once

#include <GL/gl.h>
#include <algorithm>
#include <array>
#include <assert.h>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>

using __int8 = std::int8_t;
using __int16 = std::int16_t;
using __int32 = std::int32_t;
using __int64 = std::int64_t;
using BOOL = int;
using HDC = void*;
using HGLRC = void*;
using HWND = void*;

#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif
"""


COMPAT_LOADER_HEADER = r"""
#pragma once

#include "puzzle/state.h"
#include "puzzle/twist.h"

class CLoader
{
public:
    CLoader();
    void saveToFile(int, const CState&, const std::vector<STwist>&, bool);
    void saveScrambledFile(const CState&, const std::vector<STwist>&);
    bool loadFromFile(int&, CState&, std::vector<STwist>&);
    bool loadScrambledFile(CState&, std::vector<STwist>&);
};
"""


COMPAT_RENDERER_HEADER = r"""
#pragma once

#include "vectorND.h"

class IRenderable
{
public:
    virtual ~IRenderable() = default;
    virtual void render(const CVector3D& lookFrom, bool forPicking) = 0;
};
"""


COMPAT_SUPPORT_CPP = r"""
#include <stdafx.h>
#include "helper.h"
#include "loader.h"

void CColor::generateRandom()
{
    m_r = getRandomDouble(1.0);
    m_g = getRandomDouble(1.0);
    m_b = getRandomDouble(1.0);
    m_a = 1.0;
}

void CColor::setColorHLS(double h, double l, double s, double a)
{
    // Color values are irrelevant to permutation extraction, but state
    // initialization requires a stable in-range palette.
    m_r = std::fmod(h + s, 1.0);
    m_g = l;
    m_b = std::fmod(h + 0.5, 1.0);
    m_a = a;
}

void CColor::lighten()
{
    m_r = std::min(1.0, m_r * 1.1);
    m_g = std::min(1.0, m_g * 1.1);
    m_b = std::min(1.0, m_b * 1.1);
}

CLoader::CLoader() = default;
void CLoader::saveToFile(int, const CState&, const std::vector<STwist>&, bool) {}
void CLoader::saveScrambledFile(const CState&, const std::vector<STwist>&) {}
bool CLoader::loadFromFile(int&, CState&, std::vector<STwist>&) { return false; }
bool CLoader::loadScrambledFile(CState&, std::vector<STwist>&) { return false; }
"""


EXTRACTOR_CPP = r"""
#include <stdafx.h>
#include "magic120Cell.h"

namespace
{
    constexpr int CellCount = 120;
    constexpr int StickersPerCell = 63;
    constexpr int StateSize = CellCount * StickersPerCell;

    class CInspectableMagic120Cell final : public CMagic120Cell
    {
    public:
        void setUniqueIdentity()
        {
            for (int cell = 0; cell < CellCount; ++cell)
            {
                for (int sticker = 0; sticker < StickersPerCell; ++sticker)
                {
                    m_state.setStickerColorIndex(
                        cell,
                        sticker,
                        cell * StickersPerCell + sticker);
                }
            }
            m_state.commitChanges();
            m_twistHistory.clear();
        }

        int getIdentityAt(const int destination) const
        {
            return m_state.getStickerColorIndex(
                destination / StickersPerCell,
                destination % StickersPerCell);
        }
    };

    void writeU16(std::ofstream& output, const std::uint16_t value)
    {
        output.write(reinterpret_cast<const char*>(&value), sizeof(value));
    }

    void writeU32(std::ofstream& output, const std::uint32_t value)
    {
        output.write(reinterpret_cast<const char*>(&value), sizeof(value));
    }
}

int main(const int argc, const char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "usage: extractor <raw-output>" << std::endl;
        return 2;
    }

    std::ofstream output(argv[1], std::ios::binary | std::ios::trunc);
    if (!output)
    {
        std::cerr << "unable to open output" << std::endl;
        return 3;
    }

    output.write("HTM120P1", 8);
    writeU32(output, CellCount);
    writeU32(output, StickersPerCell);
    writeU32(output, CellCount * (StickersPerCell - 1));
    writeU32(output, StateSize);

    auto puzzle = std::make_unique<CInspectableMagic120Cell>();
    std::uint64_t totalPairs = 0;
    std::uint16_t minimumPairs = UINT16_MAX;
    std::uint16_t maximumPairs = 0;
    for (int cell = 0; cell < CellCount; ++cell)
    {
        for (int sticker = 1; sticker < StickersPerCell; ++sticker)
        {
            puzzle->setUniqueIdentity();

            STwist twist;
            twist.m_cell = cell;
            twist.m_sticker = sticker;
            twist.m_leftClick = false;
            twist.m_viewRotation = false;
            twist.m_slicemask = 1;
            puzzle->startRotate(twist);
            puzzle->finishRotate();

            std::vector<std::pair<std::uint16_t, std::uint16_t>> changed;
            changed.reserve(256);
            for (int destination = 0; destination < StateSize; ++destination)
            {
                const int source = puzzle->getIdentityAt(destination);
                if (source != destination)
                {
                    if (source < 0 || source >= StateSize)
                    {
                        std::cerr << "invalid source identity" << std::endl;
                        return 4;
                    }
                    changed.emplace_back(
                        static_cast<std::uint16_t>(destination),
                        static_cast<std::uint16_t>(source));
                }
            }

            if (changed.empty() || changed.size() > UINT16_MAX)
            {
                std::cerr << "invalid changed-slot count" << std::endl;
                return 5;
            }
            const auto changedCount =
                static_cast<std::uint16_t>(changed.size());
            writeU16(output, changedCount);
            for (const auto& pair : changed)
            {
                writeU16(output, pair.first);
                writeU16(output, pair.second);
            }

            totalPairs += changedCount;
            minimumPairs = std::min(minimumPairs, changedCount);
            maximumPairs = std::max(maximumPairs, changedCount);
        }
        if ((cell + 1) % 10 == 0)
        {
            std::cerr << "extracted cells " << (cell + 1)
                      << " / " << CellCount << std::endl;
        }
    }

    output.flush();
    if (!output)
    {
        std::cerr << "write failed" << std::endl;
        return 6;
    }
    std::cerr << "moves=" << CellCount * (StickersPerCell - 1)
              << " pairs=" << totalPairs
              << " min=" << minimumPairs
              << " max=" << maximumPairs << std::endl;
    return 0;
}
"""


SOURCE_FILES = (
    "Magic120Cell/workFiles/magic120Cell.cpp",
    "Magic120Cell/workFiles/puzzle.cpp",
    "Magic120Cell/workFiles/cell.cpp",
    "Magic120Cell/workFiles/cellDodec.cpp",
    "Magic120Cell/workFiles/primitives.cpp",
    "Magic120Cell/workFiles/settings.cpp",
    "geometryLib/vectorND.cpp",
    "geometryLib/polygon.cpp",
    "geometryLib/gl/displayList.cpp",
    "geometryLib/puzzle/state.cpp",
    "geometryLib/puzzle/twist.cpp",
    "geometryLib/puzzle/twistHistory.cpp",
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--donor-root",
        type=Path,
        default=Path(
            "/home/dev/src/Workspaces/HyperTwist/mirrors/permissive/"
            "roice3/Magic120Cell"
        ),
    )
    parser.add_argument("--output-inl", required=True, type=Path)
    parser.add_argument("--output-manifest", required=True, type=Path)
    parser.add_argument("--keep-raw", type=Path)
    return parser.parse_args()


def patch_compatibility_tree(root: Path) -> None:
    (root / "Magic120Cell/stdafx.h").write_text(COMPAT_STDAFX, encoding="utf-8")
    (root / "Magic120Cell/workFiles/loader.h").write_text(
        COMPAT_LOADER_HEADER, encoding="utf-8"
    )
    (root / "geometryLib/renderer.h").write_text(
        COMPAT_RENDERER_HEADER, encoding="utf-8"
    )
    matrix_text = (root / "geometryLib/matrix4D.h").read_text(encoding="utf-8-sig")
    matrix_text = matrix_text.replace(
        "m[i][j] operation= rhs.m[i][j];",
        "m[i][j] operation ## = rhs.m[i][j];",
    )
    matrix_text = matrix_text.replace(
        "m[i][j] operation= a;",
        "m[i][j] operation ## = a;",
    )
    matrix_text = matrix_text.replace(
        "DO_OP_MATRIX_INPLACE(+);",
        """for( int i=0; i<4; i++ )
\t\t\tfor( int j=0; j<4; j++ )
\t\t\t\tm[i][j] += rhs.m[i][j];
\t\treturn( *this );""",
    )
    matrix_text = matrix_text.replace(
        "DO_OP_M_SCALAR_INPLACE(*);",
        """for( int i=0; i<4; i++ )
\t\t\tfor( int j=0; j<4; j++ )
\t\t\t\tm[i][j] *= a;
\t\treturn( *this );""",
    )
    (root / "geometryLib/matrix4D.h").write_text(matrix_text, encoding="utf-8")
    (root / "geometryLib/matrix4d.h").write_text(matrix_text, encoding="utf-8")
    (root / "geometryLib/gl/displayList.h").write_text(
        """#pragma once

#include <GL/gl.h>

class CDisplayList
{
public:
    CDisplayList();
    ~CDisplayList();
    CDisplayList(const CDisplayList&);
    CDisplayList& operator=(const CDisplayList&);
    void start(bool execute = true);
    void end() const;
    bool isRecorded() const;
    bool call() const;
    void clear();

private:
    GLuint m_dl;
};
""",
        encoding="utf-8",
    )

    vector_header = root / "geometryLib/vectorND.h"
    vector_text = vector_header.read_text(encoding="utf-8-sig")
    vector_text = vector_text.replace(
        "m_components[i] operation= rhs.m_components[i];",
        "m_components[i] operation ## = rhs.m_components[i];",
    )
    vector_text = vector_text.replace(
        "m_components[i] operation= a;",
        "m_components[i] operation ## = a;",
    )
    old_nan_check = """inline bool isNaN( double value )
{
\tint floatType = _fpclass( value );
\treturn
\t\t_FPCLASS_SNAN == floatType ||
\t\t_FPCLASS_QNAN == floatType ||
\t\t_FPCLASS_NINF == floatType ||
\t\t_FPCLASS_PINF == floatType;
}"""
    if old_nan_check not in vector_text:
        raise RuntimeError("expected donor floating-point classification block missing")
    vector_text = vector_text.replace(
        old_nan_check,
        """inline bool isNaN( double value )
{
\treturn !std::isfinite( value );
}""",
    )
    vector_header.write_text(vector_text, encoding="utf-8")

    polygon_path = root / "geometryLib/polygon.cpp"
    polygon_text = polygon_path.read_text(encoding="cp1252")
    polygon_text = polygon_text.replace(
        "s.m_p2 * finiteScale : s.m_p1",
        "CVector3D( s.m_p2 * finiteScale ) : s.m_p1",
    )
    polygon_text = polygon_text.replace(
        "s.m_p1 * finiteScale : s.m_p2",
        "CVector3D( s.m_p1 * finiteScale ) : s.m_p2",
    )
    polygon_path.write_text(polygon_text, encoding="utf-8")

    replacements = {
        "Magic120Cell/workFiles/cellDodec.cpp": (
            ('#include "CellDodec.h"', '#include "cellDodec.h"'),
            ("__super::operator = ( rhs );", "Cell::operator = ( rhs );"),
            ("__super::getSticker( sticker )", "Cell::getSticker( sticker )"),
        ),
        "Magic120Cell/workFiles/magic120Cell.cpp": (
            ('#include "magic120cell.h"', '#include "magic120Cell.h"'),
            ("__super::render( lookFrom, forPicking );", "CPuzzle::render( lookFrom, forPicking );"),
            ("__super::resetView();", "CPuzzle::resetView();"),
        ),
    }
    for relative_path, path_replacements in replacements.items():
        path = root / relative_path
        text = path.read_text(encoding="utf-8-sig")
        for old, new in path_replacements:
            if old not in text:
                raise RuntimeError(f"expected donor source token missing: {old}")
            text = text.replace(old, new)
        path.write_text(text, encoding="utf-8")


def compile_and_extract(root: Path, raw_output: Path) -> str:
    support_cpp = root / "compat_support.cpp"
    extractor_cpp = root / "extractor.cpp"
    executable = root / "magic120cell-extractor"
    support_cpp.write_text(COMPAT_SUPPORT_CPP, encoding="utf-8")
    extractor_cpp.write_text(EXTRACTOR_CPP, encoding="utf-8")

    command = [
        "clang++",
        "-std=c++14",
        "-O2",
        "-g",
        "-fno-omit-frame-pointer",
        "-fsanitize=address,undefined",
        "-ffunction-sections",
        "-fdata-sections",
        "-Wno-unknown-pragmas",
        "-I",
        str(root / "Magic120Cell"),
        "-I",
        str(root / "Magic120Cell/workFiles"),
        "-I",
        str(root / "geometryLib"),
        str(extractor_cpp),
        str(support_cpp),
        *(str(root / relative_path) for relative_path in SOURCE_FILES),
        "-Wl,--gc-sections",
        "-lGL",
        "-o",
        str(executable),
    ]
    compile_result = subprocess.run(
        command, check=False, capture_output=True, text=True
    )
    if compile_result.returncode != 0:
        raise RuntimeError(
            "Magic120Cell extractor compile failed:\n"
            f"{compile_result.stdout}\n{compile_result.stderr}"
        )

    extract_result = subprocess.run(
        [str(executable), str(raw_output)],
        check=False,
        capture_output=True,
        text=True,
    )
    if extract_result.returncode != 0:
        raise RuntimeError(
            "Magic120Cell extractor execution failed:\n"
            f"exit_code={extract_result.returncode}\n"
            f"{extract_result.stdout}\n{extract_result.stderr}"
        )
    return extract_result.stderr.strip()


def validate_raw_table(raw: bytes) -> dict[str, int]:
    if len(raw) < 24 or raw[:8] != RAW_MAGIC:
        raise RuntimeError("raw permutation table has an invalid header")
    cell_count, sticker_count, move_count, state_size = struct.unpack_from(
        "<IIII", raw, 8
    )
    if (
        cell_count != CELL_COUNT
        or sticker_count != STICKERS_PER_CELL
        or move_count != MOVE_COUNT
        or state_size != STATE_SIZE
    ):
        raise RuntimeError("raw permutation table dimensions are invalid")

    cursor = 24
    minimum_pairs = 1 << 30
    maximum_pairs = 0
    total_pairs = 0
    for _ in range(move_count):
        if cursor + 2 > len(raw):
            raise RuntimeError("raw permutation table ended before a move header")
        pair_count = struct.unpack_from("<H", raw, cursor)[0]
        cursor += 2
        byte_count = pair_count * 4
        if pair_count == 0 or cursor + byte_count > len(raw):
            raise RuntimeError("raw permutation table contains an invalid move")

        destinations: set[int] = set()
        sources: set[int] = set()
        for pair_index in range(pair_count):
            destination, source = struct.unpack_from(
                "<HH", raw, cursor + pair_index * 4
            )
            if (
                destination >= state_size
                or source >= state_size
                or destination in destinations
                or source in sources
            ):
                raise RuntimeError("raw move is not a bijection over changed slots")
            destinations.add(destination)
            sources.add(source)
        if destinations != sources:
            raise RuntimeError("raw move does not close over its changed-slot set")

        cursor += byte_count
        minimum_pairs = min(minimum_pairs, pair_count)
        maximum_pairs = max(maximum_pairs, pair_count)
        total_pairs += pair_count
    if cursor != len(raw):
        raise RuntimeError("raw permutation table contains trailing bytes")
    return {
        "minimum_pairs_per_move": minimum_pairs,
        "maximum_pairs_per_move": maximum_pairs,
        "total_pairs": total_pairs,
    }


def write_inl(path: Path, compressed: bytes, raw_size: int, sha256: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    encoded = base64.b64encode(compressed).decode("ascii")
    wrapped = "\n".join(
        f'        TEXT("{encoded[index:index + 120]}"),'
        for index in range(0, len(encoded), 120)
    )
    content = f"""#pragma once

// Generated from roice3/Magic120Cell (MIT), Copyright (c) 2016 Roice Nelson.
// Raw table SHA-256: {sha256}
namespace HyperTwistMagic120CellGenerated
{{
    constexpr int32 RawPermutationTableSize = {raw_size};
    constexpr int32 CompressedPermutationTableSize = {len(compressed)};
    constexpr const TCHAR* CompressedPermutationTableBase64Chunks[] =
    {{
{wrapped}
    }};
}}
"""
    path.write_text(content, encoding="utf-8", newline="\n")


def main() -> int:
    args = parse_args()
    donor_root = args.donor_root.resolve()
    if not (donor_root / "Magic120Cell/workFiles/magic120Cell.cpp").is_file():
        raise RuntimeError(f"Magic120Cell donor root is incomplete: {donor_root}")

    with tempfile.TemporaryDirectory(prefix="hypertwist-magic120cell-") as temporary:
        compatibility_root = Path(temporary) / "source"
        shutil.copytree(donor_root, compatibility_root)
        patch_compatibility_tree(compatibility_root)
        raw_output = Path(temporary) / "magic120cell-permutations.raw"
        extractor_summary = compile_and_extract(compatibility_root, raw_output)
        raw = raw_output.read_bytes()
        table_stats = validate_raw_table(raw)

        compressed = zlib.compress(raw, level=9)
        raw_sha256 = hashlib.sha256(raw).hexdigest()
        write_inl(args.output_inl, compressed, len(raw), raw_sha256)

        if args.keep_raw is not None:
            args.keep_raw.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(raw_output, args.keep_raw)

    manifest = {
        "schema_version": "hypertwist/magic120cell-permutation-table/v1",
        "source_repository": "roice3/Magic120Cell",
        "source_license": "MIT",
        "source_copyright": "Copyright (c) 2016 Roice Nelson",
        "cell_count": CELL_COUNT,
        "stickers_per_cell": STICKERS_PER_CELL,
        "state_size": STATE_SIZE,
        "move_count": MOVE_COUNT,
        "raw_bytes": len(raw),
        "compressed_bytes": len(compressed),
        "raw_sha256": raw_sha256,
        "extractor_summary": extractor_summary,
        **table_stats,
    }
    args.output_manifest.parent.mkdir(parents=True, exist_ok=True)
    args.output_manifest.write_text(
        json.dumps(manifest, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )
    print(json.dumps(manifest, indent=2, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
