from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
from matplotlib.tri import Triangulation

from image_preparing import fix_image_resolution


def prepare_image(path):
    pil = Image.open(path).convert("RGB").resize((800, 600))
    return pil
def auto_correspondences_grid(pil, nx=8, ny=6):
    w, h = pil.size
    xs = np.linspace(0, w - 1, nx)
    ys = np.linspace(0, h - 1, ny)
    pts = []
    for y in ys:
        for x in xs:
            pts.append([float(x), float(y)])
    corners = [[0.0, 0.0], [w - 1.0, 0.0], [w - 1.0, h - 1.0], [0.0, h - 1.0]]
    for c in corners:
        if c not in pts:
            pts.insert(0, c)
    return np.array(pts, dtype=float), np.array(pts, dtype=float).copy()
def compute_delaunay_on_points(pts):
    pts = np.asarray(pts)
    tri = Triangulation(pts[:, 0], pts[:, 1])
    return tri.triangles
def _affine_coeffs(from_tri, to_tri):
    A = []
    B = []
    for (x_dst, y_dst), (x_src, y_src) in zip(from_tri, to_tri):
        A.append([x_dst, y_dst, 1, 0, 0, 0])
        A.append([0, 0, 0, x_dst, y_dst, 1])
        B.append(x_src)
        B.append(y_src)
    sol, *_ = np.linalg.lstsq(np.array(A), np.array(B), rcond=None)
    return tuple(sol.tolist())
def _warp_triangle(src_pil, src_tri, dst_tri, out_size, offset):
    dx, dy = offset
    dst_local = np.array(dst_tri) - np.array([dx, dy])
    coeffs = _affine_coeffs(dst_local, src_tri)
    return src_pil.transform(out_size, Image.AFFINE, coeffs, resample=Image.BILINEAR)
def reconstruct_goal_from_source(src_pil, pts1, pts2):
    w, h = src_pil.size
    pts1 = np.asarray(pts1)
    pts2 = np.asarray(pts2)
    triangles = compute_delaunay_on_points(pts2)
    out = Image.new("RGB", (w, h))
    for tri in triangles:
        tri = np.asarray(tri, dtype=int)
        src_tri = pts1[tri]
        dst_tri = pts2[tri]
        min_x = max(int(np.floor(dst_tri[:, 0].min())), 0)
        max_x = min(int(np.ceil(dst_tri[:, 0].max())), w)
        min_y = max(int(np.floor(dst_tri[:, 1].min())), 0)
        max_y = min(int(np.ceil(dst_tri[:, 1].max())), h)
        if max_x <= min_x or max_y <= min_y:
            continue
        out_w = max_x - min_x
        out_h = max_y - min_y
        warped = _warp_triangle(src_pil, src_tri, dst_tri, (out_w, out_h), (min_x, min_y))
        mask = Image.new("L", (out_w, out_h), 0)
        draw = ImageDraw.Draw(mask)
        tri_local = [(x - min_x, y - min_y) for x, y in dst_tri.tolist()]
        draw.polygon(tri_local, fill=255)
        out.paste(warped, (min_x, min_y), mask)
    return out
def transform_images(src_path, goal_path):
    fix_image_resolution(src_path)
    fix_image_resolution(goal_path)
    src = prepare_image(src_path)
    goal = prepare_image(goal_path)
    frames = tile_reposition_transition(src, goal, grid=(40, 30), n_frames=24)
    fig, ax = plt.subplots(figsize=(8, 6))
    im = ax.imshow(frames[0])
    ax.axis("off")
    for frame in frames:
        im.set_data(frame)
        fig.canvas.draw_idle()
        plt.pause(0.05)
    plt.show()
def tile_reposition_transition(src_pil, goal_pil, grid=(40, 30), n_frames=24):
    
    src = src_pil.convert("RGBA")
    goal = goal_pil.convert("RGBA")
    w, h = src.size
    nx, ny = grid

    def make_tiles(pil):
        tiles = []
        for iy in range(ny):
            y0 = int(round(iy * h / ny))
            y1 = int(round((iy + 1) * h / ny))
            for ix in range(nx):
                x0 = int(round(ix * w / nx))
                x1 = int(round((ix + 1) * w / nx))
                box = (x0, y0, x1, y1)
                crop = pil.crop(box).convert("RGB")
                arr = np.array(crop, dtype=float)
                avg = arr.reshape(-1, 3).mean(axis=0)
                center = ((x0 + x1) / 2.0, (y0 + y1) / 2.0)
                tiles.append({"box": box, "img": crop, "avg": avg, "center": center})
        return tiles

    src_tiles = make_tiles(src)
    goal_tiles = make_tiles(goal)

    max_spatial = np.hypot(w, h)
    src_available = set(range(len(src_tiles)))
    mapping = {} 
    for g_idx, g in enumerate(goal_tiles):
        best = None
        best_cost = None
        for s_idx in list(src_available):
            s = src_tiles[s_idx]
            color_dist = np.linalg.norm(g["avg"] - s["avg"]) / (255.0 * np.sqrt(3))
            spatial_dist = np.linalg.norm(np.array(g["center"]) - np.array(s["center"])) / max_spatial
            cost = color_dist + 0.35 * spatial_dist
            if best_cost is None or cost < best_cost:
                best_cost = cost
                best = s_idx
        if best is None:
            best = src_available.pop()
        else:
            src_available.remove(best)
        mapping[best] = g_idx

    targets = {}
    for s_idx, g_idx in mapping.items():
        s = src_tiles[s_idx]
        g = goal_tiles[g_idx]
        targets[s_idx] = {"start": (s["box"][0], s["box"][1]), "end": (g["box"][0], g["box"][1]), "img": s["img"], "size": (s["box"][2]-s["box"][0], s["box"][3]-s["box"][1])}

    frames = []
    for k in range(n_frames + 1):
        t = k / float(n_frames)
        frame = Image.new("RGBA", (w, h), (0, 0, 0, 255))
        for s_idx, info in targets.items():
            sx, sy = info["start"]
            ex, ey = info["end"]
            cx = int(round(sx + (ex - sx) * t))
            cy = int(round(sy + (ey - sy) * t))
            frame.paste(info["img"], (cx, cy))
        frames.append(frame.convert("RGB"))

    final = Image.new("RGB", (w, h))
    for s_idx, g_idx in mapping.items():
        src_img = src_tiles[s_idx]["img"].convert("RGB")
        x0, y0, x1, y1 = goal_tiles[g_idx]["box"]
        final.paste(src_img, (x0, y0))
    frames[-1] = final
    return frames


