import cv2
import numpy as np
from PIL import Image

# ==========================
# CONFIG
# ==========================
ROOM_IMAGE  = r"E:\xampp\htdocs\python\jan_jatra_improvement\Gemini_Generated_Image_rct6syrct6syrct6.png"
TILE_IMAGE  = r"E:\xampp\htdocs\python\jan_jatra_improvement\img_002.jpg"
OUTPUT      = "final_grid_engine.png"
GROUT_WIDTH = 3
GROUT_COLOR = (160, 160, 160)

# ==========================
# GLOBALS
# ==========================
original_img     = None
room_img         = None
tile_img         = None

vertical_lines   = []
horizontal_lines = []
obstacles        = []

current_points   = []
obstacle_points  = []
mouse_pos        = None
mode             = "vertical"
show_grid        = True
grout_on         = True
tile_cols        = 5     # user adjustable with +/-
tile_rows_offset = 0     # fine-tune rows with [ / ]


# ==========================
# IMAGE LOADING
# ==========================
def load_image(path):
    pil = Image.open(path).convert("RGB")
    return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)


# ==========================
# LINE MATH
# ==========================
def line_intersect(l1, l2):
    x1, y1, x2, y2 = map(float, l1)
    x3, y3, x4, y4 = map(float, l2)
    denom = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4)
    if abs(denom) < 1e-8:
        return None
    px = ((x1*y2 - y1*x2)*(x3-x4) - (x1-x2)*(x3*y4 - y3*x4)) / denom
    py = ((x1*y2 - y1*x2)*(y3-y4) - (y1-y2)*(x3*y4 - y3*x4)) / denom
    return (int(round(px)), int(round(py)))


def extend_line(line, scale=10000):
    x1, y1, x2, y2 = line
    dx, dy = x2 - x1, y2 - y1
    L = max(np.hypot(dx, dy), 1e-6)
    ux, uy = dx / L, dy / L
    return (int(x1 - ux*scale), int(y1 - uy*scale),
            int(x2 + ux*scale), int(y2 + uy*scale))


def sort_vertical(lines, img_h):
    def x_at_mid(l):
        x1, y1, x2, y2 = l
        if abs(y2 - y1) < 1e-6: return (x1 + x2) / 2
        t = (img_h / 2 - y1) / (y2 - y1)
        return x1 + t * (x2 - x1)
    return sorted(lines, key=x_at_mid)


def sort_horizontal(lines, img_w):
    def y_at_mid(l):
        x1, y1, x2, y2 = l
        if abs(x2 - x1) < 1e-6: return (y1 + y2) / 2
        t = (img_w / 2 - x1) / (x2 - x1)
        return y1 + t * (y2 - y1)
    return sorted(lines, key=y_at_mid)


# ==========================
# CORNER ORDERING (from reference code — prevents stretching)
# ==========================
def order_points_perspective(pts):
    """
    Order 4 points as: TL, TR, BR, BL
    Same method as reference code — guaranteed correct ordering.
    """
    pts = np.array(pts, dtype=np.float32)
    rect = np.zeros((4, 2), dtype=np.float32)
    s = pts.sum(axis=1)          # x + y
    d = np.diff(pts, axis=1)[:, 0]  # x - y
    rect[0] = pts[np.argmin(s)]  # top-left     (smallest x+y)
    rect[2] = pts[np.argmax(s)]  # bottom-right (largest x+y)
    rect[1] = pts[np.argmax(d)]  # top-right    (largest x-y)
    rect[3] = pts[np.argmin(d)]  # bottom-left  (smallest x-y)
    return rect


# ==========================
# GRID FROM LINES
# ==========================
def compute_line_grid():
    """Compute intersection grid from drawn lines (for visualization)."""
    h, w = original_img.shape[:2]
    v_sorted = sort_vertical(vertical_lines, h)
    h_sorted = sort_horizontal(horizontal_lines, w)
    grid = []
    for hl in h_sorted:
        row = []
        for vl in v_sorted:
            pt = line_intersect(vl, hl)
            if pt is not None:
                row.append(pt)
        grid.append(row)
    return grid


def get_outer_corners():
    """Extract and properly order the 4 outer corners from grid."""
    grid = compute_line_grid()
    if not grid or len(grid) < 2:
        return None
    n_cols = min(len(row) for row in grid)
    if n_cols < 2:
        return None

    tl = grid[0][0]
    tr = grid[0][n_cols - 1]
    br = grid[-1][n_cols - 1]
    bl = grid[-1][0]

    # KEY FIX: use reference code's ordering method
    return order_points_perspective(np.float32([tl, tr, br, bl]))


# ==========================
# TILE COUNT COMPUTATION (aspect-ratio preserving)
# ==========================
def compute_tile_counts(corners):
    """
    Auto-compute num_rows so tiles maintain their aspect ratio.
    num_cols = user-set tile_cols
    num_rows = computed from floor proportions + tile aspect ratio
    """
    h_t, w_t = tile_img.shape[:2]

    # Measure floor quad dimensions in screen space
    top_w    = np.linalg.norm(corners[1] - corners[0])
    bottom_w = np.linalg.norm(corners[2] - corners[3])
    left_h   = np.linalg.norm(corners[3] - corners[0])
    right_h  = np.linalg.norm(corners[2] - corners[1])

    avg_w = (top_w + bottom_w) / 2
    avg_h = (left_h + right_h) / 2

    num_cols = max(1, tile_cols)

    # KEY: auto-compute rows to preserve tile aspect ratio
    # We want: (num_cols * w_t) / (num_rows * h_t) ≈ avg_w / avg_h
    # → num_rows = num_cols * w_t * avg_h / (h_t * avg_w)
    num_rows = max(1, int(round(
        num_cols * (avg_h / max(avg_w, 1)) * (w_t / max(h_t, 1))
    )))

    # Apply user fine-tune offset
    num_rows = max(1, num_rows + tile_rows_offset)

    return num_cols, num_rows


# ==========================
# TILE PLACEMENT (reference code's place_grid method)
# ==========================
def apply_tiles():
    """
    Exact same approach as reference code's place_grid():
    1. Build rectangular tiled texture at NATIVE tile resolution
    2. Single perspective warp to floor quad
    3. Mask + composite
    """
    global room_img

    corners = get_outer_corners()
    if corners is None:
        print("  ⚠  Need at least 2 vertical + 2 horizontal lines!")
        return

    h, w = original_img.shape[:2]
    h_t, w_t = tile_img.shape[:2]
    result = original_img.copy()

    num_cols, num_rows = compute_tile_counts(corners)
    print(f"  Tiles: {num_cols} cols × {num_rows} rows = {num_cols * num_rows}")
    print(f"  Tile size: {w_t}×{h_t}px")

    # ── Step 1: Build tiled texture (reference code style) ──
    width_rect  = num_cols * w_t
    height_rect = num_rows * h_t

    tiled = np.zeros((height_rect, width_rect, 3), dtype=np.uint8)
    for row in range(num_rows):
        for col in range(num_cols):
            tiled[row * h_t : (row + 1) * h_t,
                  col * w_t : (col + 1) * w_t] = tile_img

    # ── Step 1b: Bake grout lines into texture ──
    if grout_on and GROUT_WIDTH > 0:
        gw = max(1, GROUT_WIDTH)
        for r in range(1, num_rows):
            y = r * h_t
            tiled[max(0, y - gw):min(height_rect, y + gw), :] = GROUT_COLOR
        for c in range(1, num_cols):
            x = c * w_t
            tiled[:, max(0, x - gw):min(width_rect, x + gw)] = GROUT_COLOR

    # ── Step 2: Perspective warp (EXACTLY like reference code) ──
    src_pts = np.float32([
        [0,          0          ],   # TL
        [width_rect, 0          ],   # TR
        [width_rect, height_rect],   # BR
        [0,          height_rect]    # BL
    ])

    M = cv2.getPerspectiveTransform(src_pts, corners)

    warped = cv2.warpPerspective(
        tiled, M, (w, h),
        flags=cv2.INTER_LANCZOS4,
        borderMode=cv2.BORDER_CONSTANT,
        borderValue=(0, 0, 0)
    )

    # ── Step 3: Mask (floor quad minus obstacles) ──
    mask = np.zeros((h, w), dtype=np.uint8)
    cv2.fillPoly(mask, [corners.astype(np.int32).reshape(-1, 1, 2)], 255)

    for obs in obstacles:
        if len(obs) >= 3:
            cv2.fillPoly(mask, [np.array(obs, np.int32).reshape(-1, 1, 2)], 0)

    # ── Step 4: Composite (same as reference) ──
    mask3 = cv2.merge([mask, mask, mask])
    room_img = np.where(mask3 == 255, warped, result).astype(np.uint8)

    print(f"  ✓  Done! No stretch — tile aspect ratio preserved.")


# ==========================
# TILE PREVIEW GRID (computed from transform matrix)
# ==========================
def compute_tile_preview():
    """
    Show where tiles will ACTUALLY go (not grid line intersections).
    Uses the perspective transform matrix to project tile boundaries.
    """
    corners = get_outer_corners()
    if corners is None:
        return None

    h_t, w_t = tile_img.shape[:2]
    num_cols, num_rows = compute_tile_counts(corners)

    width_rect  = num_cols * w_t
    height_rect = num_rows * h_t

    src_pts = np.float32([
        [0, 0], [width_rect, 0],
        [width_rect, height_rect], [0, height_rect]
    ])
    M = cv2.getPerspectiveTransform(src_pts, corners)

    # Project each tile corner through the transform
    preview = []
    for r in range(num_rows + 1):
        row = []
        for c in range(num_cols + 1):
            pt = np.float64([c * w_t, r * h_t, 1.0])
            t = M @ pt
            if abs(t[2]) > 1e-8:
                t /= t[2]
                row.append((int(t[0]), int(t[1])))
            else:
                row.append(None)
        preview.append(row)
    return preview, num_cols, num_rows


# ==========================
# MOUSE
# ==========================
def mouse_cb(event, x, y, flags, param):
    global current_points, mouse_pos, obstacle_points

    if event == cv2.EVENT_MOUSEMOVE:
        mouse_pos = (x, y)

    elif event == cv2.EVENT_LBUTTONDOWN:
        if mode in ("vertical", "horizontal"):
            current_points.append((x, y))
            if len(current_points) == 2:
                line = (*current_points[0], *current_points[1])
                if mode == "vertical":
                    vertical_lines.append(line)
                    print(f"  + V-line #{len(vertical_lines)}")
                else:
                    horizontal_lines.append(line)
                    print(f"  + H-line #{len(horizontal_lines)}")
                current_points = []
        elif mode == "obstacle":
            obstacle_points.append((x, y))

    elif event == cv2.EVENT_RBUTTONDOWN:
        current_points  = []
        obstacle_points = []


# ==========================
# OVERLAY
# ==========================
def draw_overlay():
    img  = room_img.copy()
    h, w = img.shape[:2]

    # ── V lines (green) ──
    for l in vertical_lines:
        el = extend_line(l)
        cv2.line(img, (el[0],el[1]), (el[2],el[3]), (0,140,0), 1, cv2.LINE_AA)
        cv2.line(img, (l[0],l[1]), (l[2],l[3]), (0,255,0), 2, cv2.LINE_AA)
        cv2.circle(img, (l[0],l[1]), 5, (255,255,255), -1)
        cv2.circle(img, (l[2],l[3]), 5, (255,255,255), -1)

    # ── H lines (orange) ──
    for l in horizontal_lines:
        el = extend_line(l)
        cv2.line(img, (el[0],el[1]), (el[2],el[3]), (140,60,0), 1, cv2.LINE_AA)
        cv2.line(img, (l[0],l[1]), (l[2],l[3]), (255,120,0), 2, cv2.LINE_AA)
        cv2.circle(img, (l[0],l[1]), 5, (255,255,255), -1)
        cv2.circle(img, (l[2],l[3]), 5, (255,255,255), -1)

    # ── Tile preview grid (where tiles ACTUALLY go) ──
    if show_grid and len(vertical_lines) >= 2 and len(horizontal_lines) >= 2:
        result = compute_tile_preview()
        if result:
            preview, nc, nr = result
            for i in range(len(preview) - 1):
                for j in range(len(preview[i]) - 1):
                    quad = [preview[i][j],   preview[i][j+1],
                            preview[i+1][j+1], preview[i+1][j]]
                    if all(p is not None for p in quad):
                        pts = np.array(quad, np.int32)
                        cv2.polylines(img, [pts], True,
                                      (0, 255, 255), 1, cv2.LINE_AA)

            # Corner dots
            for row in preview:
                for pt in row:
                    if pt and -50 <= pt[0] < w+50 and -50 <= pt[1] < h+50:
                        cv2.circle(img, pt, 4, (0,255,255), -1)
                        cv2.circle(img, pt, 4, (0,0,0), 1)

            # Highlight 4 outer corners
            corners = get_outer_corners()
            if corners is not None:
                for i, c in enumerate(corners):
                    ci = (int(c[0]), int(c[1]))
                    cv2.circle(img, ci, 8, (0,0,255), 2)
                    labels = ["TL","TR","BR","BL"]
                    cv2.putText(img, labels[i], (ci[0]+12, ci[1]-5),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 2)

    # ── Live line preview ──
    if len(current_points) == 1 and mouse_pos:
        c = ((0,255,0) if mode == "vertical"
             else (255,120,0) if mode == "horizontal"
             else (0,0,255))
        cv2.line(img, current_points[0], mouse_pos, c, 2, cv2.LINE_AA)
        cv2.circle(img, current_points[0], 5, (255,255,255), -1)

    # ── Finished obstacles ──
    for obs in obstacles:
        ov = img.copy()
        cv2.fillPoly(ov, [np.array(obs, np.int32)], (0,0,180))
        img = cv2.addWeighted(ov, 0.25, img, 0.75, 0)
        cv2.polylines(img, [np.array(obs, np.int32)], True,
                      (0,0,255), 2, cv2.LINE_AA)

    # ── Obstacle in progress ──
    if obstacle_points:
        for p in obstacle_points:
            cv2.circle(img, p, 4, (0,0,255), -1)
        if len(obstacle_points) > 1:
            cv2.polylines(img, [np.array(obstacle_points)],
                          False, (0,0,255), 2)
        if mouse_pos:
            cv2.line(img, obstacle_points[-1], mouse_pos,
                     (0,0,200), 1, cv2.LINE_AA)

    # ── Tile thumbnail ──
    thumb_h = 80
    th_o, tw_o = tile_img.shape[:2]
    thumb_w = int(thumb_h * tw_o / th_o)
    thumb = cv2.resize(tile_img, (thumb_w, thumb_h), interpolation=cv2.INTER_AREA)
    tx, ty = w - thumb_w - 10, 10
    if tx > 0:
        img[ty:ty+thumb_h, tx:tx+thumb_w] = thumb
        cv2.rectangle(img, (tx-1, ty-1),
                      (tx+thumb_w, ty+thumb_h), (255,255,255), 1)
        cv2.putText(img, f"{tw_o}x{th_o}", (tx, ty+thumb_h+16),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,255,255), 1)

    # ── HUD ──
    ov = img.copy()
    cv2.rectangle(ov, (0, 0), (540, 95), (30, 30, 30), -1)
    img = cv2.addWeighted(ov, 0.7, img, 0.3, 0)

    mc = {"vertical":(0,255,0), "horizontal":(255,120,0), "obstacle":(0,0,255)}
    cv2.putText(img, f"Mode: {mode.upper()}", (10, 22),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, mc[mode], 2)

    # Tile count info
    corners = get_outer_corners()
    if corners is not None:
        nc, nr = compute_tile_counts(corners)
        info = (f"V:{len(vertical_lines)}  H:{len(horizontal_lines)}  "
                f"Tiles:{nc}x{nr}={nc*nr}  "
                f"Cols(+/-):{tile_cols}  RowAdj([/]):{tile_rows_offset:+d}")
    else:
        info = f"V:{len(vertical_lines)}  H:{len(horizontal_lines)}  (need 2V + 2H)"

    cv2.putText(img, info, (10, 48),
                cv2.FONT_HERSHEY_SIMPLEX, 0.38, (220, 220, 220), 1)
    cv2.putText(img,
        f"Grid:{'ON' if show_grid else 'OFF'}  "
        f"Grout:{'ON' if grout_on else 'OFF'}  "
        f"|  ENTER=place  +/-=cols  [/]=rows",
        (10, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.38, (180, 180, 180), 1)

    return img


# ==========================
# MAIN
# ==========================
def main():
    global original_img, room_img, tile_img, mode
    global current_points, obstacle_points, obstacles
    global vertical_lines, horizontal_lines
    global show_grid, grout_on, tile_cols, tile_rows_offset

    original_img = load_image(ROOM_IMAGE)
    room_img     = original_img.copy()
    tile_img     = load_image(TILE_IMAGE)

    cv2.namedWindow("GRID ENGINE", cv2.WINDOW_NORMAL)
    cv2.resizeWindow("GRID ENGINE", 1200, 800)
    cv2.setMouseCallback("GRID ENGINE", mouse_cb)

    print("""
  ╔════════════════════════════════════════════════════╗
  ║     TILE GRID ENGINE v4  (No-Stretch Edition)      ║
  ╠════════════════════════════════════════════════════╣
  ║  V            → Vertical-line mode                 ║
  ║  H            → Horizontal-line mode               ║
  ║  O            → Obstacle mode                      ║
  ║  Click ×2     → Add line (auto)                    ║
  ║  Right-click  → Cancel current drawing             ║
  ║  C            → Close obstacle polygon             ║
  ║  Z            → Undo last action                   ║
  ║  X            → Clear all                          ║
  ║  R            → Reset image (keep lines)           ║
  ║  G            → Toggle grid preview                ║
  ║  T            → Toggle grout                       ║
  ║  + / -        → Adjust tile COLUMNS                ║
  ║  [ / ]        → Fine-tune tile ROWS                ║
  ║  ENTER        → Place tiles (no stretch!)          ║
  ║  S            → Save result                        ║
  ║  ESC          → Exit                               ║
  ╚════════════════════════════════════════════════════╝
    """)

    while True:
        display = draw_overlay()
        cv2.imshow("GRID ENGINE", display)
        key = cv2.waitKey(20) & 0xFF

        if key == 27:
            break

        elif key == ord('v'):
            mode = "vertical"; current_points = []
            print("  → Vertical-line mode")

        elif key == ord('h'):
            mode = "horizontal"; current_points = []
            print("  → Horizontal-line mode")

        elif key == ord('o'):
            mode = "obstacle"; current_points = []
            print("  → Obstacle mode  (click pts, C to close)")

        elif key == ord('c'):
            if mode == "obstacle" and len(obstacle_points) >= 3:
                obstacles.append(obstacle_points.copy())
                obstacle_points = []
                print(f"  ✓ Obstacle #{len(obstacles)} closed")

        elif key == ord('z'):
            if mode == "vertical" and vertical_lines:
                vertical_lines.pop(); print("  ↩ Undid V-line")
            elif mode == "horizontal" and horizontal_lines:
                horizontal_lines.pop(); print("  ↩ Undid H-line")
            elif mode == "obstacle":
                if obstacle_points: obstacle_points.pop()
                elif obstacles: obstacles.pop(); print("  ↩ Undid obstacle")

        elif key == ord('x'):
            vertical_lines.clear(); horizontal_lines.clear()
            obstacles.clear(); obstacle_points.clear(); current_points.clear()
            room_img = original_img.copy()
            tile_rows_offset = 0
            print("  ✗ Cleared all")

        elif key == ord('r'):
            room_img = original_img.copy()
            print("  ↻ Image reset (lines kept)")

        elif key == ord('g'):
            show_grid = not show_grid
            print(f"  Grid: {'ON' if show_grid else 'OFF'}")

        elif key == ord('t'):
            grout_on = not grout_on
            print(f"  Grout: {'ON' if grout_on else 'OFF'}")

        elif key in (ord('+'), ord('=')):
            tile_cols = min(tile_cols + 1, 50)
            print(f"  Cols: {tile_cols}")

        elif key in (ord('-'), ord('_')):
            tile_cols = max(tile_cols - 1, 1)
            print(f"  Cols: {tile_cols}")

        elif key == ord(']'):
            tile_rows_offset += 1
            print(f"  Row offset: {tile_rows_offset:+d}")

        elif key == ord('['):
            tile_rows_offset -= 1
            print(f"  Row offset: {tile_rows_offset:+d}")

        elif key == 13:  # ENTER
            print("  Placing tiles …")
            apply_tiles()

        elif key == ord('s'):
            cv2.imwrite(OUTPUT, room_img)
            print(f"  💾 Saved → {OUTPUT}")

    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()