#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#   spots_removal_mask_g3.py Ver. 0.11
#   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 spots_removal_mask (Gimp.PlugIn):
    def do_query_procedures(self):
        return [ "py-plug-in-spots-removal-mask" ]

    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("傷ほこり除去マスクを付加...")
        procedure.add_menu_path('<Image>/Layer/')

        procedure.set_documentation("傷ほこり除去マスク付のガウスぼかしを掛けた補正レイヤーを追加します",
                                    "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):
        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
            image2 = image.duplicate()
            self.layers = image2.get_layers()
            self.layers[0].set_name("Gray")
        # desplay mask making image
        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)
            dialog1.set_transient_for(None)
            dialog1.set_modal(True)
            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
            self.nega = dialog1.nega # get film stock type
            dialog1.destroy()
            # delete unnecessary channel image layers
            if self.chName == "Red":
                image2.remove_layer(self.layers[2])
                image2.remove_layer(self.layers[1])
            elif self.chName == "Green":
                image2.remove_layer(self.layers[2])
                image2.remove_layer(self.layers[0])
            else: #Blue
                image2.remove_layer(self.layers[1])
                image2.remove_layer(self.layers[0])

        else: # monochrome image
            self.chName = "Gray" # get target channel name
            self.nega = True # get film stock type

        print("[spots mask] selected channel name: " + self.chName)
        print("[spots mask] film stock is negative?: " + str(self.nega))
        layers_active = image2.get_selected_layers()
        if self.nega == True:
            layers_active[0].invert(True) #invert selected color channel layer
        blur_layer = Gimp.Layer.new_from_drawable(layers_active[0], image2)
        blur_layer.set_name("blur layer")
        if self.gray == True:
            position = 1
        else: # color image
            position = len(self.layers) - 1
        image2.insert_layer(blur_layer, None, position)
        layers_active[0].set_mode(Gimp.LayerMode.SUBTRACT)
                    # set mode of original color channel image
        layers_active[0].set_visible(False)
        blur_layer.set_visible(True)
        Gimp.displays_flush()

        dialog2 = MaskParametersDialog(None, self, image2)
        dialog2.set_transient_for(None)
        dialog2.set_modal(True)
        response = Gtk.Dialog.run(dialog2)
        blur_sigma = dialog2.selected_blur_sigma
        dialog2.destroy()
        if response == Gtk.ResponseType.CANCEL:
            self.display_on_cancel()
            dialog2.destroy()  # Fully remove the channel selection dialog
            disp2.delete()
            return procedure.new_return_values \
                    (Gimp.PDBStatusType.CANCEL, GLib.Error())

        image2_layers = dialog2.layers #get layers from dialog2
        image2_layers[0].set_visible(True)
        Gimp.displays_flush()
        save_mask = dialog2.save_mask # get save_mask from dialog2

        # Start a grouped undo step
        image.undo_group_start()

        # mask image from visible
        mask_image = Gimp.Layer.new_from_visible(image2, image2,"mask_image")
        image2.insert_layer(mask_image, None, 0)
        mask_images = [mask_image]
        Gimp.edit_copy(mask_images)

        # add the blur layer to original image
        blur_layer = Gimp.Layer.new_from_visible(image, image, "blur layer")
        image.insert_layer(blur_layer, None, 0)
        self.apply_blur_filter(blur_layer, blur_sigma)
        # add mask to blur layer
        if blur_layer.has_alpha() == False:
            blur_layer.add_alpha()
        if blur_layer.get_mask() == None:
            mask = blur_layer.create_mask(Gimp.AddMaskType.WHITE)
            blur_layer.add_mask(mask)
        else:
            mask = blur_layer.get_mask()
        blur_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!
        blur_layer.set_show_mask(True)
        image.set_selected_layers([blur_layer])

        # End a grouped undo step
        image.undo_group_end()
        if save_mask == True:
            # save mask image as tiff
            mask_filepath = basename + "_spots_mask.tif"
            image2.merge_visible_layers(Gimp.MergeType.CLIP_TO_IMAGE)
            #image2.merge_visible_layers(Gimp.MergeType.FLATTEN_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 apply_blur_filter(self, layer, blur_sigma):
        # Initializes the Gaussian blur filter on the colour/tone layer.
        #Gimp.message("layers Length: " + str(self.layerslen))
        try:
            self.blur_filter = Gimp.DrawableFilter.new(layer,
                                "gegl:gaussian-blur", "Gaussian Blur Filter")
            self.blur_filter.set_blend_mode(Gimp.LayerMode.REPLACE)
            self.blur_filter.set_opacity(1.0)
            self.config = self.blur_filter.get_config()

            # Apply blur value
            self.config.set_property("std-dev-x", blur_sigma)
            self.config.set_property("std-dev-y", blur_sigma)
            self.blur_filter.update()

            # Append filter to the drawable
            layer.append_filter(self.blur_filter)

        except Exception as e:
            Gimp.message(f"❌ ガウスぼかしフィルターの初期化に失敗しました:\n\n{e}")

    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()


class MaskParametersDialog(Gtk.Dialog):
    def __init__(self, parent, plugin, image2):
        Gtk.Dialog.__init__(self, title="マスクパラメータの設定",
                            parent=parent, flags=Gtk.DialogFlags.MODAL)
        self.plugin = plugin
        self.image = image2
        self.layers = image2.get_layers()
        self.layerslen = len(self.layers)
        self.blur_timer_id = None

        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(400, 200)

        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 scale (slider) for sigma of blur---
        box_4_scale1 = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(box_4_scale1)
        label = Gtk.Label\
            (label="ぼかし量 (相対値 Sigma)\n(1920p 画像 換算値で指定)")
        label.set_xalign(0)
        box_4_scale1.pack_start(label, False, False, 0)
        imageWidth = image2.get_width()
        imageheight = image2.get_height()
        self. SigmaRatio = float(max(imageWidth, imageheight)) / 1920.0
        self.scale1_4_sigma = \
            Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0.0, 20.0, 0.01)
        self.scale1_4_sigma.add_mark(10.0, Gtk.PositionType.TOP, None)
        # 🔹 Store the selected blur sigma
        self.selected_blur_sigma = 3.0  # Default value
        self.scale1_4_sigma.set_value(self.selected_blur_sigma)
        self.scale1_4_sigma.set_digits(2)
        self.scale1_4_sigma.connect("value-changed", self.on_scale_value_changed)
        box_4_scale1.pack_start(self.scale1_4_sigma, True, True, 0) # not use fixed
        #---add view upper layer check box----
        label_check_box = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(label_check_box)
        self.view_layer0 = False
        self.view_check = Gtk.CheckButton.new_with_label(" マスク画像の確認")
        label_check_box.pack_start(self.view_check, False, False, 0)
        self.view_check.connect("toggled", self.on_view_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直下に \"_spots_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()

        # Initialize the blur filter and apply the default value
        self.initialize_blur_filter()
        self.update_blur(self.scale1_4_sigma.get_value())  # Apply initial blur
        # Only one filter is available?

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

    def on_view_check_toggled(self, check): #check: view_check
        self.view_layer0 = check.get_active()
        if self.view_layer0 == True:
            self.layers[0].set_visible(True)
            Gimp.displays_flush()
        else:
            self.layers[0].set_visible(False)
            Gimp.displays_flush()

    def initialize_blur_filter(self):
        try:
            self.blur_filter = \
                Gimp.DrawableFilter.new(self.layers[self.layerslen - 1],
                                "gegl:gaussian-blur", "Gaussian Blur Filter")
            self.blur_filter.set_blend_mode(Gimp.LayerMode.REPLACE)
                                        #REPLACE is starndard layer mode
            self.blur_filter.set_opacity(1.0)
            self.config = self.blur_filter.get_config()

            # Apply default blur value
            default_blur_sigma = self.scale1_4_sigma.get_value()
            self.config.set_property("std-dev-x", default_blur_sigma)
            self.config.set_property("std-dev-y", default_blur_sigma)
            self.blur_filter.update()

            # Append filter to the drawable
            self.layers[self.layerslen - 1].append_filter(self.blur_filter)

        except Exception as e:
            Gimp.message(f"❌ ガウスぼかしフィルターの初期化に失敗しました:\n\n{e}")

    def update_blur(self, blur_sigma):
        # Updates the blur filter dynamically when the user changes the value.
        try:
            if hasattr(self, "blur_filter") and self.blur_filter:
                self.config.set_property("std-dev-x", blur_sigma)
                self.config.set_property("std-dev-y", blur_sigma)

                # 🔹 Ensure the filter update is processed
                self.blur_filter.update()
                # 🔹 Force GIMP to redraw the image
                Gimp.displays_flush()

            else:
                Gimp.message("❌ Error: \n\nぼかしフィルターは初期化されません")

        except Exception as e:
            Gimp.message(f"❌ ぼかしの更新に失敗:\n\n{e}")

    def on_scale_value_changed(self, scale):
        # Trigger the blur update when the scale1 value changes, but debounce updates.
        blur_sigma = scale.get_value() * self.SigmaRatio
        self.selected_blur_sigma = blur_sigma  #Store the selected blur sigma

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

        # Set a new timer (e.g., 100ms delay)
        self.blur_timer_id = \
            GLib.timeout_add(100, self._delayed_update, blur_sigma)

    def _delayed_update(self, blur_sigma):
        #Executes the update_blur function after a short delay.
        self.update_blur(blur_sigma)
        self.blur_timer_id = None  # Reset timer ID
        return False  # Ensures the timeout runs only once



class GetChSelectionDialog(Gtk.Dialog):
    def __init__(self, parent, plugin, image2):
        Gtk.Dialog.__init__(self, title="マスク作成元チャンネルの選択",
                            parent=parent, flags=Gtk.DialogFlags.MODAL)

        self.plugin = plugin
        self.image = image2
        self.layers = image2.get_layers()

        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(400, 200)

        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"
        ]
        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 negative film check box----
        label_check_box = \
            Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        box.add(label_check_box)

        self.nega = False
        self.nega_check = \
            Gtk.CheckButton.new_with_label(" ネガフィルム画像")
        label_check_box.pack_start(self.nega_check, False, False, 0)
        self.nega_check.connect("toggled", self.on_nega_check_toggled)
        #---add end----
        self.add_buttons("Cancel", Gtk.ResponseType.CANCEL,
                         "OK", Gtk.ResponseType.OK)
        self.show_all()

    def on_channel_combo_changed(self, combo): #combo: channel_combo
        chName = combo.get_active_text()
        self.chName = chName
        if chName is not None:
            #Gimp.message(chName)
            print("[spots mask] Selected: channel=%s" % chName)
            if chName == "Red":
                self.layers[0].set_visible(True)
            elif  chName == "Green":
                self.layers[0].set_visible(False)
                self.layers[1].set_visible(True)
            else: # Blue
                self.layers[0].set_visible(False)
                self.layers[1].set_visible(False)
                self.layers[2].set_visible(True)
            Gimp.displays_flush()

    def on_nega_check_toggled(self, check): #check: nega_check
        self.nega = check.get_active()


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