#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# GIMP plugin to interleave layers from two different images
# (c) Ofnuts 2012-2024
#
#   History:
#
#   v0.0: 2012-01-30: First published version
#   v0.1: 2012-01-30: Fix image created grayscale
#                     Add Merge option
#   v0.2: 2012-01-30: Minor fixes (labels in dialog)
#   v0.3: 2012-02-07: Add single-layer mode
#   v0.4: 2012-07-09: Add 'sprite' single-layer mode
#   v0.5: 2014-05-30: Fix bug when adding invisible layer
#   v0.6: 2019-10-05: Repackage, add 2.10 layer modes, better control on layer names
#   v0.7: 2020-04-17: Fix bug when issuing error message.
#   v0.8: 2020-04-25: Make layer mode generation dynamic to support more Gimp releases.
#   v1.0: 2024-11-xx: Port to Gimp3
#
#   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 2 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, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

# This plugin is dedicated to PowerPoint presentations and their authors.
# Nothing looks more like someone taking notes than someone coding a plugin.

import gi

gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
gi.require_version('Babl', '0.1')
from gi.repository import Babl
import sys
from gimphelpers import *
import time

def N_(message): return message
def _(message): return GLib.dgettext(None, message)

LAYERS_MISMATCH=_("Numbers of applicable layers in both images don't match! {:s} {:d}, {:s} {:d}")

layerModes=Choice('layerMode',_('Layer blend mode'),_('Blend mode applied to top layer'),'NORMAL',
             [
                 Option('NORMAL', _('Normal'), blurb=_('Normal blend mode'), value=Gimp.LayerMode.NORMAL),
                 Option('DISSOLVE', _('Dissolve'), blurb=_('Dissolve blend mode'), value=Gimp.LayerMode.DISSOLVE),
                 Option('COLOR_ERASE', _('Color Erase'), blurb=_('Color Erase blend mode'),
                        value=Gimp.LayerMode.COLOR_ERASE),
                 Option('ERASE', _('Erase'), blurb=_('Erase blend mode'), value=Gimp.LayerMode.ERASE),
                 Option('MERGE', _('Merge'), blurb=_('Merge blend mode'), value=Gimp.LayerMode.MERGE),
                 Option('SPLIT', _('Split'), blurb=_('Split blend mode'), value=Gimp.LayerMode.SPLIT),

                 Option('LIGHTEN_ONLY', _('Lighten Only'), blurb=_('Lighten Only blend mode'),
                        value=Gimp.LayerMode.LIGHTEN_ONLY),
                 Option('LUMA_LIGHTEN_ONLY', _('Luma Lighten Only'), blurb=_('Luma Lighten Only blend mode'),
                        value=Gimp.LayerMode.LUMA_LIGHTEN_ONLY),
                 Option('SCREEN', _('Screen'), blurb=_('Screen blend mode'), value=Gimp.LayerMode.SCREEN),
                 Option('DODGE', _('Dodge'), blurb=_('Dodge blend mode'), value=Gimp.LayerMode.DODGE),
                 Option('ADDITION', _('Addition'), blurb=_('Addition blend mode'), value=Gimp.LayerMode.ADDITION),

                 Option('DARKEN_ONLY', _('Darken Only'), blurb=_('Darken Only blend mode'),
                        value=Gimp.LayerMode.DARKEN_ONLY),
                 Option('LUMA_DARKEN_ONLY', _('Luma Darken Only'), blurb=_('Luma Darken Only blend mode'),
                        value=Gimp.LayerMode.LUMA_DARKEN_ONLY),
                 Option('MULTIPLY', _('Multiply'), blurb=_('Multiply blend mode'), value=Gimp.LayerMode.MULTIPLY),
                 Option('BURN', _('Burn'), blurb=_('Burn blend mode'), value=Gimp.LayerMode.BURN),
                 Option('LINEAR_BURN', _('Linear Burn'), blurb=_('Linear Burn blend mode'),
                        value=Gimp.LayerMode.LINEAR_BURN),

                 Option('OVERLAY', _('Overlay'), blurb=_('Overlay blend mode'), value=Gimp.LayerMode.OVERLAY),
                 Option('SOFTLIGHT', _('Soft light'), blurb=_('Soft light blend mode'), value=Gimp.LayerMode.SOFTLIGHT),
                 Option('HARDLIGHT', _('Hard light'), blurb=_('Hard light blend mode'), value=Gimp.LayerMode.HARDLIGHT),
                 Option('VIVID_LIGHT', _('Vivid Light'), blurb=_('Vivid Light blend mode'),
                        value=Gimp.LayerMode.VIVID_LIGHT),
                 Option('PIN_LIGHT', _('Pin Light'), blurb=_('Pin Light blend mode'), value=Gimp.LayerMode.PIN_LIGHT),
                 Option('LINEAR_LIGHT', _('Linear Light'), blurb=_('Linear Light blend mode'),
                        value=Gimp.LayerMode.LINEAR_LIGHT),
                 Option('HARD_MIX', _('Hard Mix'), blurb=_('Hard Mix blend mode'), value=Gimp.LayerMode.HARD_MIX),

                 Option('DIFFERENCE', _('Difference'), blurb=_('Difference blend mode'),
                        value=Gimp.LayerMode.DIFFERENCE),
                 Option('EXCLUSION', _('Exclusion'), blurb=_('Exclusion blend mode'), value=Gimp.LayerMode.EXCLUSION),
                 Option('SUBTRACT', _('Subtract'), blurb=_('Subtract blend mode'), value=Gimp.LayerMode.SUBTRACT),
                 Option('GRAIN_EXTRACT', _('Grain Extract'), blurb=_('Grain Extract blend mode'),
                        value=Gimp.LayerMode.GRAIN_EXTRACT),
                 Option('GRAIN_MERGE', _('Grain Merge'), blurb=_('Grain Merge blend mode'),
                        value=Gimp.LayerMode.GRAIN_MERGE),
                 Option('DIVIDE', _('Divide'), blurb=_('Divide blend mode'), value=Gimp.LayerMode.DIVIDE),

                 Option('HSV_HUE', _('HSV Hue'), blurb=_('HSV Hue blend mode'), value=Gimp.LayerMode.HSV_HUE),
                 Option('HSV_SATURATION', _('HSV Saturation'), blurb=_('HSV Saturation blend mode'),
                        value=Gimp.LayerMode.HSV_SATURATION),
                 Option('HSL_COLOR', _('HSL Color'), blurb=_('HSL Color blend mode'), value=Gimp.LayerMode.HSL_COLOR),
                 Option('HSV_VALUE', _('HSV Value'), blurb=_('HSV Value blend mode'), value=Gimp.LayerMode.HSV_VALUE),

                 Option('LCH_HUE', _('LCh Hue'), blurb=_('LCh Hue blend mode'), value=Gimp.LayerMode.LCH_HUE),
                 Option('LCH_CHROMA', _('LCh Chroma'), blurb=_('LCh Chroma blend mode'),
                        value=Gimp.LayerMode.LCH_CHROMA),
                 Option('LCH_COLOR', _('LCh Color'), blurb=_('LCh Color blend mode'), value=Gimp.LayerMode.LCH_COLOR),
                 Option('LCH_LIGHTNESS', _('LCh Lightness'), blurb=_('LCh Lightness blend mode'),
                        value=Gimp.LayerMode.LCH_LIGHTNESS),
                 Option('LUMINANCE', _('Luminance'), blurb=_('Luminance blend mode'), value=Gimp.LayerMode.LUMINANCE),

                 Option('NORMAL_LEGACY', _('Normal (legacy)'), blurb=_('Normal (legacy) blend mode'),
                        value=Gimp.LayerMode.NORMAL_LEGACY),

                 Option('LIGHTEN_ONLY_LEGACY', _('Lighten Only (legacy)'), blurb=_('Lighten Only (legacy) blend mode'),
                        value=Gimp.LayerMode.LIGHTEN_ONLY_LEGACY),
                 Option('SCREEN_LEGACY', _('Screen (legacy)'), blurb=_('Screen (legacy) blend mode'),
                        value=Gimp.LayerMode.SCREEN_LEGACY),
                 Option('DODGE_LEGACY', _('Dodge (legacy)'), blurb=_('Dodge (legacy) blend mode'),
                        value=Gimp.LayerMode.DODGE_LEGACY),
                 Option('ADDITION_LEGACY', _('Addition (legacy)'), blurb=_('Addition (legacy) blend mode'),
                        value=Gimp.LayerMode.ADDITION_LEGACY),

                 Option('DARKEN_ONLY_LEGACY', _('Darken Only (legacy)'), blurb=_('Darken Only (legacy) blend mode'),
                        value=Gimp.LayerMode.DARKEN_ONLY_LEGACY),
                 Option('MULTIPLY_LEGACY', _('Multiply (legacy)'), blurb=_('Multiply (legacy) blend mode'),
                        value=Gimp.LayerMode.MULTIPLY_LEGACY),
                 Option('BURN_LEGACY', _('Burn (legacy)'), blurb=_('Burn (legacy) blend mode'),
                        value=Gimp.LayerMode.BURN_LEGACY),

                 Option('OVERLAY_LEGACY', _('Overlay (legacy)'), blurb=_('Overlay (legacy) blend mode'),
                        value=Gimp.LayerMode.OVERLAY_LEGACY),
                 Option('SOFTLIGHT_LEGACY', _('Soft light (legacy)'), blurb=_('Soft light (legacy) blend mode'),
                        value=Gimp.LayerMode.SOFTLIGHT_LEGACY),
                 Option('HARDLIGHT_LEGACY', _('Hard light (legacy)'), blurb=_('Hard light (legacy) blend mode'),
                        value=Gimp.LayerMode.HARDLIGHT_LEGACY),

                 Option('DIFFERENCE_LEGACY', _('Difference (legacy)'), blurb=_('Difference (legacy) blend mode'),
                        value=Gimp.LayerMode.DIFFERENCE_LEGACY),
                 Option('SUBTRACT_LEGACY', _('Subtract (legacy)'), blurb=_('Subtract (legacy) blend mode'),
                        value=Gimp.LayerMode.SUBTRACT_LEGACY),
                 Option('GRAIN_EXTRACT_LEGACY', _('Grain Extract (legacy)'),
                        blurb=_('Grain Extract (legacy) blend mode'), value=Gimp.LayerMode.GRAIN_EXTRACT_LEGACY),
                 Option('GRAIN_MERGE_LEGACY', _('Grain Merge (legacy)'), blurb=_('Grain Merge (legacy) blend mode'),
                        value=Gimp.LayerMode.GRAIN_MERGE_LEGACY),
                 Option('DIVIDE_LEGACY', _('Divide (legacy)'), blurb=_('Divide (legacy) blend mode'),
                        value=Gimp.LayerMode.DIVIDE_LEGACY),

                 Option('HSV_HUE_LEGACY', _('HSV Hue (legacy)'), blurb=_('HSV Hue (legacy) blend mode'),
                        value=Gimp.LayerMode.HSV_HUE_LEGACY),
                 Option('HSV_SATURATION_LEGACY', _('HSV Saturation (legacy)'),
                        blurb=_('HSV Saturation (legacy) blend mode'), value=Gimp.LayerMode.HSV_SATURATION_LEGACY),
                 Option('HSL_COLOR_LEGACY', _('HSL Color (legacy)'), blurb=_('HSL Color (legacy) blend mode'),
                        value=Gimp.LayerMode.HSL_COLOR_LEGACY),
                 Option('HSV_VALUE_LEGACY', _('HSV Value (legacy)'), blurb=_('HSV Value (legacy) blend mode'),
                        value=Gimp.LayerMode.HSV_VALUE_LEGACY),
             ])

positions=Choice('positions', _('Position'), _('Position of source layers'), 'under',
                 [
                    Option('over', _('Over'), _('Source over layers in current image')),
                    Option('under', _('Under'), _('Source under layers in current image')),
                ])

def copyLayers(outputImage: Gimp.Image,
               imageLayers: list[Gimp.Layer],
               sourceLayers: list[Gimp.Layer],
               position: str,
               opacity: int, mode: Gimp.LayerMode, merge: bool):

    if position=='over':
        overs=sourceLayers
        bases=imageLayers
    else:
        overs=imageLayers
        bases=sourceLayers

    Gimp.progress_init(_('Starting...'))
    count=len(bases)
    countWidth=len(str(count))
    for i,(base, over, name) in enumerate(reversed(list(zip(bases, overs, imageLayers))),1):
        Gimp.progress_set_text(_('Layer {0:{1}} of {2}').format(i,countWidth,count))
        # time.sleep(.5)
        baseCopy= Gimp.Layer.new_from_drawable(base, outputImage)
        overCopy= Gimp.Layer.new_from_drawable(over, outputImage)
        outputImage.insert_layer(baseCopy,None, 0)
        outputImage.insert_layer(overCopy,None, 0)
        overCopy.set_mode(mode)
        overCopy.set_opacity(opacity)
        baseCopy.set_visible(True)
        overCopy.set_visible(True)
        if merge:
            # Collapse groups
            if baseCopy.is_group():
                baseCopy=baseCopy.merge()
            if overCopy.is_group():
                overCopy=overCopy.merge()
            baseCopy=outputImage.merge_down(overCopy, Gimp.MergeType.EXPAND_AS_NECESSARY)
            baseCopy.set_name(name.get_name())
            baseCopy.set_visible(True)
            trace(f'Base: {baseCopy.get_name()}, visible={baseCopy.get_visible()}')
        else:
            baseCopy.set_name(base.get_name())
            overCopy.set_name(over.get_name())
        progress=i/count
        trace('Progress: {:.3f}',progress)
        Gimp.progress_update(progress)

argNames=['source','positions','opacity','merge','layerMode']
def interleaveImage(image: Gimp.Image,
                    drawables: list[Drawable],
                    config: Gimp.ProcedureConfig,
                    outputImage: Gimp.Image):

    source,position,opacity,merge,layerMode=getConfigArgs(config,argNames)
    trace(f'interleaveImage: {source=}, {position=}, {opacity=}, {merge=}, {layerMode=}')
    source=source.get_image() # Obtain image from layer

    # if image.get_height()!=source.get_height() or image.get_width()!=source.get_width():
    #     raise Exception(SIZES_MISMATCH.format(image.get_name(),image.get_width(),image.get_height(),
    #                                            source.get_name,source.get_width,source.get_height))

    # Obtain list of layers
    imageLayers=image.get_selected_layers()
    sourceLayers=source.get_selected_layers()

    imageLayersCount= len(imageLayers)
    sourceLayersCount= len(sourceLayers)
    if imageLayersCount != sourceLayersCount:
        raise Exception(LAYERS_MISMATCH.format(image.get_name(),imageLayersCount,
                                               source.get_name(),sourceLayersCount))
    copyLayers(outputImage,imageLayers,sourceLayers,position,opacity,layerModes[layerMode],merge)

def interleaveLayer(image: Gimp.Image,
                    drawables: list[Drawable],
                    config: Gimp.ProcedureConfig,
                    outputImage: Gimp.Image):
    source,position,opacity,merge,layerMode=getConfigArgs(config,argNames)
    trace(f'interleaveLayer: {source=}, {position=}, {opacity=}, {merge=}, {layerMode=}')

    # Obtain list of layers
    imageLayers=image.get_selected_layers()
    sourceLayers=[source]*len(imageLayers)
    copyLayers(outputImage,imageLayers,sourceLayers,position,opacity,layerModes[layerMode],merge)

#
# Wrapper of implementation functions. Handles final image creation
# and final image deletion if things go wrong.
#
def protect(interleave: Callable[ [Gimp.Image, list [Gimp.Drawable], Gimp.ProcedureConfig, Gimp.Image],Any]):
    def protectedInterleave(procedure: Gimp.Procedure, run_mode: Gimp.RunMode,
                            image: Gimp.Image, drawables: list[Gimp.Drawable],
                            config: Gimp.ProcedureConfig, data: Any):
        # Output image is same size, mode and precision as calling image
        outputImage=Gimp.Image.new_with_precision(image.get_width(),image.get_height(),
                                                   image.get_base_type(),image.get_precision())
        outputImage.undo_disable()
        try:
            interleave(image, drawables, config, outputImage)
            Gimp.Display.new(outputImage)
            outputImage.undo_enable()
            outputImage.clean_all()
            result = procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
        except Exception as e:
            traceback.print_exc()
            # Gimp.message(e.args[0])
            # error=GLib.Error(GLib.quark_from_string('ofn3'),1,e.args[0])
            error=GLib.Error(e.args[0])
            result=procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR,error)
        # If every went well above, the outputImage has a display and the delete will fail
        imageDeleted=outputImage.delete()
        trace(f'Deleting image ID={outputImage.get_id()}: {imageDeleted}')
        return result
    return protectedInterleave

#
# Registrations
#
layerArg=Layer('source',_('Layer'),_('Layer to copy and interleave'),noneOK=True)
imageArg=Layer('source',_('Image'),_(
    '''Source image for the layers to interleave.
    For the time being, enter any layer in the image'''),noneOK=True)
opacityArg=Int('opacity',_('Opacity'),_('Opacity of top layer in pairs'), 0, 100, 100)
mergeArg=Boolean('merge',_('Merge'),_('Merge the layer pairs'),True)
menuPath='<Image>/'+_('Image/Interleave layers')
descLayer=_('Interleaves copies of a single layer with layers of the current image')
descImage=_('Interleaves layers from another image with layers of the current image')

def beforeDialog(procedure: Gimp.ImageProcedure,
               run_mode: Gimp.RunMode,
               image: Gimp.Image,
               drawables: list[Gimp.Drawable],
               config: Gimp.ProcedureConfig, data: Any):
    source=config.get_properties('source')[0]
    trace(f'#### beforeDialog() called, {type(source)=}, {source=}')

class Ofn3Interleave(HelpedPlugin):
    procedures=[
        ProcedureDescription(
            'ofn3-interleave-layer',
            'RGB*, GRAY*',
            Gimp.ProcedureSensitivityMask.DRAWABLES,
            protect(interleaveLayer),
            args=[layerArg, positions, opacityArg, layerModes, mergeArg],
            menuLabel=_('Interleave single layer'),
            menuPath=menuPath,
            descShort=descLayer,
            descLong=descLayer,
            catchErrors=False,
        ),
        ProcedureDescription(
            'ofn3-interleave-image',
            'RGB*, GRAY*',
            Gimp.ProcedureSensitivityMask.DRAWABLES,
            protect(interleaveImage),
            args=[imageArg, positions, opacityArg, layerModes, mergeArg],
            menuLabel=_('Interleave layers from other image'),
            menuPath=menuPath,
            descShort=descImage,
            descLong=descImage,
            catchErrors=False,
        ),
    ]
    #domain=getDomain(__file__)
    # icon=getIcon(__file__)
    author='Ofnuts'
    copyright='Ofnuts'
    year='2012-2024'

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