import base64
from collections import defaultdict
import hashlib
import mimetypes
from pydicom.uid import ExplicitVRLittleEndian

from io import BytesIO

from PIL import Image, ImageFile
from blake3 import blake3

import pydicom
from pydicom.errors import InvalidDicomError
import sys
import glob

import itertools

try:
    from .pydicom_PIL import get_PIL_image
except:
    from pydicom_PIL import get_PIL_image
import numpy as np

def image_as_base64(image_file):
    """
    :param `image_file` for the complete path of image.
    :param `format` is format for image, eg: `png` or `jpg`.
    """
    # if not os.path.isfile(image_file):
    #    return None

    try:
        encoded_string = ""
        # with open(image_file, 'rb') as img_f:
        #    encoded_string = base64.b64encode(img_f.read())
        encoded_string = base64.b64encode(image_file.file.read())
        mimetype, enc = mimetypes.guess_type(image_file.path)
        # Treat unknown files as dicom
        if None == mimetype:
            return "data:application/dicom;base64,{}".format(encoded_string.decode("utf-8"))
        if "dicom" in mimetype or "octet-stream" in mimetype:
            return "data:{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
        return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
    except FileNotFoundError:
        return "data:image/png;base64,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"




def pil_dicom_image(source, exif_orientation=True, **options):
    """
    Try to open the source file directly using PIL, ignoring any errors.
    exif_orientation
        If EXIF orientation data is present, perform any required reorientation
        before passing the data along the processing pipeline.
    """
    # Use a BytesIO wrapper because if the source is an incomplete file like
    # object, PIL may have problems with it. For example, some image types
    # require tell and seek methods that are not present on all storage
    # File objects.
    #if not source:
    #    return
    #source = BytesIO(source.read())

    # open file with pydicom
    ds = pydicom.dcmread(source)

    # return the image file
    return get_PIL_image(ds)

    #with Image.open(source) as image:
    #    # Fully load the image now to catch any problems with the image contents.
    #    try:
    #        ImageFile.LOAD_TRUNCATED_IMAGES = True
    #        image.load()
    #    finally:
    #        ImageFile.LOAD_TRUNCATED_IMAGES = False

    #if exif_orientation:
    #    image = utils.exif_orientation(image)



def show_PIL(dataset):
    """Display an image using the Python Imaging Library (PIL)"""
    im = get_PIL_image(dataset)
    im.show()




def get_dicom_order(path):
    # load the DICOM files
    files = []

    for fname in glob.glob(path, recursive=False):
        files.append((fname, pydicom.dcmread(fname)))

    # skip files with no SliceLocation (eg scout views)
    slices = []
    map = {}
    skipcount = 0
    for fname, f in files:
        if hasattr(f, 'SliceLocation'):
            slices.append(f)
            map[f.SliceLocation] = fname
        else:
            skipcount = skipcount + 1


    # ensure they are in the correct order
    slices = sorted(slices, key=lambda s: s.SliceLocation)

    return slices

#get_dicom_order("/home/ross/Downloads/DICOM HEAD/STD4/SER1/*.dcm")

def pretty_print_dicom(dataset, indent=0):
    l = print_dicom(dataset, indent)
    return "".join(l)


def print_dicom(dataset, indent=0, include_tag_ids=True, join=True, join_text="\n<br/>"):
    """Go through all items in the dataset and print them with custom format

    Modelled after Dataset._pretty_str()
    """
    dont_print = ['Pixel Data', 'File Meta Information Version']

    indent_string = "---" * indent
    next_indent_string = "---" * (indent + 1)

    l = []

    for data_element in dataset:
        if data_element.VR == "SQ":   # a sequence
            l.append(f"{data_element.tag}: {indent_string}{data_element.name}")
            for sequence_item in data_element.value:
                l_ = print_dicom(sequence_item, indent + 1, join=join, join_text=join_text)
                if join:
                    l.append(l_)
                else:
                    l.extend(l_)

                l.append(next_indent_string + "---------")
        else:
            if data_element.name in dont_print:
                l.append("""item not printed -- in the "don't print" list""")
            else:
                repr_value = repr(data_element.value)
                if len(repr_value) > 50:
                    repr_value = repr_value[:50] + "..."
                l.append(f"{data_element.tag}: {indent_string:s} {data_element.name:s} = {repr_value:s}")
    if not join:
        return l
    
    return join_text.join(l)

def get_image_dicom_hash(img, dataset=None, hash_type="md5", direct_pixel_data=True) -> (str, bool):
    if dataset is None:
        with pydicom.dcmread(img) as ds:
            dataset = ds

    # TODO: improve?
    match hash_type:
        case "md5":
            hasher = hashlib.md5()
        case "blake3":
            hasher = blake3()
        case _:
            raise NotImplemented

    if direct_pixel_data:
        try:
            hasher.update(dataset.PixelData)
        except AttributeError as e:
            print("Error getting pixel data hash from dataset")
            raise InvalidDicomError("Error getting pixel data hash from dataset")

    else:
        first = True
        for i in dataset.pixel_array.astype(str).flatten():
            if first:
                first = False
                hasher.update(f"{i}".encode())
            else:
                hasher.update(f",{i}".encode())

    hash = hasher.hexdigest()
    return hash

def get_image_hash(img, dataset=None, hash_type="blake3", direct_pixel_data=True) -> (str, bool):
    is_dicom = False
    # Try and read the file as a dicom
    hash = "1234"
    try:
        # and generate a hash from the pixel data
        hash = get_image_dicom_hash(img, dataset=dataset, hash_type=hash_type, direct_pixel_data=direct_pixel_data)
        is_dicom = True
        # ----

    # Random error catching is bad
    except (pydicom.errors.InvalidDicomError, TypeError) as e:
        print("dicom error", img)
        print(e)
        try:  # This is horrible (but needed for current unit tests)
            # (we use a temporary file that breaks here)
            img.file.open()
            match hash_type:
                case "md5":
                    hash = hashlib.md5(img.read()).hexdigest()
                case "blake3":
                    hash = blake3(img.read()).hexdigest()
        except AttributeError:
            print("BAD")
            return hash, False

    return hash, is_dicom


def compare_dicom_elements(element1: pydicom.DataElement, element2: pydicom.DataElement):
    dont_print = ['Pixel Data', 'File Meta Information Version']

    if element1.name in dont_print:
        return {}

    differences = {}

    vr_exists = False

    # Compare the element tags
    try:
        if element1.tag != element2.tag:
            differences['tag'] = (element1.tag, element2.tag)
    except AttributeError: # private elements won't/might not? have a tag
        pass

    # Compare the VR (Value Representation)
    try:
        if element1.VR != element2.VR:
            vr_exists = True
            differences['VR'] = (element1.VR, element2.VR)

    except AttributeError: 
        pass

    # Compare the value
    if element1.value != element2.value:
        differences['value'] = (element1.value, element2.value)

    # Recursively compare the child elements
    if vr_exists and element1.VR == 'SQ' and element2.VR == 'SQ':
        for i, (item1, item2) in enumerate(zip(element1.value, element2.value)):
            item_differences = compare_dicom_elements(item1, item2)
            if item_differences:
                differences[f'item{i+1}'] = item_differences

    return differences

def compare_dicom_datasets(*datasets: pydicom.Dataset):
    differences = defaultdict(list)

    # Compare the dataset elements
    for i, dataset in enumerate(datasets):
        for element1 in dataset:
            for j in range(i+1, len(datasets)):
                dataset2 = datasets[j]
                element2 = dataset2.get(element1.tag)
                if element2 is None:
                    differences[f"{element1.tag} {element1.name}"].append(('missing', element1.value))
                else:
                    element_differences = compare_dicom_elements(element1, element2)
                    if element_differences:
                        differences[f"{element1.tag} {element1.name}"].append(element_differences)

    # Check for any extra elements in the datasets
    for i, dataset in enumerate(datasets):
        for element2 in dataset:
            for j in range(i+1, len(datasets)):
                dataset2 = datasets[j]
                element1 = dataset2.get(element2.tag)
                if element1 is None:
                    differences[f"{element1.tag} {element1.name}"].append(('extra', element2.value))

    return differences


def combine_dicom_images_side_by_side(dicom_path1, dicom_path2):
    """
    Combines two DICOM images side by side, using the metadata from the first image.
    If the heights differ, pads the shorter image with zeros at the bottom.
    Returns a new pydicom.Dataset with the combined image.
    """
    ds1 = pydicom.dcmread(dicom_path1)
    ds2 = pydicom.dcmread(dicom_path2)

    arr1 = ds1.pixel_array
    arr2 = ds2.pixel_array

    h1, w1 = arr1.shape[:2]
    h2, w2 = arr2.shape[:2]

    # Pad arrays if heights differ
    if h1 != h2:
        max_h = max(h1, h2)
        def pad(arr, target_h):
            pad_height = target_h - arr.shape[0]
            if pad_height > 0:
                pad_shape = ((0, pad_height), (0, 0)) if arr.ndim == 2 else ((0, pad_height), (0, 0), (0, 0))
                arr = np.pad(arr, pad_shape, mode='constant', constant_values=0)
            return arr
        arr1 = pad(arr1, max_h)
        arr2 = pad(arr2, max_h)

    combined = np.hstack((arr1, arr2))

    ds1.PixelData = combined.tobytes()
    ds1.Rows, ds1.Columns = combined.shape[:2]

    if hasattr(ds1, 'PhotometricInterpretation'):
        ds1.PhotometricInterpretation = ds1.PhotometricInterpretation

    # Ensure output is uncompressed
    ds1.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian

    return ds1
