"""3D LUT creation window implementation for DisplayCAL.

It includes functionality for managing ICC profiles, configuring 3D LUT
settings, handling user interactions, and integrating with wxPython for the
graphical interface. The module also supports various 3D LUT formats and
advanced options for color space and tone mapping adjustments.
"""

import os
import re
import shutil
import sys
import traceback

from DisplayCAL.argyll import check_set_argyll_bin

if sys.platform == "win32":
    import win32api

from wx import xrc

from DisplayCAL import (
    colormath,
    config,
    floatspin,
    worker,
    xh_bitmapctrls,
    xh_filebrowsebutton,
    xh_floatspin,
)
from DisplayCAL import (
    localization as lang,
)
from DisplayCAL.argyll_cgats import cal_to_fake_profile
from DisplayCAL.argyll_names import VIDEO_ENCODINGS
from DisplayCAL.config import (
    DEFAULTS,
    PROFILE_EXT,
    get_data_path,
    get_icon,
    get_verified_path,
)
from DisplayCAL.icc_profile import (
    CurveType,
    DictType,
    ICCProfile,
    ICCProfileInvalidError,
    LUT16Type,
    VideoCardGammaType,
)
from DisplayCAL.meta import NAME as APPNAME
from DisplayCAL.meta import VERSION_STRING
from DisplayCAL.options import DEBUG
from DisplayCAL.util_decimal import stripzeros
from DisplayCAL.util_os import islink, readlink, safe_glob, waccess
from DisplayCAL.util_str import strtr
from DisplayCAL.worker import (
    Error,
    UnloggedInfo,
    UnloggedWarning,
    get_current_profile_path,
    show_result_dialog,
)
from DisplayCAL.wx_addons import CustomEvent
from DisplayCAL.wx_fixes import TempXmlResource
from DisplayCAL.wx_windows import (
    BaseApp,
    BaseFrame,
    ConfirmDialog,
    FileDrop,
    wx,
)


class LUT3DMixin:
    """Mixin class that adds some ``lut3d_`` functions to the mixed-in class."""

    def lut3d_bind_event_handlers(self):
        """Bind event handlers for the 3D LUT controls."""
        # Shared with main window
        self.lut3d_apply_cal_cb.Bind(wx.EVT_CHECKBOX, self.lut3d_apply_cal_ctrl_handler)
        self.lut3d_create_btn.Bind(wx.EVT_BUTTON, self.lut3d_create_handler)
        self.lut3d_hdr_peak_luminance_ctrl.Bind(
            floatspin.EVT_FLOATSPIN, self.lut3d_hdr_peak_luminance_handler
        )
        self.lut3d_hdr_display_ctrl.Bind(wx.EVT_CHOICE, self.lut3d_hdr_display_handler)
        self.lut3d_bind_trc_handlers()
        self.lut3d_bind_hdr_trc_handlers()
        self.lut3d_bind_content_colorspace_handlers()
        self.lut3d_bind_common_handlers()

    def lut3d_bind_trc_handlers(self):
        """Bind event handlers for TRC-related controls in the 3D LUT settings."""
        self.lut3d_trc_ctrl.Bind(wx.EVT_CHOICE, self.lut3d_trc_ctrl_handler)
        self.lut3d_trc_gamma_ctrl.Bind(
            wx.EVT_COMBOBOX, self.lut3d_trc_gamma_ctrl_handler
        )
        self.lut3d_trc_gamma_ctrl.Bind(
            wx.EVT_KILL_FOCUS, self.lut3d_trc_gamma_ctrl_handler
        )
        self.lut3d_trc_gamma_type_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_trc_gamma_type_ctrl_handler
        )
        self.lut3d_trc_black_output_offset_ctrl.Bind(
            wx.EVT_SLIDER, self.lut3d_trc_black_output_offset_ctrl_handler
        )
        self.lut3d_trc_black_output_offset_intctrl.Bind(
            wx.EVT_TEXT, self.lut3d_trc_black_output_offset_ctrl_handler
        )

    def lut3d_bind_hdr_trc_handlers(self):
        """Bind event handlers for HDR-related controls in the 3D LUT settings."""
        self.lut3d_hdr_ambient_luminance_ctrl.Bind(
            floatspin.EVT_FLOATSPIN, self.lut3d_hdr_ambient_luminance_handler
        )
        self.lut3d_hdr_minmll_ctrl.Bind(
            floatspin.EVT_FLOATSPIN, self.lut3d_hdr_minmll_handler
        )
        self.lut3d_hdr_maxmll_ctrl.Bind(
            floatspin.EVT_FLOATSPIN, self.lut3d_hdr_maxmll_handler
        )
        self.lut3d_hdr_maxmll_alt_clip_cb.Bind(
            wx.EVT_CHECKBOX, self.lut3d_hdr_maxmll_alt_clip_handler
        )
        self.lut3d_hdr_sat_ctrl.Bind(wx.EVT_SLIDER, self.lut3d_hdr_sat_ctrl_handler)
        self.lut3d_hdr_hue_ctrl.Bind(wx.EVT_SLIDER, self.lut3d_hdr_hue_ctrl_handler)
        self.lut3d_hdr_hue_intctrl.Bind(wx.EVT_TEXT, self.lut3d_hdr_hue_ctrl_handler)

    def lut3d_bind_content_colorspace_handlers(self):
        """Bind event handlers for the content colorspace controls."""
        self.lut3d_content_colorspace_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_content_colorspace_handler
        )
        for color in ("white", "red", "green", "blue"):
            for coord in "xy":
                _ = self.getcfg(f"3dlut.content.colorspace.{color}.{coord}")
                getattr(self, f"lut3d_content_colorspace_{color}_{coord}").Bind(
                    floatspin.EVT_FLOATSPIN, self.lut3d_content_colorspace_xy_handler
                )

    def lut3d_bind_common_handlers(self):
        """Bind common event handlers for 3D LUT controls."""
        self.encoding_input_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_encoding_input_ctrl_handler
        )
        self.encoding_output_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_encoding_output_ctrl_handler
        )
        self.gamut_mapping_inverse_a2b.Bind(
            wx.EVT_RADIOBUTTON, self.lut3d_gamut_mapping_mode_handler
        )
        self.gamut_mapping_b2a.Bind(
            wx.EVT_RADIOBUTTON, self.lut3d_gamut_mapping_mode_handler
        )
        self.lut3d_rendering_intent_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_rendering_intent_ctrl_handler
        )
        self.lut3d_format_ctrl.Bind(wx.EVT_CHOICE, self.lut3d_format_ctrl_handler)
        self.lut3d_size_ctrl.Bind(wx.EVT_CHOICE, self.lut3d_size_ctrl_handler)
        self.lut3d_bitdepth_input_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_bitdepth_input_ctrl_handler
        )
        self.lut3d_bitdepth_output_ctrl.Bind(
            wx.EVT_CHOICE, self.lut3d_bitdepth_output_ctrl_handler
        )

    def lut3d_trc_apply_ctrl_handler(self, event=None):
        """Handle changes to the "Apply TRC" checkbox for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                state of the checkbox.
        """
        v = self.lut3d_trc_apply_ctrl.GetValue()
        self.lut3d_trc_ctrl.Enable(v)
        self.lut3d_trc_gamma_label.Enable(v)
        self.lut3d_trc_gamma_ctrl.Enable(v)
        self.lut3d_trc_gamma_type_ctrl.Enable(v)
        if event:
            self.setcfg("3dlut.apply_trc", int(v))
            self.setcfg(
                "3dlut.apply_black_offset",
                int(self.lut3d_trc_apply_black_offset_ctrl.GetValue()),
            )
        self.lut3d_hdr_peak_luminance_label.Enable(v)
        self.lut3d_hdr_peak_luminance_ctrl.Enable(v)
        self.lut3d_hdr_peak_luminance_ctrl_label.Enable(v)
        self.lut3d_hdr_minmll_label.Enable(v)
        self.lut3d_hdr_minmll_ctrl.Enable(v)
        self.lut3d_hdr_minmll_ctrl_label.Enable(v)
        self.lut3d_hdr_maxmll_label.Enable(v)
        self.lut3d_hdr_maxmll_ctrl.Enable(v)
        self.lut3d_hdr_maxmll_ctrl_label.Enable(v)
        self.lut3d_hdr_diffuse_white_label.Enable(v)
        self.lut3d_hdr_diffuse_white_txt.Enable(v)
        self.lut3d_hdr_diffuse_white_txt_label.Enable(v)
        self.lut3d_hdr_ambient_luminance_label.Enable(v)
        self.lut3d_hdr_ambient_luminance_ctrl.Enable(v)
        self.lut3d_hdr_ambient_luminance_ctrl_label.Enable(v)
        self.lut3d_hdr_system_gamma_label.Enable(v)
        self.lut3d_hdr_system_gamma_txt.Enable(v)
        self.lut3d_content_colorspace_label.Enable(v)
        self.lut3d_content_colorspace_ctrl.Enable(v)
        for color in ("white", "red", "green", "blue"):
            for coord in "xy":
                getattr(self, f"lut3d_content_colorspace_{color[0]}_label").Enable(v)
                getattr(self, f"lut3d_content_colorspace_{color}_{coord}").Enable(v)
                getattr(self, f"lut3d_content_colorspace_{color}_{coord}_label").Enable(
                    v
                )
        self.lut3d_trc_black_output_offset_label.Enable(v)
        self.lut3d_trc_black_output_offset_ctrl.Enable(v)
        self.lut3d_trc_black_output_offset_intctrl.Enable(v)
        self.lut3d_trc_black_output_offset_intctrl_label.Enable(v)
        self.lut3d_show_input_value_clipping_warning(event)
        self.lut3d_show_hdr_display_control()

    def lut3d_show_input_value_clipping_warning(self, layout):
        """Show or hide the input value clipping warning based on the current settings.

        Args:
            layout (bool): If True, the layout will be updated after showing or
                hiding the warning.
        """
        self.panel.Freeze()
        show = (
            self.lut3d_trc_apply_none_ctrl.GetValue()
            and self.XYZbpout > self.XYZbpin
            and self.getcfg("3dlut.rendering_intent")
            not in ("la", "p", "pa", "ms", "s", "lp")
        )
        self.lut3d_input_value_clipping_bmp.Show(show)
        self.lut3d_input_value_clipping_label.Show(show)
        if layout:
            self.panel.Sizer.Layout()
            self.update_layout()
        self.panel.Thaw()

    def lut3d_hdr_maxmll_alt_clip_handler(self, event):
        """Handle changes to the HDR maximum MLL alternative clipping checkbox.

        MLL stands for Minimum Light Level, which is used to adjust the maximum
        light level in HDR content.

        Args:
            event: The event that triggered this handler, which contains the
                state of the checkbox.
        """
        self.lut3d_set_option(
            "3dlut.hdr_maxmll_alt_clip",
            int(not self.lut3d_hdr_maxmll_alt_clip_cb.GetValue()),
        )
        self.lut3d_hdr_update_diffuse_white()

    def lut3d_apply_cal_ctrl_handler(self, event):
        """Handle changes to the "Apply calibration" checkbox for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                state of the checkbox.
        """
        self.setcfg(
            "3dlut.output.profile.apply_cal", int(self.lut3d_apply_cal_cb.GetValue())
        )

    def lut3d_hdr_display_handler(self, event):
        """Handle changes to the HDR display selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the HDR display control.
        """
        if (
            self.lut3d_hdr_display_ctrl.GetSelection()
            and not self.getcfg("3dlut.hdr_display")
            and not show_result_dialog(
                UnloggedInfo(lang.getstr("3dlut.format.madVR.hdr.confirm")),
                self,
                confirm=lang.getstr("ok"),
            )
        ):
            self.lut3d_hdr_display_ctrl.SetSelection(0)
            return
        self.lut3d_set_option(
            "3dlut.hdr_display", self.lut3d_hdr_display_ctrl.GetSelection()
        )

    def lut3d_hdr_peak_luminance_handler(self, event):
        """Handle changes to the peak luminance value for HDR 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the peak luminance control.
        """
        target_peak = self.lut3d_hdr_peak_luminance_ctrl.GetValue()
        maxmll = self.lut3d_hdr_maxmll_ctrl.GetValue()
        if maxmll < target_peak:
            self.setcfg("3dlut.hdr_maxmll", target_peak)
        self.lut3d_hdr_maxmll_ctrl.SetRange(target_peak, 10000)
        self.lut3d_set_option(
            "3dlut.hdr_peak_luminance", self.lut3d_hdr_peak_luminance_ctrl.GetValue()
        )

    def lut3d_hdr_ambient_luminance_handler(self, event):
        """Handle changes to the ambient luminance value for HDR 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the ambient luminance control.
        """
        self.lut3d_set_option(
            "3dlut.hdr_ambient_luminance",
            self.lut3d_hdr_ambient_luminance_ctrl.GetValue(),
        )

    def lut3d_hdr_minmll_handler(self, event):
        """Handle changes to the minimum MLL value for HDR 3D LUTs.

        MLL stands for Minimum Light Level, which is used to adjust the minimum
        light level in HDR content.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the minimum MLL control.
        """
        self.lut3d_set_option("3dlut.hdr_minmll", self.lut3d_hdr_minmll_ctrl.GetValue())

    def lut3d_hdr_maxmll_handler(self, event):
        """Handle changes to the maximum MLL value for HDR 3D LUTs.

        MLL stands for Minimum Light Level, which is used to adjust the maximum
        light level in HDR content.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the maximum MLL control.
        """  # noqa: E501
        self.lut3d_set_option("3dlut.hdr_maxmll", self.lut3d_hdr_maxmll_ctrl.GetValue())

    def lut3d_hdr_sat_ctrl_handler(self, event):
        """Handle changes to the saturation value for HDR 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the saturation control.
        """
        self.lut3d_set_option(
            "3dlut.hdr_sat", self.lut3d_hdr_sat_ctrl.GetValue() / 100.0
        )
        self.lut3d_hdr_update_sat_val()

    def lut3d_hdr_hue_ctrl_handler(self, event):
        """Handle changes to the hue value for HDR 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the hue control.
        """
        if event.GetId() == self.lut3d_hdr_hue_intctrl.GetId():
            self.lut3d_hdr_hue_ctrl.SetValue(self.lut3d_hdr_hue_intctrl.GetValue())
        else:
            self.lut3d_hdr_hue_intctrl.SetValue(self.lut3d_hdr_hue_ctrl.GetValue())
        v = self.lut3d_hdr_hue_ctrl.GetValue() / 100.0
        if v != self.getcfg("3dlut.hdr_hue"):
            self.lut3d_set_option("3dlut.hdr_hue", v)

    def lut3d_trc_black_output_offset_ctrl_handler(self, event):
        """Handle changes to the TRC black output offset value for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the TRC black output offset control.
        """
        if event.GetId() == self.lut3d_trc_black_output_offset_intctrl.GetId():
            self.lut3d_trc_black_output_offset_ctrl.SetValue(
                self.lut3d_trc_black_output_offset_intctrl.GetValue()
            )
        else:
            self.lut3d_trc_black_output_offset_intctrl.SetValue(
                self.lut3d_trc_black_output_offset_ctrl.GetValue()
            )
        v = self.lut3d_trc_black_output_offset_ctrl.GetValue() / 100.0
        if v != self.getcfg("3dlut.trc_output_offset"):
            self.lut3d_set_option("3dlut.trc_output_offset", v)
            self.lut3d_update_trc_control()
        # self.lut3d_show_trc_controls()

    def lut3d_trc_gamma_ctrl_handler(self, event):
        """Handle changes to the TRC gamma value for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                new value of the TRC gamma control.
        """
        try:
            v = float(self.lut3d_trc_gamma_ctrl.GetValue().replace(",", "."))
            if (
                v < config.VALID_RANGES["3dlut.trc_gamma"][0]
                or v > config.VALID_RANGES["3dlut.trc_gamma"][1]
            ):
                raise ValueError
        except ValueError:
            wx.Bell()
            self.lut3d_trc_gamma_ctrl.SetValue(str(self.getcfg("3dlut.trc_gamma")))
        else:
            if str(v) != self.lut3d_trc_gamma_ctrl.GetValue():
                self.lut3d_trc_gamma_ctrl.SetValue(str(v))
            if v != self.getcfg("3dlut.trc_gamma"):
                self.lut3d_set_option("3dlut.trc_gamma", v)
                self.lut3d_update_trc_control()
            # self.lut3d_show_trc_controls()
        event.Skip()

    def lut3d_trc_ctrl_handler(self, event):
        """Handle changes to the TRC selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the TRC.
        """
        self.Freeze()
        if self.lut3d_trc_ctrl.GetSelection() == 1:
            # BT.1886
            self.lut3d_set_option("3dlut.trc_gamma", 2.4)
            self.lut3d_set_option("3dlut.trc_gamma_type", "B")
            self.lut3d_set_option("3dlut.trc_output_offset", 0.0)
            trc = "bt1886"
        elif self.lut3d_trc_ctrl.GetSelection() == 0:
            # Pure power gamma 2.2
            self.lut3d_set_option("3dlut.trc_gamma", 2.2)
            self.lut3d_set_option("3dlut.trc_gamma_type", "b")
            self.lut3d_set_option("3dlut.trc_output_offset", 1.0)
            trc = "gamma2.2"
        elif self.lut3d_trc_ctrl.GetSelection() == 2:  # SMPTE 2084, hard clip
            self.lut3d_set_option("3dlut.trc_output_offset", 0.0)
            trc = "smpte2084.hardclip"
            self.lut3d_set_option("3dlut.hdr_maxmll", 10000)
        elif self.lut3d_trc_ctrl.GetSelection() == 3:  # SMPTE 2084, roll-off clip
            self.lut3d_set_option("3dlut.trc_output_offset", 0.0)
            trc = "smpte2084.rolloffclip"
            self.lut3d_set_option("3dlut.hdr_maxmll", 10000)
        elif self.lut3d_trc_ctrl.GetSelection() == 4:  # HLG
            self.lut3d_set_option("3dlut.trc_output_offset", 0.0)
            trc = "hlg"
        else:
            trc = "customgamma"
            # Have to use CallAfter, otherwise only part of the text will
            # be selected (wxPython bug?)
            wx.CallAfter(self.lut3d_trc_gamma_ctrl.SetFocus)
            wx.CallLater(1, self.lut3d_trc_gamma_ctrl.SelectAll)
        self.lut3d_set_option("3dlut.trc", trc)
        if trc != "customgamma":
            self.lut3d_update_trc_controls()
        self.lut3d_show_trc_controls()
        self.Thaw()

    def lut3d_trc_gamma_type_ctrl_handler(self, event):
        """Handle changes to the TRC gamma type selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the TRC gamma type.
        """
        v = self.trc_gamma_types_ab[self.lut3d_trc_gamma_type_ctrl.GetSelection()]
        if v != self.getcfg("3dlut.trc_gamma_type"):
            self.lut3d_set_option("3dlut.trc_gamma_type", v)
            self.lut3d_update_trc_control()
            self.lut3d_show_trc_controls()

    def lut3d_encoding_input_ctrl_handler(self, event):
        """Handle changes to the input encoding selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the input encoding.
        """
        encoding = self.encoding_input_ab[self.encoding_input_ctrl.GetSelection()]
        self.lut3d_set_option("3dlut.encoding.input", encoding)
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_encoding_controls()
        elif self.Parent:
            self.Parent.lut3d_update_encoding_controls()

    def lut3d_encoding_output_ctrl_handler(self, event):
        """Handle changes to the output encoding selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the output encoding.
        """
        encoding = self.encoding_output_ab[self.encoding_output_ctrl.GetSelection()]
        if self.getcfg("3dlut.format") == "madVR" and encoding != "t":
            profile = getattr(self, "output_profile", None)
            if (
                profile
                and "meta" in profile.tags
                and isinstance(profile.tags.meta, DictType)
                and "EDID_model" in profile.tags.meta
            ):
                devicename = profile.tags.meta["EDID_model"]
            else:
                devicename = None
            dlg = ConfirmDialog(
                self,
                msg=lang.getstr(
                    "3dlut.encoding.output.warning.madvr",
                    devicename or lang.getstr("device.name.placeholder"),
                ),
                ok=lang.getstr("ok"),
                cancel=lang.getstr("cancel"),
                bitmap=get_icon(32, "dialog-warning"),
            )
            result = dlg.ShowModal()
            dlg.Destroy()
            if result != wx.ID_OK:
                self.encoding_output_ctrl.SetSelection(
                    self.encoding_output_ba[self.getcfg("3dlut.encoding.output")]
                )
                return False
        self.lut3d_set_option("3dlut.encoding.output", encoding)
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_encoding_controls()
        elif self.Parent:
            self.Parent.lut3d_update_encoding_controls()
        return None

    def lut3d_bitdepth_input_ctrl_handler(self, event):
        """Handle changes to the input bit depth selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the input bit depth.
        """
        self.lut3d_set_option(
            "3dlut.bitdepth.input",
            self.lut3d_bitdepth_ab[self.lut3d_bitdepth_input_ctrl.GetSelection()],
        )
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_shared_controls()
        elif self.Parent:
            self.Parent.lut3d_update_shared_controls()

    def lut3d_bitdepth_output_ctrl_handler(self, event):
        """Handle changes to the output bit depth selection for 3D LUTs.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the output bit depth.
        """
        if self.getcfg("3dlut.format") in ("png", "ReShade") and self.lut3d_bitdepth_ab[
            self.lut3d_bitdepth_output_ctrl.GetSelection()
        ] not in (8, 16):
            wx.Bell()
            self.lut3d_bitdepth_output_ctrl.SetSelection(self.lut3d_bitdepth_ba[8])
        self.lut3d_set_option(
            "3dlut.bitdepth.output",
            self.lut3d_bitdepth_ab[self.lut3d_bitdepth_output_ctrl.GetSelection()],
        )
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_shared_controls()
        elif self.Parent:
            self.Parent.lut3d_update_shared_controls()

    def lut3d_content_colorspace_handler(self, event):
        """Handle changes to the content colorspace selection.

        Args:
            event: The event that triggered this handler, which contains the
                selection index of the content colorspace.
        """
        sel = self.lut3d_content_colorspace_ctrl.Selection
        try:
            rgb_space = self.lut3d_content_colorspace_names[sel]
        except IndexError:
            # Custom
            rgb_space = None
        else:
            rgb_space = colormath.get_rgb_space(rgb_space)
            for i, color in enumerate(("white", "red", "green", "blue")):
                if i == 0:
                    xyY = colormath.XYZ2xyY(*rgb_space[1])
                else:
                    xyY = rgb_space[2:][i - 1]
                for j, coord in enumerate("xy"):
                    v = round(xyY[j], 4)
                    self.lut3d_set_option(
                        f"3dlut.content.colorspace.{color}.{coord}", v
                    )
            self.lut3d_update_trc_controls()
        self.panel.Freeze()
        sizer = self.lut3d_content_colorspace_red_x.ContainingSizer
        sizer.ShowItems(not rgb_space)
        self.panel.Layout()
        self.panel.Thaw()
        if isinstance(self, LUT3DFrame):
            self.update_layout()

    def lut3d_content_colorspace_xy_handler(self, event):
        """Handle changes to the content colorspace XY coordinates.

        Args:
            event: The event that triggered this handler, which contains the
                name of the control that was changed.
        """
        option = event.GetEventObject().Name.replace("_", ".")[5:]
        self.lut3d_set_option("3dlut" + option, event.GetEventObject().GetValue())
        self.lut3d_update_trc_controls()

    def lut3d_create_consumer(self, result=None):
        """Handle the result of the 3D LUT creation process.

        Args:
            result: The result of the 3D LUT creation process, which can be
                either a success message or an exception.
        """
        if isinstance(result, Exception):
            show_result_dialog(result, self)
        # Remove temporary files
        self.worker.wrapup(False)
        if (
            not isinstance(result, Exception)
            and result
            and not isinstance(self, LUT3DFrame)
            and getattr(self, "lut3d_path", None)
        ):
            # 3D LUT tab is part of main window
            if self.getcfg("3dlut.create"):
                # 3D LUT was created automatically after profiling, show
                # usual profile summary window
                self.profile_finish(
                    True,
                    self.getcfg("calibration.file", False),
                    lang.getstr("calibration_profiling.complete"),
                    lang.getstr("profiling.incomplete"),
                    install_3dlut=True,
                )
            else:
                # 3D LUT was created manually
                self.profile_finish(
                    True,
                    self.getcfg("calibration.file", False),
                    "",
                    lang.getstr("profiling.incomplete"),
                    install_3dlut=True,
                )

    def lut3d_create_handler(self, event, path=None, copy_from_path=None):
        """Create a 3D LUT.

        Args:
            event: The event that triggered this handler.
            path (str, optional): The path to save the 3D LUT file. If not
                provided, a file dialog will be shown to select the path.
            copy_from_path (str, optional): If provided, the function will copy
                an existing 3D LUT from this path instead of creating a new
                one.
        """
        if sys.platform == "darwin" or DEBUG:
            self.focus_handler(event)
        if not check_set_argyll_bin():
            return
        if isinstance(self, LUT3DFrame):
            profile_in = self.set_profile("input")
            if self.getcfg("3dlut.use_abstract_profile"):
                profile_abst = self.set_profile("abstract")
            else:
                profile_abst = None
            profile_out = self.set_profile("output")
        else:
            profile_abst = None
            profile_in_path = self.getcfg("3dlut.input.profile")
            if not profile_in_path or not os.path.isfile(profile_in_path):
                show_result_dialog(
                    Error(
                        lang.getstr(
                            "error.profile.file_missing",
                            self.getcfg("3dlut.input.profile", raw=True),
                        )
                    ),
                    parent=self,
                )
                return

            try:
                profile_in = ICCProfile(profile_in_path)
            except (OSError, ICCProfileInvalidError):
                show_result_dialog(
                    Error(lang.getstr("profile.invalid") + "\n" + profile_in_path),
                    parent=self,
                )
                return
            profile_out = config.get_current_profile()
            if not profile_out:
                show_result_dialog(
                    Error(
                        lang.getstr("profile.invalid")
                        + "\n{}".format(self.getcfg("calibration.file", False))
                    ),
                    parent=self,
                )
                return
            if path:
                # Called from script client
                self.lut3d_path = None
            elif not copy_from_path:
                self.lut3d_set_path()
                path = self.lut3d_path
        if None not in (profile_in, profile_out) or (
            profile_in and profile_in.profileClass == "link"
        ):
            if (
                profile_out
                and profile_in.is_same(profile_out, force_calculation=True)
                and not show_result_dialog(
                    Warning(lang.getstr("error.source_dest_same")),
                    self,
                    confirm=lang.getstr("continue"),
                )
            ):
                return
            checkoverwrite = True
            remember_last_3dlut_path = False
            if not path:
                defaultDir, defaultFile = get_verified_path("last_3dlut_path")
                if copy_from_path:
                    defaultFile = os.path.basename(copy_from_path)
                # Only remember last used path if it was a deliberate user
                # choice via the filedialog
                remember_last_3dlut_path = True
                if copy_from_path and config.get_display_name() == "Resolve":
                    # Find path to Resolve LUT folder
                    if sys.platform == "win32":
                        drives = win32api.GetLogicalDriveStrings()
                        for drive in drives.split("\0")[:-1]:
                            lut_dir = os.path.join(
                                drive,
                                "ProgramData",
                                "Blackmagic Design",
                                "DaVinci Resolve",
                                "Support",
                                "LUT",
                            )
                            if os.path.isdir(lut_dir):
                                defaultDir = lut_dir
                                remember_last_3dlut_path = False
                                break
                    else:
                        # Assume OS X
                        volumes = ["/", *safe_glob("/Volumes/*")]
                        for volume in volumes:
                            lut_dir = os.path.join(
                                volume,
                                "Library",
                                "Application Support",
                                "Blackmagic Design",
                                "DaVinci Resolve",
                                "LUT",
                            )
                            if os.path.isdir(lut_dir):
                                defaultDir = lut_dir
                                remember_last_3dlut_path = False
                                break
                ext = self.getcfg("3dlut.format")
                if (
                    ext != "madVR"
                    and not isinstance(self, LUT3DFrame)
                    and self.getcfg("3dlut.output.profile.apply_cal")
                ):
                    # Check if there's a clash between current videoLUT
                    # and 3D LUT (do both contain non-linear calibration?)
                    profile_display_name = profile_out.getDeviceModelDescription()
                    tempdir = self.worker.create_tempdir()
                    if isinstance(tempdir, Exception):
                        show_result_dialog(tempdir, self)
                        return
                    tempcal = os.path.join(tempdir, "temp.cal")
                    cancel = False
                    real_displays_count = 0
                    show_nonlinear_videolut_warning = False
                    for i, display_name in enumerate(self.worker.display_names):
                        if (
                            display_name == profile_display_name
                            and display_name != "madVR"
                            and self.worker.lut_access[i]
                            and self.worker.save_current_video_lut(
                                i + 1, tempcal, silent=True
                            )
                            is True
                            and not cal_to_fake_profile(tempcal).tags.vcgt.is_linear()
                        ):
                            show_nonlinear_videolut_warning = True
                            break
                        if not config.is_virtual_display(i):
                            real_displays_count += 1
                    if show_nonlinear_videolut_warning or real_displays_count == 1:
                        if copy_from_path:
                            confirm = lang.getstr("3dlut.save_as")
                        else:
                            confirm = lang.getstr("3dlut.install")
                        if not show_result_dialog(
                            UnloggedWarning(
                                lang.getstr("3dlut.1dlut.videolut.nonlinear")
                            ),
                            self,
                            confirm=confirm,
                        ):
                            cancel = True
                    if os.path.isfile(tempcal):
                        os.remove(tempcal)
                    if cancel:
                        return
                if ext == "ReShade":
                    dlg = wx.DirDialog(
                        self, lang.getstr("3dlut.install"), defaultPath=defaultDir
                    )
                else:
                    if ext == "eeColor":
                        ext = "txt"
                    elif ext == "madVR":
                        ext = "3dlut"
                    elif ext == "icc":
                        ext = PROFILE_EXT[1:]
                    defaultFile = (
                        os.path.splitext(
                            defaultFile
                            or os.path.basename(config.DEFAULTS.get("last_3dlut_path"))
                        )[0]
                        + "."
                        + ext
                    )
                    dlg = wx.FileDialog(
                        self,
                        lang.getstr("3dlut.save_as"),
                        defaultDir=defaultDir,
                        defaultFile=defaultFile,
                        wildcard="*." + ext,
                        style=wx.SAVE | wx.FD_OVERWRITE_PROMPT,
                    )
                dlg.Center(wx.BOTH)
                if dlg.ShowModal() == wx.ID_OK:
                    path = dlg.GetPath()
                    if ext == "ReShade":
                        path = os.path.join(
                            path.rstrip(os.path.sep), "ColorLookupTable.png"
                        )
                dlg.Destroy()
                checkoverwrite = False
            if path:
                if not waccess(path, os.W_OK):
                    show_result_dialog(
                        Error(lang.getstr("error.access_denied.write", path)), self
                    )
                    return
                if remember_last_3dlut_path:
                    self.setcfg("last_3dlut_path", path)
                if checkoverwrite and os.path.isfile(path):
                    dlg = ConfirmDialog(
                        self,
                        msg=lang.getstr("dialog.confirm_overwrite", (path)),
                        ok=lang.getstr("overwrite"),
                        cancel=lang.getstr("cancel"),
                        bitmap=get_icon(32, "dialog-warning"),
                    )
                    result = dlg.ShowModal()
                    dlg.Destroy()
                    if result != wx.ID_OK:
                        return
                if self.Parent:
                    config.writecfg()
                elif isinstance(self, LUT3DFrame):
                    config.writecfg(
                        module="3DLUT-maker",
                        options=(
                            "3dlut.",
                            "last_3dlut_path",
                            "position.lut3dframe",
                            "size.lut3dframe",
                        ),
                    )
                if copy_from_path:
                    # Instead of creating a 3D LUT, copy existing one
                    src_name = os.path.splitext(copy_from_path)[0]
                    dst_name = os.path.splitext(path)[0]
                    src_paths = [copy_from_path]
                    dst_paths = [path]
                    if self.getcfg("3dlut.format") == "eeColor":
                        # eeColor: 3D LUT + 6x 1D LUT
                        for part in ("first", "second"):
                            for channel in ("blue", "green", "red"):
                                src_path = f"{src_name}-{part}1d{channel}.txt"
                                if os.path.isfile(src_path):
                                    src_paths.append(src_path)
                                    dst_paths.append(
                                        f"{dst_name}-{part}1d{channel}.txt"
                                    )
                    elif self.getcfg("3dlut.format") == "ReShade":
                        dst_dir = os.path.dirname(path)
                        # Check if MasterEffect is installed
                        me_header_path = os.path.join(dst_dir, "MasterEffect.h")
                        use_mclut3d = False
                        if (
                            use_mclut3d
                            and os.path.isfile(me_header_path)
                            and os.path.isdir(os.path.join(dst_dir, "MasterEffect"))
                        ):
                            dst_paths = [
                                os.path.join(dst_dir, "MasterEffect", "mclut3d.png")
                            ]
                            # Alter existing MasterEffect.h
                            with open(me_header_path, "rb") as me_header_file:
                                me_header = me_header_file.read()
                            # Enable LUT
                            me_header = re.sub(
                                r"#define\s+USE_LUT\s+0", "#define USE_LUT 1", me_header
                            )
                            # iLookupTableMode 2 = use mclut3d.png
                            me_header = re.sub(
                                r"#define\s+iLookupTableMode\s+\d+",
                                "#define iLookupTableMode 2",
                                me_header,
                            )
                            # Amount of color change by lookup table.
                            # 1.0 means full effect.
                            me_header = re.sub(
                                r"#define\s+fLookupTableMix\s+\d+(?:\.\d+)",
                                "#define fLookupTableMix 1.0",
                                me_header,
                            )
                            with open(me_header_path, "wb") as me_header_file:
                                me_header_file.write(me_header)
                        else:
                            # Write out our own shader
                            clut_fx_path = get_data_path("ColorLookupTable.fx")
                            if not clut_fx_path:
                                show_result_dialog(
                                    Error(
                                        lang.getstr(
                                            "file.missing", "ColorLookupTable.fx"
                                        )
                                    ),
                                    self,
                                )
                                return
                            with open(clut_fx_path, "rb") as clut_fx_file:
                                clut_fx = clut_fx_file.read()
                            clut_size = self.getcfg("3dlut.size")
                            clut_fx = strtr(
                                clut_fx,
                                {
                                    "${VERSION}": VERSION_STRING,
                                    "${WIDTH}": str(clut_size**2),
                                    "${HEIGHT}": str(clut_size),
                                    "${FORMAT}": "RGBA{}".format(
                                        self.getcfg("3dlut.bitdepth.output")
                                    ),
                                },
                            )
                            reshade_shaders = os.path.join(dst_dir, "reshade-shaders")
                            if os.path.isdir(reshade_shaders):
                                # ReShade >= 3.x with default shaders
                                path = os.path.join(
                                    reshade_shaders, "Textures", os.path.basename(path)
                                )
                                dst_paths = [path]
                                dst_dir = os.path.join(reshade_shaders, "Shaders")
                            else:
                                reshade_fx_path = os.path.join(dst_dir, "ReShade.fx")
                                # Adjust path for correct installation if ReShade.fx
                                # is a symlink.
                                if islink(reshade_fx_path):
                                    # ReShade < 3.0
                                    reshade_fx_path = readlink(reshade_fx_path)
                                    dst_dir = os.path.dirname(reshade_fx_path)
                                    path = os.path.join(dst_dir, os.path.basename(path))
                                    dst_paths = [path]
                                if os.path.isfile(reshade_fx_path):
                                    # ReShade < 3.0
                                    # Alter existing ReShade.fx
                                    with open(reshade_fx_path, "rb") as reshade_fx_file:
                                        reshade_fx = reshade_fx_file.read()
                                    # Remove existing shader include
                                    reshade_fx = re.sub(
                                        r"[ \t]*//\s*Automatically\s+\S+\s+by\s+"
                                        f"{APPNAME}"
                                        r"\s+.+[ \t]*\r?\n?",
                                        "",
                                        reshade_fx,
                                    )
                                    reshade_fx = re.sub(
                                        r'[ \t]*#include\s+"ColorLookupTable.fx"'
                                        "[ \t]*\r?\n?",
                                        "",
                                        reshade_fx,
                                    ).rstrip("\r\n")
                                    reshade_fx += (
                                        f"{os.linesep * 2}// Automatically added by "
                                        f"{APPNAME} {VERSION_STRING}{os.linesep}"
                                    )
                                    reshade_fx += (
                                        '#include "ColorLookupTable.fx"' + os.linesep
                                    )
                                    with open(reshade_fx_path, "wb") as reshade_fx_file:
                                        reshade_fx_file.write(reshade_fx)
                            clut_fx_path = os.path.join(dst_dir, "ColorLookupTable.fx")
                            with open(clut_fx_path, "wb") as clut_fx_file:
                                clut_fx_file.write(clut_fx)
                    for src_path, dst_path in zip(src_paths, dst_paths):
                        shutil.copyfile(src_path, dst_path)
                    return
                self.worker.interactive = False
                self.worker.start(
                    self.lut3d_create_consumer,
                    self.lut3d_create_producer,
                    wargs=(profile_in, profile_abst, profile_out, path),
                    progress_msg=lang.getstr("3dlut.create"),
                    resume=not isinstance(self, LUT3DFrame)
                    and self.getcfg("3dlut.create"),
                )

    def lut3d_create_producer(self, profile_in, profile_abst, profile_out, path):
        """Create a 3D LUT.

        Args:
            profile_in (ICCProfile): Input profile.
            profile_abst (ICCProfile or None): Abstract profile, if used.
            profile_out (ICCProfile): Output profile.
            path (str): Path to save the 3D LUT file.

        Returns:
            bool or Exception: True if successful, or an Exception if an error
                occurred.
        """
        apply_cal = (
            profile_out
            and isinstance(profile_out.tags.get("vcgt"), VideoCardGammaType)
            and (
                self.getcfg("3dlut.output.profile.apply_cal")
                or not hasattr(self, "lut3d_apply_cal_cb")
            )
        )
        input_encoding = self.getcfg("3dlut.encoding.input")
        output_encoding = self.getcfg("3dlut.encoding.output")
        if self.getcfg("3dlut.apply_trc") or not hasattr(
            self, "lut3d_trc_apply_none_ctrl"
        ):
            if (
                self.getcfg("3dlut.trc").startswith("smpte2084")
                or self.getcfg("3dlut.trc") == "hlg"  # SMPTE ST.2084 (PQ)
            ):  # Hybrid Log-Gamma (HLG)
                trc_gamma = self.getcfg("3dlut.trc")
            else:
                trc_gamma = self.getcfg("3dlut.trc_gamma")
        else:
            trc_gamma = None
        XYZwp = None
        if not isinstance(self, LUT3DFrame):
            x = self.getcfg("3dlut.whitepoint.x", False)
            y = self.getcfg("3dlut.whitepoint.y", False)
            if x and y:
                XYZwp = colormath.xyY2XYZ(x, y)
        trc_gamma_type = self.getcfg("3dlut.trc_gamma_type")
        outoffset = self.getcfg("3dlut.trc_output_offset")
        intent = self.getcfg("3dlut.rendering_intent")
        file_format = self.getcfg("3dlut.format")
        size = self.getcfg("3dlut.size")
        input_bits = self.getcfg("3dlut.bitdepth.input")
        output_bits = self.getcfg("3dlut.bitdepth.output")
        apply_black_offset = self.getcfg("3dlut.apply_black_offset")
        use_b2a = self.getcfg("3dlut.gamap.use_b2a")
        white_cdm2 = self.getcfg("3dlut.hdr_peak_luminance")
        minmll = self.getcfg("3dlut.hdr_minmll")
        maxmll = self.getcfg("3dlut.hdr_maxmll")
        ambient_cdm2 = self.getcfg("3dlut.hdr_ambient_luminance")
        content_rgb_space = [1.0, [], [], [], []]
        for i, color in enumerate(("white", "red", "green", "blue")):
            for coord in "xy":
                v = self.getcfg(f"3dlut.content.colorspace.{color}.{coord}")
                content_rgb_space[i + 1].append(v)
            # Dummy Y value, not used for primaries but needs to be present
            content_rgb_space[i + 1].append(1.0)
        content_rgb_space[1] = colormath.xyY2XYZ(*content_rgb_space[1])
        content_rgb_space = colormath.get_rgb_space(content_rgb_space)
        try:
            # We need to make sure the link between the original profile object
            # and the one we're eventually going to change is broken
            profile_in = ICCProfile(profile_in.filename)
            self.worker.create_3dlut(
                profile_in,
                path,
                profile_abst,
                profile_out,
                apply_cal=apply_cal,
                intent=intent,
                file_format=file_format,
                size=size,
                input_bits=input_bits,
                output_bits=output_bits,
                input_encoding=input_encoding,
                output_encoding=output_encoding,
                trc_gamma=trc_gamma,
                trc_gamma_type=trc_gamma_type,
                trc_output_offset=outoffset,
                apply_black_offset=apply_black_offset,
                use_b2a=use_b2a,
                white_cdm2=white_cdm2,
                minmll=minmll,
                maxmll=maxmll,
                use_alternate_master_white_clip=self.getcfg(
                    "3dlut.hdr_maxmll_alt_clip"
                ),
                hdr_sat=self.getcfg("3dlut.hdr_sat"),
                hdr_hue=self.getcfg("3dlut.hdr_hue"),
                ambient_cdm2=ambient_cdm2,
                content_rgb_space=content_rgb_space,
                hdr_display=self.getcfg("3dlut.hdr_display"),
                XYZwp=XYZwp,
            )
        except Exception as exception:
            # if exception.__class__.__name__ in dir(exceptions):
            #     raise
            if DEBUG:
                messages = traceback.format_exception(
                    type(exception), exception, exception.__traceback__
                )
                print("[D] Worker raised exception: \n" + "\n".join(messages))
            return exception
        return True

    def lut3d_format_ctrl_handler(self, event):
        """Handle changes to the 3D LUT format control.

        Args:
            event (wx.Event): The event triggered by the control.
        """
        # Get selected format
        file_format = self.lut3d_formats_ab[self.lut3d_format_ctrl.GetSelection()]
        encoding_overrides = ("dcl", "eeColor", "madVR", "ReShade")
        size_overrides = ("dcl", "eeColor", "madVR", "mga", "ReShade")
        if (
            self.getcfg("3dlut.format") in encoding_overrides
            and file_format not in encoding_overrides
        ):
            # If previous format forced specific encoding, restore encoding
            self.setcfg(
                "3dlut.encoding.input", self.getcfg("3dlut.encoding.input.backup")
            )
            self.setcfg(
                "3dlut.encoding.output", self.getcfg("3dlut.encoding.output.backup")
            )
        if self.getcfg("3dlut.format") in size_overrides:
            # If previous format forced specific size, restore size
            self.setcfg("3dlut.size", self.getcfg("3dlut.size.backup"))
        if (
            self.getcfg("3dlut.format") not in encoding_overrides
            and file_format in encoding_overrides
        ):
            # If selected format forces specific encoding, backup current encoding
            self.setcfg(
                "3dlut.encoding.input.backup", self.getcfg("3dlut.encoding.input")
            )
            self.setcfg(
                "3dlut.encoding.output.backup", self.getcfg("3dlut.encoding.output")
            )
        # Set selected format
        self.lut3d_set_option("3dlut.format", file_format)
        if file_format in size_overrides:
            # If selected format forces specific size, backup current size
            self.setcfg("3dlut.size.backup", self.getcfg("3dlut.size"))
        if file_format == "eeColor":
            # -et -Et for eeColor
            if self.getcfg("3dlut.encoding.input") not in ("t", "T"):
                self.lut3d_set_option("3dlut.encoding.input", "t")
            self.lut3d_set_option("3dlut.encoding.output", "t")
            # eeColor uses a fixed size of 65x65x65
            self.lut3d_set_option("3dlut.size", 65)
        elif file_format == "mga":
            # Pandora uses a fixed bitdepth of 16
            self.lut3d_set_option("3dlut.bitdepth.output", 16)
            self.lut3d_bitdepth_output_ctrl.SetSelection(self.lut3d_bitdepth_ba[16])
        elif file_format == "madVR":
            # -et -Et for madVR
            if self.getcfg("3dlut.encoding.input") not in ("t", "T"):
                self.lut3d_set_option("3dlut.encoding.input", "t")
            self.lut3d_set_option("3dlut.encoding.output", "t")
            # collink says madVR works best with 65
            self.lut3d_set_option("3dlut.size", 65)
        elif file_format in ("png", "ReShade"):
            if file_format == "ReShade":
                self.lut3d_set_option("3dlut.encoding.input", "n")
                self.lut3d_set_option("3dlut.encoding.output", "n")
                self.lut3d_set_option("3dlut.bitdepth.output", 8)
            elif self.getcfg("3dlut.bitdepth.output") not in (8, 16):
                self.lut3d_set_option("3dlut.bitdepth.output", 8)
            self.lut3d_bitdepth_output_ctrl.SetSelection(
                self.lut3d_bitdepth_ba[self.getcfg("3dlut.bitdepth.output")]
            )
        elif file_format == "dcl":
            self.lut3d_set_option("3dlut.encoding.input", "n")
            self.lut3d_set_option("3dlut.encoding.output", "n")
            self.lut3d_set_option("3dlut.size", 33)
            self.lut3d_set_option("3dlut.bitdepth.output", 12)
            self.lut3d_bitdepth_output_ctrl.SetSelection(self.lut3d_bitdepth_ba[12])
        size = self.getcfg("3dlut.size")
        snap_size = self.lut3d_snap_size(size)
        if snap_size != size:
            self.lut3d_set_option("3dlut.size", snap_size)
        self.lut3d_size_ctrl.SetSelection(self.lut3d_size_ba[self.getcfg("3dlut.size")])
        self.lut3d_update_encoding_controls()
        self.lut3d_enable_size_controls()
        self.lut3d_show_bitdepth_controls()
        if not isinstance(self, LUT3DFrame):
            self.panel.Freeze()
            self.lut3d_show_controls()
            self.calpanel.Layout()
            self.update_main_controls()
            self.panel.Thaw()
            if getattr(self, "lut3dframe", None):
                self.lut3dframe.lut3d_update_shared_controls()
            return
        self.panel.Freeze()
        self.lut3d_show_hdr_display_control()
        self.panel.Layout()
        self.panel.Thaw()
        self.update_layout()
        if self.Parent:
            self.Parent.lut3d_update_shared_controls()
        self.lut3d_create_btn.Enable(
            file_format != "madVR" or self.output_profile_ctrl.IsShown()
        )

    def lut3d_size_ctrl_handler(self, event):
        """Handle changes to the 3D LUT size control.

        Args:
            event (wx.Event): The event triggered by the control.
        """
        size = self.lut3d_size_ab[self.lut3d_size_ctrl.GetSelection()]
        snap_size = self.lut3d_snap_size(size)
        if snap_size != size:
            wx.Bell()
            self.lut3d_size_ctrl.SetSelection(self.lut3d_size_ba[snap_size])
        self.lut3d_set_option("3dlut.size", snap_size)
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_shared_controls()
        elif self.Parent:
            self.Parent.lut3d_update_shared_controls()

    def lut3d_snap_size(self, size):
        """Snap size to valid 3D LUT size.

        Returns:
            int: Snapped size.
        """
        if self.getcfg("3dlut.format") == "mga" and size not in (17, 33):
            size = 17 if size < 33 else 33
        elif self.getcfg("3dlut.format") == "ReShade" and size not in (16, 32, 64):
            if size < 32:
                size = 16
            elif size < 64:
                size = 32
            else:
                size = 64
        return size

    def lut3d_gamut_mapping_mode_handler(self, event):
        """Handle changes to the gamut mapping mode control."""
        self.lut3d_set_option(
            "3dlut.gamap.use_b2a", int(self.gamut_mapping_b2a.GetValue())
        )
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.update_controls()
        elif self.Parent:
            self.Parent.lut3d_update_b2a_controls()

    def lut3d_rendering_intent_ctrl_handler(self, event):
        """Handle changes to the rendering intent control."""
        self.lut3d_set_option(
            "3dlut.rendering_intent",
            self.rendering_intents_ab[self.lut3d_rendering_intent_ctrl.GetSelection()],
        )
        if getattr(self, "lut3dframe", None):
            self.lut3dframe.lut3d_update_shared_controls()
        else:
            if isinstance(self, LUT3DFrame):
                self.lut3d_show_input_value_clipping_warning(True)
            if self.Parent:
                self.Parent.lut3d_update_shared_controls()

    def lut3d_setup_language(self):
        """Set up language for 3D LUT controls."""
        # Shared with main window
        tone_curve_names = (
            "Gamma 2.2",
            "trc.rec1886",
            "trc.smpte2084.hardclip",
            "trc.smpte2084.rolloffclip",
            "trc.hlg",
            "custom",
        )
        self.lut3d_trc_ctrl.SetItems([lang.getstr(item) for item in tone_curve_names])
        self.trc_gamma_types_ab = {0: "b", 1: "B"}
        self.trc_gamma_types_ba = {"b": 0, "B": 1}
        self.lut3d_trc_gamma_type_ctrl.SetItems(
            [lang.getstr("trc.type.relative"), lang.getstr("trc.type.absolute")]
        )

        self.lut3d_content_colorspace_names = ["Rec. 2020", "DCI P3 D65", "Rec. 709"]
        self.lut3d_content_colorspace_ctrl.SetItems(
            [*self.lut3d_content_colorspace_names, lang.getstr("custom")]
        )

        self.rendering_intents_ab = {}
        self.rendering_intents_ba = {}
        self.lut3d_rendering_intent_ctrl.Clear()
        intents = list(config.VALID_VALUES["3dlut.rendering_intent"])
        if self.worker.argyll_version < [1, 8, 3]:
            intents.remove("lp")
        for i, ri in enumerate(intents):
            self.lut3d_rendering_intent_ctrl.Append(lang.getstr("gamap.intents." + ri))
            self.rendering_intents_ab[i] = ri
            self.rendering_intents_ba[ri] = i

        self.lut3d_formats_ab = {}
        self.lut3d_formats_ba = {}
        self.lut3d_format_ctrl.Clear()
        i = 0
        for file_format in config.VALID_VALUES["3dlut.format"]:
            if file_format != "madVR" or self.worker.argyll_version >= [1, 6]:
                self.lut3d_format_ctrl.Append(
                    lang.getstr(f"3dlut.format.{file_format}")
                )
                self.lut3d_formats_ab[i] = file_format
                self.lut3d_formats_ba[file_format] = i
                i += 1

        self.lut3d_hdr_display_ctrl.SetItems(
            [
                lang.getstr(item)
                for item in ("3dlut.format.madVR.hdr_to_sdr", "3dlut.format.madVR.hdr")
            ]
        )

        self.lut3d_size_ab = {}
        self.lut3d_size_ba = {}
        self.lut3d_size_ctrl.Clear()
        for i, size in enumerate(config.VALID_VALUES["3dlut.size"]):
            self.lut3d_size_ctrl.Append("%sx%sx%s" % ((size,) * 3))
            self.lut3d_size_ab[i] = size
            self.lut3d_size_ba[size] = i

        self.lut3d_bitdepth_ab = {}
        self.lut3d_bitdepth_ba = {}
        self.lut3d_bitdepth_input_ctrl.Clear()
        self.lut3d_bitdepth_output_ctrl.Clear()
        for i, bitdepth in enumerate(config.VALID_VALUES["3dlut.bitdepth.input"]):
            self.lut3d_bitdepth_input_ctrl.Append(str(bitdepth))
            self.lut3d_bitdepth_output_ctrl.Append(str(bitdepth))
            self.lut3d_bitdepth_ab[i] = bitdepth
            self.lut3d_bitdepth_ba[bitdepth] = i

    def lut3d_setup_encoding_ctrl(self):
        """Set up encoding controls for 3D LUTs."""
        file_format = self.getcfg("3dlut.format")
        # Shared with amin window
        if file_format == "madVR":
            encodings = ["t"]
            config.DEFAULTS["3dlut.encoding.input"] = "t"
            config.DEFAULTS["3dlut.encoding.output"] = "t"
        else:
            encodings = ["n"] if file_format == "dcl" else list(VIDEO_ENCODINGS)
            config.DEFAULTS["3dlut.encoding.input"] = "n"
            config.DEFAULTS["3dlut.encoding.output"] = "n"
        if (
            self.worker.argyll_version >= [1, 7]
            and self.worker.argyll_version != [1, 7, 0, "_beta"]
            and file_format != "dcl"
        ):
            # Argyll 1.7 beta 3 (2015-04-02) added clip WTW on input TV encoding
            encodings.insert(2, "T")
        config.VALID_VALUES["3dlut.encoding.input"] = encodings
        # collink: xvYCC output encoding is not supported
        config.VALID_VALUES["3dlut.encoding.output"] = [
            v for v in encodings if v not in ("T", "x", "X")
        ]
        self.encoding_input_ab = {}
        self.encoding_input_ba = {}
        self.encoding_output_ab = {}
        self.encoding_output_ba = {}
        self.encoding_input_ctrl.Freeze()
        self.encoding_input_ctrl.Clear()
        self.encoding_output_ctrl.Freeze()
        self.encoding_output_ctrl.Clear()
        for i, encoding in enumerate(config.VALID_VALUES["3dlut.encoding.input"]):
            lstr = lang.getstr(f"3dlut.encoding.type_{encoding}")
            self.encoding_input_ctrl.Append(lstr)
            self.encoding_input_ab[i] = encoding
            self.encoding_input_ba[encoding] = i
        for o, encoding in enumerate(config.VALID_VALUES["3dlut.encoding.output"]):
            lstr = lang.getstr(f"3dlut.encoding.type_{encoding}")
            self.encoding_output_ctrl.Append(lstr)
            self.encoding_output_ab[o] = encoding
            self.encoding_output_ba[encoding] = o
        self.encoding_input_ctrl.Thaw()
        self.encoding_output_ctrl.Thaw()

    def lut3d_set_option(self, option, v, set_changed=True):
        """Set option to value and update settings state."""
        if (
            hasattr(self, "profile_settings_changed")
            and set_changed
            and self.getcfg("3dlut.create")
            and v != self.getcfg(option)
        ):
            self.profile_settings_changed()
        self.setcfg(option, v)
        if option in (
            "3dlut.hdr_peak_luminance",
            "3dlut.hdr_minmll",
            "3dlut.hdr_maxmll",
        ):
            self.lut3d_show_hdr_maxmll_alt_clip_ctrl()
            self.lut3d_hdr_update_diffuse_white()
        elif option == "3dlut.hdr_ambient_luminance":
            self.lut3d_hdr_update_system_gamma()

    def lut3d_hdr_update_diffuse_white(self):
        """Update diffuse white for HDR roll-off."""
        # Update knee start info for BT.2390-3 roll-off
        bt2390 = colormath.BT2390(
            0,
            self.getcfg("3dlut.hdr_peak_luminance"),
            self.getcfg("3dlut.hdr_minmll"),
            self.getcfg("3dlut.hdr_maxmll"),
            self.getcfg("3dlut.hdr_maxmll_alt_clip"),
        )
        diffuse_ref_cdm2 = 94.37844
        diffuse_PQ = colormath.special_pow(diffuse_ref_cdm2 / 10000, 1.0 / -2084)
        # Determine white cd/m2 after roll-off
        diffuse_tgt_cdm2 = (
            colormath.special_pow(bt2390.apply(diffuse_PQ), -2084) * 10000
        )
        signalcolor = "#CC0000" if diffuse_tgt_cdm2 < diffuse_ref_cdm2 else "#008000"
        self.lut3d_hdr_diffuse_white_txt.ForegroundColour = signalcolor
        self.lut3d_hdr_diffuse_white_txt.Label = f"{diffuse_tgt_cdm2:.2f}"
        self.lut3d_hdr_diffuse_white_txt_label.ForegroundColour = signalcolor
        self.lut3d_hdr_diffuse_white_txt.ContainingSizer.Layout()

    def lut3d_hdr_update_sat_val(self):
        """Update saturation and value for HDR saturation control."""
        v = self.getcfg("3dlut.hdr_sat") * 100
        self.lut3d_hdr_sat_ctrl_lum_val.Label = f"{100 - v}%"
        self.lut3d_hdr_sat_ctrl_sat_val.Label = f"{v}%"

    def lut3d_hdr_update_system_gamma(self):
        """Update system gamma for HLG based on ambient luminance."""
        # Update system gamma for HLG based on ambient luminance (BT.2390-3)
        hlg = colormath.HLG(ambient_cdm2=self.getcfg("3dlut.hdr_ambient_luminance"))
        self.lut3d_hdr_system_gamma_txt.Label = str(stripzeros(f"{hlg.gamma:.4f}"))

    def lut3d_update_shared_controls(self):
        """Update shared controls in the main window."""
        # Shared with main window
        self.lut3d_update_trc_controls()
        self.lut3d_rendering_intent_ctrl.SetSelection(
            self.rendering_intents_ba[self.getcfg("3dlut.rendering_intent")]
        )
        # MadVR only available with Argyll 1.6+, fall back to default
        self.lut3d_format_ctrl.SetSelection(
            self.lut3d_formats_ba.get(
                self.getcfg("3dlut.format"),
                self.lut3d_formats_ba[DEFAULTS["3dlut.format"]],
            )
        )
        self.lut3d_hdr_display_ctrl.SetSelection(self.getcfg("3dlut.hdr_display"))
        self.lut3d_size_ctrl.SetSelection(self.lut3d_size_ba[self.getcfg("3dlut.size")])
        self.lut3d_enable_size_controls()
        self.lut3d_bitdepth_input_ctrl.SetSelection(
            self.lut3d_bitdepth_ba[self.getcfg("3dlut.bitdepth.input")]
        )
        self.lut3d_bitdepth_output_ctrl.SetSelection(
            self.lut3d_bitdepth_ba[self.getcfg("3dlut.bitdepth.output")]
        )
        self.lut3d_show_bitdepth_controls()
        if self.Parent:
            self.Parent.lut3d_update_shared_controls()

    def lut3d_update_trc_control(self):
        """Update the TRC control based on the current settings."""
        if self.getcfg("3dlut.trc").startswith("smpte2084"):  # SMPTE 2084
            sel = 2 if self.getcfg("3dlut.trc") == "smpte2084.hardclip" else 3
            self.lut3d_trc_ctrl.SetSelection(sel)
        elif self.getcfg("3dlut.trc") == "hlg":  # Hybrid Log-Gamma (HLG)
            self.lut3d_trc_ctrl.SetSelection(4)
        elif (
            self.getcfg("3dlut.trc_gamma_type") == "B"
            and self.getcfg("3dlut.trc_output_offset") == 0
            and self.getcfg("3dlut.trc_gamma") == 2.4
        ):
            self.lut3d_trc_ctrl.SetSelection(1)  # BT.1886
            self.setcfg("3dlut.trc", "bt1886")
        elif (
            self.getcfg("3dlut.trc_gamma_type") == "b"
            and self.getcfg("3dlut.trc_output_offset") == 1
            and self.getcfg("3dlut.trc_gamma") == 2.2
        ):
            self.lut3d_trc_ctrl.SetSelection(0)  # Pure power gamma 2.2
            self.setcfg("3dlut.trc", "gamma2.2")
        else:
            self.lut3d_trc_ctrl.SetSelection(5)  # Custom
            self.setcfg("3dlut.trc", "customgamma")

    def lut3d_update_trc_controls(self):
        """Update the TRC controls based on the current settings."""
        self.lut3d_update_trc_control()
        self.lut3d_trc_gamma_ctrl.SetValue(str(self.getcfg("3dlut.trc_gamma")))
        self.lut3d_trc_gamma_type_ctrl.SetSelection(
            self.trc_gamma_types_ba[self.getcfg("3dlut.trc_gamma_type")]
        )
        outoffset = int(self.getcfg("3dlut.trc_output_offset") * 100)
        self.lut3d_trc_black_output_offset_ctrl.SetValue(outoffset)
        self.lut3d_trc_black_output_offset_intctrl.SetValue(outoffset)
        target_peak = self.getcfg("3dlut.hdr_peak_luminance")
        maxmll = self.getcfg("3dlut.hdr_maxmll")
        # Don't allow maxmll < target peak. Technically this restriction does
        # not exist, but practically maxmll < target peak doesn't make sense.
        if maxmll < target_peak:
            maxmll = target_peak
            self.setcfg("3dlut.hdr_maxmll", maxmll)
        self.lut3d_hdr_maxmll_ctrl.SetRange(target_peak, 10000)
        self.lut3d_hdr_peak_luminance_ctrl.SetValue(target_peak)
        self.lut3d_hdr_minmll_ctrl.SetValue(self.getcfg("3dlut.hdr_minmll"))
        self.lut3d_hdr_maxmll_ctrl.SetValue(maxmll)
        self.lut3d_hdr_maxmll_alt_clip_cb.SetValue(
            not bool(self.getcfg("3dlut.hdr_maxmll_alt_clip"))
        )
        self.lut3d_hdr_update_diffuse_white()
        self.lut3d_hdr_ambient_luminance_ctrl.SetValue(
            self.getcfg("3dlut.hdr_ambient_luminance")
        )
        self.lut3d_hdr_update_system_gamma()
        # Content colorspace (currently only used for SMPTE 2084)
        content_colors = []
        for color in ("red", "green", "blue", "white"):
            for coord in "xy":
                v = self.getcfg(f"3dlut.content.colorspace.{color}.{coord}")
                getattr(self, f"lut3d_content_colorspace_{color}_{coord}").SetValue(v)
                content_colors.append(round(v, 4))
        rgb_space_name = colormath.find_primaries_wp_xy_rgb_space_name(
            content_colors, self.lut3d_content_colorspace_names
        )
        if rgb_space_name:
            i = self.lut3d_content_colorspace_names.index(rgb_space_name)
        else:
            i = self.lut3d_content_colorspace_ctrl.Count - 1
        self.lut3d_content_colorspace_ctrl.SetSelection(i)
        self.lut3d_hdr_sat_ctrl.SetValue(round(self.getcfg("3dlut.hdr_sat") * 100))
        self.lut3d_hdr_update_sat_val()
        hue = round(self.getcfg("3dlut.hdr_hue") * 100)
        self.lut3d_hdr_hue_ctrl.SetValue(hue)
        self.lut3d_hdr_hue_intctrl.SetValue(hue)

    def lut3d_show_bitdepth_controls(self):
        """Show or hide the bitdepth controls."""
        frozen = self.IsFrozen()
        if not frozen:
            self.Freeze()
        show = True
        input_show = show and self.getcfg("3dlut.format") == "3dl"
        self.lut3d_bitdepth_input_label.Show(input_show)
        self.lut3d_bitdepth_input_ctrl.Show(input_show)
        output_show = show and self.getcfg("3dlut.format") in ("3dl", "png")
        self.lut3d_bitdepth_output_label.Show(output_show)
        self.lut3d_bitdepth_output_ctrl.Show(output_show)
        if isinstance(self, LUT3DFrame):
            self.panel.GetSizer().Layout()
            self.update_layout()
        else:
            self.update_scrollbars()
        if not frozen:
            self.Thaw()

    def lut3d_show_hdr_display_control(self):
        """Show or hide the HDR display control."""
        self.lut3d_hdr_display_ctrl.Show(
            (
                self.getcfg("3dlut.apply_trc")
                or not hasattr(self, "lut3d_trc_apply_none_ctrl")
            )
            and self.getcfg("3dlut.trc").startswith("smpte2084")
            and self.getcfg("3dlut.format") == "madVR"
        )

    def lut3d_show_hdr_maxmll_alt_clip_ctrl(self):
        """Show or hide the alternate master white clipping control."""
        self.panel.Freeze()
        show = self.lut3d_hdr_maxmll_ctrl.IsShown()  # BT.2390 (roll-off)
        self.lut3d_hdr_maxmll_alt_clip_cb.Show(
            show and self.getcfg("3dlut.hdr_maxmll") < 10000
        )
        self.panel.Layout()
        self.panel.Thaw()

    def lut3d_show_trc_controls(self, show=True):
        """Show or hide the TRC controls based on the Argyll version.

        Args:
            show (bool): Whether to show the controls. Defaults to True.
        """
        self.panel.Freeze()
        show = show and self.worker.argyll_version >= [1, 6]
        if hasattr(self, "lut3d_trc_apply_ctrl"):
            self.lut3d_trc_apply_ctrl.Show(show)
        self.lut3d_trc_ctrl.Show(show)
        smpte2084 = self.getcfg("3dlut.trc").startswith("smpte2084")
        hlg = self.getcfg("3dlut.trc") == "hlg"
        hdr = smpte2084 or hlg
        show = show and (
            self.getcfg("3dlut.trc") == "customgamma"
            or (isinstance(self, LUT3DFrame) or self.getcfg("show_advanced_options"))
        )
        self.lut3d_trc_gamma_label.Show(show and not hdr)
        self.lut3d_trc_gamma_ctrl.Show(show and not hdr)
        smpte2084r = self.getcfg("3dlut.trc") == "smpte2084.rolloffclip"
        # Show items in this order so we end up with the correct controls shown
        showcc = (smpte2084r or hlg) and (
            isinstance(self, LUT3DFrame) or self.getcfg("show_advanced_options")
        )
        self.lut3d_content_colorspace_label.ContainingSizer.ShowItems(showcc)
        sel = self.lut3d_content_colorspace_ctrl.Selection
        lastsel = self.lut3d_content_colorspace_ctrl.Count - 1
        sizer = self.lut3d_content_colorspace_red_x.ContainingSizer
        sizer.ShowItems(showcc and sel == lastsel)
        self.lut3d_hdr_minmll_label.Show(show and smpte2084)
        self.lut3d_hdr_minmll_ctrl.Show(show and smpte2084)
        self.lut3d_hdr_minmll_ctrl_label.Show(show and smpte2084)
        self.lut3d_hdr_maxmll_label.Show(show and smpte2084r)
        self.lut3d_hdr_maxmll_ctrl.Show(show and smpte2084r)
        self.lut3d_hdr_maxmll_ctrl_label.Show(show and smpte2084r)
        self.lut3d_show_hdr_maxmll_alt_clip_ctrl()
        self.lut3d_hdr_diffuse_white_label.Show(show and smpte2084r)
        self.lut3d_hdr_diffuse_white_txt.Show(show and smpte2084r)
        self.lut3d_hdr_diffuse_white_txt_label.Show(show and smpte2084r)
        self.lut3d_hdr_ambient_luminance_label.Show(show and hlg)
        self.lut3d_hdr_ambient_luminance_ctrl.Show(show and hlg)
        self.lut3d_hdr_ambient_luminance_ctrl_label.Show(show and hlg)
        self.lut3d_hdr_system_gamma_label.Show(show and hlg)
        self.lut3d_hdr_system_gamma_txt.Show(show and hlg)
        sizer = self.lut3d_hdr_sat_ctrl.ContainingSizer
        sizer.ShowItems(show and smpte2084r)
        sizer = self.lut3d_hdr_hue_ctrl.ContainingSizer
        sizer.ShowItems(show and smpte2084r)
        show = (show or smpte2084) and not hlg
        show = show and (
            (hasattr(self, "lut3d_create_cb") and self.getcfg("3dlut.create"))
            or self.XYZbpout > [0, 0, 0]
        )
        self.lut3d_trc_gamma_type_ctrl.Show(show and not hdr)
        self.lut3d_trc_black_output_offset_label.Show(show)
        self.lut3d_trc_black_output_offset_ctrl.Show(show)
        self.lut3d_trc_black_output_offset_intctrl.Show(show)
        self.lut3d_trc_black_output_offset_intctrl_label.Show(show)
        self.lut3d_hdr_peak_luminance_label.Show(smpte2084)
        self.lut3d_hdr_peak_luminance_ctrl.Show(smpte2084)
        self.lut3d_hdr_peak_luminance_ctrl_label.Show(smpte2084)
        self.lut3d_show_hdr_display_control()
        self.panel.Layout()
        self.panel.Thaw()
        if isinstance(self, LUT3DFrame):
            self.update_layout()

    def lut3d_show_encoding_controls(self, show=True):
        """Show or hide the encoding controls based on the Argyll version.

        Args:
            show (bool): Whether to show the controls. Defaults to True.
        """
        show = show and (
            (
                self.worker.argyll_version >= [1, 7]
                and self.worker.argyll_version != [1, 7, 0, "_beta"]
            )
            or self.worker.argyll_version >= [1, 6]
        )
        # Argyll 1.7 beta 3 (2015-04-02) added clip WTW on input TV encoding
        self.encoding_input_label.Show(show)
        self.encoding_input_ctrl.Show(show)
        show = show and self.worker.argyll_version >= [1, 6]
        self.encoding_output_label.Show(show)
        self.encoding_output_ctrl.Show(show)

    def lut3d_update_encoding_controls(self):
        """Update the encoding controls based on the selected format."""
        self.lut3d_setup_encoding_ctrl()
        self.encoding_input_ctrl.SetSelection(
            self.encoding_input_ba[self.getcfg("3dlut.encoding.input")]
        )
        self.encoding_input_ctrl.Enable(self.encoding_input_ctrl.Count > 1)
        self.encoding_output_ctrl.SetSelection(
            self.encoding_output_ba[self.getcfg("3dlut.encoding.output")]
        )
        self.encoding_output_ctrl.Enable(
            self.getcfg("3dlut.format") not in ("dcl", "madVR")
        )

    def lut3d_enable_size_controls(self):
        """Enable or disable the 3D LUT size controls based on the selected format."""
        self.lut3d_size_ctrl.Enable(
            self.getcfg("3dlut.format") not in ("eeColor", "madVR")
        )


class LUT3DFrame(BaseFrame, LUT3DMixin):
    """3D LUT creation window.

    Args:
        parent (wx.Window, optional): The parent window. Defaults to None.
        setup (bool, optional): Whether to set up the frame immediately.
            Defaults to True.
    """

    def __init__(self, parent=None, setup=True):
        self.input_profile = None
        self.res = TempXmlResource(get_data_path(os.path.join("xrc", "3dlut.xrc")))
        self.res.InsertHandler(
            xh_filebrowsebutton.FileBrowseButtonWithHistoryXmlHandler()
        )
        self.res.InsertHandler(xh_floatspin.FloatSpinCtrlXmlHandler())
        self.res.InsertHandler(xh_bitmapctrls.BitmapButton())
        self.res.InsertHandler(xh_bitmapctrls.StaticBitmap())
        if hasattr(wx, "PreFrame"):
            # Classic
            pre = wx.PreFrame()
            self.res.LoadOnFrame(pre, parent, "lut3dframe")
            self.PostCreate(pre)
        else:
            # Phoenix
            wx.Frame.__init__(self)
            self.res.LoadFrame(self, parent, "lut3dframe")
        self.init()
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        if sys.platform == "win32":
            self.Bind(wx.EVT_SIZE, self.OnSize)

        self.SetIcons(
            config.get_icon_bundle([256, 48, 32, 16], APPNAME + "-3DLUT-maker")
        )

        self.set_child_ctrls_as_attrs(self)

        self.panel = xrc.XRCCTRL(self, "panel")

        if setup:
            self.setup()

    def OnSize(self, event):
        """Handle the size event for the 3D LUT frame.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        event.Skip()
        self.Refresh()  # Prevents distorted drawing under Windows

    def setup(self):
        """Set up the 3D LUT frame."""
        self.worker = worker.Worker(self)
        self.worker.set_argyll_version("collink")

        for which in ("input", "abstract", "output"):
            ctrl = xrc.XRCCTRL(self, f"{which}_profile_ctrl")
            setattr(self, f"{which}_profile_ctrl", ctrl)
            ctrl.changeCallback = getattr(self, f"{which}_profile_ctrl_handler")
            if which not in ("abstract", "output"):
                ctrl.SetHistory(get_data_path("ref", r"\.(icc|icm)$"))
            ctrl.SetMaxFontSize(11)
            # Drop targets
            droptarget = FileDrop(
                self,
                {
                    ".icc": getattr(self, f"{which}_drop_handler"),
                    ".icm": getattr(self, f"{which}_drop_handler"),
                },
            )
            ctrl.SetDropTarget(droptarget)

        # Bind event handlers
        self.abstract_profile_cb.Bind(
            wx.EVT_CHECKBOX, self.use_abstract_profile_ctrl_handler
        )
        self.output_profile_current_btn.Bind(
            wx.EVT_BUTTON, self.output_profile_current_ctrl_handler
        )
        self.lut3d_trc_apply_none_ctrl.Bind(
            wx.EVT_RADIOBUTTON, self.lut3d_trc_apply_ctrl_handler
        )
        self.lut3d_trc_apply_black_offset_ctrl.Bind(
            wx.EVT_RADIOBUTTON, self.lut3d_trc_apply_ctrl_handler
        )
        self.lut3d_trc_apply_ctrl.Bind(
            wx.EVT_RADIOBUTTON, self.lut3d_trc_apply_ctrl_handler
        )
        self.lut3d_bind_event_handlers()

        self.lut3d_create_btn.SetDefault()

        self.setup_language()
        self.XYZbpin = [0, 0, 0]
        # XYZbpout will be set to the blackpoint of the selected profile. This
        # is used to determine if lack output offset controls should be shown.
        # Set a initial value slightly above zero so output offset controls are
        # shown if the selected profile doesn't exist.
        self.XYZbpout = [0.001, 0.001, 0.001]
        self.update_controls()
        self.update_layout()
        scrollrate_x = 2 if self.panel.VirtualSize[0] > self.panel.Size[0] else 0
        self.panel.SetScrollRate(scrollrate_x, 2)

        config.DEFAULTS.update(
            {
                "position.lut3dframe.x": self.GetDisplay().ClientArea[0] + 40,
                "position.lut3dframe.y": self.GetDisplay().ClientArea[1] + 60,
                "size.lut3dframe.w": self.ClientSize[0],
                "size.lut3dframe.h": self.ClientSize[1],
            }
        )

        if (
            self.hascfg("position.lut3dframe.x")
            and self.hascfg("position.lut3dframe.y")
            and self.hascfg("size.lut3dframe.w")
            and self.hascfg("size.lut3dframe.h")
        ):
            self.SetSaneGeometry(
                int(self.getcfg("position.lut3dframe.x")),
                int(self.getcfg("position.lut3dframe.y")),
                int(self.getcfg("size.lut3dframe.w")),
                int(self.getcfg("size.lut3dframe.h")),
            )
        else:
            self.Center()

    def OnClose(self, event=None):
        """Handle the close event for the 3D LUT frame.

        Args:
            event (wx.Event, optional): The event that triggered this handler.
        """
        if getattr(self.worker, "thread", None) and self.worker.thread.is_alive():
            self.worker.abort_subprocess(True)
            return
        if sys.platform == "darwin" or DEBUG:
            self.focus_handler(event)
        if self.IsShownOnScreen() and not self.IsMaximized() and not self.IsIconized():
            x, y = self.GetScreenPosition()
            self.setcfg("position.lut3dframe.x", x)
            self.setcfg("position.lut3dframe.y", y)
            self.setcfg("size.lut3dframe.w", self.ClientSize[0])
            self.setcfg("size.lut3dframe.h", self.ClientSize[1])
        if self.Parent:
            config.writecfg()
        else:
            config.writecfg(
                module="3DLUT-maker",
                options=(
                    "3dlut.",
                    "last_3dlut_path",
                    "position.lut3dframe",
                    "size.lut3dframe",
                ),
            )
        if event:
            # Hide first (looks nicer)
            self.Hide()
            # Need to use CallAfter to prevent hang under Windows if minimized
            wx.CallAfter(self.Destroy)

    def use_abstract_profile_ctrl_handler(self, event):
        """Handle the abstract profile checkbox.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.setcfg(
            "3dlut.use_abstract_profile", int(self.abstract_profile_cb.GetValue())
        )
        enable = bool(self.getcfg("3dlut.use_abstract_profile"))
        self.abstract_profile_ctrl.Enable(enable)

    def abstract_drop_handler(self, path):
        """Set abstract profile to selected profile path.

        Args:
            path (str): Path to the abstract profile.
        """
        if not self.worker.is_working():
            self.abstract_profile_ctrl.SetPath(path)
            self.set_profile("abstract")

    def input_drop_handler(self, path):
        """Set input profile to selected profile path.

        Args:
            path (str): Path to the input profile.
        """
        if not self.worker.is_working():
            self.input_profile_ctrl.SetPath(path)
            self.set_profile("input")

    def output_drop_handler(self, path):
        """Set output profile to selected profile path.

        Args:
            path (str): Path to the output profile.
        """
        if not self.worker.is_working():
            self.output_profile_ctrl.SetPath(path)
            self.set_profile("output")

    def abstract_profile_ctrl_handler(self, event):
        """Set abstract profile to selected profile path.

        Args:
            event (wx.Event): The event that triggered this handler, if any.
        """
        self.set_profile("abstract", silent=not event)

    def input_profile_ctrl_handler(self, event):
        """Set input profile to selected profile path.

        Args:
            event (wx.Event): The event that triggered this handler, if any.
        """
        self.set_profile("input", silent=not event)
        if self.Parent:
            self.Parent.lut3d_init_input_profiles()
            self.Parent.lut3d_update_controls()

    def output_profile_ctrl_handler(self, event):
        """Set output profile to selected profile path.

        Args:
            event (wx.Event): The event that triggered this handler, if any.
        """
        self.set_profile("output", silent=not event)

    def output_profile_current_ctrl_handler(self, event):
        """Set output profile to current profile path.

        Args:
            event (wx.Event): The event that triggered this handler, if any.
        """
        profile_path = get_current_profile_path(True, True)
        if profile_path and os.path.isfile(profile_path):
            self.output_profile_ctrl.SetPath(profile_path)
            self.set_profile("output", profile_path or False, silent=not event)

    def get_commands(self):
        """Get commands for the command line or IPC.

        Returns:
            list: List of commands that can be used to control the 3D LUT
                maker.
        """
        return [*self.get_common_commands(), "3DLUT-maker [create <filename>]"]

    def process_data(self, data):
        """Process data from command line or IPC.

        Args:
            data (list): List of strings, where the first element is the command
                and the second element is the action or additional data.

        Returns:
            str: "ok" if the command was processed successfully, "invalid" if
                the command was not recognized
        """
        if data[0] == "3DLUT-maker" and (
            len(data) == 1 or (len(data) == 3 and data[1] == "create")
        ):
            if self.IsIconized():
                self.Restore()
            self.Raise()
            if len(data) == 3:
                wx.CallAfter(
                    self.lut3d_create_handler,
                    CustomEvent(wx.EVT_BUTTON.evtType[0], self.lut3d_create_btn),
                    path=data[2],
                )
            return "ok"
        return "invalid"

    def set_profile(self, which, profile_path=None, silent=False):
        """Set profile for input, abstract or output profile controls.

        Args:
            which (str): Which profile to set, one of "input", "abstract" or "output".
            profile_path (str, optional): Path to the profile to set. If None, the
                current profile path is used for the output profile.
            silent (bool): If True, do not show any dialogs or messages.

        Returns:
            None | ICCProfile: None if the profile was set successfully, or if
                the profile is a device link profile that was set as input
                profile. Otherwise, returns None if the profile is invalid or
                missing, or if an error occurred.
        """
        path = getattr(self, f"{which}_profile_ctrl").GetPath()
        if which == "output":
            if profile_path is None:
                profile_path = get_current_profile_path(True, True)
            self.output_profile_current_btn.Enable(
                self.output_profile_ctrl.IsShown()
                and bool(profile_path)
                and os.path.isfile(profile_path)
                and profile_path != path
            )
        if path:
            if not os.path.isfile(path):
                if not silent:
                    show_result_dialog(
                        Error(lang.getstr("file.missing", path)), parent=self
                    )
                return None
            try:
                profile = ICCProfile(path)
            except ICCProfileInvalidError:
                if not silent:
                    show_result_dialog(
                        Error(f"{lang.getstr('profile.invalid')}\n{path}"), parent=self
                    )
            except OSError as exception:
                if not silent:
                    show_result_dialog(exception, parent=self)
            else:
                if (
                    which in ("input", "output")
                    and (
                        profile.profileClass not in (b"mntr", b"link", b"scnr", b"spac")
                        or profile.colorSpace != b"RGB"
                    )
                ) or (
                    which == "abstract"
                    and (
                        profile.profileClass != b"abst"
                        or profile.colorSpace not in (b"Lab", b"XYZ")
                    )
                ):
                    show_result_dialog(
                        Error(
                            lang.getstr(
                                "profile.unsupported",
                                (profile.profileClass, profile.colorSpace),
                            )
                        ),
                        parent=self,
                    )
                else:
                    if profile.profileClass == b"link":
                        if which == "output":
                            self.input_profile_ctrl.SetPath(path)
                            if self.getcfg("3dlut.output.profile") == path:
                                # The original file was probably overwritten
                                # by the device link. Reset.
                                self.setcfg("3dlut.output.profile", None)
                            self.output_profile_ctrl.SetPath(
                                self.getcfg("3dlut.output.profile")
                            )
                            self.set_profile("input", silent=silent)
                            return None
                        self.Freeze()
                        self.abstract_profile_cb.SetValue(False)
                        self.abstract_profile_cb.Hide()
                        self.abstract_profile_ctrl.Hide()
                        self.output_profile_label.Hide()
                        self.output_profile_ctrl.Hide()
                        self.output_profile_current_btn.Hide()
                        self.lut3d_apply_cal_cb.Hide()
                        self.lut3d_trc_label.Hide()
                        self.lut3d_trc_apply_none_ctrl.Hide()
                        self.lut3d_input_value_clipping_bmp.Hide()
                        self.lut3d_input_value_clipping_label.Hide()
                        self.lut3d_trc_apply_black_offset_ctrl.Hide()
                        self.gamut_mapping_mode.Hide()
                        self.gamut_mapping_inverse_a2b.Hide()
                        self.gamut_mapping_b2a.Hide()
                        self.lut3d_show_encoding_controls(False)
                        self.lut3d_show_trc_controls(False)
                        self.lut3d_rendering_intent_label.Hide()
                        self.lut3d_rendering_intent_ctrl.Hide()
                        self.panel.GetSizer().Layout()
                        self.update_layout()
                        self.Thaw()
                    else:
                        if which == "input":
                            if (
                                not hasattr(self, f"{which}_profile")
                                or self.getcfg(f"3dlut.{which}.profile")
                                != profile.filename
                            ):
                                # Get profile blackpoint so we can check if it makes
                                # sense to show TRC type and output offset controls
                                try:
                                    odata = self.worker.xicclu(
                                        profile, (0, 0, 0), pcs="x"
                                    )
                                except Exception as exception:
                                    show_result_dialog(exception, self)
                                    self.set_profile_ctrl_path(which)
                                    return None
                                else:
                                    if len(odata) != 1 or len(odata[0]) != 3:
                                        show_result_dialog(
                                            f"Blackpoint is invalid: {odata}", self
                                        )
                                        self.set_profile_ctrl_path(which)
                                        return None
                                    self.XYZbpin = odata[0]
                            self.Freeze()
                            self.lut3d_trc_label.Show()
                            self.lut3d_trc_apply_none_ctrl.Show()
                            self.lut3d_trc_apply_black_offset_ctrl.Show()
                            self.gamut_mapping_mode.Show()
                            self.gamut_mapping_inverse_a2b.Show()
                            self.gamut_mapping_b2a.Show()
                            enable = bool(self.getcfg("3dlut.use_abstract_profile"))
                            self.abstract_profile_cb.SetValue(enable)
                            self.abstract_profile_cb.Show()
                            self.abstract_profile_ctrl.Enable(enable)
                            self.abstract_profile_ctrl.Show()
                            self.output_profile_label.Show()
                            self.output_profile_ctrl.Show()
                            self.output_profile_current_btn.Show()
                            self.lut3d_apply_cal_cb.Show()
                            self.lut3d_show_encoding_controls()
                            self.lut3d_update_encoding_controls()
                            # Update controls related to output profile
                            self.input_profile = profile
                            if not self.set_profile("output", silent=silent):
                                self.update_linking_controls()
                                self.lut3d_trc_apply_ctrl_handler()
                        elif which == "output":
                            if (
                                not hasattr(self, f"{which}_profile")
                                or self.getcfg(f"3dlut.{which}.profile")
                                != profile.filename
                            ):
                                # Get profile blackpoint so we can check if input
                                # values would be clipped
                                try:
                                    odata = self.worker.xicclu(
                                        profile, (0, 0, 0), pcs="x"
                                    )
                                except Exception as exception:
                                    show_result_dialog(exception, self)
                                    self.set_profile_ctrl_path(which)
                                    return None
                                else:
                                    if len(odata) != 1 or len(odata[0]) != 3:
                                        show_result_dialog(
                                            f"Blackpoint is invalid: {odata}", self
                                        )
                                        self.set_profile_ctrl_path(which)
                                        return None
                                    if odata[0][1]:
                                        # Got above zero blackpoint from lookup
                                        self.XYZbpout = odata[0]
                                    else:
                                        # Got zero blackpoint from lookup.
                                        # Try chardata instead.
                                        XYZbp = profile.get_chardata_bkpt()
                                        if XYZbp:
                                            self.XYZbpout = XYZbp
                                        else:
                                            self.XYZbpout = [0, 0, 0]
                            self.Freeze()
                            enable_apply_cal = isinstance(
                                profile.tags.get("vcgt"), VideoCardGammaType
                            )
                            self.lut3d_apply_cal_cb.SetValue(
                                enable_apply_cal
                                and bool(self.getcfg("3dlut.output.profile.apply_cal"))
                            )
                            self.lut3d_apply_cal_cb.Enable(enable_apply_cal)
                            self.gamut_mapping_inverse_a2b.Enable()
                            allow_b2a_gamap = (
                                "B2A0" in profile.tags
                                and isinstance(profile.tags.B2A0, LUT16Type)
                                and profile.tags.B2A0.clut_grid_steps >= 17
                            )
                            # Allow using B2A instead of inverse A2B?
                            self.gamut_mapping_b2a.Enable(allow_b2a_gamap)
                            if not allow_b2a_gamap:
                                self.setcfg("3dlut.gamap.use_b2a", 0)
                            self.update_linking_controls()
                            self.lut3d_trc_apply_ctrl_handler()
                            self.lut3d_rendering_intent_label.Show()
                            self.lut3d_rendering_intent_ctrl.Show()
                        self.panel.GetSizer().Layout()
                        self.update_layout()
                        if which != "abstract":
                            self.Thaw()
                    setattr(self, f"{which}_profile", profile)
                    if which == "output" and not self.output_profile_ctrl.IsShown():
                        return None
                    self.setcfg(f"3dlut.{which}.profile", profile.filename)
                    self.lut3d_create_btn.Enable(
                        bool(self.getcfg("3dlut.input.profile"))
                        and os.path.isfile(self.getcfg("3dlut.input.profile"))
                        and (
                            (
                                bool(self.getcfg("3dlut.output.profile"))
                                and os.path.isfile(self.getcfg("3dlut.output.profile"))
                            )
                            or profile.profileClass == b"link"
                        )
                        and (
                            self.getcfg("3dlut.format") != "madVR"
                            or self.output_profile_ctrl.IsShown()
                        )
                    )
                    return profile
            self.set_profile_ctrl_path(which)
        elif which == "input":
            self.set_profile_ctrl_path(which)
            self.lut3d_update_encoding_controls()
        elif not silent:
            setattr(self, f"{which}_profile", None)
            self.setcfg(f"3dlut.{which}.profile", None)
            if which == "output":
                self.lut3d_apply_cal_cb.Disable()
                self.lut3d_create_btn.Disable()
        return None

    def set_profile_ctrl_path(self, which):
        """Set the profile path for the specified profile control."""
        getattr(self, f"{which}_profile_ctrl").SetPath(
            self.getcfg(f"3dlut.{which}.profile")
        )

    def setup_language(self):
        """Setup language for the 3D LUT frame."""
        BaseFrame.setup_language(self)

        for which in ("input", "abstract", "output"):
            msg = {
                "input": lang.getstr("3dlut.input.profile"),
                "abstract": lang.getstr("3dlut.use_abstract_profile"),
                "output": lang.getstr("output.profile"),
            }[which]
            kwargs = {
                "toolTip": msg.rstrip(":"),
                "dialogTitle": msg,
                "fileMask": lang.getstr("filetype.icc") + "|*.icc;*.icm",
            }
            ctrl = getattr(self, f"{which}_profile_ctrl")
            for name in kwargs:
                value = kwargs[name]
                setattr(ctrl, name, value)

        self.lut3d_setup_language()

    def update_controls(self):
        """Update controls with values from the configuration."""
        self.panel.Freeze()
        self.lut3d_create_btn.Disable()
        self.input_profile_ctrl.SetPath(self.getcfg("3dlut.input.profile"))
        self.output_profile_ctrl.SetPath(self.getcfg("3dlut.output.profile"))
        self.input_profile_ctrl_handler(None)
        enable = bool(self.getcfg("3dlut.use_abstract_profile"))
        self.abstract_profile_cb.SetValue(enable)
        self.abstract_profile_ctrl.SetPath(self.getcfg("3dlut.abstract.profile"))
        self.abstract_profile_ctrl_handler(None)
        self.output_profile_ctrl_handler(None)
        self.lut3d_update_shared_controls()
        self.panel.Thaw()

    def update_linking_controls(self):
        """Update controls related to linking input and output profiles."""
        self.gamut_mapping_inverse_a2b.SetValue(not self.getcfg("3dlut.gamap.use_b2a"))
        self.gamut_mapping_b2a.SetValue(bool(self.getcfg("3dlut.gamap.use_b2a")))
        if (
            hasattr(self, "input_profile")
            and "rTRC" in self.input_profile.tags
            and "gTRC" in self.input_profile.tags
            and "bTRC" in self.input_profile.tags
            and self.input_profile.tags.rTRC
            == self.input_profile.tags.gTRC
            == self.input_profile.tags.bTRC
            and isinstance(self.input_profile.tags.rTRC, CurveType)
        ):
            tf = self.input_profile.tags.rTRC.get_transfer_function(outoffset=1.0)
            if self.getcfg("3dlut.input.profile") != self.input_profile.filename:
                # Use BT.1886 gamma mapping for SMPTE 240M /
                # Rec. 709 TRC
                self.setcfg(
                    "3dlut.apply_trc",
                    int(
                        tf[0][1] in (-240, -709)
                        or (tf[0][0].startswith("Gamma") and tf[1] >= 0.95)
                    ),
                )
                # Use only BT.1886 black output offset
                self.setcfg(
                    "3dlut.apply_black_offset",
                    int(
                        tf[0][1] not in (-240, -709)
                        and (not tf[0][0].startswith("Gamma") or tf[1] < 0.95)
                        and self.XYZbpin != self.XYZbpout
                    ),
                )
                # Set gamma to profile gamma if single gamma
                # profile
                if tf[0][0].startswith("Gamma") and tf[1] >= 0.95:
                    if not self.getcfg("3dlut.trc_gamma.backup", False):
                        # Backup current gamma
                        self.setcfg(
                            "3dlut.trc_gamma.backup", self.getcfg("3dlut.trc_gamma")
                        )
                    self.setcfg("3dlut.trc_gamma", round(tf[0][1], 2))
                # Restore previous gamma if not single gamma
                # profile
                elif self.getcfg("3dlut.trc_gamma.backup", False):
                    self.setcfg(
                        "3dlut.trc_gamma", self.getcfg("3dlut.trc_gamma.backup")
                    )
                    self.setcfg("3dlut.trc_gamma.backup", None)
            self.lut3d_trc_apply_black_offset_ctrl.Enable(
                tf[0][1] not in (-240, -709) and self.XYZbpin != self.XYZbpout
            )
            self.lut3d_update_trc_controls()
        elif (
            hasattr(self, "input_profile")
            and isinstance(self.input_profile.tags.get("A2B0"), LUT16Type)
            and isinstance(self.input_profile.tags.get("A2B1", LUT16Type()), LUT16Type)
            and isinstance(self.input_profile.tags.get("A2B2", LUT16Type()), LUT16Type)
        ):
            self.lut3d_trc_apply_black_offset_ctrl.Enable(self.XYZbpin != self.XYZbpout)
            if self.XYZbpin == self.XYZbpout:
                self.setcfg("3dlut.apply_black_offset", 0)
        else:
            self.lut3d_trc_apply_black_offset_ctrl.Disable()
            self.setcfg("3dlut.apply_black_offset", 0)
        if self.getcfg("3dlut.apply_black_offset"):
            self.lut3d_trc_apply_black_offset_ctrl.SetValue(True)
        elif self.getcfg("3dlut.apply_trc"):
            self.lut3d_trc_apply_ctrl.SetValue(True)
        else:
            self.lut3d_trc_apply_none_ctrl.SetValue(True)
        self.lut3d_show_trc_controls()


def main():
    """Main function to run the 3D LUT maker application."""
    config.initcfg("3DLUT-maker")
    lang.init()
    lang.update_defaults()
    app = BaseApp(0)
    app.TopWindow = LUT3DFrame(setup=False)
    if sys.platform == "darwin":
        app.TopWindow.init_menubar()
    wx.CallLater(1, _main, app)
    app.MainLoop()


def _main(app):
    app.TopWindow.listen()
    app.TopWindow.setup()
    app.TopWindow.Show()


if __name__ == "__main__":
    main()
