# Made with the help of Adobe's Cube LUT Specification: https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf

import glob, os, re, cv2, io, copy
import numpy as np

# find all files with ".cube" extension
os.chdir('.')
file_names = glob.glob('*.cube')

PACK = False

if len(file_names) >= 1:
    # read first file
    raw_file = open(file_names[0], 'r')
    raw_text = raw_file.read()

    # find metadata
    meta_1D_list = re.findall(r'^LUT_1D_SIZE ([\d]+)$', raw_text, re.M)
    meta_3D_list = re.findall(r'^LUT_3D_SIZE ([\d]+)$', raw_text, re.M)
    meta_domain_min_list = re.findall(r'^DOMAIN_MIN ([\d\. ]+)$', raw_text, re.M)
    meta_domain_max_list = re.findall(r'^DOMAIN_MAX ([\d\. ]+)$', raw_text, re.M)
    columns = re.findall(r'^ *([\d\. e+-]+) *$', raw_text, re.M)

    # find custom options
    raw_text_lower = raw_text.lower()
    viewing_transform = re.findall(r'^# *viewing transform *: *(\d+) *$', raw_text_lower, re.M)
    output_colorspace = re.findall(r'^# *output colorspace *: *(\d+) *$', raw_text_lower, re.M)
    gamma_correction = re.findall(r'^# *gamma correction *: *(\w+) *$', raw_text_lower, re.M)
    if viewing_transform:
        print('found viewing transform setting')
        viewing_transform = viewing_transform[0]
    if output_colorspace:
        print('found output colorspace setting')
        output_colorspace = output_colorspace[0]
    if gamma_correction:
        print('found gamma correction setting')
        gamma_correction = gamma_correction[0]

    # check for linear mapping (to avoid unnecessary calculations)
    meta_domain_min = re.findall(r'\d+(?:\.\d+)?', meta_domain_min_list[0]) if len(meta_domain_min_list) >= 1 else ['0.0', '0.0', '0.0']
    meta_domain_max = re.findall(r'\d+(?:\.\d+)?', meta_domain_max_list[0]) if len(meta_domain_max_list) >= 1 else ['1.0', '1.0', '1.0']


    # ensure file validity
    if len(columns) >= 6 and (len(meta_1D_list) == 1 or len(meta_3D_list) == 1):
        # intepret data as 2D array
        data = []
        for item in columns:
            rgb_data_str = re.findall(r'\d+(?:\.\d+)?(?:e[+-]\d+)?', item)
            rgb_data = []
            for channel in rgb_data_str:
                rgb_data.append(float(channel))
            data.append(rgb_data)

        meta_max_color = 0.0
        for color in data:
            for channel in color:
                if channel > meta_max_color:
                    meta_max_color = channel

        # extract first instance of metadata from found lists
        meta_1D = len(meta_1D_list) == 1
        meta_dimensions = 1 if meta_1D else 3
        meta_size = int(meta_1D_list[0]) if meta_1D else int(meta_3D_list[0])

        # pregenerate some meta values
        meta_domain_mult = []
        linear_mapping = True
        for i in range(len(meta_domain_min)):
            minNum = float(meta_domain_min[i])
            maxNum = float(meta_domain_max[i])
            if(minNum != 0.0 or maxNum != 1.0):
                linear_mapping = False
            meta_domain_mult.append(str(maxNum - minNum))

        meta_domain_add_str = ", ".join(meta_domain_min)
        meta_domain_mult_str = ", ".join(meta_domain_mult)

        # write metadata to file
        lut_meta = open('lut_meta.glsl', 'w')
        write_str = f"""\
#define LUT_DIM {meta_dimensions}
#define LUT_SIZE {meta_size}
#define LUT_SIZE_RCP {1 / meta_size}
#define LUT_SIZE_RCP1 {1 / (meta_size - 1)}
{"" if linear_mapping else "// "}#define LUT_LINEAR_MAPPING
#define LUT_DOMAIN_ADD vec3({meta_domain_add_str.strip()})
#define LUT_DOMAIN_MULT vec3({meta_domain_mult_str.strip()})
#define LUT_RANGE_MULT {1 / meta_max_color}

"""
        
        if viewing_transform:
            write_str += f'#define LUT_LMT_MODE {viewing_transform}\n'
        if output_colorspace:
            write_str += f'#define LUT_OUTPUT_COLORSPACE {output_colorspace}\n'
        if gamma_correction:
            write_str += f'#define LUT_OVERRIDE_GAMMA_CORRECT\n{"" if gamma_correction.lower() == "on" else "// "}#define LUT_GAMMA_CORRECT'

        # write the proper specs in shaders.properties
        PROPS_FILE_DIR='../shaders.properties'
        with open(PROPS_FILE_DIR, 'r') as props_file:
            text = props_file.read()

        text = re.sub(r'texture\.(\w+\.\w+(?:\.\d)? *)= *\/LUTs\/lut.dat +TEXTURE_\dD +\w+ \w+ \w+ \w+ \w+ \w+',
            fr'texture.\g<1>=/LUTs/lut.dat TEXTURE_{meta_dimensions}D RGB16F {meta_size} {meta_size} {meta_size} RGB FLOAT',
            text)
        
        with open(PROPS_FILE_DIR, 'w') as props_file:
            props_file.write(text)

        # write to lut_meta file
        lut_meta.write(write_str)

        # reformat data into 3D array to use as intake for openCV
        buffer = io.BytesIO()
        img = []
        dat = []
        tex_size = 0;
        if(meta_dimensions == 1):
            file = open('error.md', 'w')
            file.write("Error: One-dimensional LUTs are currently unsupported.")
        else:
            if PACK:
                num_tiles = int(np.ceil(np.sqrt(meta_size)))
                tex_size = num_tiles * meta_size
                for y_id in range(tex_size):
                    row = []
                    for x_id in range(tex_size):
                        pixel_id = y_id * meta_size + x_id
                        
                        tile_pos = [int(np.floor(x_id / meta_size)), int(np.floor(y_id / meta_size))]
                        tile_id = tile_pos[1] * num_tiles + tile_pos[0]
                        
                        within_tile_pos = [x_id - tile_pos[0] * meta_size, y_id - tile_pos[1] * meta_size]
                        within_tile_id = within_tile_pos[1] * num_tiles + within_tile_pos[0]

                        index = np.clip(within_tile_pos[1] + within_tile_pos[0] * meta_size + tile_id * pow(meta_size, 2), 0, pow(meta_size, 3) - 1)
                        rgba = data[index]
                        # rgba = within_tile_id / pow(32, 2) * 4
                        # rgba.append(1.0)
                        row.append(rgba)
                    img.append(row)
            else:
                tex_size = meta_size
                for column_id in range(pow(tex_size, 2)):
                    column = []
                    for pixel_id in range(tex_size):
                        index = column_id * tex_size + pixel_id
                        rgba = copy.deepcopy(data[index])
                        rgba.reverse()
                        rgba.append(1.0)
                        column.append(rgba)
                    img.append(column)
            for z_id in range(meta_size):
                slice = []
                for y_id in range(meta_size):
                    column = []
                    for x_id in range(meta_size):
                        index = z_id * pow(tex_size, 2) + y_id * tex_size + x_id
                        rgb = copy.deepcopy(data[index])
                        # rgb.append(0.0)
                        column.append(rgb)
                    slice.append(column)
                dat.append(slice)

        np_img = np.asarray(img, dtype=np.float32)

        dat_dtype = np.float32

        np_dat = np.asarray(dat, dtype=dat_dtype)

        np_dat = np.transpose(np_dat, (0, 1, 2, 3))
        np_dat = np_dat.flatten()
        for data in np_dat:
            buffer.write(data)

        # write image
        with open("lut.dat", "wb") as outfile:
            # Copy the BytesIO stream to the output file
            outfile.write(buffer.getbuffer())

        test = open('lut.dat', 'r')
        test_np = np.fromfile(test, dtype=dat_dtype)

        print(f"File Size: {os.path.getsize('lut.dat')} bytes")

        cv2.imwrite('lut.png', 255 * np_img)
        print('done')
    else:
        print(len(meta_1D_list))
        print(len(meta_3D_list))
        print(len(meta_domain_min_list))
        print(len(meta_domain_max_list))
        print(len(columns))
        print(raw_text[0:300])
        file = open('error.md', 'w')
        file.write("Error: Invalid CUBE file. Please make sure the file you're using is formatted correctly.")
else:
    file = open('error.md', 'w')
    file.write("Error: No files recognized. Make sure your file has the `.cube` extension in all-lowercase.")