#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#   relative_rgb_mask_g3.py Ver. 0.1 (June, 2025)
#   Copyright (C) 2025 Ohnishi, Yasuo
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.

import sys

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('GimpUi', '3.0')
from gi.repository import GimpUi

from gi.repository import Gtk, GLib, Gegl, GObject, Gio
from gimphelpers import pdb_run
import os, sys

class relative_rgb_mask (Gimp.PlugIn):
    def do_query_procedures(self):
        return [ "py-plug-in-relative-rgb-mask-g3j" ]

    def do_set_i18n (self, name):
        return False

    def do_create_procedure(self, name):
        procedure = Gimp.ImageProcedure.new(self, name,
                                            Gimp.PDBProcType.PLUGIN,
                                            self.run, None)

        procedure.set_image_types("*")
        #procedure.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.ALWAYS)
        procedure.set_menu_label("相対RGB色マスクを付加...")
        procedure.add_menu_path('<Image>/Layer/Mask/パラメータ指定マスク')

        procedure.set_documentation("RGB の相対値に基づくマスクを作成します",
                                    "add spots removal mask for GIMP 3.0",
                                    name)
        procedure.set_attribution\
                ("Ohnishi, Yasuo", "Ohnishi, Yasuo", "(c) 2024-25")

        return procedure

    def run(self, procedure, run_mode, image, drawables, config, run_data):
        GimpUi.init("Python-Relative_rgb_mask_g3.py")
        self.gray = drawables[0].is_gray()
        print("[spots mask] Gray?: " + str(self.gray))
        self.selected_layers = image.get_selected_layers()
        try:
            filepath = image.get_file().get_path()
        except:
            home_directory = (
                os.environ.get('HOME') or
                os.environ.get('HOMEPATH') or
                os.environ.get('USERPROFILE')
                )
            if os.name == 'nt':
                separator = '\\'
            else:
                separator = '/'
            filepath = home_directory + separator
        print("[spots mask] File path: " + filepath)
        basename, ext = os.path.splitext(filepath)
        if self.gray == False: # RGB image
            result = pdb_run('plug-in-decompose', {
                'run-mode': Gimp.RunMode.NONINTERACTIVE,
                'image': image,
                'drawables': drawables,
                'decompose-type': 'rgb',
                'layers-mode': True
                })
            print("[spots mask] Decompose successed?: " \
                                    + str(result.index(0)))
            image2 = result.index(1)
            self.layers = image2.get_layers()
            self.layers[0].set_name("Red")
            self.layers[1].set_name("Green")
            self.layers[2].set_name("Blue")
        else: # monochrome image
            self.display_on_gray()
            return procedure.new_return_values \
                (Gimp.PDBStatusType.CANCEL, GLib.Error())

        # desplay mask making image
        new_layer_g = self.layers[1].copy()
        image2.insert_layer(new_layer_g, None, 3)
        new_layer_b = self.layers[2].copy()
        image2.insert_layer(new_layer_b, None, 4)
        new_layer_r = self.layers[0].copy()
        image2.insert_layer(new_layer_r, None, 5)
        self.layers1 = image2.get_layers()
        self.layers[0].set_visible(False)
        self.layers[1].set_visible(False)
        self.layers[2].set_visible(False)
        self.layers1[3].set_visible(True)
        self.layers1[4].set_visible(True)
        self.layers1[5].set_visible(True)
        self.layers1[3].set_mode(Gimp.LayerMode.SUBTRACT_LEGACY)
        self.layers1[4].set_mode(Gimp.LayerMode.SUBTRACT_LEGACY)

        disp2 = Gimp.Display.new(image2)

        if self.gray == False:
            # Ensure the blur dialog is created with a valid parent window
            dialog1 = GetChSelectionDialog(None, self, image2, self.layers)
            dialog1.set_transient_for(None)
            dialog1.set_modal(True)
            ########## run - dialog1 ##############
            response = Gtk.Dialog.run(dialog1)
            if response == Gtk.ResponseType.CANCEL:
                self.display_on_cancel()

                dialog1.destroy()  # Fully remove the channel selection dialog
                disp2.delete()
                return procedure.new_return_values \
                    (Gimp.PDBStatusType.CANCEL, GLib.Error())
            self.chName = dialog1.chName # get target channel name
            mask_image = dialog1.mask_image_layer
            save_mask = dialog1.save_mask
            mask_images = [mask_image]
            Gimp.edit_copy(mask_images)
            chName = dialog1.chName
            dialog1.destroy()
            # change the posion of channel image layers
            # target ch layer(s) must be bottom

        print("[rgb mask] selected channel name: " + self.chName)
        layers_active = image2.get_selected_layers()

        # Start a grouped undo step
        image.undo_group_start()

        # add the editing layer to original image
        editing_layer = \
            Gimp.Layer.new_from_drawable(drawables[0], image)
        editing_layer.set_name(chName + " mask") 
        image.insert_layer(editing_layer, None, 0)

        # add mask to editing layer
        if editing_layer.has_alpha() == False:
            editing_layer.add_alpha()
        if editing_layer.get_mask() == None:
            mask = editing_layer.create_mask(Gimp.AddMaskType.WHITE)
            editing_layer.add_mask(mask)
        else:
            mask = editing_layer.get_mask()
        editing_layer.set_edit_mask(True)
        # paste the image in clipboard to mask
        Gimp.edit_paste(mask, False)
        Gimp.floating_sel_anchor(image.get_floating_sel())
                        #get floating_sel as layer!
        editing_layer.set_show_mask(True)
        image.set_selected_layers([editing_layer])

        # End a grouped undo step
        image.undo_group_end()
        if save_mask == True:
            # save mask image as tiff
            mask_filepath = basename + "_rel_" + chName + "_mask.tif"
            image2.merge_visible_layers(Gimp.MergeType.CLIP_TO_IMAGE)
            image2.flatten()
            # Once merge with "CLIP_TO_IMAGE", after that flatten the image
            # to avoid saving error
            Gimp.file_save(run_mode=Gimp.RunMode.NONINTERACTIVE, image=image2,
                    file=Gio.File.new_for_path(mask_filepath), options=None)
        # delete mask making image
        disp2.delete()

        # do what you want to do, then, in case of success, return:
        return procedure.new_return_values \
                (Gimp.PDBStatusType.SUCCESS, GLib.Error())

    def display_on_cancel(self):
        # Show a warning dialog instead of Gimp.message()
        warning_dialog = Gtk.MessageDialog(
            transient_for=None,
            flags=Gtk.DialogFlags.MODAL,
            message_type=Gtk.MessageType.WARNING,  # ⚠️ Yellow warning triangle
            buttons=Gtk.ButtonsType.OK,
            text="Process canceled"
        )
        warning_dialog.set_position(Gtk.WindowPosition.CENTER)
            # Center the dialog
        warning_dialog.format_secondary_text("ユーザによって" + \
                    "動作が取り消されました")
        warning_dialog.run()
        warning_dialog.destroy()

    def display_on_gray(self):
        # Show a warning dialog instead of Gimp.message()
        warning_dialog = Gtk.MessageDialog(
            transient_for=None,
            flags=Gtk.DialogFlags.MODAL,
            message_type=Gtk.MessageType.WARNING,  # ⚠️ Yellow warning triangle
            buttons=Gtk.ButtonsType.OK,
            text="Process canceled"
        )
        warning_dialog.set_position(Gtk.WindowPosition.CENTER)
            # Center the dialog
        warning_dialog.format_secondary_text("画像が RGB カラー" + \
                    "ではありません")
        warning_dialog.run()
        warning_dialog.destroy()

    def insert_layer(self, n_org_layer, n_layer, layers, image):
        copyed_layer = [layers[n_org_layer]] # set copy source layer
        Gimp.edit_copy(copyed_layer)
            # Copy mask image as list v. of layer(s) even if only one image exits.
        target_layer =  layers[n_layer] # Get target layer
        # add mask to the target layer
        mask = target_layer.create_mask(Gimp.AddMaskType.WHITE)
        target_layer.add_mask(mask)
        target_layer.set_edit_mask(True)
        Gimp.edit_paste(mask, False) # paste mask image to mask
        Gimp.floating_sel_anchor(image.get_floating_sel()) #get floating_sel as layer!
        target_layer.set_edit_mask(False)


####### dialog1 #########
class GetChSelectionDialog(Gtk.Dialog):
    def __init__(self, parent, plugin, image2, layers0):
        Gtk.Dialog.__init__(self, title="相対RGB色マスク作成ダイアログ",
                            parent=parent, flags=Gtk.DialogFlags.MODAL)

        self.plugin = plugin
        self.image = image2
        self.layers = image2.get_layers()
        self.layers0 = layers0 # original layers
        self.update_timer_id = None
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(400, 200)
        self.threshold = 5.0
        width = image2.get_width()
        height = image2.get_height()
        long = max(width, height)
        self.factor_timer = (long /1920.0) ** 2
            # factor for delay timer depending on pixel size

        box = self.get_content_area()
        box.set_spacing(10)
        box.set_margin_top(20)
        box.set_margin_bottom(20)
        box.set_margin_start(20)
        box.set_margin_end(20)
        #---add combo box for channel selection---
        label_combo_box = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(label_combo_box)

        label = Gtk.Label(label="作成マスクを選択:")
        label.set_xalign(0)
        label_combo_box.pack_start(label, False, False, 0)

        channels = [
            "Red",
            "Green",
            "Blue",
            "Cyan",
            "Magenta",
            "Yellow"
        ]
        self.chName = channels[0]
        self.channel_combo = Gtk.ComboBoxText()
        self.channel_combo.set_entry_text_column(0)
        self.channel_combo.connect("changed", self.on_channel_combo_changed)
        for channel in channels:
            self.channel_combo.append_text(channel)
        self.channel_combo.set_active(0)
        self.channel_combo.set_tooltip_text("マスクを作成したい原色または補色を選びます")
        label_combo_box.pack_start(self.channel_combo, False, False, 0)

        #---add scale1 (slider) for threshold of the color---
        box_4_scale1 = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(box_4_scale1)
        label = Gtk.Label\
            (label="対象色の閾値")
        label.set_xalign(0)
        box_4_scale1.pack_start(label, False, False, 0)
        self.scale1 = \
            Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, -20.0, 20.0, 0.01)
        self.scale1.add_mark(0.0, Gtk.PositionType.TOP, None)
        # 🔹 Store the selected blur sigma
        self.threshold = 5.0  # Default value
        self.scale1.set_value(self.threshold)
        self.scale1.set_digits(2)
        self.scale1.set_tooltip_text("値を増やすとマスクの範囲が拡がり\n減らすと狭まります")
        self.scale1.connect("value-changed", self.on_scale_value_changed, 1)
        box_4_scale1.pack_start(self.scale1, True, True, 0) # not use fixed


        #---add scale2 (slider) for skew of the color---
        box_4_scale2 = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(box_4_scale2)
        label = Gtk.Label\
            (label="対象色の傾き")
        label.set_xalign(0)
        box_4_scale2.pack_start(label, False, False, 0)
        self.scale2 = \
            Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, -100.0, 100.0, 0.1)
        self.scale2.add_mark(0.0, Gtk.PositionType.TOP, None)
        # 🔹 Store the selected blur sigma
        self.skew = 0.0  # Default value
        self.scale2.set_value(self.skew)
        self.scale2.set_digits(1)
        self.scale2.set_tooltip_text("Red マスク作成時: 左 青感度高 --- 右 緑感度高\n"\
             + "Green マスク作成時: 左 青感度高 --- 右 赤感度高\n"\
             + "Blue マスク作成時: 左 緑感度高 --- 右 赤感度高\n"\
             + "Cyan マスク作成時: 左 緑感度高 --- 右 青感度高\n"\
             + "Magenta マスク作成時: 左 赤感度高 --- 右 青感度高\n"\
             + "Yellow マスク作成時: 左 赤感度高 --- 右 緑青感度高\n")

        self.scale2.connect("value-changed", self.on_scale_value_changed, 2)
        box_4_scale2.pack_start(self.scale2, True, True, 0) # not use fixed

        #---add scale3 (slider) for transparency of the mask---
        box_4_scale3 = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(box_4_scale3)
        label = Gtk.Label\
            (label="マスク透過度")
        label.set_xalign(0)
        box_4_scale3.pack_start(label, False, False, 0)
        self.scale3 = \
            Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0.0, 5.0, 0.1)
        self.scale3.add_mark(1.0, Gtk.PositionType.TOP, None)
        # 🔹 Store the selected blur sigma
        self.transparency = 1.0  # Default value
        self.scale3.set_value(self.transparency)
        self.scale3.set_digits(1)
        self.scale3.connect("value-changed", self.on_scale_value_changed, 3)
        box_4_scale3.pack_start(self.scale3, True, True, 0) # not use fixed


        #---apply uniform value check box----
        label_check_box = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(label_check_box)
        self.uni_value = False
        self.uni_value_check = Gtk.CheckButton.new_with_label("均一の透過度を適用")
        label_check_box.pack_start(self.uni_value_check, False, False, 0)
        self.uni_value_check.connect("toggled", self.on_uni_value_check_toggled)
        #---add end----
        #---add save mask check box----
        self.save_mask = False
        label_check2_box = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(label_check2_box)
        self.save_check = \
            Gtk.CheckButton.new_with_label(" マスク画像を tiff として保存")
        self.save_check.set_tooltip_text("マスク画像は元画像ファイルと同じ"\
             +"ディレクトリに保存されます\nもし元画像のファイル名が未設定なら"\
            + "マスク画像はユーザディレクトリの\n直下に \"_rel_<カラー名>_mask.tif\" "\
            + "という名で保存されます")
        label_check2_box.pack_start(self.save_check, False, False, 0)
        self.save_check.connect("toggled", self.on_save_check_toggled)
        #---add end----

        self.add_buttons("Cancel", Gtk.ResponseType.CANCEL,
                         "OK", Gtk.ResponseType.OK)
        self.show_all()
        self.update_mask_image(self.chName, 47.5, 47.5, 100)

    def on_channel_combo_changed(self, combo): #combo: channel_combo
        chName = combo.get_active_text()
        self.chName = chName
        if self.threshold != 0.0:
            self.threshold = 0.0
        else:
            self.threshold = self.threshold + 0.1
        self.skew =0.0
        try:
            self.scale1.set_value(self.threshold)
            self.scale2.set_value(self.skew)
        except:
            return

    def on_scale_value_changed(self, scale, data):
        # Trigger the blur update when the scale1 value changes, but debounce updates.
        if data == 1: # threshold -20.0 - 20.0 def. 5.0
            self.threshold = scale.get_value()
        elif data == 2: # skew -100.0 - 100.0 def. 0.0
            self.skew = scale.get_value()
        elif data == 3: # transparency 0.0 - 5.0 def. 1.0
            self.transparency = scale.get_value()

        # Cancel any existing pending update
        if self.update_timer_id:
            GLib.source_remove(self.update_timer_id)

        # Set a new timer (e.g., 200ms delay)
        self.update_timer_id = \
            GLib.timeout_add(200 * self.factor_timer, self._delayed_update)

    def _delayed_update(self):
        #Executes the update_blur function after a short delay.
        # calculate opacity3, 4, 5
        skew_a = (100 + self.skew) / 100.0
        skew_b = (100 - self.skew) / 100.0
        ratio_threshold = (100 - self.threshold) / 100.0

        if self.chName == "Red" or \
            self.chName == "Green" or self.chName == "Blue":
            opacity3 = 50
            opacity4 = 50
            opacity5 = 100
            if self.skew != 0.0:
                opacity3 = opacity3 * skew_a #
                opacity4 = opacity4 * skew_b #
            if self.threshold != 0.0:
                opacity3 = opacity3 * ratio_threshold
                opacity4 = opacity4 * ratio_threshold

        else:
            opacity3 = 100
            opacity4 = 50
            opacity5 = 50
            if self.skew != 0.0:
                opacity4 = opacity4 * skew_a #
                opacity5 = opacity5 * skew_b #
            if self.threshold != 0.0:
                #Gimp.message("ratio_threshold: " + str(opacity3))
                opacity3 = opacity3 * ratio_threshold
                #Gimp.message("opacity3: " + str(opacity3) + \
                # " threshold: " + str(self.threshold) + \
                # " ratio: " + str(ratio_threshold))

        self.update_mask_image(self.chName, opacity3, opacity4, opacity5)
        self.update_timer_id = None  # Reset timer ID
        return False  # Ensures the timeout runs only once


    def update_mask_image(self, chName, opacity3, opacity4, opacity5):
        if chName is not None:
            #Gimp.message(chName)
            print("[rgb mask] Selected: channel=%s" % chName)
            self.image.remove_layer(self.layers[5])
            self.image.remove_layer(self.layers[4])
            self.image.remove_layer(self.layers[3])
            #self.layers[0].set_visible(True)
            #self.layers[1].set_visible(True)
            #self.layers[2].set_visible(True)
            if chName == "Red": #  or chName == "Cyan"
                self.layers[0].set_visible(False)
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 3)
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 4)
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 5)
            elif  chName == "Green": #  or chName == "Magenta"
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 3)
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 4)
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 5)
            elif chName == "Blue":
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 3)
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 4)
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 5)
            elif chName == "Cyan": #  or chName == "Cyan"
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 3)
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 4)
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 5)
            elif  chName == "Magenta": #  or chName == "Magenta"
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 3)
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 4)
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 5)
            elif chName == "Yellow":
                new_layer_b = self.layers0[2].copy()
                self.image.insert_layer(new_layer_b, None, 3)
                new_layer_g = self.layers0[1].copy()
                self.image.insert_layer(new_layer_g, None, 4)
                new_layer_r = self.layers0[0].copy()
                self.image.insert_layer(new_layer_r, None, 5)
            self.layers = self.image.get_layers()
            self.layers[3].set_visible(True)
            self.layers[4].set_visible(True)
            self.layers[5].set_visible(True)
            channel = Gimp.HistogramChannel.VALUE
            pointsX=[i/255. for i in range(256)]
            pointsY=[((x ** (1.0 / 2.2)) * \
                      (opacity3 / 100.0)) ** 2.2 for x in pointsX]
                        # manupulate in TRC encoded color space
            curve=[p for xy in zip(pointsX,pointsY) for p in xy]
            self.layers[3].curves_spline(channel, curve)

            pointsY=[((x ** (1.0 / 2.2)) * \
                      (opacity4 / 100.0)) ** 2.2 for x in pointsX]
                        # manupulate in TRC encoded color space
            curve=[p for xy in zip(pointsX,pointsY) for p in xy]
            self.layers[4].curves_spline(channel, curve)
            self.layers[3].set_mode(Gimp.LayerMode.SUBTRACT_LEGACY)

            if chName == "Red" or chName == "Green" or chName == "Blue":
                self.layers[4].set_mode(Gimp.LayerMode.SUBTRACT_LEGACY)
            else:
                channel = Gimp.HistogramChannel.VALUE
                pointsX=[i/255. for i in range(256)]
                pointsY=[((x ** (1.0 / 2.2)) * \
                          (opacity5 / 100.0)) ** 2.2 for x in pointsX]
                        # manupulate in TRC encoded color space
                curve=[p for xy in zip(pointsX,pointsY) for p in xy]
                self.layers[5].curves_spline(channel, curve)
                self.layers[4].set_mode(Gimp.LayerMode.ADDITION_LEGACY)
            self.layers[5].set_mode(Gimp.LayerMode.NORMAL_LEGACY)
            if len(self.image.get_layers()) == 7: # if already exist mask_image_layer
                self.image.remove_layer(self.mask_image_layer)
            self.mask_image_layer = Gimp.Layer.new_from_visible\
                                    (self.image, self.image,"created mask")
            #Gimp.message("self.transparency: " + str(self.transparency))
            self.image.insert_layer(self.mask_image_layer, None, 6)
            self.mask_image_layer.set_visible(True)
            if self.uni_value == False:
                pointsY = \
                    [((x ** (1.0 / 2.2)) * (4.0 * self.transparency)) ** 2.2 \
                                                                for x in pointsX]
            else: # apply uni value
                pointsY = [1.0 if x > 0.0 else 0.0 for x in pointsX]
            curve=[p for xy in zip(pointsX,pointsY) for p in xy]
            self.mask_image_layer.curves_spline(channel, curve)

            self.layers[3].set_visible(False)
            self.layers[4].set_visible(False)
            self.layers[5].set_visible(False)

            Gimp.displays_flush()

    def on_save_check_toggled(self, check): #check: view_check
        self.save_mask = check.get_active()

    def on_uni_value_check_toggled(self, check): #check: view_check
        self.uni_value = check.get_active()
        self._delayed_update()



Gimp.main(relative_rgb_mask.__gtype__, sys.argv)
