# utils/image_utils.py
import os
from io import BytesIO
import cairosvg
import base64
import numpy as np
#from decimal import ROUND_CEILING
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageOps, ImageMath
from typing import List, Union, is_typeddict
#import numpy as np
#import math
from pathlib import Path
from utils.constants import default_lut_example_img, PRE_RENDERED_MAPS_JSON_LEVELS
from utils.color_utils import (
    detect_color_format,
    update_color_opacity
)
from utils.file_utils import rename_file_to_lowercase_extension, get_file_parts




def save_image_to_temp_png(image_source, user_dir: str = None, file_name: str = None):
    """
    Opens an image from a file path, URL, or DataURL and saves it as a PNG in the user's temporary directory.
        
    Parameters:
        image_source (str, dict or PIL.Image.Image): The source of the image to open.
        
    Returns:
        str: The file path of the saved PNG image in the temporary directory.
    """
    import tempfile
    import uuid

    # Open the image using the existing utility function
    img = open_image(image_source)
        
    # Ensure the image is in a format that supports PNG (convert if necessary)
    if img.mode not in ("RGB", "RGBA"):
        img = img.convert("RGBA")
        
    # Generate a unique filename in the system temporary directory
    if user_dir is None:
        user_dir = tempfile.gettempdir()

    if file_name is None:
        file_name = "{uuid.uuid4()}"

    temp_filepath = os.path.join(user_dir, file_name.lower() + ".png")
    os.makedirs(user_dir, exist_ok=True)

    # Save the image as PNG
    img.save(temp_filepath, format="PNG")
        
    return temp_filepath

def get_image_from_dict(image_path):
    if isinstance(image_path, dict) :
        if 'composite' in image_path:
            image_path = image_path.get('composite')
        elif 'image' in image_path:
            image_path = image_path.get('image')
        else:
            print("\n Unknown image dictionary.\n")
            raise UserWarning("Unknown image dictionary.")
        return image_path, True
    else:
        return image_path, False

def open_image(image_path):
    """
    Opens an image from a file path or URL, or decodes a DataURL string into an image.
    Supports SVG and ICO by converting them to PNG.
    
    Parameters:
        image_path (str): The file path, URL, or DataURL string of the image to open.
    
    Returns:
        Image: A PIL Image object of the opened image.
    
    Raises:
        Exception: If there is an error opening the image.
    """

    if isinstance(image_path, Image.Image):
        return image_path
    elif isinstance(image_path, dict):
        image_path, is_dict = get_image_from_dict(image_path)

    image_path = rename_file_to_lowercase_extension(image_path)

    import requests
    try:
        # Strip leading and trailing double quotation marks, if present
        image_path = image_path.strip('"')
        if image_path.startswith('http'):
            response = requests.get(image_path)
            if image_path.lower().endswith('.svg'):
                png_data = cairosvg.svg2png(bytestring=response.content)
                img = Image.open(BytesIO(png_data))
            elif image_path.lower().endswith('.ico'):
                img = Image.open(BytesIO(response.content)).convert('RGBA')
            else:
                img = Image.open(BytesIO(response.content))
        elif image_path.startswith('data'):
            encoded_data = image_path.split(',')[1]
            decoded_data = base64.b64decode(encoded_data)
            if image_path.lower().endswith('.svg'):
                png_data = cairosvg.svg2png(bytestring=decoded_data)
                img = Image.open(BytesIO(png_data))
            elif image_path.lower().endswith('.ico'):
                img = Image.open(BytesIO(decoded_data)).convert('RGBA')
            else:
                img = Image.open(BytesIO(decoded_data))
        else:
            if image_path.lower().endswith('.svg'):
                png_data = cairosvg.svg2png(url=image_path)
                img = Image.open(BytesIO(png_data))
            elif image_path.lower().endswith('.ico'):
                img = Image.open(image_path).convert('RGBA')
            else:
                img = Image.open(image_path)
    except Exception as e:
        raise Exception(f'Error opening image: {e}')
    return img

def build_prerendered_images(images_list):
    """
    Opens a list of images from file paths, URLs, or DataURL strings.

    Parameters:
        images_list (list): A list of file paths, URLs, or DataURL strings of the images to open.

    Returns:
        list: A list of PIL Image objects of the opened images.
    """
    return [open_image(image) for image in images_list]

# Example usage
# filtered_maps = get_maps_with_quality_less_than(3)
# print(filtered_maps)
def build_prerendered_images_by_quality(quality_limit, key='file'):
    """
    Retrieve and sort file paths from PRE_RENDERED_MAPS_JSON_LEVELS where quality is <= quality_limit.
    Sorts by quality and case-insensitive alphanumeric key.

    Args:
        quality_limit (int): Maximum quality threshold
        key (str): Key to extract file path from map info (default: 'file')

    Returns:
        tuple: (sorted file paths list, list of corresponding map names)
    """
    # Pre-compute lowercase alphanumeric key once per item
    def get_sort_key(item):
        name, info = item
        return (info['quality'], ''.join(c for c in name.lower() if c.isalnum()))

    # Single pass: sort and filter
    filtered_maps = [
        (info[key].replace("\\", "/"), name)
        for name, info in sorted(PRE_RENDERED_MAPS_JSON_LEVELS.items(), key=get_sort_key)
        if info['quality'] <= quality_limit
    ]
    
    # Split into separate lists efficiently
    if filtered_maps:
        #file_paths, map_names = zip(*filtered_maps)
        #return (build_prerendered_images(file_paths), list(map_names))
        return [(open_image(file_path), map_name) for file_path, map_name in filtered_maps]
    return (None,"")


def build_encoded_images(images_list):
    """
    Encodes a list of images to base64 strings.

    Parameters:
        images_list (list): A list of file paths, URLs, DataURL strings, or PIL Image objects of the images to encode.

    Returns:
        list: A list of base64-encoded strings of the images.
    """
    return [image_to_base64(image) for image in images_list]

def image_to_base64(image):
    """
    Encodes an image to a base64 string.
    Supports ICO files by converting them to PNG with RGBA channels.
    
    Parameters:
        image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to encode.
    
    Returns:
        str: A base64-encoded string of the image.
    """
    buffered = BytesIO()
    if isinstance(image, str):
        image = open_image(image)
    image.save(buffered, format="PNG")
    return "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode()

def change_color(image, color, opacity=0.75):
    """
    Changes the color of an image by overlaying it with a specified color and opacity.

    Parameters:
        image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to change.
        color (str or tuple): The color to overlay on the image.
        opacity (float): The opacity of the overlay color (0.0 to 1.0).

    Returns:
        PIL.Image.Image: The image with the color changed.
    """
    if type(image) is str:
        image = open_image(image)
    try:
        # Convert the color to RGBA format
        rgba_color = detect_color_format(color)
        rgba_color = update_color_opacity(rgba_color, opacity)
    
        # Convert the image to RGBA mode
        image = image.convert("RGBA")
    
        # Create a new image with the same size and mode
        new_image = Image.new("RGBA", image.size, rgba_color)
    
        # Composite the new image with the original image
        result = Image.alpha_composite(image, new_image)
    except Exception as e:
        print(f"Error changing color: {e}")
        return image
    return result

def convert_str_to_int_or_zero(value):
    """
    Converts a string to an integer, or returns zero if the conversion fails.

    Parameters:
        value (str): The string to convert.

    Returns:
        int: The converted integer, or zero if the conversion fails.
    """
    try:
        return int(value)
    except ValueError:
        return 0

def upscale_image(image, scale_factor):
    """
    Upscales an image by a given scale factor using the LANCZOS filter.

    Parameters:
        image (PIL.Image.Image): The input image to be upscaled.
        scale_factor (float): The factor by which to upscale the image.

    Returns:
        PIL.Image.Image: The upscaled image.
    """
    # Calculate the new size
    new_width = int(image.width * scale_factor)
    new_height = int(image.height * scale_factor)
    
    # Upscale the image using the LANCZOS filter
    upscaled_image = image.resize((new_width, new_height), Image.LANCZOS)
    
    return upscaled_image

def crop_and_resize_image(image, width, height):
    """
    Crops the image to a centered square and resizes it to the specified width and height.

    Parameters:
        image (PIL.Image.Image): The input image to be cropped and resized.
        width (int): The desired width of the output image.
        height (int): The desired height of the output image.

    Returns:
        PIL.Image.Image: The cropped and resized image.
    """
    # Get original dimensions
    original_width, original_height = image.size

    # Determine the smaller dimension to make a square crop
    min_dim = min(original_width, original_height)
    
    # Calculate coordinates for cropping to a centered square
    left = (original_width - min_dim) // 2
    top = (original_height - min_dim) // 2
    right = left + min_dim
    bottom = top + min_dim

    # Crop the image
    cropped_image = image.crop((left, top, right, bottom))
    
    # Resize the image to the desired dimensions
    resized_image = cropped_image.resize((width, height), Image.LANCZOS)
    
    return resized_image

def resize_image_with_aspect_ratio(image, target_width, target_height):
    """
    Resizes the image to fit within the target dimensions while maintaining aspect ratio.
    If the aspect ratio does not match, the image will be padded with black pixels.

    Parameters:
        image (PIL.Image.Image): The input image to be resized.
        target_width (int): The target width.
        target_height (int): The target height.

    Returns:
        PIL.Image.Image: The resized image.
    """
    # Calculate aspect ratios
    original_width, original_height = image.size
    target_aspect = target_width / target_height
    original_aspect = original_width / original_height
    #print(f"Original size: {image.size}\ntarget_aspect: {target_aspect}\noriginal_aspect: {original_aspect}\n")
    # Decide whether to fit width or height
    if original_aspect > target_aspect:
        # Image is wider than target aspect ratio
        new_width = target_width
        new_height = int(target_width / original_aspect)
    else:
        # Image is taller than target aspect ratio
        new_height = target_height
        new_width = int(target_height * original_aspect)

    # Resize the image
    resized_image = image.resize((new_width, new_height), Image.LANCZOS)
    #print(f"Resized size: {resized_image.size}\n")

    # Create a new image with target dimensions and black background
    new_image = Image.new("RGB", (target_width, target_height), (0, 0, 0))
    # Paste the resized image onto the center of the new image
    paste_x = (target_width - new_width) // 2
    paste_y = (target_height - new_height) // 2
    new_image.paste(resized_image, (paste_x, paste_y))

    return new_image

def lerp_imagemath(img1, img2, alpha_percent: int = 50):
    """
    Performs linear interpolation (LERP) between two images based on the given alpha value.

    Parameters:
        img1 (str or PIL.Image.Image): The first image or its file path.
        img2 (str or PIL.Image.Image): The second image or its file path.
        alpha (int): The interpolation factor (0 to 100).

    Returns:
        PIL.Image.Image: The interpolated image.
    """
    if isinstance(img1, str):
        img1 = open_image(img1)
    if isinstance(img2, str):
        img2 = open_image(img2)

    # Ensure both images are in the same mode (e.g., RGBA)
    img1 = img1.convert('RGBA')
    img2 = img2.convert('RGBA')

    # Convert images to NumPy arrays
    arr1 = np.array(img1, dtype=np.float32)
    arr2 = np.array(img2, dtype=np.float32)

    # Perform linear interpolation
    alpha = alpha_percent / 100.0
    result_arr = (arr1 * (1 - alpha)) + (arr2 * alpha)

    # Convert the result back to a PIL image
    result_img = Image.fromarray(np.uint8(result_arr))

    #result_img.show()
    return result_img

def shrink_and_paste_on_blank(current_image, mask_width, mask_height, blank_color:tuple[int, int, int, int] = (0,0,0,0)):
    """
    Decreases size of current_image by mask_width pixels from each side,
    then adds a mask_width width transparent frame,
    so that the image the function returns is the same size as the input.

    Parameters:
        current_image (PIL.Image.Image): The input image to transform.
        mask_width (int): Width in pixels to shrink from each side.
        mask_height (int): Height in pixels to shrink from each side.
        blank_color (tuple): The color of the blank frame (default is transparent).

    Returns:
        PIL.Image.Image: The transformed image.
    """
    # calculate new dimensions
    width, height = current_image.size
    new_width = width - (2 * mask_width)
    new_height = height - (2 * mask_height)

    # resize and paste onto blank image
    prev_image = current_image.resize((new_width, new_height))
    blank_image = Image.new("RGBA", (width, height), blank_color)
    blank_image.paste(prev_image, (mask_width, mask_height))

    return blank_image

def multiply_and_blend_images(base_image, image2, alpha_percent=50):
    """
    Multiplies two images and blends the result with the original image.

    Parameters:
        image1 (PIL.Image.Image): The first input image.
        image2 (PIL.Image.Image): The second input image.
        alpha (float): The blend factor (0.0 to 100.0) for blending the multiplied result with the original image.

    Returns:
        PIL.Image.Image: The blended image.
    """
    name = None
    directory = None
    alpha = alpha_percent / 100.0
    if isinstance(base_image, str):
        directory, _, name,_,_ = get_file_parts(base_image)
        base_image = open_image(base_image)
    if isinstance(image2, str):
        image2 = open_image(image2)
    # Ensure both images are in the same mode and size
    base_image = base_image.convert('RGBA')
    image2 = image2.convert('RGBA')
    image2 = image2.resize(base_image.size)

    # Multiply the images
    multiplied_image = ImageChops.multiply(base_image, image2)

    # Blend the multiplied result with the original
    blended_image = Image.blend(base_image, multiplied_image, alpha)
    if name is not None:
        new_image_path = os.path.join(directory, name + f"_mb{str(alpha_percent)}.png")
        blended_image.save(new_image_path)
        return new_image_path
    return blended_image

def alpha_composite_with_control(base_image, image_with_alpha, alpha_percent=100):
    """
    Overlays image_with_alpha onto base_image with controlled alpha transparency.

    Parameters:
        base_image (PIL.Image.Image): The base image.
        image_with_alpha (PIL.Image.Image): The image to overlay with an alpha channel.
        alpha_percent (float): The multiplier for the alpha channel (0.0 to 100.0).

    Returns:
        PIL.Image.Image: The resulting image after alpha compositing.
    """
    name = None
    directory = None
    image_with_alpha, isdict = get_image_from_dict(image_with_alpha)
    alpha_multiplier = alpha_percent / 100.0
    if isinstance(base_image, str):
        directory, _, name,_, new_ext = get_file_parts(base_image)
        base_image = open_image(base_image)
    if isinstance(image_with_alpha, str):
        image_with_alpha = open_image(image_with_alpha)

    # Ensure both images are in RGBA mode
    base_image = base_image.convert('RGBA')
    image_with_alpha = image_with_alpha.convert('RGBA')

    # Extract the alpha channel and multiply by alpha_multiplier
    alpha_channel = image_with_alpha.split()[3]
    alpha_channel = alpha_channel.point(lambda p: p * alpha_multiplier)

    # Apply the modified alpha channel back to the image
    image_with_alpha.putalpha(alpha_channel)

    # Composite the images
    result = Image.alpha_composite(base_image, image_with_alpha)
    if name is not None:
        new_image_path = os.path.join(directory, name + f"_alpha{str(alpha_percent)}.png")
        result.save(new_image_path)
        return new_image_path
    return blended_image
    return result

def apply_alpha_mask(image, mask_image, invert = False):
    """
    Applies a mask image as the alpha channel of the input image.

    Parameters:
        image (PIL.Image.Image): The image to apply the mask to.
        mask_image (PIL.Image.Image): The alpha mask to apply.
        invert (bool): Whether to invert the mask (default is False).

    Returns:
        PIL.Image.Image: The image with the applied alpha mask.
    """
    # Resize the mask to match the current image size
    mask_image = resize_and_crop_image(mask_image, image.width, image.height).convert('L') # convert to grayscale
    if invert:
        mask_image = ImageOps.invert(mask_image)
    # Apply the mask as the alpha layer of the current image
    result_image = image.copy() 
    result_image.putalpha(mask_image) 
    return result_image

def resize_and_crop_image(image: Image, new_width: int = 512, new_height: int = 512) -> Image:
    """
    Resizes and crops an image to a specified width and height. This ensures that the entire new_width and new_height 
    dimensions are filled by the image, and the aspect ratio is maintained.

    Parameters:
        image (PIL.Image.Image): The image to be resized and cropped.
        new_width (int): The desired width of the new image (default is 512).
        new_height (int): The desired height of the new image (default is 512).

    Returns:
        PIL.Image.Image: The resized and cropped image.
    """
    # Get the dimensions of the original image
    orig_width, orig_height = image.size
    # Calculate the aspect ratios of the original and new images
    orig_aspect_ratio = orig_width / float(orig_height)
    new_aspect_ratio = new_width / float(new_height)
    # Calculate the new size of the image while maintaining aspect ratio
    if orig_aspect_ratio > new_aspect_ratio:
        # The original image is wider than the new image, so we need to crop the sides
        resized_width = int(new_height * orig_aspect_ratio)
        resized_height = new_height
        left_offset = (resized_width - new_width) // 2
        top_offset = 0
    else:
        # The original image is taller than the new image, so we need to crop the top and bottom
        resized_width = new_width
        resized_height = int(new_width / orig_aspect_ratio)
        left_offset = 0
        top_offset = (resized_height - new_height) // 2
    # Resize the image with Lanczos resampling filter
    resized_image = image.resize((resized_width, resized_height), resample=Image.Resampling.LANCZOS)
    # Crop the image to fill the entire height and width of the new image
    cropped_image = resized_image.crop((left_offset, top_offset, left_offset + new_width, top_offset + new_height))
    return cropped_image

##################################################### LUTs ############################################################

def is_3dlut_row(row: List[str]) -> bool:
    """
    Check if one line in the file has exactly 3 numeric values.

    Parameters:
        row (list): A list of strings representing the values in a row.

    Returns:
        bool: True if the row has exactly 3 numeric values, False otherwise.
    """
    try:
        row_values = [float(val) for val in row]
        return len(row_values) == 3
    except ValueError:
        return False

def get_lut_type(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> str:

    with open(path_lut) as f:
        lines = f.read().splitlines()

    lut_type = "3D"  # Initially assume 3D LUT
    size = None
    table = []

    # Parse the file
    for line in lines:
        line = line.strip()
        if line.startswith("#") or not line:
            continue  # Skip comments and empty lines
        parts = line.split()
        if parts[0] == "LUT_3D_SIZE":
            size = int(parts[1])
            lut_type = "3D"
        elif parts[0] == "LUT_1D_SIZE":
            size = int(parts[1])
            lut_type = "1D"
        elif is_3dlut_row(parts):
            table.append(tuple(float(val) for val in parts))
    return lut_type


def read_3d_lut(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> ImageFilter.Color3DLUT:
    """
    Read LUT from a raw file.

    Each line in the file is considered part of the LUT table. The function
    reads the file, parses the rows, and constructs a Color3DLUT object.

    Args:
        path_lut: A string or os.PathLike object representing the path to the LUT file.
        num_channels: An integer specifying the number of color channels in the LUT (default is 3).

    Returns:
        An instance of ImageFilter.Color3DLUT representing the LUT.

    Raises:
        FileNotFoundError: If the LUT file specified by path_lut does not exist.
    """
    with open(path_lut) as f:
        lut_raw = f.read().splitlines()
    size = round(len(lut_raw) ** (1 / 3))
    row2val = lambda row: tuple([float(val) for val in row.split(" ")])
    lut_table = [row2val(row) for row in lut_raw if is_3dlut_row(row.split(" "))]
    return ImageFilter.Color3DLUT(size, lut_table, num_channels)

def apply_1d_lut(image, lut_file):
    # Read the 1D LUT
    with open(lut_file) as f:
        lines = f.read().splitlines()
    table = []
    for line in lines:
        if not line.startswith(("#", "LUT", "TITLE", "DOMAIN")) and line.strip():
            values = [float(v) for v in line.split()]
            table.append(tuple(values))
    
    # Convert image to grayscale
    if image.mode != 'L':
        image = image.convert('L')
    img_array = np.array(image) / 255.0  # Normalize to [0, 1]
    
    # Map grayscale values to colors
    lut_size = len(table)
    indices = (img_array * (lut_size - 1)).astype(int)
    colors = np.array(table)[indices]
    
    # Create RGB image
    rgb_image = Image.fromarray((colors * 255).astype(np.uint8), mode='RGB')
    return rgb_image

def apply_3d_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None) -> Image:
    """
    Apply a LUT to an image and return a PIL Image with the LUT applied.

    The function applies the LUT to the input image using the filter() method of the PIL Image class.

    Args:
        img: A PIL Image object to which the LUT should be applied.
        lut_path: A string representing the path to the LUT file (optional if lut argument is provided).
        lut: An instance of ImageFilter.Color3DLUT representing the LUT (optional if lut_path is provided).

    Returns:
        A PIL Image object with the LUT applied.

    Raises:
        ValueError: If both lut_path and lut arguments are not provided.
    """
    if lut is None:
        if lut_path == "":
            raise ValueError("Either lut_path or lut argument must be provided.")
        lut = read_3d_lut(lut_path)
    return img.filter(lut)

def apply_lut(image, lut_filename: str) -> Image:
    """
    Apply a LUT to an image and return the result.
    Args:
        image (str or PIL.Image.Image): The image to apply the LUT to.
        lut_filename (str): The path to the LUT file.
    Returns:
        PIL.Image.Image: The image with the LUT applied.
    """
    if isinstance(image, str):
        image = open_image(image)
    if lut_filename is not None:
        if (get_lut_type(lut_filename) == "3D"):
            lut = read_3d_lut(lut_filename)
            image =  apply_3d_lut(image, lut=lut)
        else:
            image = apply_1d_lut(image, lut_filename)
    return image

def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img) -> Image:
    if lut_filename is not None:
        try:
            lut_example_image = apply_lut(lut_example_image, lut_filename)
        except Exception as e:
            print(f"BAD LUT: Error applying LUT {str(e)}.")
    else:
        lut_example_image = open_image(default_lut_example_img)
    return lut_example_image

def apply_1d_lut(image, lut_file):
    # Read the 1D LUT
    with open(lut_file) as f:
        lines = f.read().splitlines()
    table = []
    for line in lines:
        if not line.startswith(("#", "LUT", "TITLE", "DOMAIN")) and line.strip():
            values = [float(v) for v in line.split()]
            table.append(tuple(values))
    
    # Convert image to grayscale
    if image.mode != 'L':
        image = image.convert('L')
    img_array = np.array(image) / 255.0  # Normalize to [0, 1]
    
    # Map grayscale values to colors
    lut_size = len(table)
    indices = (img_array * (lut_size - 1)).astype(int)
    colors = np.array(table)[indices]
    
    # Create RGB image
    rgb_image = Image.fromarray((colors * 255).astype(np.uint8), mode='RGB')
    return rgb_image

def apply_lut_to_image_path(lut_filename: str, image_path: str) -> tuple[Image, str]:
    """
    Apply a LUT to an image and return the result.
    Supports ICO files by converting them to PNG with RGBA channels.
    
    Args:
        lut_filename: A string representing the path to the LUT file.
        image_path: A string representing the path to the input image.
    
    Returns:
        tuple: A tuple containing the PIL Image object with the LUT applied and the new image path as a string.
    """
    import gradio as gr

    img_lut = None
    if image_path is None:
        raise UserWarning("No image provided.")
        return None, None

    # Split the path into directory and filename
    directory, file_name = os.path.split(image_path)
    lut_directory, lut_file_name = os.path.split(lut_filename)
    
    # Split the filename into name and extension
    name, ext = os.path.splitext(file_name)
    lut_name, lut_ext = os.path.splitext(lut_file_name)

    # Convert the extension to lowercase
    new_ext = ext.lower()

    path = Path(image_path)
    img = open_image(image_path)
    if not ((path.suffix.lower() == '.png' and img.mode == 'RGBA')):
        if image_path.lower().endswith(('.jpg', '.jpeg')):
            img, new_image_path = convert_jpg_to_rgba(path)
        elif image_path.lower().endswith('.ico'):
            img, new_image_path = convert_to_rgba_png(image_path)
        elif image_path.lower().endswith(('.gif', '.webp')):
            img, new_image_path = convert_to_rgba_png(image_path)
        else:
            img, new_image_path = convert_to_rgba_png(image_path)
        if image_path != new_image_path:
            delete_image(image_path)
    else:
        # ensure the file extension is lower_case, otherwise leave as is
        new_filename = name + new_ext
        new_image_path = os.path.join(directory, new_filename)
    # Apply the LUT to the image
    if (lut_filename is not None and img is not None):
        try:
            img_lut = apply_lut(img, lut_filename)
        except Exception as e:
            print(f"BAD LUT: Error applying LUT {str(e)}.")
        if img_lut is not None:
            new_filename = name + "_"+ lut_name + new_ext
            new_image_path = os.path.join(directory, new_filename)
            #delete_image(image_path) - renamed with lut name
            img = img_lut
    img.save(new_image_path, format='PNG')
    print(f"Image with LUT saved as {new_image_path}")
    return img, gr.update(value=str(new_image_path))

############################################# RGBA ###########################################################
def convert_rgb_to_rgba_safe(image: Image) -> Image:
    """
    Converts an RGB image to RGBA by adding an alpha channel.
    Ensures that the original image remains unaltered.

    Parameters:
        image (PIL.Image.Image): The RGB image to convert.

    Returns:
        PIL.Image.Image: The converted RGBA image.
    """
    if image.mode != 'RGB':
        if image.mode == 'RGBA':
            return image
        elif image.mode == 'P':
            # Convert palette image to RGBA
            image = image.convert('RGB')
        else:
            raise ValueError("Unsupported image mode for conversion to RGBA.")
    # Create a copy of the image to avoid modifying the original
    rgba_image = image.copy()
    # Optionally, set a default alpha value (e.g., fully opaque)
    alpha = Image.new('L', rgba_image.size, 255)  # 255 for full opacity
    rgba_image.putalpha(alpha)
    return rgba_image

# Example usage
# convert_jpg_to_rgba('input.jpg', 'output.png')
def convert_jpg_to_rgba(input_path) -> tuple[Image, str]:
    """
    Convert a JPG image to RGBA format and save it as a PNG.

    Args:
    input_path (str or Path): Path to the input JPG image file.

    Raises:
    FileNotFoundError: If the input file does not exist.
    ValueError: If the input file is not a JPG.
    OSError: If there's an error reading or writing the file.

    Returns:
    tuple: A tuple containing the RGBA image and the output path as a string.
    """
    try:
        # Convert input_path to Path object if it's a string
        input_path = Path(input_path)
        output_path = input_path.with_suffix('.png')

        # Check if the input file exists
        if not input_path.exists():
            #if file was renamed to lower case, update the input path
            input_path = output_path
            if not input_path.exists():
                raise FileNotFoundError(f"The file {input_path} does not exist.")
        
        # Check file extension first to skip unnecessary processing
        if input_path.suffix.lower() not in ('.jpg', '.jpeg'):
            print(f"Skipping conversion: {input_path} is not a JPG or JPEG file.")
            return None, None
        
        print(f"Converting to PNG: {input_path} is a JPG or JPEG file.")
        
        # Open the image file
        with Image.open(input_path) as img:
            # Convert the image to RGBA mode
            rgba_img = img.convert('RGBA')
            
            # Ensure the directory exists for the output file
            output_path.parent.mkdir(parents=True, exist_ok=True)
            
            # Save the image with RGBA mode as PNG
            rgba_img.save(output_path)
    
    except FileNotFoundError as e:
        print(f"Error: {e}")
    except ValueError as e:
        print(f"Error: {e}")
    except OSError as e:
        print(f"Error: An OS error occurred while processing the image - {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")
    return rgba_img, str(output_path)


def convert_to_rgba_png(file_path: str) -> tuple[Image, str]:
    """
    Converts an image to RGBA PNG format and saves it with the same base name and a .png extension.
    Supports ICO files.
    
    Args:
        file_path (str): The path to the input image file.
    
    Returns:
        tuple: A tuple containing the RGBA image and the new file path as a string.
    """
    new_file_path = None
    rgba_img = None
    img = None
    if file_path is None:
        raise UserWarning("No image provided.")
        return None, None
    try:
        file_path, is_dict = get_image_from_dict(file_path)
        img = open_image(file_path)
        print(f"Opened image: {file_path}\n")
        # Handle ICO files
        if file_path.lower().endswith(('.ico','.webp','.gif')):
            rgba_img = img.convert('RGBA')
            new_file_path = Path(file_path).with_suffix('.png')
            rgba_img.save(new_file_path, format='PNG')
            print(f"Converted ICO to PNG: {new_file_path}")
        else:
            rgba_img, new_file_path = convert_jpg_to_rgba(file_path)
            if rgba_img is None:
                rgba_img = convert_rgb_to_rgba_safe(img)
                new_file_path = Path(file_path).with_suffix('.png')
                rgba_img.save(new_file_path, format='PNG')
                print(f"Image saved as {new_file_path}")
    except ValueError as ve:
        print(f"ValueError: {ve}")
    except Exception as e:
        print(f"Error converting image: {e}")
    return rgba_img if rgba_img else img, str(new_file_path)

def delete_image(file_path: str) -> None:
    """
    Deletes the specified image file.

    Parameters:
        file_path (str): The path to the image file to delete.

    Raises:
        FileNotFoundError: If the file does not exist.
        Exception: If there is an error deleting the file.
    """
    try:
        path = Path(file_path)
        path.unlink()
        print(f"Deleted original image: {file_path}")
    except FileNotFoundError:
        print(f"File not found: {file_path}")
    except Exception as e:
        print(f"Error deleting image: {e}")


def resize_all_images_in_folder(target_width: int, output_folder: str = "resized", file_prefix: str = "resized_") -> tuple[int, int]:
    """
    Resizes all images in the current folder to a specified width while maintaining aspect ratio.
    Creates a new folder for the resized images.
    
    Parameters:
        target_width (int): The desired width for all images
        output_folder (str): Name of the folder to store resized images (default: "resized")
        file_prefix (str): Prefix for resized files (default: "resized_")
        
    Returns:
        tuple[int, int]: (number of successfully resized images, number of failed attempts)

    Example Usage: 
        successful_count, failed_count = resize_all_images_in_folder(target_width=800, output_folder="th", file_prefix="th_")
    """
    # Supported image extensions
    valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff')    
    # Create output folder if it doesn't exist
    output_path = Path(output_folder)
    output_path.mkdir(exist_ok=True)    
    successful = 0
    failed = 0    
    # Get current directory
    current_dir = Path.cwd()    
    # Iterate through all files in current directory
    for file_path in current_dir.iterdir():
        if file_path.is_file() and file_path.suffix.lower() in valid_extensions:
            try:
                # Open the image
                with Image.open(file_path) as img:
                    # Convert to RGB if needed (handles RGBA, CMYK, etc.)
                    if img.mode != 'RGB':
                        img = img.convert('RGB')
                    # Calculate target height maintaining aspect ratio
                    original_width, original_height = img.size
                    aspect_ratio = original_height / original_width
                    target_height = int(target_width * aspect_ratio)
                    # Resize using the reference function
                    resized_img = resize_image_with_aspect_ratio(img, target_width, target_height)
                    # Create output filename
                    output_filename = output_path / f"{file_prefix}{file_path.name.lower()}"
                    # Save the resized image
                    resized_img.save(output_filename, quality=95)
                successful += 1
                print(f"Successfully resized: {file_path.name.lower()}")
            except Exception as e:
                failed += 1
                print(f"Failed to resize {file_path.name.lower()}: {str(e)}")
    
    print(f"\nResizing complete. Successfully processed: {successful}, Failed: {failed}")
    return successful, failed

def get_image_quality(file_path):
    """Determine quality based on image width."""
    try:
        with Image.open(file_path) as img:
            width, _ = img.size
            if width < 1025:
                return 0
            elif width < 1537:
                return 1
            elif width < 2680:
                return 2
            else:  # width >= 2680
                return 3
    except Exception as e:
        print(f"Error opening {file_path}: {e}")
        return 0  # Default to 0 if there's an error

def update_quality():
    """Update quality for each file in PRE_RENDERED_MAPS_JSON_LEVELS."""
    possible_paths = ["./", "./images/prerendered/"]    
    for key, value in PRE_RENDERED_MAPS_JSON_LEVELS.items():
        file_path = value['file']
        found = False        
        # Check both possible locations
        for base_path in possible_paths:
            full_path = os.path.join(base_path, os.path.basename(file_path))
            if os.path.exists(full_path):
                quality = get_image_quality(full_path)
                PRE_RENDERED_MAPS_JSON_LEVELS[key]['quality'] = quality
                print(f"Updated {key}: Quality set to {quality} (Width checked at {full_path})")
                found = True
                break        
        if not found:
            print(f"Warning: File not found for {key} at any location. Keeping quality as {value['quality']}")

def print_json():
    """Print the updated PRE_RENDERED_MAPS_JSON_LEVELS in a formatted way."""
    print("\nUpdated PRE_RENDERED_MAPS_JSON_LEVELS = {")
    for key, value in PRE_RENDERED_MAPS_JSON_LEVELS.items():
        print(f"    '{key}': {{'file': '{value['file']}', 'thumbnail': '{value['thumbnail']}', 'quality': {value['quality']}}},")
    print("}")

def calculate_optimal_fill_dimensions(image: Image.Image):
    # Extract the original dimensions
    original_width, original_height = image.size
    
    # Set constants
    MIN_ASPECT_RATIO = 9 / 16
    MAX_ASPECT_RATIO = 16 / 9
    FIXED_DIMENSION = 1024

    # Calculate the aspect ratio of the original image
    original_aspect_ratio = original_width / original_height

    # Determine which dimension to fix
    if original_aspect_ratio > 1:  # Wider than tall
        width = FIXED_DIMENSION
        height = round(FIXED_DIMENSION / original_aspect_ratio)
    else:  # Taller than wide
        height = FIXED_DIMENSION
        width = round(FIXED_DIMENSION * original_aspect_ratio)

    # Ensure dimensions are multiples of 8
    width = (width // 8) * 8
    height = (height // 8) * 8

    # Enforce aspect ratio limits
    calculated_aspect_ratio = width / height
    if calculated_aspect_ratio > MAX_ASPECT_RATIO:
        width = (height * MAX_ASPECT_RATIO // 8) * 8
    elif calculated_aspect_ratio < MIN_ASPECT_RATIO:
        height = (width / MIN_ASPECT_RATIO // 8) * 8

    # Ensure width and height remain above the minimum dimensions
    width = max(width, 576) if width == FIXED_DIMENSION else width
    height = max(height, 576) if height == FIXED_DIMENSION else height

    return width, height