"""
This little library provides functions to add corner pin functionality
into python in general, and pyglet specifically.

Long time ago I used a set of javascript functions from Cihad Turhan (see
https://stackoverflow.com/a/30646172), which I now ported to python.
These computations might be faster with numpy, but for me these run only
incidently so won't really hurt performance.
"""

import pyglet
from typing import Optional
import logging
import copy

def adj(m): # Compute the adjugate of m
  return [
    m[4]*m[8]-m[5]*m[7], m[2]*m[7]-m[1]*m[8], m[1]*m[5]-m[2]*m[4],
    m[5]*m[6]-m[3]*m[8], m[0]*m[8]-m[2]*m[6], m[2]*m[3]-m[0]*m[5],
    m[3]*m[7]-m[4]*m[6], m[1]*m[6]-m[0]*m[7], m[0]*m[4]-m[1]*m[3]
  ]

def multmm(a, b) -> list[float]: # multiply two matrices
  c = [0.]*9
  for i in range(3):
    for j in range(3):
        cij = 0
        for k in range(3):
            cij += a[3*i + k]*b[3*k + j]
        c[3 * i + j] = cij
  return c

def multmv(m, v): #  multiply matrix and vector
  return [
    m[0]*v[0] + m[1]*v[1] + m[2]*v[2],
    m[3]*v[0] + m[4]*v[1] + m[5]*v[2],
    m[6]*v[0] + m[7]*v[1] + m[8]*v[2]
  ]

# def pdbg(m, v) :
#   r = multmv(m, v)
#   return r + " (" + r[0]/r[2] + ", " + r[1]/r[2] + ")"

def basisToPoints(x1, y1, x2, y2, x3, y3, x4, y4) :
  m = [
    x1, x2, x3,
    y1, y2, y3,
     1,  1,  1
  ]
  v = multmv(adj(m), [x4, y4, 1])
  return multmm(m, [
    v[0], 0, 0,
    0, v[1], 0,
    0, 0, v[2]
  ])

def general2DProjection(
  x1s, y1s, x1d, y1d,
  x2s, y2s, x2d, y2d,
  x3s, y3s, x3d, y3d,
  x4s, y4s, x4d, y4d
) :
  s = basisToPoints(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s)
  d = basisToPoints(x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d)
  return multmm(d, adj(s))

def transform2d_wh(w, h, x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d):
   """
   Transform the image corners based on width and height of the image
   """
   return transform2d(0, 0, x1d, y1d, w, 0, x2d, y2d, 0, h, x3d, y3d, w, h, x4d, y4d)

def transform2d(x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s, x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d):
    """
    Transform the image based on 4 arbitrarily placed pins
    """
    t = general2DProjection(x1s, y1s, x1d, y1d, x2s, y2s, x2d, y2d, x3s, y3s, x3d, y3d, x4s, y4s, x4d, y4d)
    for i in range(9):
        t[i] = t[i] / t[8]
    
    t = [t[0], t[3], 0, t[6],
       t[1], t[4], 0, t[7],
       0   , 0   , 1, 0   ,
       t[2], t[5], 0, t[8]]
    return t

# Below are all pyglet specific functions

class PygletCornerPin(pyglet.event.EventDispatcher):
    def __init__(self, window: pyglet.window.Window, corners: Optional[list[list[float]]] = None, source_points: Optional[list[list[float]]] = None) -> None:
       """
       Creates CornerPin utility for a given Pyglet window. If not given, corners default to the
       bottom left, bottom right, top left, top right corners of the window (i.e. no transform)

       Do not forget to register event handlers by running `window.register_handlers(pins)`.
       """
       self.window = window
       self.source_points = source_points if source_points else [
          [0, 0],
          [self.window.width, 0],
          [0, self.window.height],
          [self.window.width, self.window.height],
       ]
       self.pin_positions = corners if corners else copy.deepcopy(self.source_points)
       
       self.batch = pyglet.graphics.Batch()
       self.dragging: Optional[int] = None
       self.handles = [
          pyglet.shapes.Arc(c[0],c[1],20, thickness=2, color=(0,0,255,255), batch=self.batch) for c in self.pin_positions
       ]
       self.current_corner = None

      
       self.window.view = self.transform2d_matrix(*[x for c in self.source_points for x in c], *[x for c in self.pin_positions for x in c])
    
    def update_handles(self):
       for i, c in enumerate(self.pin_positions):
          self.handles[i].x = c[0]
          self.handles[i].y = c[1]
        
    def draw(self):
       """
       Draw the handles in the on_draw loop
       """
       self.batch.draw()

    def transform2d_matrix(self,x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s, x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d):
        """
        Calculate the transform matrix
        """
        t = transform2d(
            x1s, y1s, x2s, y2s, x3s, y3s, x4s, y4s, x1d, y1d, x2d, y2d, x3d, y3d, x4d, y4d)
        return pyglet.math.Mat4(t)
    
    # event handlers

    def on_mouse_press(self, x, y, button, modifiers):
        r = 20
        currentcorner = None
        dist = r * r
        for i in range(4):
           dx = x - self.pin_positions[i][0]
           dy = y - self.pin_positions[i][1]
           if dist > dx**2 + dy ** 2:
              dist = dx**2 + dy ** 2
              currentcorner = i
        
        self.dragging = currentcorner
        return pyglet.event.EVENT_HANDLED if self.dragging else pyglet.event.EVENT_UNHANDLED

    def on_mouse_release(self, x, y, button, modifiers):
       if self.dragging is None:
          return pyglet.event.EVENT_UNHANDLED
       self.dragging = None
       logging.debug(f"Corner pins set to {self.pin_positions}")
       return pyglet.event.EVENT_HANDLED

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        if self.dragging is None:
            return pyglet.event.EVENT_UNHANDLED
        
        self.pin_positions[self.dragging][0] = x
        self.pin_positions[self.dragging][1] = y
        self.update_view()

    def on_key_release(self, symbol: int, modifiers: int):
      if symbol not in [
         pyglet.window.key._1, pyglet.window.key._2, pyglet.window.key._3, pyglet.window.key._4,
         pyglet.window.key.ESCAPE,
         pyglet.window.key.UP, pyglet.window.key.DOWN, pyglet.window.key.LEFT, pyglet.window.key.RIGHT,
        ]:
         return pyglet.event.EVENT_UNHANDLED
      
      if symbol == pyglet.window.key._1:
        self.current_corner = 2
      if symbol == pyglet.window.key._2:
        self.current_corner = 3
      if symbol == pyglet.window.key._3:
        self.current_corner = 0
      if symbol == pyglet.window.key._4:
        self.current_corner = 1
      if symbol == pyglet.window.key.ESCAPE:
        self.current_corner = None

      for i, h in enumerate(self.handles):
        h.color = (255,0,0,255) if i == self.current_corner else (0,0,255,255)
      
      if self.current_corner is None:
        return
      
        
      if symbol not in [pyglet.window.key.UP,pyglet.window.key.DOWN, pyglet.window.key.LEFT, pyglet.window.key.RIGHT]:
         return
      
      base = 2
      if modifiers & pyglet.window.key.MOD_SHIFT:
         base += 10
      if modifiers & pyglet.window.key.MOD_CTRL:
         base *= 2
      
      if symbol == pyglet.window.key.RIGHT:
        self.pin_positions[self.current_corner][0] += base
      if symbol == pyglet.window.key.LEFT:
        self.pin_positions[self.current_corner][0] -= base
      if symbol == pyglet.window.key.UP:
        self.pin_positions[self.current_corner][1] += base
      if symbol == pyglet.window.key.DOWN:
        self.pin_positions[self.current_corner][1] -= base
         
      self.update_view()
    
    def update_view(self):
       self.window.view = self.transform2d_matrix(*[x for c in self.source_points for x in c], *[x for c in self.pin_positions for x in c])
