"""
Copyright (c) 2025 HIPP developers
Description: core functions for the preprocessing of KH-9 PC images
"""

import glob
import math
import os
import subprocess

import cv2
import numpy as np
import rasterio

# import pyvips
from hipp.image import apply_clahe, read_image_block_grayscale
from hipp.math import transform_coord
from hipp.tools import pick_point_from_image

####################################################################################################################################
#                                                   MAIN FUNCTIONS
####################################################################################################################################


def image_mosaic(
    image_paths: list[str],
    output_image_path: str,
    overwrite: bool = False,
    threads: int = 0,
    cleanup: bool = True,
    verbose: bool = True,
    dryrun: bool = False,
) -> None:
    """
    Mosaics a list of images into a single output image using the external 'image_mosaic' command.

    Parameters:
        image_paths (list[str]): List of paths to input image tiles.
        output_image_path (str): Path to the output mosaic image.
        overwrite (bool): If False and the output file exists, the function will skip processing. Default is False.
        threads (int): Number of threads to use for processing. Default is 0 (let the tool decide).
        cleanup (bool): Whether to remove temporary log and auxiliary files after processing. Default is True.
        verbose (bool): If True, prints detailed progress and command information. Default is True.
        dryrun (bool): If True, builds the command but does not execute it. Useful for debugging. Default is False.

    Returns:
        None
    """
    # Skip processing if the output exists and overwriting is disabled
    if os.path.exists(output_image_path) and not overwrite:
        if verbose:
            print(f"Skipping {output_image_path}: output already exists")
        return

    if verbose:
        print(f"\nMosaicking {output_image_path} with {len(image_paths)} tiles...\n")

    # Build the command for the external 'image_mosaic' tool
    cmd = [
        "image_mosaic",
        *image_paths,
        "--ot",
        "byte",
        "--overlap-width",
        "3000",
        "--threads",
        str(threads),
        "-o",
        output_image_path,
    ]

    # Display the constructed command
    if verbose:
        print(" ".join(cmd))

    # Execute the command unless in dry run mode
    if not dryrun:
        try:
            subprocess.run(
                cmd,
                check=True,
                stdout=None if verbose else subprocess.DEVNULL,
                stderr=None if verbose else subprocess.DEVNULL,
            )
        except subprocess.CalledProcessError as e:
            print(f"Error while processing {output_image_path}: {e}")

    # Optionally remove temporary log and auxiliary files generated by the tool
    if cleanup:
        for f in glob.glob(f"{output_image_path}-log-image_mosaic-*.txt") + glob.glob(f"{output_image_path}.aux.xml"):
            os.remove(f)


def pick_points_in_corners(
    image_path: str, grid_shape: tuple[int, int] = (5, 20), clahe_enhancement: bool = True, destroy_window: bool = True
) -> dict[str, tuple[int, int]] | None:
    corners_indices = {
        "top_left": (0, 0),
        "top_right": (0, grid_shape[1] - 1),
        "bottom_right": (grid_shape[0] - 1, grid_shape[1] - 1),
        "bottom_left": (grid_shape[0] - 1, 0),
    }
    result = {}
    window_name = "Corner Point Picker"

    for key, indices in corners_indices.items():
        image_bloc, (offset_x, offset_y) = read_image_block_grayscale(image_path, *indices, grid_shape)

        if clahe_enhancement:
            image_bloc = apply_clahe(image_bloc)

        window_title = f"[{os.path.basename(image_path)}] Select {key} with Ctrl + Click"
        point = pick_point_from_image(image_bloc, window_name, window_title)
        if point is None:
            return None
        result[key] = (point[0] + offset_x, point[1] + offset_y)

    if destroy_window:
        cv2.destroyWindow(window_name)
    return result


def compute_cropping_matrix(
    input_path: str, points: list[tuple[int, int]]
) -> tuple[cv2.typing.MatLike, tuple[int, int]]:
    """
    Compute a transformation matrix to rotate and crop an image around two reference points.

    The function computes the affine transformation matrix that rotates the image so that the line
    defined by the first two input points becomes horizontal. Then it computes the minimal bounding box
    of the transformed points and applies a translation to crop the rotated image to this region.

    Args:
        input_path (str): Path to the input image.
        points (list[tuple[float, float]]): List of at least two (x, y) points used to compute the rotation.

    Returns:
        tuple:
            - final_matrix (np.ndarray): 3x3 affine transformation matrix (rotation + translation).
            - (out_width, out_height): Size of the cropped output image.
    """
    # Calculate angle in degrees between two points relative to horizontal axis
    angle = angle_from_points(*points[0], *points[1])

    # Open the input image to get its dimensions
    with rasterio.open(input_path) as src:
        img_width = src.width
        img_height = src.height

    # Create a rotation matrix around the center of the image
    rotation_matrix = np.vstack([cv2.getRotationMatrix2D((img_width // 2, img_height // 2), angle, 1), [0, 0, 1]])

    # Rotate the input points using the rotation matrix
    rotated_points = [transform_coord(coord, rotation_matrix) for coord in points]

    # Compute the bounding box of the rotated points
    left, top, out_width, out_height = bounding_rect(rotated_points)

    # Create a translation matrix to crop the image to the bounding box
    translation_matrix = np.array([[1, 0, -left], [0, 1, -top], [0, 0, 1]])

    # Combine translation and rotation matrices
    final_matrix = translation_matrix @ rotation_matrix
    return final_matrix, (out_width, out_height)


####################################################################################################################################
#                                                   PRIVATE FUNCTIONS
####################################################################################################################################


def angle_from_points(x1: float, y1: float, x2: float, y2: float) -> float:
    """Calculate angle in degrees between two points relative to horizontal axis"""
    dx = x2 - x1
    dy = y2 - y1
    angle_rad = math.atan2(dy, dx)
    return math.degrees(angle_rad)


def bounding_rect(points: list[tuple[float, float]]) -> tuple[int, int, int, int]:
    """Get bounding rectangle (left, top, width, height) from list of (x,y) points"""
    xs = [p[0] for p in points]
    ys = [p[1] for p in points]
    left = int(min(xs))
    top = int(min(ys))
    width = int(max(xs)) - left
    height = int(max(ys)) - top
    return left, top, width, height
