bl_info = {
    "name": "GT7 Tonemapping",
    "author": "BeamNG",
    "version": (1, 0, 1),
    "blender": (4, 5, 0),
    "location": "Color Management",
    "description": "Adds Gran Turismo 7 style OCIO tonemapping through Blender's user datafiles.",
    "category": "Render",
}

import hashlib
import shutil
from pathlib import Path

import bpy

PROFILE_45 = "blender45lts"
PROFILE_5 = "blender5"
TEMP_DIR_NAME = "colormanagement_gt7_tmp"
MARKER_FILE_NAME = ".gt7_tonemapping_generated"
RESTART_MESSAGE = "GT7 Tonemapping installed. Restart Blender to show it in Color Management."
CURRENT_MESSAGE = "GT7 Tonemapping is already installed for this Blender version."
GENERATED_CONFIG_MARKER = "# Generated by GT7 Tonemapping patch 10"
GENERATED_CONFIG_PREFIX = "# Generated by GT7 Tonemapping"

GT7_LUTS = {
    PROFILE_45: [
        "gt7_sdr_ictcp_shaper_65.cube",
    ],
    PROFILE_5: [
        "gt7_sdr_ictcp_shaper_65.cube",
        "gt7_hdr_400nit_ictcp_shaper_65.cube",
        "gt7_hdr_1000nit_ictcp_shaper_65.cube",
        "gt7_hdr_2000nit_ictcp_shaper_65.cube",
    ],
}

GT7_COLORSPACE_45 = """
  - !<ColorSpace>
    name: GT7 SDR sRGB (CS)
    family: GT7
    bitdepth: 32f
    isdata: false
    description: GT7 SDR tonemap as a display ColorSpace, with lg2 shaper like PBR Neutral.
    from_scene_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: Linear CIE-XYZ E, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_sdr_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: sRGB}
"""

GT7_HDR_VIEW_TRANSFORMS_5 = """
  - !<ViewTransform>
    name: GT7 - HDR 400 nits (PQ)
    family: GT7
    description: |
      GT7 HDR tonemap targeting 400 nits peak, for PQ displays (1.0=100 nits).
    from_scene_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: ACES2065-1, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_hdr_400nit_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: cie_xyz_d65_interchange}

  - !<ViewTransform>
    name: GT7 - HDR 1000 nits (PQ)
    family: GT7
    description: |
      GT7 HDR tonemap targeting 1000 nits peak, for PQ displays (1.0=100 nits).
    from_scene_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: ACES2065-1, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_hdr_1000nit_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: cie_xyz_d65_interchange}

  - !<ViewTransform>
    name: GT7 - HDR 2000 nits (PQ)
    family: GT7
    description: |
      GT7 HDR tonemap targeting 2000 nits peak, for PQ displays (1.0=100 nits).
    from_scene_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: ACES2065-1, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_hdr_2000nit_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: cie_xyz_d65_interchange}
"""

GT7_SDR_VIEW_TRANSFORM_5 = """
  - !<ViewTransform>
    name: GT7 SDR sRGB (CS)
    family: GT7
    description: GT7 SDR tonemap as a display ColorSpace, with lg2 shaper like PBR Neutral.
    from_scene_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: ACES2065-1, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_sdr_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: cie_xyz_d65_interchange}
"""

GT7_DISPLAY_COLORSPACE_5 = """
  - !<ColorSpace>
    name: GT7 SDR sRGB (CS)
    family: GT7
    bitdepth: 32f
    encoding: sdr-video
    isdata: false
    description: GT7 SDR tonemap as a display ColorSpace, with lg2 shaper like PBR Neutral.
    from_display_reference: !<GroupTransform>
      children:
        - !<ColorSpaceTransform> {src: cie_xyz_d65_interchange, dst: Linear Rec.709}
        - !<AllocationTransform> {allocation: lg2, vars: [-9, 10]}
        - !<FileTransform> {src: gt7_sdr_ictcp_shaper_65.cube, interpolation: tetrahedral}
        - !<ColorSpaceTransform> {src: Linear Rec.709, dst: sRGB}
"""


def _addon_dir():
    return Path(__file__).resolve().parent


def _profile_key():
    major, minor, _patch = bpy.app.version
    if major >= 5:
        return PROFILE_5
    return PROFILE_45


def _source_colormanagement_dir(profile=None):
    profile = profile or _profile_key()
    return _addon_dir() / "ocio" / profile / "colormanagement"


def _source_luts_dir(profile=None):
    return _source_colormanagement_dir(profile) / "luts"


def _user_colormanagement_dir():
    return Path(bpy.utils.user_resource("DATAFILES", path="colormanagement", create=True))


def _user_marker_path():
    return _user_colormanagement_dir() / MARKER_FILE_NAME


def _valid_colormanagement_dir(path):
    return path and path.is_absolute() and (path / "config.ocio").is_file()


def _system_colormanagement_dir():
    version = f"{bpy.app.version[0]}.{bpy.app.version[1]}"
    binary_dir = Path(bpy.app.binary_path).resolve().parent
    user_dir = _user_colormanagement_dir()
    candidates = []

    for path in (
        bpy.utils.system_resource("DATAFILES", path="colormanagement"),
        bpy.utils.system_resource("DATAFILES", path="colormanagement/config.ocio"),
    ):
        if path:
            candidate = Path(path)
            candidates.append(candidate.parent if candidate.name == "config.ocio" else candidate)

    for resource_type in ("SYSTEM", "LOCAL"):
        try:
            resource_path = Path(bpy.utils.resource_path(resource_type))
        except Exception:
            continue
        candidates.append(resource_path / "datafiles" / "colormanagement")

    candidates.extend(
        [
            binary_dir / "datafiles" / "colormanagement",
            binary_dir / version / "datafiles" / "colormanagement",
            binary_dir.parent / "share" / "blender" / version / "datafiles" / "colormanagement",
        ]
    )

    for candidate in candidates:
        try:
            if candidate.resolve() == user_dir.resolve():
                continue
        except OSError:
            pass
        if _valid_colormanagement_dir(candidate):
            return candidate

    checked_paths = "\n".join(str(path) for path in candidates) or "none"
    raise RuntimeError(f"Blender OCIO config not found. Checked:\n{checked_paths}")


def _blender_version_string():
    return ".".join(str(part) for part in bpy.app.version)


def _file_sha256(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _data_sha256(profile):
    digest = hashlib.sha256()
    source_luts = _source_luts_dir(profile)
    for lut_name in GT7_LUTS[profile]:
        digest.update(lut_name.encode("utf-8"))
        digest.update((source_luts / lut_name).read_bytes())
    return digest.hexdigest()


def _user_config_is_current(profile, source_hash, data_hash):
    config_path = _user_colormanagement_dir() / "config.ocio"
    if not config_path.is_file():
        return False
    try:
        config_text = config_path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return (
        GENERATED_CONFIG_MARKER in config_text
        and f"# Blender version: {_blender_version_string()}" in config_text
        and f"# GT7 profile: {profile}" in config_text
        and f"# Source config sha256: {source_hash}" in config_text
        and f"# GT7 data sha256: {data_hash}" in config_text
    )


def _user_config_was_generated():
    if _user_marker_path().is_file():
        return True

    config_path = _user_colormanagement_dir() / "config.ocio"
    if not config_path.is_file():
        return False
    try:
        return GENERATED_CONFIG_PREFIX in config_path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False


def _insert_after(config_text, marker, insertion):
    if insertion.strip() in config_text:
        return config_text
    if marker not in config_text:
        raise RuntimeError(f"OCIO config marker not found: {marker.strip()}")
    return config_text.replace(marker, marker + insertion, 1)


def _insert_before(config_text, marker, insertion):
    if insertion.strip() in config_text:
        return config_text
    if marker not in config_text:
        raise RuntimeError(f"OCIO config marker not found: {marker.strip()}")
    return config_text.replace(marker, insertion + "\n" + marker, 1)


def _patch_active_views_single_line(config_text, view_names):
    lines = config_text.splitlines()
    for index, line in enumerate(lines):
        if not line.startswith("active_views: ["):
            continue
        for view_name in view_names:
            if view_name not in line:
                line = line.replace(", AgX", f", {view_name}, AgX", 1)
        lines[index] = line
        return "\n".join(lines) + "\n"
    raise RuntimeError("OCIO config is missing active_views")


def _patch_active_views_block(config_text, view_names):
    start = config_text.find("active_views: [")
    if start == -1:
        raise RuntimeError("OCIO config is missing active_views")
    end = config_text.find("]\n", start)
    if end == -1:
        raise RuntimeError("OCIO config active_views block is not closed")
    active_views = config_text[start + len("active_views: ["):end]
    existing_view_names = [view_name.strip() for view_name in active_views.replace("\n", "").split(",") if view_name.strip()]
    if all(view_name in existing_view_names for view_name in view_names):
        return config_text
    insert_index = len(existing_view_names)
    for marker in ("False Color", "Raw"):
        if marker in existing_view_names:
            insert_index = existing_view_names.index(marker)
            break
    for view_name in reversed(view_names):
        if view_name not in existing_view_names:
            existing_view_names.insert(insert_index, view_name)
    replacement = "active_views: [\n" + "\n".join(f"  {view_name}," for view_name in existing_view_names[:-1])
    replacement += f"\n  {existing_view_names[-1]}\n]"
    return config_text[:start] + replacement + config_text[end + 1:]


def _patch_config_45(config_text):
    config_text = _insert_after(
        config_text,
        "    - !<View> {name: Khronos PBR Neutral, colorspace: Khronos PBR Neutral sRGB}\n",
        "    - !<View> {name: Gran Turismo 7 SDR, colorspace: GT7 SDR sRGB (CS)}\n",
    )
    config_text = _patch_active_views_single_line(config_text, ["Gran Turismo 7 SDR"])
    return _insert_before(config_text, "looks:\n", GT7_COLORSPACE_45)


def _patch_config_5(config_text):
    config_text = _insert_after(
        config_text,
        "    - !<View> {name: Khronos PBR Neutral, view_transform: Khronos PBR Neutral sRGB, display_colorspace: sRGB}\n",
        "    - !<View> {name: Gran Turismo 7 SDR, view_transform: GT7 SDR sRGB (CS), display_colorspace: sRGB}\n",
    )
    if "  Rec.2100-PQ:\n" in config_text:
        config_text = _insert_after(
            config_text,
            "    - !<View> {name: AgX - SDR, view_transform: AgX Base Display P3, display_colorspace: Rec.2100-PQ}\n",
            (
                "    - !<View> {name: Gran Turismo 7 - HDR 400 nits, view_transform: GT7 - HDR 400 nits (PQ), display_colorspace: Rec.2100-PQ}\n"
                "    - !<View> {name: Gran Turismo 7 - HDR 1000 nits, view_transform: GT7 - HDR 1000 nits (PQ), display_colorspace: Rec.2100-PQ}\n"
                "    - !<View> {name: Gran Turismo 7 - HDR 2000 nits, view_transform: GT7 - HDR 2000 nits (PQ), display_colorspace: Rec.2100-PQ}\n"
            ),
        )
    config_text = _patch_active_views_block(
        config_text,
        [
            "Gran Turismo 7 SDR",
            "Gran Turismo 7 - HDR 400 nits",
            "Gran Turismo 7 - HDR 1000 nits",
            "Gran Turismo 7 - HDR 2000 nits",
        ],
    )
    config_text = _insert_before(config_text, "default_view_transform:", GT7_DISPLAY_COLORSPACE_5)
    config_text = _insert_before(
        config_text,
        "  - !<ViewTransform>\n    name: Khronos PBR Neutral sRGB\n",
        GT7_HDR_VIEW_TRANSFORMS_5,
    )
    return _insert_before(
        config_text,
        "  - !<ViewTransform>\n    name: ACES 1.3 Display P3\n",
        GT7_SDR_VIEW_TRANSFORM_5,
    )


def _generate_config(profile, config_path, source_hash, data_hash):
    config_text = config_path.read_text(encoding="utf-8")
    if profile == PROFILE_5:
        config_text = _patch_config_5(config_text)
    else:
        config_text = _patch_config_45(config_text)
    header = "\n".join(
        [
            GENERATED_CONFIG_MARKER,
            f"# Blender version: {_blender_version_string()}",
            f"# GT7 profile: {profile}",
            f"# Source config sha256: {source_hash}",
            f"# GT7 data sha256: {data_hash}",
        ]
    )
    return f"{header}\n{config_text}"


def install_colormanagement_override():
    profile = _profile_key()
    src_luts = _source_luts_dir(profile)
    base_dir = _system_colormanagement_dir()
    source_config = base_dir / "config.ocio"
    source_hash = _file_sha256(source_config)
    data_hash = _data_sha256(profile)
    if _user_config_is_current(profile, source_hash, data_hash):
        return CURRENT_MESSAGE

    dst = _user_colormanagement_dir()
    temp = dst.parent / TEMP_DIR_NAME

    if not src_luts.is_dir():
        raise RuntimeError(f"Bundled GT7 LUTs are missing: {src_luts}")
    if temp.exists():
        shutil.rmtree(temp)

    shutil.copytree(base_dir, temp)
    dst_luts = temp / "luts"
    dst_luts.mkdir(exist_ok=True)
    for lut_name in GT7_LUTS[profile]:
        shutil.copy2(src_luts / lut_name, dst_luts / lut_name)
    config_path = temp / "config.ocio"
    config_path.write_text(_generate_config(profile, config_path, source_hash, data_hash), encoding="utf-8")
    (temp / MARKER_FILE_NAME).write_text(
        (
            "Generated by GT7 Tonemapping\n"
            f"Blender version: {_blender_version_string()}\n"
            f"GT7 profile: {profile}\n"
            f"Source config sha256: {source_hash}\n"
            f"GT7 data sha256: {data_hash}\n"
        ),
        encoding="utf-8",
    )
    if dst.exists():
        shutil.rmtree(dst)
    temp.rename(dst)
    return RESTART_MESSAGE


def _draw_message(self, context):
    self.layout.label(text=_LAST_MESSAGE)


_LAST_MESSAGE = ""


def _show_message():
    print(_LAST_MESSAGE)
    try:
        bpy.context.window_manager.popup_menu(_draw_message, title="GT7 Tonemapping", icon="INFO")
    except Exception:
        pass
    return None


class GT7TonemappingPreferences(bpy.types.AddonPreferences):
    bl_idname = __name__

    def draw(self, context):
        layout = self.layout
        colormanagement_dir = _user_colormanagement_dir()
        colormanagement_exists = (colormanagement_dir / "config.ocio").is_file()
        marker_exists = _user_marker_path().is_file()

        layout.label(text=f"User color management: {colormanagement_dir}")
        layout.label(text=f"Generated override installed: {'yes' if colormanagement_exists and marker_exists else 'no'}")
        layout.label(text="Restart Blender after enabling or disabling this add-on.")


def register():
    global _LAST_MESSAGE
    bpy.utils.register_class(GT7TonemappingPreferences)
    _LAST_MESSAGE = install_colormanagement_override()
    if _LAST_MESSAGE == CURRENT_MESSAGE:
        print(_LAST_MESSAGE)
    elif bpy.app.background:
        print(_LAST_MESSAGE)
    else:
        bpy.app.timers.register(_show_message, first_interval=0.2)


def unregister():
    bpy.utils.unregister_class(GT7TonemappingPreferences)


if __name__ == "__main__":
    register()
