#
# histogram utilities file
#
#
# author: Christos Choutouridis <cchoutou@ece.auth.gr>
# date:   29/04/2025
#

try:
    import numpy as np
except ImportError as e:
    print("Missing package: ", e)
    print("Run: pip install -r requirements.txt to install.")
    exit(1)


def calculate_hist_of_img(
        img_array: np.ndarray,
        return_normalized: bool
) -> dict:
    """
    Calculate the histogram of an input image array.

    Args:
        img_array (np.ndarray):   2D grayscale image array with float values in [0, 1].
        return_normalized (bool): If True, return normalized histogram (probabilities),
                                  otherwise return absolute counts.

    Returns:
        dict: A dictionary mapping each unique input level to its count or probability.
    """
    # Flatten the 2D image into a 1D array for easier processing
    flat_img = img_array.flatten()

    # Create an empty dictionary to store the histogram
    hist = {}

    # Traverse each pixel value
    for value in flat_img:
        # Increment the count for the corresponding pixel value
        if value in hist:
            hist[value] += 1
        else:
            hist[value] = 1

    if return_normalized:
        total_pixels = flat_img.size
        for key in hist:
            hist[key] /= total_pixels

    return hist



def apply_hist_modification_transform(
        img_array: np.ndarray,
        modification_transform: dict
) -> np.ndarray:
    """
    Apply a histogram modification transform to an image.

    Args:
        img_array (np.ndarray):        2D grayscale image array with float values.
        modification_transform (dict): Dictionary mapping each input level to an output level.

    Returns:
        np.ndarray: The modified image array.
    """
    # Create a copy of the input image to avoid modifying it in-place
    modified_img = np.copy(img_array)

    # Get the unique levels in the image
    unique_levels = np.unique(modified_img)

    # Apply the transform
    for level in unique_levels:
        if level in modification_transform:
            modified_img[modified_img == level] = modification_transform[level]
        else:
            # Optional: raise an error or warning if level is missing
            raise ValueError(f"Input level {level} not found in modification_transform.")

    return modified_img









#
# =========================== Functional tests ===========================
#
# To execute the tests run:
#   python hist_utils.py
#

def test_calculate_hist_counts():
    """
    Test calculate_hist_of_img with return_normalized = False.
    """
    # Create a dummy 3x3 image
    img = np.array([
        [0.0, 0.5, 1.0],
        [0.5, 0.5, 0.0],
        [1.0, 0.0, 1.0]
    ])

    # Call our function
    my_hist = calculate_hist_of_img(img, return_normalized=False)

    # Use numpy to verify
    values, counts = np.unique(img, return_counts=True)
    np_hist = dict(zip(values, counts))

    # Compare
    print("Test case: Counts")
    #print("Our histogram:", my_hist)
    #print("Numpy histogram:", np_hist)
    print("Test passed:", my_hist == np_hist)
    print()


def test_calculate_hist_normalized():
    """
    Test calculate_hist_of_img with return_normalized = True.
    """
    # Create a dummy 3x3 image
    img = np.array([
        [0.0, 0.5, 1.0],
        [0.5, 0.5, 0.0],
        [1.0, 0.0, 1.0]
    ])

    # Call our function
    my_hist = calculate_hist_of_img(img, return_normalized=True)

    # Use numpy to verify (manual normalization)
    values, counts = np.unique(img, return_counts=True)
    total_pixels = img.size
    np_hist = dict(zip(values, counts / total_pixels))

    # Compare
    print("Test case: Normalized")
    #print("Our histogram:", my_hist)
    #print("Numpy histogram:", np_hist)

    # Because of floating point division, use np.isclose instead of exact equality
    test_passed = True
    for key in my_hist:
        if not np.isclose(my_hist[key], np_hist[key]):
            test_passed = False
            break

    print("Test passed:", test_passed)
    print()



def test_apply_hist_modification_identity():
    """
    Test apply_hist_modification_transform with identity transform (no changes).
    """
    # Create a simple 3x3 image
    img = np.array([
        [0.0, 0.5, 1.0],
        [0.5, 0.5, 0.0],
        [1.0, 0.0, 1.0]
    ])

    # Define an identity transform (each value maps to itself)
    mod_transform = {
        0.0: 0.0,
        0.5: 0.5,
        1.0: 1.0
    }

    # Apply our function
    modified_img = apply_hist_modification_transform(img, mod_transform)

    # Compare
    print("Test case: Identity transform")
    # print("Modified image:\n", modified_img)
    # print("Original image:\n", img)
    print("Test passed:", np.allclose(modified_img, img))
    print()


def test_apply_hist_modification_simple():
    """
    Test apply_hist_modification_transform with a simple transform.
    """
    # Create a simple 3x3 image
    img = np.array([
        [0.0, 0.5, 1.0],
        [0.5, 0.5, 0.0],
        [1.0, 0.0, 1.0]
    ])

    # Define a simple modification transform
    mod_transform = {
        0.0: 0.2,
        0.5: 0.7,
        1.0: 1.0
    }

    # Expected result
    expected_img = np.array([
        [0.2, 0.7, 1.0],
        [0.7, 0.7, 0.2],
        [1.0, 0.2, 1.0]
    ])

    # Apply our function
    modified_img = apply_hist_modification_transform(img, mod_transform)

    # Compare
    print("Test case: Simple transform")
    # print("Modified image:\n", modified_img)
    # print("Expected image:\n", expected_img)
    print("Test passed:", np.allclose(modified_img, expected_img))
    print()


if __name__ == '__main__':
    test_calculate_hist_counts()
    test_calculate_hist_normalized()
    test_apply_hist_modification_identity()
    test_apply_hist_modification_simple()
