###################################################
# ArtiLED Python Color Library v1.03 (22-02-2026) #
# Written by: Michel van Osenbruggen              #
# Copyright 2026 ArtiLED B.V. All Rights Reserved #
###################################################

import numpy as np
import colorsys
from collections import Counter

#Get Average Color
def average_color(img_arr):
    avg_rgb = np.mean(img_arr, axis=(0, 1)).astype(np.uint8)
    red, green, blue = avg_rgb
    return red, green, blue

#Get Dominant Color
def dominant_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    unique_rgb, counts = np.unique(flattened_arr, axis=0, return_counts=True)
    dominant_index = np.argmax(counts)
    dominant_rgb = unique_rgb[dominant_index]
    red, green, blue = dominant_rgb
    return red, green, blue

#Get Brightest Color
def brightest_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    brightness = np.mean(flattened_arr, axis=1)
    brightest_index = np.argmax(brightness)
    brightest_rgb = flattened_arr[brightest_index]
    red, green, blue = brightest_rgb
    return red, green, blue

#Get Darkest Color
def darkest_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    brightness = np.mean(flattened_arr, axis=1)
    darkest_index = np.argmin(brightness)
    darkest_rgb = flattened_arr[darkest_index]
    red, green, blue = darkest_rgb
    return red, green, blue

#Get Most Common Color
def most_common_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    color_counts = Counter(map(tuple, flattened_arr))
    most_common_color = color_counts.most_common(1)[0][0]
    red, green, blue = most_common_color
    return red, green, blue

#Get Most Saturated Color
def most_saturated_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    saturation = np.max(flattened_arr, axis=1) - np.min(flattened_arr, axis=1)
    saturation = saturation / (np.max(flattened_arr, axis=1) + np.finfo(float).eps)
    most_saturated_index = np.argmax(saturation)
    most_saturated_rgb = flattened_arr[most_saturated_index]
    red, green, blue = most_saturated_rgb
    return red, green, blue

#Get Least Saturated Color
def least_saturated_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    saturation = np.max(flattened_arr, axis=1) - np.min(flattened_arr, axis=1)
    saturation = saturation / (np.max(flattened_arr, axis=1) + np.finfo(float).eps)
    least_saturated_index = np.argmin(saturation)
    least_saturated_rgb = flattened_arr[least_saturated_index]
    red, green, blue = least_saturated_rgb
    return red, green, blue

#Get Most Contrasted Color
def most_contrasted_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    brightness = np.mean(flattened_arr, axis=1)
    normalized_brightness = brightness / (np.max(brightness) + np.finfo(float).eps)
    mean_brightness = np.mean(normalized_brightness)
    contrast = np.abs(normalized_brightness - mean_brightness)
    most_contrasted_index = np.argmax(contrast)
    most_contrasted_rgb = flattened_arr[most_contrasted_index]
    red, green, blue = most_contrasted_rgb
    return red, green, blue

#Get Least Contrasted Color
def least_contrasted_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    brightness = np.mean(flattened_arr, axis=1)
    normalized_brightness = brightness / (np.max(brightness) + np.finfo(float).eps)
    mean_brightness = np.mean(normalized_brightness)
    contrast = np.abs(normalized_brightness - mean_brightness)
    least_contrasted_index = np.argmin(contrast)
    least_contrasted_rgb = flattened_arr[least_contrasted_index]
    red, green, blue = least_contrasted_rgb
    return red, green, blue

#Get Median Color
def median_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    flattened_arr = flattened_arr[np.lexsort(flattened_arr.T)]
    median_index = flattened_arr.shape[0] // 2
    median_rgb = flattened_arr[median_index]     
    red, green, blue = median_rgb
    return red, green, blue
    
#Get Mode Color
def mode_color(img_arr):
    flattened_arr = img_arr.reshape(-1, 3)
    unique_rgb, counts = np.unique(flattened_arr, axis=0, return_counts=True)
    mode_index = np.argmax(counts)
    mode_rgb = unique_rgb[mode_index]
    red, green, blue = mode_rgb
    return red, green, blue

#Gamma Correct Single Color
def gamma_correct(value, gamma):
    if gamma <= 0:
        gamma = 0.1
    return int(255 * ((value / 255) ** gamma))

#Gamma Correct an Array of Colors
def gamma(input_array, gamma_r, gamma_g, gamma_b):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)
    
    # Apply gamma correction on each channel
    rgb_array[..., 0] = 255 * (rgb_array[..., 0] / 255) ** gamma_r
    rgb_array[..., 1] = 255 * (rgb_array[..., 1] / 255) ** gamma_g
    rgb_array[..., 2] = 255 * (rgb_array[..., 2] / 255) ** gamma_b
    
    # Clip the values to be in the range [0, 255] and convert to integers
    gamma_corrected_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
    
    # If the input was a list, convert back to a list
    if is_list:
        gamma_corrected_array = gamma_corrected_array.tolist()
    return gamma_corrected_array

#White Balance Adjustment on an Array of Colors
def white_balance(input_array, white_point):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)
    
    # Calculate scale factors for each channel based on the white point
    scale_factors = 255 / (np.array(white_point, dtype=np.float32) + np.finfo(float).eps)
    
    # Apply scale factors to each channel
    rgb_array[..., 0] *= scale_factors[0]
    rgb_array[..., 1] *= scale_factors[1]
    rgb_array[..., 2] *= scale_factors[2]
    
    # Clip the values to be in the range [0, 255] and convert to integers
    white_balanced_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
    
    # If the input was a list, convert back to a list
    if is_list:
        white_balanced_array = white_balanced_array.tolist()
    return white_balanced_array

#Contrast Adjustment on an Array of Colors
def contrast(input_array, contrast_factor):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)
    
    # Adjust contrast
    rgb_array = 128 + contrast_factor * (rgb_array - 128)
    
    # Clip the values to be in the range [0, 255] and convert to integers
    contrast_adjusted_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
    
    # If the input was a list, convert back to a list
    if is_list:
        contrast_adjusted_array = contrast_adjusted_array.tolist()
    return contrast_adjusted_array

#Saturation Adjustment on an Array of Colors
def saturation(input_array, saturation_factor):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32) / 255.0  # Normalize to 0-1
    
    # Prepare an output array
    output_array = np.zeros_like(rgb_array)
    
    # Iterate over the input array and adjust saturation
    it = np.nditer(rgb_array[..., 0], flags=['multi_index'])
    while not it.finished:
        idx = it.multi_index
        # Convert RGB to HSV
        hsv = colorsys.rgb_to_hsv(*rgb_array[idx])
        # Adjust saturation
        new_s = min(max(hsv[1] * saturation_factor, 0), 1)  # Ensure new saturation is within [0, 1]
        # Convert back to RGB and store in output array
        new_rgb = colorsys.hsv_to_rgb(hsv[0], new_s, hsv[2])
        output_array[idx] = new_rgb
        it.iternext()
    
    # Scale back to 0-255 and clip
    output_array = np.clip(output_array * 255.0, 0, 255).astype(np.uint8)
    
    # If the input was a list, convert back to a list
    if is_list:
        output_array = output_array.tolist()
    return output_array

#Color Balance Adjustment on an Array of Colors
def color_balance(input_array, red_factor, green_factor, blue_factor):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)
    
    # Apply scale factors to each channel
    rgb_array[..., 0] *= red_factor
    rgb_array[..., 1] *= green_factor
    rgb_array[..., 2] *= blue_factor
    
    # Clip the values to be in the range [0, 255] and convert to integers
    color_balanced_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
    
    # If the input was a list, convert back to a list
    if is_list:
        color_balanced_array = color_balanced_array.tolist()
    return color_balanced_array

#Hue Adjustment on an Array of Colors
def adjust_hue(input_array, hue_shift):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32) / 255.0  # Normalize to 0-1 for colorsys
    
    # Prepare an output array
    output_array = np.zeros_like(rgb_array)
    
    # Iterate over the input array and adjust hue
    it = np.nditer(rgb_array[..., 0], flags=['multi_index'])
    while not it.finished:
        idx = it.multi_index
        # Convert RGB to HSV
        hsv = colorsys.rgb_to_hsv(*rgb_array[idx])
        # Adjust hue and ensure it wraps around properly
        new_h = (hsv[0] + hue_shift / 360.0) % 1.0
        # Convert back to RGB and store in output array
        new_rgb = colorsys.hsv_to_rgb(new_h, hsv[1], hsv[2])
        output_array[idx] = new_rgb
        it.iternext()

    # Scale back to 0-255 and clip
    output_array = np.clip(output_array * 255.0, 0, 255).astype(np.uint8)

    # If the input was a list, convert back to a list
    if is_list:
        output_array = output_array.tolist()
    return output_array

#Brightness Adjustment on an Array of Colors
def brightness(input_array, brightness_value):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)

    # Add brightness value to all channels
    rgb_array += brightness_value

    # Clip the values to be in the range [0, 255] and convert to integers
    brightness_adjusted_array = np.clip(rgb_array, 0, 255).astype(np.uint8)

    # If the input was a list, convert back to a list
    if is_list:
        brightness_adjusted_array = brightness_adjusted_array.tolist()
    return brightness_adjusted_array

#Color Temperature Adjustment on an Array of Colors
def color_temperature(input_array, temperature):
    # Convert the input to a numpy array for easier manipulation
    is_list = isinstance(input_array, list)
    rgb_array = np.array(input_array, dtype=np.float32)

    # Apply temperature shift: positive = warmer (more red, less blue), negative = cooler (more blue, less red)
    rgb_array[..., 0] *= 1 + (temperature / 100)
    rgb_array[..., 2] *= 1 - (temperature / 100)

    # Clip the values to be in the range [0, 255] and convert to integers
    temperature_adjusted_array = np.clip(rgb_array, 0, 255).astype(np.uint8)

    # If the input was a list, convert back to a list
    if is_list:
        temperature_adjusted_array = temperature_adjusted_array.tolist()
    return temperature_adjusted_array

#Blend Two Arrays of Colors
def blend(input_array_1, input_array_2, factor):
    # Convert the inputs to numpy arrays for easier manipulation
    is_list = isinstance(input_array_1, list)
    rgb_array_1 = np.array(input_array_1, dtype=np.float32)
    rgb_array_2 = np.array(input_array_2, dtype=np.float32)

    # Linear interpolation between the two arrays
    rgb_array = rgb_array_1 * (1 - factor) + rgb_array_2 * factor

    # Clip the values to be in the range [0, 255] and convert to integers
    blended_array = np.clip(rgb_array, 0, 255).astype(np.uint8)

    # If the input was a list, convert back to a list
    if is_list:
        blended_array = blended_array.tolist()
    return blended_array
