#!/usr/bin/env python3
"""
Copyright (c) 2020, Carleton University Biomedical Informatics Collaboratory

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
"""

from typing import List

import numpy as np

from digitizer.report_components.line import Line

def compute_deviation_sum(angle: float, lines: List[Line]) -> float:
    """Given a candidate angle and a list of lines, computes the sum of the
    deviation of these lines from the horizontal or vertical axis.

    Parameters
    ----------
    angle : float
    The candidate angle in degrees.
    lines : List[Line]
    All the lines used in computing the sum of deviation.

    Returns
    -------
    float
    The sum of the deviations from all lines with the vertical or horizontal
    axis.
    """
    residual_angle_sum = 0
    for line in lines:
        if line.get_angle() > -45  and line.get_angle() < 45:
            residual = abs((line.get_angle() - angle))
            residual_angle_sum += residual
        else:
            residual = 90 - abs(line.get_angle() - angle)
            residual_angle_sum += residual
    return abs(residual_angle_sum)

def compute_rotation_angle(perpendicular_lines: List[Line]) -> float:
    """Given a list of lines, returns the angle that must be applied to
    the image so that lines that all lines that intersect another line
    at roughly a right angle (+/- some tolerance) as close to vertical
    or horizontal as possible.

    Parameters
    ----------
    perpendicular_lines : List[Line}
    The lines extracted from the image. These lines are expected to come
    from an isolated audiogram grid.

    tolerance : float
    Two lines intersecting at 90 +/- `tolerance` degrees are considered
    perpendicular.

    Returns
    -------
    float
    The correction angle that must be applied to the image so as to
    make perpendicular lines as close as possible to horizontal or vertical.
    """
    
    # Find the angle that minimizes the sum of distances to the nearest axis
    #angle.
    angle_range = np.arange(-44, 44, step=0.5)
    errors = [
        compute_deviation_sum(angle, perpendicular_lines)
        for angle in angle_range
    ]
    correction_angle = angle_range[np.argmin(errors)]

    return correction_angle

def apply_rotation(point: dict, rotation_angle: float) -> dict:
    new_x = (np.cos(rotation_angle) * point["x"]
                - np.sin(rotation_angle) * -point["y"])
    new_y = (np.sin(rotation_angle) * point["x"]
                + np.cos(rotation_angle) * -point["y"])
    return { **point, "x": new_x, "y": -new_y }


def get_bounding_box_relative_to_original_report(bounding_box, audiogram_coordinates, correction_angle):

    absolute_corrected_coordinates = {
        "x": bounding_box["x"] + audiogram_coordinates["x"],
        "y": bounding_box["y"] + audiogram_coordinates["y"]
    }

    raw_coordinates = apply_rotation(absolute_corrected_coordinates, -correction_angle)
    correction_angle_rad = np.radians(correction_angle)

    side_length = bounding_box["width"] * np.sin(correction_angle_rad) + bounding_box["width"] * np.cos(correction_angle_rad)
    if correction_angle_rad <= 0:
        return {
            "x": absolute_corrected_coordinates["x"] - bounding_box["width"] * np.sin(correction_angle_rad),
            "y": absolute_corrected_coordinates["y"],
            "width": side_length,
            "height": side_length
        }
    else:
        return {
            "x": absolute_corrected_coordinates["x"],
            "y": absolute_corrected_coordinates["y"] - bounding_box["height"] * np.sin(correction_angle_rad),
            "width": side_length,
            "height": side_length
        }