#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Defines objects to generate various kind of 1D and 3D LUTs in various file
formats.
"""

from __future__ import division

import array
import os

import OpenImageIO as oiio

from aces_ocio.process import Process

__author__ = 'ACES Developers'
__copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
__license__ = ''
__maintainer__ = 'ACES Developers'
__email__ = 'aces@oscars.org'
__status__ = 'Production'

__all__ = ['generate_1d_LUT_image',
           'write_SPI_1d',
           'write_CSP_1d',
           'write_CTL_1d',
           'write_1d',
           'generate_1d_LUT_from_image',
           'generate_3d_LUT_image',
           'generate_3d_LUT_from_image',
           'apply_CTL_to_image',
           'convert_bit_depth',
           'generate_1d_LUT_from_CTL',
           'correct_LUT_image',
           'generate_3d_LUT_from_CTL',
           'main']


def generate_1d_LUT_image(ramp_1d_path,
                          resolution=1024,
                          min_value=0,
                          max_value=1):
    """
    Generates a 1D LUT image, i.e. a simple ramp, going from the min_value to 
    the max_value.

    Parameters
    ----------
    ramp_1d_path : str or unicode
        The path of the 1D ramp image to be written
    resolution : int, optional
        The resolution of the 1D ramp image to be written
    min_value : float, optional
        The lowest value in the 1D ramp
    max_value : float, optional
        The highest value in the 1D ramp

    Returns
    -------
    None
    """

    ramp = oiio.ImageOutput.create(ramp_1d_path)

    spec = oiio.ImageSpec()
    spec.set_format(oiio.FLOAT)
    # spec.format.basetype = oiio.FLOAT
    spec.width = resolution
    spec.height = 1
    spec.nchannels = 3

    ramp.open(ramp_1d_path, spec, oiio.Create)

    data = array.array('f',
                       '\0' * spec.width * spec.height * spec.nchannels * 4)
    for i in range(resolution):
        value = float(i) / (resolution - 1) * (
            max_value - min_value) + min_value
        data[i * spec.nchannels + 0] = value
        data[i * spec.nchannels + 1] = value
        data[i * spec.nchannels + 2] = value

    ramp.write_image(spec.format, data)
    ramp.close()


def write_SPI_1d(filename,
                 from_min,
                 from_max,
                 data,
                 entries,
                 channels,
                 components=3):
    """
    Writes a 1D LUT in the Sony Pictures Imageworks .spi1d format.

    Credit to *Alex Fry* for the original single channel version of the spi1d
    writer.

    Parameters
    ----------
    filename : str or unicode
        The path of the 1D LUT to be written
    from_min : float
        The lowest value in the 1D ramp
    from_max : float
        The highest value in the 1D ramp
    data : array of floats
        The entries in the LUT
    entries : int
        The resolution of the LUT, i.e. number of entries in the data set
    channels : int
        The number of channels in the data
    components : int, optional
        The number of channels in the data to actually write

    Returns
    -------
    None
    """

    # May want to use fewer components than there are channels in the data
    # Most commonly used for single channel LUTs
    components = min(3, components, channels)

    with open(filename, 'w') as fp:
        fp.write('Version 1\n')
        fp.write('From %f %f\n' % (from_min, from_max))
        fp.write('Length %d\n' % entries)
        fp.write('Components %d\n' % components)
        fp.write('{\n')
        for i in range(0, entries):
            entry = ''
            for j in range(0, components):
                entry = '%s %s' % (entry, data[i * channels + j])
            fp.write('        %s\n' % entry)
        fp.write('}\n')


def write_CSP_1d(filename,
                 from_min,
                 from_max,
                 data,
                 entries,
                 channels,
                 components=3):
    """
    Writes a 1D LUT in the Rising Sun Research Cinespace .csp format.

    Parameters
    ----------
    filename : str or unicode
        The path of the 1D LUT to be written
    from_min : float
        The lowest value in the 1D ramp
    from_max : float
        The highest value in the 1D ramp
    data : array of floats
        The entries in the LUT
    entries : int
        The resolution of the LUT, i.e. number of entries in the data set
    channels : int
        The number of channels in the data
    components : int, optional
        The number of channels in the data to actually write

    Returns
    -------
    None
    """

    # May want to use fewer components than there are channels in the data
    # Most commonly used for single channel LUTs
    components = min(3, components, channels)

    with open(filename, 'w') as fp:
        fp.write('CSPLUTV100\n')
        fp.write('1D\n')
        fp.write('\n')
        fp.write('BEGIN METADATA\n')
        fp.write('END METADATA\n')

        fp.write('\n')

        fp.write('2\n')
        fp.write('%f %f\n' % (from_min, from_max))
        fp.write('0.0 1.0\n')
        fp.write('2\n')
        fp.write('%f %f\n' % (from_min, from_max))
        fp.write('0.0 1.0\n')
        fp.write('2\n')
        fp.write('%f %f\n' % (from_min, from_max))
        fp.write('0.0 1.0\n')

        fp.write('\n')

        fp.write('%d\n' % entries)
        if components == 1:
            for i in range(0, entries):
                entry = ''
                for j in range(3):
                    entry = '%s %s' % (entry, data[i * channels])
                fp.write('%s\n' % entry)
        else:
            for i in range(entries):
                entry = ''
                for j in range(components):
                    entry = '%s %s' % (entry, data[i * channels + j])
                fp.write('%s\n' % entry)
        fp.write('\n')


def write_CTL_1d(filename,
                 from_min,
                 from_max,
                 data,
                 entries,
                 channels,
                 components=3):
    """
    Writes a 1D LUT in the Academy Color Transformation Language .ctl format.

    Parameters
    ----------
    filename : str or unicode
        The path of the 1D LUT to be written
    from_min : float
        The lowest value in the 1D ramp
    from_max : float
        The highest value in the 1D ramp
    data : array of floats
        The entries in the LUT
    entries : int
        The resolution of the LUT, i.e. number of entries in the data set
    channels : int
        The number of channels in the data
    components : int, optional
        The number of channels in the data to actually write

    Returns
    -------
    None
    """

    # May want to use fewer components than there are channels in the data
    # Most commonly used for single channel LUTs
    components = min(3, components, channels)

    with open(filename, 'w') as fp:
        fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
            entries, components))
        fp.write('\n')
        fp.write('const float min1d = %3.9f;\n' % from_min)
        fp.write('const float max1d = %3.9f;\n' % from_max)
        fp.write('\n')

        # Write LUT
        if components == 1:
            fp.write('const float lut[] = {\n')
            for i in range(0, entries):
                fp.write('%s' % data[i * channels])
                if i != (entries - 1):
                    fp.write(',')
                fp.write('\n')
            fp.write('};\n')
            fp.write('\n')
        else:
            for j in range(components):
                fp.write('const float lut%d[] = {\n' % j)
                for i in range(0, entries):
                    fp.write('%s' % data[i * channels])
                    if i != (entries - 1):
                        fp.write(',')
                    fp.write('\n')
                fp.write('};\n')
                fp.write('\n')

        fp.write('void main\n')
        fp.write('(\n')
        fp.write('  input varying float rIn,\n')
        fp.write('  input varying float gIn,\n')
        fp.write('  input varying float bIn,\n')
        fp.write('  input varying float aIn,\n')
        fp.write('  output varying float rOut,\n')
        fp.write('  output varying float gOut,\n')
        fp.write('  output varying float bOut,\n')
        fp.write('  output varying float aOut\n')
        fp.write(')\n')
        fp.write('{\n')
        fp.write('  float r = rIn;\n')
        fp.write('  float g = gIn;\n')
        fp.write('  float b = bIn;\n')
        fp.write('\n')
        fp.write('  // Apply LUT\n')
        if components == 1:
            fp.write('  r = lookup1D(lut, min1d, max1d, r);\n')
            fp.write('  g = lookup1D(lut, min1d, max1d, g);\n')
            fp.write('  b = lookup1D(lut, min1d, max1d, b);\n')
        elif components == 3:
            fp.write('  r = lookup1D(lut0, min1d, max1d, r);\n')
            fp.write('  g = lookup1D(lut1, min1d, max1d, g);\n')
            fp.write('  b = lookup1D(lut2, min1d, max1d, b);\n')
        fp.write('\n')
        fp.write('  rOut = r;\n')
        fp.write('  gOut = g;\n')
        fp.write('  bOut = b;\n')
        fp.write('  aOut = aIn;\n')
        fp.write('}\n')


def write_1d(filename,
             from_min,
             from_max,
             data,
             data_entries,
             data_channels,
             lut_components=3,
             format='spi1d'):
    """
    Writes a 1D LUT in the specified format.

    Parameters
    ----------
    filename : str or unicode
        The path of the 1D LUT to be written
    from_min : float
        The lowest value in the 1D ramp
    from_max : float
        The highest value in the 1D ramp
    data : array of floats
        The entries in the LUT
    data_entries : int
        The resolution of the LUT, i.e. number of entries in the data set
    data_channels : int
        The number of channels in the data
    lut_components : int, optional
        The number of channels in the data to actually use when writing
    format : str or unicode, optional
        The format of the the 1D LUT that will be written

    Returns
    -------
    None
    """

    ocio_formats_to_extensions = {'cinespace': 'csp',
                                  'flame': '3dl',
                                  'icc': 'icc',
                                  'houdini': 'lut',
                                  'lustre': '3dl',
                                  'ctl': 'ctl'}

    if format in ocio_formats_to_extensions:
        if ocio_formats_to_extensions[format] == 'csp':
            write_CSP_1d(filename,
                         from_min,
                         from_max,
                         data,
                         data_entries,
                         data_channels,
                         lut_components)
        elif ocio_formats_to_extensions[format] == 'ctl':
            write_CTL_1d(filename,
                         from_min,
                         from_max,
                         data,
                         data_entries,
                         data_channels,
                         lut_components)
    else:
        write_SPI_1d(filename,
                     from_min,
                     from_max,
                     data,
                     data_entries,
                     data_channels,
                     lut_components)


def generate_1d_LUT_from_image(ramp_1d_path,
                               output_path=None,
                               min_value=0,
                               max_value=1,
                               channels=3,
                               format='spi1d'):
    """
    Reads a 1D LUT image and writes a 1D LUT in the specified format.

    Parameters
    ----------
    ramp_1d_path : str or unicode
        The path of the 1D ramp image to be read
    output_path : str or unicode, optional
        The path of the 1D LUT to be written
    min_value : float, optional
        The lowest value in the 1D ramp
    max_value : float, optional
        The highest value in the 1D ramp
    channels : int, optional
        The number of channels in the data
    format : str or unicode, optional
        The format of the the 1D LUT that will be written

    Returns
    -------
    None
    """

    if output_path is None:
        output_path = '%s.%s' % (ramp_1d_path, 'spi1d')

    ramp = oiio.ImageInput.open(ramp_1d_path)

    ramp_spec = ramp.spec()
    ramp_width = ramp_spec.width
    ramp_channels = ramp_spec.nchannels

    # Forcibly read data as float, the Python API doesn't handle half-float
    # well yet.
    type = oiio.FLOAT
    ramp_data = ramp.read_image(type)

    write_1d(output_path, min_value, max_value,
             ramp_data, ramp_width, ramp_channels, channels, format)


def generate_3d_LUT_image(ramp_3d_path, resolution=32):
    """
    Generates a 3D LUT image covering the specified resolution
    Relies on OCIO's ociolutimage command

    Parameters
    ----------
    ramp_3d_path : str or unicode
        The path of the 3D ramp image to be written
    resolution : int, optional
        The resolution of the 3D ramp image to be written

    Returns
    -------
    None
    """

    args = ['--generate',
            '--cubesize',
            str(resolution),
            '--maxwidth',
            str(resolution * resolution),
            '--output',
            ramp_3d_path]
    lut_extract = Process(description='generate a 3d LUT image',
                          cmd='ociolutimage',
                          args=args)
    lut_extract.execute()


def generate_3d_LUT_from_image(ramp_3d_path,
                               output_path=None,
                               resolution=32,
                               format='spi3d'):
    """
    Reads a 3D LUT image and writes a 3D LUT in the specified format.
    Relies on OCIO's ociolutimage command

    Parameters
    ----------
    ramp_3d_path : str or unicode
        The path of the 3D ramp image to be read
    output_path : str or unicode, optional
        The path of the 1D LUT to be written
    resolution : int, optional
        The resolution of the 3D LUT represented in the image
    format : str or unicode, optional
        The format of the the 3D LUT that will be written

    Returns
    -------
    None
    """

    if output_path is None:
        output_path = '%s.%s' % (ramp_3d_path, 'spi3d')

    ocio_formats_to_extensions = {'cinespace': 'csp',
                                  'flame': '3dl',
                                  'icc': 'icc',
                                  'houdini': 'lut',
                                  'lustre': '3dl'}

    if format == 'spi3d' or not (format in ocio_formats_to_extensions):
        # Extract a spi3d LUT
        args = ['--extract',
                '--cubesize',
                str(resolution),
                '--maxwidth',
                str(resolution * resolution),
                '--input',
                ramp_3d_path,
                '--output',
                output_path]
        lut_extract = Process(description='extract a 3d LUT',
                              cmd='ociolutimage',
                              args=args)
        lut_extract.execute()

    else:
        output_path_spi3d = '%s.%s' % (output_path, 'spi3d')

        # Extract a spi3d LUT
        args = ['--extract',
                '--cubesize',
                str(resolution),
                '--maxwidth',
                str(resolution * resolution),
                '--input',
                ramp_3d_path,
                '--output',
                output_path_spi3d]
        lut_extract = Process(description='extract a 3d LUT',
                              cmd='ociolutimage',
                              args=args)
        lut_extract.execute()

        # Convert to a different format
        args = ['--lut',
                output_path_spi3d,
                '--format',
                format,
                output_path]
        lut_convert = Process(description='convert a 3d LUT',
                              cmd='ociobakelut',
                              args=args)
        lut_convert.execute()


def apply_CTL_to_image(input_image,
                       output_image,
                       ctl_paths=None,
                       input_scale=1,
                       output_scale=1,
                       global_params=None,
                       aces_ctl_directory=None):
    """
    Applies a set of Academy Color Transformation Language .ctl files to 
    an input image and writes a new image.
    Relies on the ACES ctlrender command

    Parameters
    ----------
    input_image : str or unicode
        The path to the image to transform using the CTL files
    output_image : str or unicode
        The path to write the result of the transforms
    ctl_paths : array of str or unicode, optional
        The path to write the result of the transforms
    input_scale : float, optional
        The argument to the ctlrender -input_scale parameter
        For images with integer bit depths, this divides image code values 
        before they are sent to the ctl commands
        For images with float or half bit depths, this multiplies image code 
        values before they are sent to the ctl commands
    output_scale : float, optional
        The argument to the ctlrender -output_scale parameter
        For images with integer bit depths, this multiplies image code values 
        before they are written to a file.
        For images with float or half bit depths, this divides image code values 
        before they are sent to the ctl commands
    global_params : dict of key value pairs, optional
        The set of parameter names and values to pass to the ctlrender 
        -global_param1 parameter
    aces_ctl_directory : str or unicode, optional
        The path to the aces 'transforms/ctl/utilities'

    Returns
    -------
    None
    """

    if ctl_paths is None:
        ctl_paths = []
    if global_params is None:
        global_params = {}

    if len(ctl_paths) > 0:
        ctlenv = os.environ

        if "/usr/local/bin" not in ctlenv['PATH'].split(':'):
            ctlenv['PATH'] = "%s:/usr/local/bin" % ctlenv['PATH']

        if aces_ctl_directory is not None:
            if os.path.split(aces_ctl_directory)[1] != 'utilities':
                ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
            else:
                ctl_module_path = aces_ctl_directory
            ctlenv['CTL_MODULE_PATH'] = ctl_module_path

        args = []
        for ctl in ctl_paths:
            args += ['-ctl', ctl]
        args += ['-force']
        args += ['-input_scale', str(input_scale)]
        args += ['-output_scale', str(output_scale)]
        args += ['-global_param1', 'aIn', '1.0']
        for key, value in global_params.iteritems():
            args += ['-global_param1', key, str(value)]
        args += [input_image]
        args += [output_image]

        ctlp = Process(description='a ctlrender process',
                       cmd='ctlrender',
                       args=args, env=ctlenv)

        ctlp.execute()


def convert_bit_depth(input_image, output_image, depth):
    """
    Convert the input image to the specified bit depth and write a new image
    Relies on the OIIO oiiotool command

    Parameters
    ----------
    input_image : str or unicode
        The path to the image to transform using the CTL files
    output_image : str or unicode
        The path to write the result of the transforms
    depth : str or unicode
        The bit depth of the output image
        Data types include: uint8, sint8, uint10, uint12, uint16, sint16, half, float, double

    Returns
    -------
    None
    """

    args = [input_image,
            '-d',
            depth,
            '-o',
            output_image]
    convert = Process(description='convert image bit depth',
                      cmd='oiiotool',
                      args=args)
    convert.execute()


def generate_1d_LUT_from_CTL(lut_path,
                             ctl_paths,
                             lut_resolution=1024,
                             identity_lut_bit_depth='half',
                             input_scale=1,
                             output_scale=1,
                             global_params=None,
                             cleanup=True,
                             aces_ctl_directory=None,
                             min_value=0,
                             max_value=1,
                             channels=3,
                             format='spi1d'):
    """
    Creates a 1D LUT from the specified CTL files by creating a 1D LUT image,
    applying the CTL files and then extracting and writing a LUT based on the
    resulting image

    Parameters
    ----------
    lut_path : str or unicode
        The path to write the 1D LUT
    ctl_paths : array of str or unicode
        The CTL files to apply
    lut_resolution : int, optional
        The resolution of the 1D LUT to generate
    identity_lut_bit_depth : string, optional
        The bit depth to use for the intermediate 1D LUT image
    input_scale : float, optional
        The argument to the ctlrender -input_scale parameter
        For images with integer bit depths, this divides image code values 
        before they are sent to the ctl commands
        For images with float or half bit depths, this multiplies image code 
        values before they are sent to the ctl commands
    output_scale : float, optional
        The argument to the ctlrender -output_scale parameter
        For images with integer bit depths, this multiplies image code values 
        before they are written to a file.
        For images with float or half bit depths, this divides image code values 
        before they are sent to the ctl commands
    global_params : dict of key, value pairs, optional
        The set of parameter names and values to pass to the ctlrender 
        -global_param1 parameter
    cleanup : bool, optional
        Whether or not to clean up the intermediate images 
    aces_ctl_directory : str or unicode, optional
        The path to the aces 'transforms/ctl/utilities'
    min_value : float, optional
        The minimum value to consider as input to the LUT
    max_value : float, optional
        The maximum value to consider as input to the LUT
    channels : int, optional
        The number of channels to use for the LUT. 1 or 3 are valid.
    format : str or unicode, optional
        The format to use when writing the LUT

    Returns
    -------
    None
    """

    if global_params is None:
        global_params = {}

    lut_path_base = os.path.splitext(lut_path)[0]

    identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
    generate_1d_LUT_image(identity_lut_image_float,
                          lut_resolution,
                          min_value,
                          max_value)

    if identity_lut_bit_depth not in ['half', 'float']:
        identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
        convert_bit_depth(identity_lut_image_float,
                          identity_lut_image,
                          identity_lut_bit_depth)
    else:
        identity_lut_image = identity_lut_image_float

    transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
    apply_CTL_to_image(identity_lut_image,
                       transformed_lut_image,
                       ctl_paths,
                       input_scale,
                       output_scale,
                       global_params,
                       aces_ctl_directory)

    generate_1d_LUT_from_image(transformed_lut_image,
                               lut_path,
                               min_value,
                               max_value,
                               channels,
                               format)

    if cleanup:
        os.remove(identity_lut_image)
        if identity_lut_image != identity_lut_image_float:
            os.remove(identity_lut_image_float)
        os.remove(transformed_lut_image)


def correct_LUT_image(transformed_lut_image,
                      corrected_lut_image,
                      lut_resolution):
    """
    For some combinations of resolution and bit depth, ctlrender would generate
    images with the right number of pixels but with the values for width and 
    height transposed. This function generating a new, corrected image based on
    the original. The function acts as a pass through if the problem is not
    detected.

    Parameters
    ----------
    transformed_lut_image : str or unicode
        The path to an image generated by cltrender
    corrected_lut_image : str or unicode
        The path to an 'corrected' image to be generated
    lut_resolution : int
        The resolution of the 3D LUT that should be contained in 
        transformed_lut_image

    Returns
    -------
    str or unicode
        The path to the corrected image, or the original, if no correction was
        needed.
    """

    transformed = oiio.ImageInput.open(transformed_lut_image)

    transformed_spec = transformed.spec()
    width = transformed_spec.width
    height = transformed_spec.height
    channels = transformed_spec.nchannels

    if width != lut_resolution * lut_resolution or height != lut_resolution:
        print(('Correcting image as resolution is off. '
               'Found %d x %d. Expected %d x %d') % (
                  width,
                  height,
                  lut_resolution * lut_resolution,
                  lut_resolution))
        print('Generating %s' % corrected_lut_image)

        # Forcibly read data as float, the Python API doesn't handle half-float
        # well yet.
        type = oiio.FLOAT
        source_data = transformed.read_image(type)

        correct = oiio.ImageOutput.create(corrected_lut_image)

        correct_spec = oiio.ImageSpec()
        correct_spec.set_format(oiio.FLOAT)
        correct_spec.width = height
        correct_spec.height = width
        correct_spec.nchannels = channels

        correct.open(corrected_lut_image, correct_spec, oiio.Create)

        dest_data = array.array('f',
                                ('\0' * correct_spec.width *
                                 correct_spec.height *
                                 correct_spec.nchannels * 4))
        for j in range(0, correct_spec.height):
            for i in range(0, correct_spec.width):
                for c in range(0, correct_spec.nchannels):
                    dest_data[(correct_spec.nchannels *
                               correct_spec.width * j +
                               correct_spec.nchannels * i + c)] = (
                        source_data[correct_spec.nchannels *
                                    correct_spec.width * j +
                                    correct_spec.nchannels * i + c])

        correct.write_image(correct_spec.format, dest_data)
        correct.close()
    else:
        # shutil.copy(transformedLUTImage, correctedLUTImage)
        corrected_lut_image = transformed_lut_image

    transformed.close()

    return corrected_lut_image


def generate_3d_LUT_from_CTL(lut_path,
                             ctl_paths,
                             lut_resolution=64,
                             identity_lut_bit_depth='half',
                             input_scale=1,
                             output_scale=1,
                             global_params=None,
                             cleanup=True,
                             aces_ctl_directory=None,
                             format='spi3d'):
    """
    Creates a 3D LUT from the specified CTL files by creating a 3D LUT image,
    applying the CTL files and then extracting and writing a LUT based on the
    resulting image

    Parameters
    ----------
    lut_path : str or unicode
        The path to write the 1D LUT
    ctl_paths : array of str or unicode
        The CTL files to apply
    lut_resolution : int, optional
        The resolution of the 1D LUT to generate
    identity_lut_bit_depth : string, optional
        The bit depth to use for the intermediate 1D LUT image
    input_scale : float, optional
        The argument to the ctlrender -input_scale parameter
        For images with integer bit depths, this divides image code values 
        before they are sent to the ctl commands
        For images with float or half bit depths, this multiplies image code 
        values before they are sent to the ctl commands
    output_scale : float, optional
        The argument to the ctlrender -output_scale parameter
        For images with integer bit depths, this multiplies image code values 
        before they are written to a file.
        For images with float or half bit depths, this divides image code values 
        before they are sent to the ctl commands
    global_params : dict of key, value pairs, optional
        The set of parameter names and values to pass to the ctlrender 
        -global_param1 parameter
    cleanup : bool, optional
        Whether or not to clean up the intermediate images 
    aces_ctl_directory : str or unicode, optional
        The path to the aces 'transforms/ctl/utilities'
    format : str or unicode, optional
        The format to use when writing the LUT

    Returns
    -------
    None
    """

    if global_params is None:
        global_params = {}

    lut_path_base = os.path.splitext(lut_path)[0]

    identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
    generate_3d_LUT_image(identity_lut_image_float, lut_resolution)

    if identity_lut_bit_depth not in ['half', 'float']:
        identity_lut_image = '%s.%s.%s' % (lut_path_base,
                                           identity_lut_bit_depth,
                                           'tiff')
        convert_bit_depth(identity_lut_image_float,
                          identity_lut_image,
                          identity_lut_bit_depth)
    else:
        identity_lut_image = identity_lut_image_float

    transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
    apply_CTL_to_image(identity_lut_image,
                       transformed_lut_image,
                       ctl_paths,
                       input_scale,
                       output_scale,
                       global_params,
                       aces_ctl_directory)

    corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
    corrected_lut_image = correct_LUT_image(transformed_lut_image,
                                            corrected_lut_image,
                                            lut_resolution)

    generate_3d_LUT_from_image(corrected_lut_image,
                               lut_path,
                               lut_resolution,
                               format)

    if cleanup:
        os.remove(identity_lut_image)
        if identity_lut_image != identity_lut_image_float:
            os.remove(identity_lut_image_float)
        os.remove(transformed_lut_image)
        if corrected_lut_image != transformed_lut_image:
            os.remove(corrected_lut_image)
        if format != 'spi3d':
            lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
            os.remove(lut_path_spi3d)


def main():
    """
    A simple main that allows the user to exercise the various functions
    defined in this file

    Parameters
    ----------
    None

    Returns
    -------
    None
    """

    import optparse

    p = optparse.OptionParser(
        description='A utility to generate LUTs from CTL',
        prog='generateLUT',
        version='0.01',
        usage='%prog [options]')

    p.add_option('--lut', '-l', type='string', default='')
    p.add_option('--format', '-f', type='string', default='')
    p.add_option('--ctl', '-c', type='string', action='append')
    p.add_option('--lutResolution1d', '', type='int', default=4096)
    p.add_option('--lutResolution3d', '', type='int', default=65)
    p.add_option('--ctlReleasePath', '-r', type='string', default='')
    p.add_option('--bitDepth', '-b', type='string', default='float')
    p.add_option('--keepTempImages', '', action='store_true')
    p.add_option('--minValue', '', type='float', default=0)
    p.add_option('--maxValue', '', type='float', default=1)
    p.add_option('--inputScale', '', type='float', default=1)
    p.add_option('--outputScale', '', type='float', default=1)
    p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
                 action='append')

    p.add_option('--generate1d', '', action='store_true')
    p.add_option('--generate3d', '', action='store_true')

    options, arguments = p.parse_args()

    lut = options.lut
    format = options.format
    ctls = options.ctl
    lut_resolution_1d = options.lutResolution1d
    lut_resolution_3d = options.lutResolution3d
    min_value = options.minValue
    max_value = options.maxValue
    input_scale = options.inputScale
    output_scale = options.outputScale
    ctl_release_path = options.ctlReleasePath
    generate_1d = options.generate1d is True
    generate_3d = options.generate3d is True
    bit_depth = options.bitDepth
    cleanup = not options.keepTempImages

    params = {}
    if options.ctlRenderParam is not None:
        for param in options.ctlRenderParam:
            params[param[0]] = float(param[1])

    if generate_1d:
        print('1D LUT generation options')
    else:
        print('3D LUT generation options')

    print('LUT                 : %s' % lut)
    print('Format              : %s' % format)
    print('CTLs                : %s' % ctls)
    print('LUT Res 1d          : %s' % lut_resolution_1d)
    print('LUT Res 3d          : %s' % lut_resolution_3d)
    print('Min Value           : %s' % min_value)
    print('Max Value           : %s' % max_value)
    print('Input Scale         : %s' % input_scale)
    print('Output Scale        : %s' % output_scale)
    print('CTL Render Params   : %s' % params)
    print('CTL Release Path    : %s' % ctl_release_path)
    print('Input Bit Depth     : %s' % bit_depth)
    print('Cleanup Temp Images : %s' % cleanup)

    if generate_1d:
        generate_1d_LUT_from_CTL(lut,
                                 ctls,
                                 lut_resolution_1d,
                                 bit_depth,
                                 input_scale,
                                 output_scale,
                                 params,
                                 cleanup,
                                 ctl_release_path,
                                 min_value,
                                 max_value,
                                 format=format)

    elif generate_3d:
        generate_3d_LUT_from_CTL(lut,
                                 ctls,
                                 lut_resolution_3d,
                                 bit_depth,
                                 input_scale,
                                 output_scale,
                                 params,
                                 cleanup,
                                 ctl_release_path,
                                 format=format)
    else:
        print(('\n\nNo LUT generated! '
               'You must choose either 1D or 3D LUT generation\n\n'))


if __name__ == '__main__':
    main()
