import numpy as np

import torch
from torch.nn import functional as F

from torch_lydorn.torch.utils.complex import complex_mul, complex_sqrt, complex_abs_squared


def framefield_align_error(c0, c2, z, complex_dim=-1):
    assert c0.shape == c2.shape == z.shape, \
        "All inputs should have the same shape. Currently c0: {}, c2: {}, z: {}".format(c0.shape, c2.shape, z.shape)
    assert c0.shape[complex_dim] == c2.shape[complex_dim] == z.shape[complex_dim] == 2, \
        "All inputs should have their complex_dim size equal 2 (real and imag parts)"

    z_squared = complex_mul(z, z, complex_dim=complex_dim)
    z_pow_4 = complex_mul(z_squared, z_squared, complex_dim=complex_dim)
    # All tensors are assimilated as being complex so adding that way works (adding a scalar wouldn't work):
    f_z = z_pow_4 + complex_mul(c2, z_squared, complex_dim=complex_dim) + c0
    loss = complex_abs_squared(f_z, complex_dim)  # Square of the absolute value of f_z
    return loss


class LaplacianPenalty:
    def __init__(self, channels: int):
        self.channels = channels
        self.filter = torch.tensor([[0.5, 1.0, 0.5],
                                    [1.0, -6., 1.0],
                                    [0.5, 1.0, 0.5]]) / 12
        self.filter = self.filter[None, None, ...].expand(self.channels, -1, -1, -1)

    def laplacian_filter(self, tensor):
        # with torch.autograd.profiler.profile(use_cuda=True) as prof:
        penalty_tensor = F.conv2d(tensor, self.filter.to(tensor.device), padding=1,
                                  groups=self.channels)
        # print("penalty_tensor min={}, max={}".format(penalty_tensor.min(), penalty_tensor.max()))
        # print(prof.key_averages().table(sort_by="cuda_time_total"))
        return torch.abs(penalty_tensor)

    def __call__(self, tensor: torch.Tensor) -> torch.Tensor:
        return self.laplacian_filter(tensor)


def c0c2_to_uv(c0c2: torch.Tensor) -> torch.Tensor:
    c0, c2 = torch.chunk(c0c2, 2, dim=1)
    c2_squared = complex_mul(c2, c2, complex_dim=1)
    c2_squared_minus_4c0 = c2_squared - 4 * c0
    sqrt_c2_squared_minus_4c0 = complex_sqrt(c2_squared_minus_4c0, complex_dim=1)
    u_squared = (c2 + sqrt_c2_squared_minus_4c0) / 2
    v_squared = (c2 - sqrt_c2_squared_minus_4c0) / 2
    uv_squared = torch.stack([u_squared, v_squared], dim=1)  # Shape (B, 'uv': 2, 'complex': 2, H, W)
    uv = complex_sqrt(uv_squared, complex_dim=2)
    return uv


def compute_closest_in_uv(directions: torch.Tensor, uv: torch.Tensor) -> torch.Tensor:
    """
    For each direction, compute if it is more aligned with {u, -u} (output 0) or {v, -v} (output 1).

    @param directions: Tensor of shape (N, 2)
    @param uv: Tensor of shape (N, 'uv': 2, 'complex': 2)
    @return: closest_in_uv of shape (N,) with the index in the 'uv' dimension of the closest vector in uv to direction
    """
    uv_dot_dir = torch.sum(uv * directions[:, None, :], dim=2)
    abs_uv_dot_dir = torch.abs(uv_dot_dir)

    closest_in_uv = torch.argmin(abs_uv_dot_dir, dim=1)

    return closest_in_uv


def detect_corners(polylines, u, v):
    def compute_direction_score(ij, edges, field_dir):
        values = field_dir[ij[:, 0], ij[:, 1]]
        edge_dot_dir = edges[:, 0] * values.real + edges[:, 1] * values.imag
        abs_edge_dot_dir = np.abs(edge_dot_dir)
        return abs_edge_dot_dir

    def compute_is_corner(points, left_edges, right_edges):
        if points.shape[0] == 0:
            return np.empty(0, dtype=np.bool)

        coords = np.round(points).astype(np.int)
        coords[:, 0] = np.clip(coords[:, 0], 0, u.shape[0] - 1)
        coords[:, 1] = np.clip(coords[:, 1], 0, u.shape[1] - 1)
        left_u_score = compute_direction_score(coords, left_edges, u)
        left_v_score = compute_direction_score(coords, left_edges, v)
        right_u_score = compute_direction_score(coords, right_edges, u)
        right_v_score = compute_direction_score(coords, right_edges, v)

        left_is_u_aligned = left_v_score < left_u_score
        right_is_u_aligned = right_v_score < right_u_score

        return np.logical_xor(left_is_u_aligned, right_is_u_aligned)

    corner_masks = []
    for polyline in polylines:
        corner_mask = np.zeros(polyline.shape[0], dtype=np.bool)
        if np.max(np.abs(polyline[0] - polyline[-1])) < 1e-6:
            # Closed polyline
            left_edges = np.concatenate([polyline[-2:-1] - polyline[-1:], polyline[:-2] - polyline[1:-1]], axis=0)
            right_edges = polyline[1:] - polyline[:-1]
            corner_mask[:-1] = compute_is_corner(polyline[:-1, :], left_edges, right_edges)
            # left_edges and right_edges do not include the redundant last vertex, thus we have to do this assignment:
            corner_mask[-1] = corner_mask[0]
        else:
            # Open polyline
            corner_mask[0] = True
            corner_mask[-1] = True
            left_edges = polyline[:-2] - polyline[1:-1]
            right_edges = polyline[2:] - polyline[1:-1]
            corner_mask[1:-1] = compute_is_corner(polyline[1:-1, :], left_edges, right_edges)
        corner_masks.append(corner_mask)

    return corner_masks
