"""wxPython GUI for creating synthetic ICC profiles.

It supports customization of color spaces, luminance, transfer functions, and
other ICC profile attributes. The interface allows users to manipulate color
settings, drop files for processing, and save profiles in various
configurations.
"""

import math
import os
import sys
from functools import partial

from wx import xrc

from DisplayCAL import (
    colormath,
    config,
    floatspin,
    worker,
    xh_bitmapctrls,
    xh_floatspin,
)
from DisplayCAL import localization as lang
from DisplayCAL.argyll_cgats import extract_device_gray_primaries
from DisplayCAL.cgats import (
    CGATS,
    CGATSInvalidError,
)
from DisplayCAL.config import (
    ENC,
    PROFILE_EXT,
    get_data_path,
    get_verified_path,
    getcfg,
    hascfg,
    setcfg,
)
from DisplayCAL.debughelpers import Error
from DisplayCAL.icc_profile import (
    CIIS,
    PROFILE_CLASS,
    TECH,
    CurveType,
    ICCProfile,
    ICCProfileInvalidError,
    SignatureType,
    Text,
    XYZType,
    create_synthetic_hdr_clut_profile,
    s15f16_is_equal,
)
from DisplayCAL.log import LOG
from DisplayCAL.meta import NAME as APPNAME
from DisplayCAL.options import DEBUG
from DisplayCAL.util_dict import dict_sort
from DisplayCAL.util_io import Files
from DisplayCAL.util_os import waccess
from DisplayCAL.worker import (
    FilteredStream,
    LineBufferedStream,
    show_result_dialog,
)
from DisplayCAL.wx_fixes import TempXmlResource
from DisplayCAL.wx_lut_3d_frame import LUT3DMixin
from DisplayCAL.wx_windows import (
    BaseApp,
    BaseFrame,
    ConfirmDialog,
    FileDrop,
    wx,
)


class SynthICCFrame(BaseFrame, LUT3DMixin):
    """Synthetic ICC creation window.

    Args:
        parent (wx.Window, optional): The parent window. Defaults to None.
    """

    cfg = config.CFG

    def __init__(self, parent=None):
        self.res = TempXmlResource(get_data_path(os.path.join("xrc", "synthicc.xrc")))
        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, "synthiccframe")
            self.PostCreate(pre)
        else:
            # Phoenix
            wx.Frame.__init__(self)
            self.res.LoadFrame(self, parent, "synthiccframe")
        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 + "-synthprofile")
        )

        self.set_child_ctrls_as_attrs(self)

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

        self._updating_ctrls = False

        # Presets
        presets = ["", *sorted(colormath.rgb_spaces.keys())]
        self.preset_ctrl.SetItems(presets)

        self.set_default_cat()

        self.worker = worker.Worker(self)

        # Drop targets
        self.droptarget = FileDrop(self)
        self.droptarget.drophandlers = {
            ".icc": self.drop_handler,
            ".icm": self.drop_handler,
            ".ti3": self.drop_handler,
        }
        self.panel.SetDropTarget(self.droptarget)

        # Bind event handlers
        self.preset_ctrl.Bind(wx.EVT_CHOICE, self.preset_ctrl_handler)
        for color in ("red", "green", "blue", "white", "black"):
            for component in "XYZxy":
                handler = "xy" if component in "xy" else "XYZ"
                self.Bind(
                    floatspin.EVT_FLOATSPIN,
                    getattr(self, f"{color}_{handler}_ctrl_handler"),
                    getattr(self, f"{color}_{component}"),
                )
        self.black_point_cb.Bind(wx.EVT_CHECKBOX, self.black_point_enable_handler)
        self.chromatic_adaptation_btn.Bind(
            wx.EVT_BUTTON, self.chromatic_adaptation_btn_handler
        )
        self.trc_ctrl.Bind(wx.EVT_CHOICE, self.trc_ctrl_handler)
        self.trc_gamma_ctrl.Bind(wx.EVT_COMBOBOX, self.trc_gamma_ctrl_handler)
        self.trc_gamma_ctrl.Bind(wx.EVT_KILL_FOCUS, self.trc_gamma_ctrl_handler)
        self.trc_gamma_type_ctrl.Bind(wx.EVT_CHOICE, self.trc_gamma_type_ctrl_handler)
        self.lut3d_bind_hdr_trc_handlers()
        self.black_output_offset_ctrl.Bind(
            wx.EVT_SLIDER, self.black_output_offset_ctrl_handler
        )
        self.black_output_offset_intctrl.Bind(
            wx.EVT_TEXT, self.black_output_offset_ctrl_handler
        )
        self.colorspace_rgb_ctrl.Bind(wx.EVT_RADIOBUTTON, self.colorspace_ctrl_handler)
        self.colorspace_gray_ctrl.Bind(wx.EVT_RADIOBUTTON, self.colorspace_ctrl_handler)
        self.black_luminance_ctrl.Bind(
            floatspin.EVT_FLOATSPIN, self.black_luminance_ctrl_handler
        )
        self.luminance_ctrl.Bind(floatspin.EVT_FLOATSPIN, self.luminance_ctrl_handler)
        self.save_as_btn.Bind(wx.EVT_BUTTON, self.save_as_btn_handler)

        self.save_as_btn.SetDefault()

        self.setup_language()

        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)

        self.save_btn.Hide()

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

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

    def OnSize(self, event):
        """Handles the size event of the frame.

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

    def OnClose(self, event=None):
        """Handles the close event of the frame.

        Args:
            event (wx.Event, optional): The event object that triggered this
                handler. Defaults to None.
        """
        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.synthiccframe.x", x)
            self.setcfg("position.synthiccframe.y", y)
            self.setcfg("size.synthiccframe.w", self.ClientSize[0])
            self.setcfg("size.synthiccframe.h", self.ClientSize[1])
        config.writecfg(
            module="synthprofile",
            options=(
                "synthprofile.",
                "last_icc_path",
                "position.synthiccframe",
                "size.synthiccframe",
                "3dlut.hdr_",
            ),
            cfg=self.cfg,
        )
        if event:
            # Hide first (looks nicer)
            self.Hide()
            # Need to use CallAfter to prevent hang under Windows if minimized
            if wx.GetApp().TopWindow is self:
                # XXX: Weird wxPython Phoenix bug under Windows: Destroying the
                # frame seems to affect subsequently loaded XRC resources
                # (wrong control classes used).
                # More investigation is needed, but it is clear this is a
                # Phoenix or wxWidgets bug because it does not happen with
                # wxPython classic (3.0.2).
                # Workaround: Only destroy the frame if running standalone.
                wx.CallAfter(self.Destroy)

    def black_luminance_ctrl_handler(self, event):
        """Handles changes in the black luminance control.

        Args:
            event (wx.Event or bool): The event object that triggered this
                handler. Can also be True to force a layout update.
        """
        v = self.black_luminance_ctrl.GetValue()
        white_Y = self.getcfg("synthprofile.luminance")
        if v >= white_Y * 0.9:
            if event:
                wx.Bell()
            v = white_Y * 0.9
        if event:
            min_Y = 0.000001  # (1 / 65535.0) * 100
            increment = 0.000001  # (1 / 65535.0) * white_Y
            if increment < min_Y:
                increment = min_Y * (white_Y / 100.0)
            min_inc = 1.0 / (10.0 ** self.black_luminance_ctrl.GetDigits())
            increment = max(increment, min_inc)
            self.black_luminance_ctrl.SetIncrement(increment)
            fmt = f"{{:.{self.black_luminance_ctrl.GetDigits()}f}}"
            if fmt.format(0) < fmt.format(v) < fmt.format(increment):
                v = increment if event else 0
            elif fmt.format(v) == fmt.format(0):
                v = 0
            v = round(v / increment) * increment
        self.black_luminance_ctrl.SetValue(v)

        old = self.getcfg("synthprofile.black_luminance")
        self.setcfg("synthprofile.black_luminance", v)
        if event:
            self.black_xy_ctrl_handler(None)
        self.black_point_cb.Enable(v > 0)
        self.black_point_enable_handler(None)
        if (v != old and (old == 0 or v == 0)) or event is True:
            self.Freeze()
            i = self.trc_ctrl.GetSelection()
            self.trc_gamma_type_ctrl.Show(i in (0, 5) and bool(v))
            if not v:
                self.bpc_ctrl.SetValue(False)
            self.bpc_ctrl.Enable(bool(v))
            black_output_offset_show = i in (0, 5, 7, 8) and bool(v)
            self.black_output_offset_label.Show(black_output_offset_show)
            self.black_output_offset_ctrl.Show(black_output_offset_show)
            self.black_output_offset_intctrl.Show(black_output_offset_show)
            self.black_output_offset_intctrl_label.Show(black_output_offset_show)
            self.panel.GetSizer().Layout()
            self.update_layout()
            self.Thaw()

    def black_output_offset_ctrl_handler(self, event):
        """Handler for black output offset control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        if event.GetId() == self.black_output_offset_intctrl.GetId():
            self.black_output_offset_ctrl.SetValue(
                self.black_output_offset_intctrl.GetValue()
            )
        else:
            self.black_output_offset_intctrl.SetValue(
                self.black_output_offset_ctrl.GetValue()
            )
        v = self.black_output_offset_ctrl.GetValue() / 100.0
        self.setcfg("synthprofile.trc_output_offset", v)
        self.update_trc_control()

    def black_point_enable_handler(self, event):
        """Enable or disable black point controls based on the checkbox state.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        v = self.getcfg("synthprofile.black_luminance")
        for component in "XYZxy":
            getattr(self, f"black_{component}").Enable(
                v > 0 and self.black_point_cb.Value
            )

    def black_XYZ_ctrl_handler(self, event):
        """Handler for black XYZ control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        luminance = self.getcfg("synthprofile.luminance")
        XYZ = []
        for component in "XYZ":
            XYZ.append(getattr(self, f"black_{component}").GetValue() / 100.0)
            if component == "Y":
                self.black_luminance_ctrl.SetValue(XYZ[-1] * luminance)
                self.black_luminance_ctrl_handler(None)
                if not XYZ[-1]:
                    XYZ = [0, 0, 0]
                    for i in range(3):
                        getattr(self, "black_{}".format("XYZ"[i])).SetValue(0)
                    break
        self.parse_XYZ("black")

    def black_xy_ctrl_handler(self, event):
        """Handler for black xy control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        # Black Y scaled to 0..1 range
        Y = self.getcfg("synthprofile.black_luminance") / self.getcfg(
            "synthprofile.luminance"
        )
        xy = []
        for component in "xy":
            xy.append(getattr(self, f"black_{component}").GetValue() or 1.0 / 3)
            getattr(self, f"black_{component}").SetValue(xy[-1])
        for i, v in enumerate(colormath.xyY2XYZ(*[*xy, Y])):
            getattr(self, "black_{}".format("XYZ"[i])).SetValue(v * 100)

    def blue_XYZ_ctrl_handler(self, event):
        """Handler for blue XYZ control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_XYZ("blue")

    def blue_xy_ctrl_handler(self, event):
        """Handler for blue xy control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_xy("blue")

    def chromatic_adaptation_btn_handler(self, event):
        """Handler for the chromatic adaptation button click event.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        scale = self.getcfg("app.dpi") / config.get_default_dpi()
        scale = max(scale, 1)
        dlg = ConfirmDialog(
            self,
            title=lang.getstr("chromatic_adaptation"),
            msg=lang.getstr("whitepoint.xy"),
            ok=lang.getstr("apply"),
            cancel=lang.getstr("cancel"),
        )
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        dlg.sizer3.Add(sizer, 0, flag=wx.TOP | wx.ALIGN_LEFT, border=8)
        x_ctrl = floatspin.FloatSpin(
            dlg,
            -1,
            size=(75 * scale, -1),
            min_val=0.00001,
            max_val=1,
            increment=0.00001,
            value=self.white_x.GetValue(),
        )
        sizer.Add(x_ctrl, 0, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, border=4)
        sizer.Add(
            wx.StaticText(dlg, -1, "x"),
            0,
            flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL,
            border=12,
        )
        y_ctrl = floatspin.FloatSpin(
            dlg,
            -1,
            size=(75 * scale, -1),
            min_val=0.00001,
            max_val=1,
            increment=0.00001,
            value=self.white_y.GetValue(),
        )
        sizer.Add(y_ctrl, 0, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, border=4)
        sizer.Add(
            wx.StaticText(dlg, -1, "y"),
            0,
            flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL,
            border=12,
        )
        dlg.sizer3.Add(
            wx.StaticText(dlg, -1, lang.getstr("chromatic_adaptation_transform")),
            0,
            flag=wx.TOP | wx.ALIGN_LEFT,
            border=12,
        )
        if self.getcfg("show_advanced_options"):
            # Only offer those that seem suitable in an ICC workflow
            # (i.e. blue primary not too far from default Bradford to
            # prevent 'blue turns purple' problems)
            cat_choices = ["Bradford", "CAT02BS"]
        else:
            cat_choices = ["Bradford"]
        cat_choices_ab = dict(
            get_mapping(((k, k) for k in colormath.CAT_MATRICES), cat_choices)
        )
        cat_choices_ba = {v: k for k, v in cat_choices_ab.items()}
        cat_ctrl = wx.Choice(dlg, -1, choices=list(cat_choices_ab.values()))
        cat_ctrl.SetStringSelection(cat_choices_ab[self.cat])
        dlg.sizer3.Add(cat_ctrl, 0, flag=wx.TOP | wx.ALIGN_LEFT, border=8)
        dlg.sizer0.SetSizeHints(dlg)
        dlg.sizer0.Layout()
        result = dlg.ShowModal()
        x = x_ctrl.GetValue()
        y = y_ctrl.GetValue()
        cat = cat_choices_ba[cat_ctrl.GetStringSelection()]
        dlg.Destroy()
        if result != wx.ID_OK:
            return
        wp_src = [getattr(self, "white_" + component).GetValue() for component in "XYZ"]
        wp_tgt = colormath.xyY2XYZ(x, y)
        self.cat = cat
        for color in ("red", "green", "blue", "white", "black"):
            ctrls = [getattr(self, f"{color}_{component}") for component in "XYZ"]
            X, Y, Z = (ctrl.GetValue() for ctrl in ctrls)
            XYZa = colormath.adapt(X, Y, Z, wp_src, wp_tgt, cat)
            for i, ctrl in enumerate(ctrls):
                ctrl.SetValue(XYZa[i])
            self.parse_XYZ(color, False)

    def colorspace_ctrl_handler(self, event):
        """Handler for colorspace control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.Freeze()
        show = bool(self.colorspace_rgb_ctrl.Value)
        for color in ("red", "green", "blue"):
            getattr(self, f"label_{color}").Show(show)
            for component in "XYZxy":
                getattr(self, f"{color}_{component}").Show(show)
        self.enable_btns()
        self.update_layout()
        self.Thaw()

    def drop_handler(self, path):
        """File dropped."""
        fn, ext = os.path.splitext(path)
        if ext.lower() == ".ti3":
            self.ti3_drop_handler(path)
        else:
            self.icc_drop_handler(path)

    def icc_drop_handler(self, path):
        """ICC profile dropped."""
        try:
            profile = ICCProfile(path)
        except (OSError, ICCProfileInvalidError):
            show_result_dialog(
                Error(lang.getstr("profile.invalid") + "\n" + path), self
            )
        else:
            if profile.version >= 4 and not profile.convert_iccv4_tags_to_iccv2():
                show_result_dialog(
                    Error(lang.getstr("profile.iccv4.unsupported")), self
                )
                return
            if profile.colorSpace not in (
                b"RGB",
                b"GRAY",
            ) or profile.connectionColorSpace not in (b"Lab", b"XYZ"):
                show_result_dialog(
                    Error(
                        lang.getstr(
                            "profile.unsupported",
                            (profile.profileClass, profile.colorSpace),
                        )
                    ),
                    self,
                )
                return
            rgb = [(1, 1, 1), (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)]
            rgb.extend(
                (1.0 / 255 * i, 1.0 / 255 * i, 1.0 / 255 * i) for i in range(256)
            )
            try:
                colors = self.worker.xicclu(profile, rgb, intent="a", pcs="x")
            except Exception as exception:
                show_result_dialog(exception, self)
            else:
                luminance = profile.tags.lumi.Y if "lumi" in profile.tags else 100
                if not colors[1][1] and isinstance(profile.tags.get("targ"), Text):
                    # The profile may not reflect the actual black point.
                    # Get it from the embedded TI3 instead if zero from lookup.
                    XYZbp = profile.get_chardata_bkpt(True)
                    if XYZbp:
                        # Use wtpt chromaticity
                        colors[1] = [v * XYZbp[1] for v in colors[0]]
                self.set_colors(colors, luminance, profile.colorSpace)

    def set_colors(self, colors, luminance, colorspace):
        """Set controls according to args."""
        self.panel.Freeze()
        for ctrl, value in [
            (self.colorspace_rgb_ctrl, colorspace == "RGB"),
            (self.colorspace_gray_ctrl, colorspace == "GRAY"),
        ]:
            ctrl.SetValue(value)
        self.colorspace_ctrl_handler(None)
        self.setcfg("synthprofile.luminance", luminance)
        self.luminance_ctrl.SetValue(luminance)
        for i, color in enumerate(("white", "black")):
            for j, component in enumerate("XYZ"):
                getattr(self, f"{color}_{component}").SetValue(
                    colors[i][j] / colors[0][1] * 100
                )
            self.parse_XYZ(color)
        for i, color in enumerate(("red", "green", "blue")):
            xyY = colormath.XYZ2xyY(*colors[2 + i])
            for j, component in enumerate("xy"):
                getattr(self, f"{color}_{component}").SetValue(xyY[j])
        self.parse_xy(None)
        self.black_XYZ_ctrl_handler(None)
        if len(colors[5:]) > 2:
            trc = CurveType()
            for XYZ in colors[5:]:
                trc.append(XYZ[1] / colors[0][1] * 65535)
            transfer_function = trc.get_transfer_function(outoffset=1.0)
            if transfer_function and transfer_function[1] >= 0.95:
                # Use detected transfer function
                gamma = transfer_function[0][1]
            else:
                # Use 50% gamma value
                gamma = math.log(colors[132][1]) / math.log(128.0 / 255)
            self.set_trc(round(gamma, 2))
            self.setcfg("synthprofile.trc_gamma_type", "g")
            self.trc_gamma_type_ctrl.SetSelection(self.trc_gamma_types_ba["g"])
        self.panel.Thaw()

    def ti3_drop_handler(self, path):
        """Handle TI3 file dropped.

        Args:
            path (str): The path to the TI3 file.
        """
        try:
            ti3 = CGATS(path)
        except (OSError, CGATSInvalidError):
            show_result_dialog(
                Error(lang.getstr("error.measurement.file_invalid", path)), self
            )
            return
        ti3[0].normalize_to_y_100()
        rgb = [(100, 100, 100), (0, 0, 0), (100, 0, 0), (0, 100, 0), (0, 0, 100)]
        colors = []
        for R, G, B in rgb:
            result = ti3.queryi1({"RGB_R": R, "RGB_G": G, "RGB_B": B})
            if result:
                color = []
                for component in "XYZ":
                    label = "XYZ_" + component
                    if label in result:
                        color.append(result[label])
            if not result or len(color) < 3:
                color = (0, 0, 0)
            colors.append(color)
        try:
            (
                ti3_extracted,
                RGB_XYZ_extracted,
                RGB_XYZ_remaining,
            ) = extract_device_gray_primaries(ti3)
        except Error as exception:
            show_result_dialog(exception, self)
        else:
            RGB_XYZ_extracted = dict_sort(RGB_XYZ_extracted)
            colors.extend(list(RGB_XYZ_extracted.values()))
            luminance = ti3.queryv1("LUMINANCE_XYZ_CDM2")
            if luminance:
                try:
                    luminance = float(luminance.split()[1])
                except (TypeError, ValueError):
                    luminance = 100
            else:
                luminance = 100
            self.set_colors(colors, luminance, "RGB")

    def enable_btns(self):
        """Enable or disable buttons based on the current state of controls."""
        enable = bool(self.get_XYZ())
        self.save_as_btn.Enable(enable)
        self.chromatic_adaptation_btn.Enable(enable)

    def get_XYZ(self):
        """Get XYZ in 0..1 range.

        Returns:
            None | dict: A dictionary containing XYZ values for white, red,
                green, blue, and black colors, or None if the values are not
                valid.
        """
        XYZ = {}
        black_Y = self.getcfg("synthprofile.black_luminance") / self.getcfg(
            "synthprofile.luminance"
        )
        for color in ("white", "red", "green", "blue", "black"):
            for component in "XYZ":
                v = getattr(self, f"{color}_{component}").GetValue() / 100.0
                if color == "black":
                    key = "k"
                    if not self.black_point_cb.Value:
                        v = XYZ[f"w{component}"] * black_Y
                else:
                    key = color[0]
                XYZ[key + component] = v
        if (
            XYZ["wX"]
            and XYZ["wY"]
            and XYZ["wZ"]
            and (
                self.colorspace_gray_ctrl.Value
                or (XYZ["rX"] and XYZ["gY"] and XYZ["bZ"])
            )
        ):
            return XYZ
        return None

    def green_XYZ_ctrl_handler(self, event):
        """Handler for green XYZ control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_XYZ("green")

    def green_xy_ctrl_handler(self, event):
        """Handler for green xy control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_xy("green")

    def luminance_ctrl_handler(self, event):
        """Handler for luminance control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        v = self.luminance_ctrl.GetValue()
        self.setcfg("synthprofile.luminance", v)
        target_peak = v
        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", v)
        self.black_luminance_ctrl_handler(event)

    def parse_XYZ(self, name, set_blackpoint=None):
        """Parse XYZ values and update controls accordingly.

        Args:
            name (str): The name of the color space to update (e.g., "white").
            set_blackpoint (bool): Whether to set the black point based on the
                white point.
        """
        if set_blackpoint is None:
            set_blackpoint = not self.black_point_cb.Value
        if not self._updating_ctrls:
            self.preset_ctrl.SetSelection(0)
        XYZ = {}
        # Black Y scaled to 0..1 range
        black_Y = self.getcfg("synthprofile.black_luminance") / self.getcfg(
            "synthprofile.luminance"
        )
        for component in "XYZ":
            v = getattr(self, f"{name}_{component}").GetValue()
            XYZ[component] = v
            if name == "white" and set_blackpoint:
                getattr(self, f"black_{component}").SetValue(v * black_Y)
        if "X" in XYZ and "Y" in XYZ and "Z" in XYZ:
            if XYZ["X"] + XYZ["Y"] + XYZ["Z"] == 0:
                # Set black chromaticity to white chromaticity if XYZ is 0
                xyY = [
                    getattr(self, f"white_{component}").GetValue() for component in "xy"
                ]
            else:
                xyY = colormath.XYZ2xyY(XYZ["X"], XYZ["Y"], XYZ["Z"])
            for i, component in enumerate("xy"):
                getattr(self, f"{name}_{component}").SetValue(xyY[i])
                if name == "white" and set_blackpoint:
                    getattr(self, f"black_{component}").SetValue(xyY[i])
        self.enable_btns()

    def parse_xy(self, name=None, set_blackpoint=False):
        """Parse xy values and update controls accordingly.

        Args:
            name (str): The name of the color space to update (e.g., "white").
            set_blackpoint (bool): Whether to set the black point based on the
                white point.
        """
        if not set_blackpoint:
            set_blackpoint = not self.black_point_cb.Value
        if not self._updating_ctrls:
            self.preset_ctrl.SetSelection(0)
        xy = {}
        for color in ("white", "red", "green", "blue"):
            for component in "xy":
                v = getattr(self, f"{color}_{component}").GetValue()
                xy[color[0] + component] = v
        if name == "white":
            wXYZ = colormath.xyY2XYZ(xy["wx"], xy["wy"], 1.0)
        else:
            wXYZ = []
            for component in "XYZ":
                wXYZ.append(getattr(self, f"white_{component}").GetValue() / 100.0)
        if name == "white":
            # Black Y scaled to 0..1 range
            black_Y = self.getcfg("synthprofile.black_luminance") / self.getcfg(
                "synthprofile.luminance"
            )
            for i, component in enumerate("XYZ"):
                getattr(self, f"white_{component}").SetValue(wXYZ[i] * 100)
                if set_blackpoint:
                    getattr(self, f"black_{component}").SetValue(
                        wXYZ[i] * black_Y * 100
                    )
        has_rgb_xy = True
        # Calculate RGB to XYZ matrix from chromaticities and white
        try:
            mtx = colormath.rgb_to_xyz_matrix(
                xy["rx"], xy["ry"], xy["gx"], xy["gy"], xy["bx"], xy["by"], wXYZ
            )
        except ZeroDivisionError:
            # Singular matrix
            has_rgb_xy = False
        rgb = {"r": (1.0, 0.0, 0.0), "g": (0.0, 1.0, 0.0), "b": (0.0, 0.0, 1.0)}
        XYZ = {}
        for color in ("red", "green", "blue"):
            if has_rgb_xy:
                # Calculate XYZ for primaries
                v = mtx * rgb[color[0]]
            if not has_rgb_xy:
                v = (0, 0, 0)
            XYZ[color[0]] = v
            for i, component in enumerate("XYZ"):
                getattr(self, f"{color}_{component}").SetValue(XYZ[color[0]][i] * 100)
        self.enable_btns()

    def preset_ctrl_handler(self, event):
        """Handler for preset control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        preset_name = self.preset_ctrl.GetStringSelection()
        if not preset_name:
            return
        self.set_default_cat()
        gamma, white, red, green, blue = colormath.rgb_spaces[preset_name]
        white = colormath.get_whitepoint(white)
        self._updating_ctrls = True
        self.panel.Freeze()
        if self.preset_ctrl.GetStringSelection() == "DCI P3":
            tech = self.tech["dcpj"]
        else:
            tech = self.tech[""]
        self.tech_ctrl.SetStringSelection(tech)

        self.white_X.SetValue(white[0] * 100)
        self.white_Y.SetValue(white[1] * 100)
        self.white_Z.SetValue(white[2] * 100)

        self.parse_XYZ("white", True)

        self.red_x.SetValue(red[0])
        self.red_y.SetValue(red[1])
        self.green_x.SetValue(green[0])
        self.green_y.SetValue(green[1])
        self.blue_x.SetValue(blue[0])
        self.blue_y.SetValue(blue[1])

        self.parse_xy(None)
        self.set_trc(gamma)
        self.panel.Thaw()
        self._updating_ctrls = False

    def get_commands(self):
        """Get a list of commands that this frame can process.

        Returns:
            list: A list of command strings that this frame can handle.
        """
        return [
            *self.get_common_commands(),
            "synthprofile [filename]",
            "load <filename>",
        ]

    def process_data(self, data):
        """Process commands from the command line or other sources.

        Args:
            data (list): A list of strings, where the first element is the command
                and the subsequent elements are arguments for that command.

        Returns:
            str: "ok" if the command was processed successfully, "fail" if it failed,
            or "invalid" if the command was not recognized.
        """
        if (data[0] == "synthprofile" and len(data) < 3) or (
            data[0] == "load" and len(data) == 2
        ):
            if self.IsIconized():
                self.Restore()
            self.Raise()
            if len(data) == 2:
                path = data[1]
                if not os.path.isfile(path) and not os.path.isabs(path):
                    path = get_data_path(path)
                if not path:
                    return "fail"
                self.droptarget.OnDropFiles(0, 0, [path])
            return "ok"
        return "invalid"

    def set_default_cat(self):
        """Set the default chromatic adaptation transform."""
        self.cat = "Bradford"

    def set_trc(self, gamma):
        """Set the transfer function control to the given gamma value.

        Args:
            gamma (float or int): The gamma value to set. Special values:
                -1023: DICOM
                -2.0: HLG
                -3.0: L*
                -709: Rec. 709
                -1886: Rec. 1886
                -240: SMPTE 240M
                -2084: SMPTE 2084 (roll-off clip)
                -2.4: sRGB
        """
        if gamma == -1023:
            # DICOM
            self.trc_ctrl.SetSelection(1)
        elif gamma == -2.0:
            # HLG
            self.trc_ctrl.SetSelection(2)
        elif gamma == -3.0:
            # L*
            self.trc_ctrl.SetSelection(3)
        elif gamma == -709:
            # Rec. 709
            self.trc_ctrl.SetSelection(4)
        elif gamma == -1886:
            # Rec. 1886
            self.trc_ctrl.SetSelection(5)
            self.trc_ctrl_handler()
        elif gamma == -240:
            # SMPTE 240M
            self.trc_ctrl.SetSelection(6)
        elif gamma == -2084:
            # SMPTE 2084, roll-off clip
            self.trc_ctrl.SetSelection(8)
        elif gamma == -2.4:
            # sRGB
            self.trc_ctrl.SetSelection(9)
        else:
            # Gamma
            self.trc_ctrl.SetSelection(0)
            self.setcfg("synthprofile.trc_gamma", gamma)
        self.update_trc_controls()

    def profile_name_ctrl_handler(self, event):
        """Handler for profile name control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.enable_btns()

    def red_XYZ_ctrl_handler(self, event):
        """Handler for red XYZ control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_XYZ("red")

    def red_xy_ctrl_handler(self, event):
        """Handler for red xy control changes.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        self.parse_xy("red")

    def save_as_btn_handler(self, event):
        """Save as ICC profile.

        Args:
            event (wx.Event): The event that triggered this handler.
        """
        try:
            gamma = float(self.trc_gamma_ctrl.Value)
        except ValueError:
            wx.Bell()
            gamma = 2.2
            self.trc_gamma_ctrl.Value = str(gamma)

        defaultDir, defaultFile = get_verified_path("last_icc_path")
        defaultFile = lang.getstr("unnamed")
        path = None
        icc_file_type_wildcard = f"{lang.getstr('filetype.icc')}|*{PROFILE_EXT}"
        save_dialog_title = lang.getstr("save_as")

        dlg = wx.FileDialog(
            self,
            save_dialog_title,
            defaultDir=defaultDir,
            defaultFile=defaultFile,
            wildcard=icc_file_type_wildcard,
            style=wx.SAVE | wx.FD_OVERWRITE_PROMPT,
        )
        dlg.Center(wx.BOTH)
        result = dlg.ShowModal()

        if result == wx.ID_OK:
            path = dlg.GetPath()
        dlg.Destroy()

        if path:
            if os.path.splitext(path)[1].lower() not in (".icc", ".icm"):
                path += PROFILE_EXT
            if not waccess(path, os.W_OK):
                show_result_dialog(
                    Error(lang.getstr("error.access_denied.write", path)), self
                )
                return
            self.setcfg("last_icc_path", path)
        else:
            return

        XYZ = self.get_XYZ()
        if self.trc_ctrl.GetSelection() in (0, 5):
            # 0 = Gamma
            # 5 = Rec. 1886 - trc set here is only used if black = 0
            trc = gamma
        elif self.trc_ctrl.GetSelection() == 1:
            # DICOM
            trc = -1
        elif self.trc_ctrl.GetSelection() == 2:
            # HLG
            trc = -2
        elif self.trc_ctrl.GetSelection() == 3:
            # L*
            trc = -3.0
        elif self.trc_ctrl.GetSelection() == 4:
            # Rec. 709
            trc = -709
        elif self.trc_ctrl.GetSelection() == 6:
            # SMPTE 240M
            trc = -240
        elif self.trc_ctrl.GetSelection() in (7, 8):
            # SMPTE 2084
            trc = -2084
        elif self.trc_ctrl.GetSelection() == 9:
            # sRGB
            trc = -2.4
        rolloff = self.trc_ctrl.GetSelection() == 8
        class_i = self.profile_class_ctrl.GetSelection()
        tech_i = self.tech_ctrl.GetSelection()
        ciis_i = self.ciis_ctrl.GetSelection()

        def consumer(result, parent):
            if isinstance(result, Exception):
                show_result_dialog(result, parent)

        wargs = (XYZ, trc, path)
        wkwargs = {
            "rgb": self.colorspace_rgb_ctrl.Value,
            "rolloff": rolloff,
            "bpc": self.bpc_ctrl.Value,
            "profile_class": list(self.profile_classes.keys())[class_i],
            "tech": list(self.tech.keys())[tech_i],
            "ciis": list(self.ciis.keys())[ciis_i],
        }
        if (trc == -2084 and rolloff) or trc == -2:
            msg = "smpte2084.rolloffclip" if trc == -2084 else "hlg"
            self.worker.recent.write(lang.getstr("trc." + msg) + "\n")
            self.worker.start(
                partial(consumer, parent=self),
                self.create_profile,
                wargs=(XYZ, trc, path),
                wkwargs=wkwargs,
                progress_msg=lang.getstr("synthicc.create"),
            )
        else:
            consumer(self.create_profile(*wargs, **wkwargs), parent=self)

    def create_profile(
        self,
        XYZ,
        trc,
        path,
        rgb=True,
        rolloff=True,
        bpc=False,
        profile_class=b"mntr",
        tech=None,
        ciis=None,
    ):
        """Create ICC profile from XYZ and TRC.

        Args:
            XYZ (dict): Dictionary with XYZ values for r, g, b, w, k.
            trc (float): Transfer function value or special value for DICOM,
                HLG, etc.
            path (str): Path to save the profile to.
            rgb (bool): Whether to create a color profile or grayscale.
            rolloff (bool): Whether to use roll-off for HDR profiles.
            bpc (bool): Whether to use black point compensation.
            profile_class (bytes): Profile class as bytes.
            tech (str): Technology string for the profile.
            ciis (str): CIIS string for the profile.

        Returns:
            ICCProfile: The created ICC profile.
        """
        white = XYZ["wX"], XYZ["wY"], XYZ["wZ"]
        if rgb:
            # Color profile
            profile = ICCProfile.from_XYZ(
                (XYZ["rX"], XYZ["rY"], XYZ["rZ"]),
                (XYZ["gX"], XYZ["gY"], XYZ["gZ"]),
                (XYZ["bX"], XYZ["bY"], XYZ["bZ"]),
                (XYZ["wX"], XYZ["wY"], XYZ["wZ"]),
                1.0,
                "",
                self.getcfg("copyright"),
                cat=self.cat,
                profile_class=profile_class,
            )
            black = colormath.adapt(XYZ["kX"], XYZ["kY"], XYZ["kZ"], white)
            profile.tags.rTRC = CurveType(profile=profile)
            profile.tags.gTRC = CurveType(profile=profile)
            profile.tags.bTRC = CurveType(profile=profile)
            channels = "rgb"
        else:
            # Grayscale profile
            profile = ICCProfile()
            # Profile class
            profile.profileClass = profile_class
            if not s15f16_is_equal(
                (XYZ["wX"], XYZ["wY"], XYZ["wZ"]), colormath.get_whitepoint("D50")
            ) and (
                profile.profileClass not in (b"mntr", b"prtr")
                or colormath.is_similar_matrix(
                    colormath.get_cat_matrix(self.cat),
                    colormath.get_cat_matrix("Bradford"),
                )
            ):
                profile.version = 2.2  # Match ArgyllCMS
            profile.colorSpace = b"GRAY"
            profile.setCopyright(self.getcfg("copyright"))
            profile.set_wtpt((XYZ["wX"], XYZ["wY"], XYZ["wZ"]), self.cat)
            black = [
                XYZ["wY"]
                * (
                    self.getcfg("synthprofile.black_luminance")
                    / self.getcfg("synthprofile.luminance")
                )
            ] * 3
            profile.tags.kTRC = CurveType(profile=profile)
            channels = "k"
        # HLG
        outoffset = 1 if trc == -2 else self.getcfg("synthprofile.trc_output_offset")
        if trc == -1:
            # DICOM
            # Absolute luminance values!
            try:
                if rgb:
                    # Color profile
                    profile.set_dicom_trc(
                        [v * self.getcfg("synthprofile.luminance") for v in black],
                        self.getcfg("synthprofile.luminance"),
                    )
                else:
                    # Grayscale profile
                    profile.tags.kTRC.set_dicom_trc(
                        self.getcfg("synthprofile.black_luminance"),
                        self.getcfg("synthprofile.luminance"),
                    )
            except ValueError as exception:
                return exception
        elif trc > -1 and black != [0, 0, 0]:
            # Gamma with output offset or Rec. 1886-like
            if rgb:
                # Color profile
                profile.set_bt1886_trc(
                    black, outoffset, trc, self.getcfg("synthprofile.trc_gamma_type")
                )
            else:
                # Grayscale profile
                profile.tags.kTRC.set_bt1886_trc(
                    black[1], outoffset, trc, self.getcfg("synthprofile.trc_gamma_type")
                )
        elif trc in (-2084, -2):
            # SMPTE 2084 or HLG
            hdr_format = "PQ" if trc == -2084 else "HLG"
            minmll = self.getcfg("3dlut.hdr_minmll")
            if rolloff:
                maxmll = self.getcfg("3dlut.hdr_maxmll")
            else:
                maxmll = self.getcfg("synthprofile.luminance")
            if rgb:
                # Color profile
                if trc == -2084:
                    profile.set_smpte2084_trc(
                        [
                            v * self.getcfg("synthprofile.luminance") * (1 - outoffset)
                            for v in black
                        ],
                        self.getcfg("synthprofile.luminance"),
                        minmll,
                        maxmll,
                        self.getcfg("3dlut.hdr_maxmll_alt_clip"),
                        rolloff=True,
                        blend_blackpoint=False,
                    )
                else:
                    # HLG
                    profile.set_hlg_trc(
                        (0, 0, 0),
                        self.getcfg("synthprofile.luminance"),
                        1.2,
                        self.getcfg("3dlut.hdr_ambient_luminance"),
                        blend_blackpoint=False,
                    )
                if rolloff or trc == -2:
                    rgb_space = profile.get_rgb_space()
                    rgb_space[0] = 1.0  # Set gamma to 1.0 (not actually used)
                    rgb_space = colormath.get_rgb_space(rgb_space)
                    linebuffered_logfiles = []
                    if (
                        sys.stdout
                        and hasattr(sys.stdout, "isatty")
                        and sys.stdout.isatty()
                    ):
                        linebuffered_logfiles.append(print)
                    else:
                        linebuffered_logfiles.append(LOG)
                    logfiles = Files(
                        [
                            LineBufferedStream(
                                FilteredStream(
                                    Files(linebuffered_logfiles),
                                    ENC,
                                    discard="",
                                    linesep_in="\n",
                                    triggers=[],
                                )
                            ),
                            self.worker.recent,
                            self.worker.lastmsg,
                        ]
                    )
                    quality = self.getcfg("profile.quality")
                    clutres = {"m": 17, "l": 9}.get(quality, 33)
                    hdr_clut_profile = create_synthetic_hdr_clut_profile(
                        hdr_format,
                        rgb_space,
                        "",
                        self.getcfg("synthprofile.black_luminance") * (1 - outoffset),
                        self.getcfg("synthprofile.luminance"),
                        minmll,
                        maxmll,
                        self.getcfg("3dlut.hdr_maxmll_alt_clip"),
                        1.2,
                        self.getcfg("3dlut.hdr_ambient_luminance"),
                        clutres=clutres,
                        sat=self.getcfg("3dlut.hdr_sat"),
                        hue=self.getcfg("3dlut.hdr_hue"),
                        generate_B2A=trc == -2,
                        worker=self.worker,
                        logfile=logfiles,
                        cat=self.cat,
                    )
                    profile.tags.A2B0 = hdr_clut_profile.tags.A2B0
                    if trc == -2:
                        # HLG
                        profile.tags.B2A0 = hdr_clut_profile.tags.B2A0
                if black != [0, 0, 0] and outoffset and not bpc:
                    profile.apply_black_offset(black)
            else:
                # Grayscale profile
                if trc == -2084:
                    profile.tags.kTRC.set_smpte2084_trc(
                        self.getcfg("synthprofile.black_luminance") * (1 - outoffset),
                        self.getcfg("synthprofile.luminance"),
                        minmll,
                        maxmll,
                        self.getcfg("3dlut.hdr_maxmll_alt_clip"),
                        rolloff=True,
                    )
                else:
                    # HLG
                    profile.tags.kTRC.set_hlg_trc(
                        0,
                        self.getcfg("synthprofile.luminance"),
                        1.2,
                        self.getcfg("3dlut.hdr_ambient_luminance"),
                    )
                if black != [0, 0, 0] and outoffset and not bpc:
                    profile.tags.kTRC.apply_bpc(black[1])
        elif black != [0, 0, 0]:
            # Color profile if rgb is True, else grayscale profile
            vmin = 0 if rgb else black[1]
            for channel in channels:
                TRC = profile.tags[f"{channel}TRC"]
                TRC.set_trc(trc, 1024, vmin=vmin * 65535)
            if rgb:
                profile.apply_black_offset(black)
        else:
            for channel in channels:
                profile.tags[f"{channel}TRC"].set_trc(trc, 1)
        if black != [0, 0, 0] and bpc:
            if rgb:
                profile.apply_black_offset((0, 0, 0))
            else:
                profile.tags.kTRC.apply_bpc()
        for tagname in ("lumi", "bkpt"):
            if tagname == "lumi":
                # Absolute
                X, Y, Z = [
                    (v / XYZ["wY"]) * self.getcfg("synthprofile.luminance")
                    for v in (XYZ["wX"], XYZ["wY"], XYZ["wZ"])
                ]
            else:
                X, Y, Z = (XYZ["kX"], XYZ["kY"], XYZ["kZ"])
            profile.tags[tagname] = XYZType()
            (
                profile.tags[tagname].X,
                profile.tags[tagname].Y,
                profile.tags[tagname].Z,
            ) = (X, Y, Z)
        # Technology type
        if tech:
            if not isinstance(tech, bytes):
                tech = tech.encode("utf-8")
            profile.tags.tech = SignatureType(b"sig \0\0\0\0" + tech, "tech")
        # Colorimetric intent image state
        if ciis:
            if not isinstance(ciis, bytes):
                ciis = ciis.encode("utf-8")
            profile.tags.ciis = SignatureType(b"sig \0\0\0\0" + ciis, "ciis")
        profile.setDescription(os.path.splitext(os.path.basename(path))[0])
        profile.calculate_id()
        try:
            profile.write(path)
        except Exception as exception:
            return exception

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

        items = [lang.getstr(item) for item in self.trc_ctrl.Items]
        self.trc_ctrl.SetItems(items)
        self.trc_ctrl.SetSelection(0)

        self.trc_gamma_types_ab = {0: "g", 1: "G"}
        self.trc_gamma_types_ba = {"g": 0, "G": 1}
        self.trc_gamma_type_ctrl.SetItems(
            [lang.getstr("trc.type.relative"), lang.getstr("trc.type.absolute")]
        )

        self.profile_classes = dict(
            get_mapping(list(PROFILE_CLASS.items()), [b"mntr", b"scnr"])
        )
        self.profile_class_ctrl.SetItems(list(self.profile_classes.values()))
        self.profile_class_ctrl.SetSelection(0)

        self.tech = dict(
            get_mapping(
                [("", "unspecified"), *list(TECH.items())],
                [
                    "",
                    "fscn",
                    "dcam",
                    "rscn",
                    "vidm",
                    "vidc",
                    "pjtv",
                    "CRT ",
                    "PMD ",
                    "AMD ",
                    "mpfs",
                    "dmpc",
                    "dcpj",
                ],
            )
        )
        self.tech_ctrl.SetItems(list(self.tech.values()))
        self.tech_ctrl.SetSelection(0)

        self.ciis = dict(
            get_mapping(
                [("", "unspecified"), *list(CIIS.items())],
                ["", "scoe", "sape", "fpce"],
            )
        )
        self.ciis_ctrl.SetItems(list(self.ciis.values()))
        self.ciis_ctrl.SetSelection(0)

    def trc_ctrl_handler(self, event=None):
        """Handler for TRC control.

        Args:
            event (wx.Event, optional): The event object. Defaults to None.
        """
        if not self._updating_ctrls:
            self.preset_ctrl.SetSelection(0)
        i = self.trc_ctrl.GetSelection()
        if i == 5:
            # BT.1886
            self.setcfg("synthprofile.trc_gamma", 2.4)
            self.setcfg("synthprofile.trc_gamma_type", "G")
            self.setcfg("synthprofile.trc_output_offset", 0.0)
        if not self._updating_ctrls:
            self.update_trc_controls()

    def trc_gamma_type_ctrl_handler(self, event):
        """Handler for TRC gamma type control.

        Args:
            event (wx.Event): The event object.
        """
        self.setcfg(
            "synthprofile.trc_gamma_type",
            self.trc_gamma_types_ab[self.trc_gamma_type_ctrl.GetSelection()],
        )
        self.update_trc_control()

    def trc_gamma_ctrl_handler(self, event):
        """Handler for TRC gamma control.

        Args:
            event (wx.Event): The event object.
        """
        if self._updating_ctrls:
            event.Skip()
            return
        try:
            v = float(self.trc_gamma_ctrl.GetValue().replace(",", "."))
            if (
                v < config.VALID_RANGES["gamma"][0]
                or v > config.VALID_RANGES["gamma"][1]
            ):
                raise ValueError
        except ValueError:
            wx.Bell()
            self.trc_gamma_ctrl.SetValue(str(self.getcfg("synthprofile.trc_gamma")))
        else:
            if str(v) != self.trc_gamma_ctrl.GetValue():
                self.trc_gamma_ctrl.SetValue(str(v))
            self.setcfg("synthprofile.trc_gamma", float(self.trc_gamma_ctrl.GetValue()))
            self.preset_ctrl.SetSelection(0)
            self.update_trc_control()
        event.Skip()

    def update_controls(self):
        """Update controls with values from the configuration."""
        self.luminance_ctrl.SetValue(self.getcfg("synthprofile.luminance"))
        self.black_luminance_ctrl.SetValue(self.getcfg("synthprofile.black_luminance"))
        self.update_trc_control()
        self.update_trc_controls()

    def update_trc_control(self):
        """Update TRC control based on the current configuration."""
        if self.trc_ctrl.GetSelection() in (0, 5):
            if (
                self.getcfg("synthprofile.trc_gamma_type") == "G"
                and self.getcfg("synthprofile.trc_output_offset") == 0
                and self.getcfg("synthprofile.trc_gamma") == 2.4
            ):
                self.trc_ctrl.SetSelection(5)  # BT.1886
            else:
                self.trc_ctrl.SetSelection(0)  # Gamma

    def update_trc_controls(self):
        """Update TRC controls with values from the configuration."""
        i = self.trc_ctrl.GetSelection()
        self.panel.Freeze()
        self.trc_gamma_label.Show(i in (0, 5))
        self.trc_gamma_ctrl.SetValue(str(self.getcfg("synthprofile.trc_gamma")))
        self.trc_gamma_ctrl.Show(i in (0, 5))
        self.trc_gamma_type_ctrl.SetSelection(
            self.trc_gamma_types_ba[self.getcfg("synthprofile.trc_gamma_type")]
        )
        if i in (0, 5, 7, 8):
            # Gamma, BT.1886, SMPTE 2084 (PQ)
            outoffset = int(self.getcfg("synthprofile.trc_output_offset") * 100)
        else:
            outoffset = 100
        self.black_output_offset_ctrl.SetValue(outoffset)
        self.black_output_offset_intctrl.SetValue(outoffset)
        target_peak = self.getcfg("synthprofile.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.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_sat_ctrl.SetValue(round(self.getcfg("3dlut.hdr_sat") * 100))
        self.lut3d_hdr_update_sat_val()
        hue = round(self.getcfg("3dlut.hdr_sat") * 100)
        self.lut3d_hdr_hue_ctrl.SetValue(hue)
        self.lut3d_hdr_hue_intctrl.SetValue(hue)
        self.setcfg("3dlut.hdr_peak_luminance", self.getcfg("synthprofile.luminance"))
        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()
        self.lut3d_hdr_minmll_label.Show(i in (7, 8))  # SMPTE 2084 (PQ)
        self.lut3d_hdr_minmll_ctrl.Show(i in (7, 8))  # SMPTE 2084 (PQ)
        self.lut3d_hdr_minmll_ctrl_label.Show(i in (7, 8))  # SMPTE 2084 (PQ)
        self.lut3d_hdr_maxmll_label.Show(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_hdr_maxmll_ctrl.Show(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_hdr_maxmll_ctrl_label.Show(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_show_hdr_maxmll_alt_clip_ctrl()
        self.lut3d_hdr_diffuse_white_label.Show(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_hdr_diffuse_white_txt.Show(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_hdr_diffuse_white_txt_label.Show(i == 8)  # SMPTE 2084 (PQ)
        sizer = self.lut3d_hdr_sat_ctrl.ContainingSizer
        sizer.ShowItems(i == 8)  # SMPTE 2084 (PQ)
        sizer = self.lut3d_hdr_hue_ctrl.ContainingSizer
        sizer.ShowItems(i == 8)  # SMPTE 2084 (PQ)
        self.lut3d_hdr_ambient_luminance_label.Show(i == 2)  # HLG
        self.lut3d_hdr_ambient_luminance_ctrl.Show(i == 2)  # HLG
        self.lut3d_hdr_ambient_luminance_ctrl_label.Show(i == 2)  # HLG
        self.lut3d_hdr_system_gamma_label.Show(i == 2)  # HLG
        self.lut3d_hdr_system_gamma_txt.Show(i == 2)  # HLG
        self.black_luminance_ctrl_handler(True)
        if i in (4, 6):
            # Rec 709 or SMPTE 240M
            # Match Adobe 'video' profiles
            self.profile_class_ctrl.SetStringSelection(self.profile_classes[b"scnr"])
            self.tech_ctrl.SetStringSelection(self.tech["vidc"])
            self.ciis_ctrl.SetStringSelection(self.ciis["fpce"])
        elif (
            self.profile_class_ctrl.GetStringSelection()
            == self.profile_classes[b"scnr"]
        ):
            # If 'input' profile, reset class/tech/colorimetric intent image state
            self.profile_class_ctrl.SetStringSelection(self.profile_classes[b"mntr"])
            self.tech_ctrl.SetStringSelection(self.tech[""])
            self.ciis_ctrl.SetStringSelection(self.ciis[""])
        self.panel.GetSizer().Layout()
        self.panel.Thaw()

    def white_XYZ_ctrl_handler(self, event):
        """Handler for white XYZ controls.

        Args:
            event: The event triggered by the control change.
        """
        self.parse_XYZ("white")
        self.parse_xy()

    def white_xy_ctrl_handler(self, event):
        """Handler for white xy controls.

        Args:
            event: The event triggered by the control change.
        """
        self.parse_xy("white")

    def getcfg(self, name, fallback=True, raw=False, cfg=None):
        """Get a configuration value.

        Args:
            name (str): The name of the configuration setting.
            fallback (bool, optional): Whether to return a fallback value if
                the setting does not exist. Defaults to True.
            raw (bool, optional): Whether to return the raw value without
                processing. Defaults to False.
            cfg (dict, optional): The configuration dictionary to retrieve from.
                Defaults to self.cfg.

        Returns:
            The value of the configuration setting, or a fallback value if it
            does not exist.
        """
        if not cfg:
            cfg = self.cfg
        return getcfg(name, fallback, raw, cfg)

    def hascfg(self, name, fallback=True, cfg=None):
        """Check if a configuration value exists.

        Args:
            name (str): The name of the configuration setting.
            fallback (bool, optional): Whether to return a fallback value if
                the setting does not exist. Defaults to True.
            cfg (dict, optional): The configuration dictionary to check.
                Defaults to self.cfg.

        Returns:
            bool: True if the configuration setting exists, False otherwise.
        """
        if not cfg:
            cfg = self.cfg
        return hascfg(name, fallback, cfg)

    def setcfg(self, name, value, cfg=None):
        """Set a configuration value.

        Args:
            name (str): The name of the configuration setting.
            value: The value to set for the configuration setting.
            cfg (dict, optional): The configuration dictionary to modify.
                Defaults to self.cfg.
        """
        if not cfg:
            cfg = self.cfg
        setcfg(name, value, cfg)


def get_mapping(mapping, keys):
    """Get a mapping of keys to values."""
    return sorted(
        [
            (k, lang.getstr(v.lower().replace(" ", "_")))
            for k, v in [item for item in mapping if item[0] in keys]
        ],
        key=lambda item: item[0],
    )


def main():
    """Main function to run the application."""
    config.initcfg("synthprofile")
    lang.init()
    lang.update_defaults()
    app = BaseApp(0)
    app.TopWindow = SynthICCFrame()
    if sys.platform == "darwin":
        app.TopWindow.init_menubar()
    wx.CallLater(1, _main, app)
    app.MainLoop()


def _main(app):
    app.TopWindow.listen()
    app.process_argv(1)
    app.TopWindow.Show()


if __name__ == "__main__":
    main()
