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

# Re-align ane or more layers over another layer

# (c) Ofnuts 2016-2024
#
#   History:
#
#   v0.0: 2016-07-11: Initial version
#   v1.0: 2024-12-xx: Convert 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
#   MERCHANDABILITY 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 script is a Python rewrite of Volker Trier's exact-aligner.scm, 
# from which some good ideas have been shamelessly borrowed

import gi

gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GLib
import sys,math
from dataclasses import dataclass
from gimphelpers import *

def _(msg): return GLib.dgettext(None, msg)

@dataclass
class PointPair:
    x1: float
    y1: float
    x2: float
    y2: float

    @classmethod
    # Extract anchors from two consecutive "flat" triplets
    def fromStride(cls, coords: list[float]):
        return cls(coords[2], coords[3], coords[8], coords[9])

    @classmethod
    def fromStroke(cls, path: Gimp.Path, stroke: int):
        __, points, __ = path.stroke_get_points(stroke)
        if len(points) < 12:
            raise Exception(_('Not enough points in stroke #{0} of active path').format(stroke + 1))
        return cls(points[2], points[3], points[8], points[9])

    def heading(self) -> float:
        return math.atan2(self.x2 - self.x1, self.y2 - self.y1)

    def distance(self) -> float:
        return math.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2)


def alignLayer(origin: PointPair, adjust: PointPair, layer: Gimp.Layer):
    scale = origin.distance() / adjust.distance()
    angle = adjust.heading() - origin.heading()

    layer.transform_2d(adjust.x1, adjust.y1, scale, scale, angle, origin.x1, origin.y1)


def getPositions(paths: list[Gimp.Path]) -> [PointPair, list[PointPair]]:
    if len(paths) == 0:
        raise Exception(_('No active path fou'
                          'nd in image'))
    elif len(paths) == 1:
        trace('Found one path')
        path = paths[0]
        strokes = path.get_strokes()
        if len(strokes) == 0:
            raise Exception(_('No strokes in active path'))
        elif len(strokes) == 1:  # One stroke: one anchor pair per layer
            trace('Found one stroke in path')
            __, points, __ = path.stroke_get_points(strokes[0])
            if len(points) < 12:
                raise Exception(_('Not enough points in active path stroke'))
            origin = PointPair.fromStride(points[0:12])
            positions = [PointPair.fromStride(points[i:i + 12]) for i in range(12, len(points), 12)]
        else:  # More than one stroke: one stroke/layer
            trace(f'Found {len(strokes)} strokes in path')
            origin = PointPair.fromStroke(path, strokes[0])
            positions = [PointPair.fromStroke(path, s) for s in strokes[1:]]
    else:
        raise Exception(_('Using multiple paths is not supported (yet)'))

    return origin, positions


def alignLayers(procedure: Gimp.ImageProcedure,
                run_mode: Gimp.RunMode,
                image: Gimp.Image,
                drawables: list[Gimp.Drawable],
                config: Gimp.ProcedureConfig,
                data: Any) -> Gimp.ValueArray:
    Gimp.context_set_transform_direction(Gimp.TransformDirection.FORWARD)
    Gimp.Selection.none(image)

    layers = drawables  # TODO: filter later!!!!
    origin, positions = getPositions(image.get_selected_paths())

    if len(positions) != len(layers):
        raise Exception(
            _('Number of layers ({0}) does not match the number of transformations ({1})').format(
                len(layers), len(positions))
        )

    for layer, position in zip(reversed(layers), positions):
        trace(f'Aligning {origin=}, {position=}, {layer.get_name()}')
        alignLayer(origin, position, layer)

    return successReturn(procedure)

### Registration
class AlignLayers(HelpedPlugin):
    author='Ofnuts'
    copyright='Ofnuts'
    year='2016-2024'
    procedures = [
        ProcedureDescription(
            'ofn3-align-layers',
            '*', Gimp.ProcedureSensitivityMask.DRAWABLE|Gimp.ProcedureSensitivityMask.DRAWABLES,
            alignLayers,
            menuLabel=_('Align layers to reference'),
            menuPath='<Image>/'+_('Layer'),
            descShort=_('Align layers to a reference layer, using path points'),
            descLong=_('''
            Align layers to reference, using path points.
            For each layer a pair of path points is placed over the same image points
            in all the layers, plus a reference layer. The plugin then rotates, scales
            and translates each layer so that the image points overlap the same points  
            in the reference layer.
            ''')
        ),
    ]

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