//
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) Contributors to the OpenEXR Project.
//

#include "PxBaseDeepHelper.h"
#include "PxDeepOutPixel.h"
#include "PxDeepOutRow.h"
#include "PxDeepUtils.h"
#include "PxFourChanDeepRgba.h"
#include "PxOneChanDeepAlpha.h"
#include "PxOneChanDeepOpacity.h"

#include <dtex.h>

#include <ImfChannelList.h>
#include <ImfDeepScanLineOutputFile.h>
#include <ImfHeader.h>
#include <ImfPartType.h>
#include <ImfStandardAttributes.h>

#include <ImathBox.h>
#include <ImathMatrix.h>
#include <ImathVec.h>

#include <half.h>

#include <algorithm>
#include <assert.h>
#include <float.h>
#include <iostream>
#include <math.h>
#include <utility>
#include <vector>

//-*****************************************************************************
// DTEX CONVERTER EXPLANATION!
//-*****************************************************************************
// There are six possible code paths through converting the dtex data. They are:
// DeepOpacity, Continuous
// DeepOpacity, Discrete
// DeepAlpha, Continuous
// DeepAlpha, Discrete
// DeepRGBA, Continuous
// DeepRGBA, Discrete
// The newer dtex usages allow for other combinations of channels, but we
// are temporarily just supporting these six paths for sake of simplicity.
// We will eventually support arbitrary outputs and multiple views.
//
// We had an earlier version of this code which condensed these six code
// pathways into a single function, with templated functors to provide
// specific differing behavior for each of the different possibilities, and
// while it may have been slightly less code, the resulting loops were very hard
// to read and understand. Because each of the different pathways
// has some specific kernel of knowledge necessary to make it work, we
// chose instead to break each one out separately, to clearly expose the
// behavior in each case.
//
// The terminology for Density, Visibility, DeepOpacity, DepthRanges,
// along with explanations of the constants are provided in
// PxDeepUtils.h
//
// Our DeepOutPixel helper class is in PxDeepOutPixel.h
//
// Our DeepOutRow helper class is in PxDeepOutRow.h
//
// The Base Helper class, which loops over rows, and then pixels of those
// rows, is in PxBaseDeepHelper.h
//
// The DeepOpacity subclasses (discrete/continuous) of BaseDeepHelper are in
// PxOneChanDeepOpacity.h
//
// The DeepAlpha subclasses (discrete/continuous) of BaseDeepHelper are in
// PxOneChanDeepAlpha.h
//
// The DeepRgba subclasses (discrete/continuous) of BaseDeepHelper are in
// PxFourChanDeepAlpha.h
//
//-*****************************************************************************

namespace PxDeep
{

//-*****************************************************************************
template <typename RGBA_T>
void
ConvertDtexFile (
    const std::string& i_fileName,
    const std::string& i_outFileName,
    const Parameters&  i_params)
{
    int        dtexOpenError = DTEX_NOERR;
    DtexFile*  dtexFile      = NULL;
    DtexCache* dtexCache     = NULL;

    if (i_fileName.length ())
    {
        // We probably don't need 10000 tiles in the cache, but it's
        // fine for now.
        dtexCache = DtexCreateCache (10000, NULL);
        dtexOpenError =
            DtexOpenFile (i_fileName.c_str (), "rb", dtexCache, &dtexFile);
    }

    if (!dtexFile)
    {
        if (i_fileName.length () == 0) { PXDU_THROW ("no filename specified"); }
        else if (dtexOpenError != DTEX_NOERR)
        {
            PXDU_THROW (
                "error (" << dtexOpenError << " opening file: " << i_fileName);
        }
        else { PXDU_THROW ("missing file: " << i_fileName); }
    }

    // Just handling the first image in the Dtex file for now.
    DtexImage* image;
    DtexGetImageByIndex (dtexFile, 0, &image);

    float NP[16];
    DtexNP (image, NP);

    float Nl[16];
    DtexNl (image, Nl);

    int numDtexChans = DtexNumChan (image);

    if (numDtexChans != 1 && numDtexChans != 3 && numDtexChans != 4)
    {
        DtexClose (dtexFile);
        PXDU_THROW (
            "ERROR: only 1, 3 or 4 channel dtex files are supported.\n"
            << "Dtex file " << i_fileName << " contains " << numDtexChans
            << " channels.\n"
            << "In the case of 3 channels, the data is assumed to be\n"
            << "3-channel opacity, and for now, at least, only the\n"
            << "first channel is used, rather than all three.\n");
    }

    int w = DtexWidth (image);
    int h = DtexHeight (image);

    // Extract the parameters so we can conditionally modify them.
    Parameters params = i_params;

    // If we're reading anything more than 1 channel,
    // we can't (for now) assume it's a deepOpacity signal,
    // so we turn off the deepOpacity flag.
    // We also make sure RGB is turned on when RGB data is present.
    if (numDtexChans == 4)
    {
        params.deepOpacity = false;
        params.doRGB       = true;
    }

    // If we're discrete, we don't necessarily need to output deepBack.
    // However, from a pipeline point of view it is often preferable to have
    // all the channels actually in existence, even if they're redundant.
    // Nonetheless, if
    if (!params.discrete) { params.doDeepBack = true; }

    // Determine the output size
    int outWidth  = w;
    int outHeight = h;
    if (params.sideways)
    {
        outWidth  = h;
        outHeight = w;
    }

    // Create the windows.
    Imath::Box2i dataWindow (
        Imath::V2i (0, 0), Imath::V2i (outWidth - 1, outHeight - 1));
    Imath::Box2i displayWindow = dataWindow;

    // Create the header.
    Imf::Header header (
        displayWindow,
        dataWindow,
        1,
        Imath::V2f (0.0f, 0.0f),
        1,
        Imf::INCREASING_Y,
        Imf::ZIPS_COMPRESSION);

    // Add Np/Nl to the header.
    Imath::M44f NPm (
        NP[0],
        NP[1],
        NP[2],
        NP[3],
        NP[4],
        NP[5],
        NP[6],
        NP[7],
        NP[8],
        NP[9],
        NP[10],
        NP[11],
        NP[12],
        NP[13],
        NP[14],
        NP[15]);
    addWorldToNDC (header, NPm);

    Imath::M44f Nlm (
        Nl[0],
        Nl[1],
        Nl[2],
        Nl[3],
        Nl[4],
        Nl[5],
        Nl[6],
        Nl[7],
        Nl[8],
        Nl[9],
        Nl[10],
        Nl[11],
        Nl[12],
        Nl[13],
        Nl[14],
        Nl[15]);
    addWorldToCamera (header, Nlm);

    // Add channels to the header.

    // RGB
    if (params.doRGB)
    {
        header.channels ().insert ("R", Imf::Channel (ImfPixelType<RGBA_T> ()));
        header.channels ().insert ("G", Imf::Channel (ImfPixelType<RGBA_T> ()));
        header.channels ().insert ("B", Imf::Channel (ImfPixelType<RGBA_T> ()));
    }

    // A
    header.channels ().insert ("A", Imf::Channel (ImfPixelType<RGBA_T> ()));

    // Deep Front (z)
    header.channels ().insert ("Z", Imf::Channel (Imf::FLOAT));

    // Deep Back
    if (params.doDeepBack)
    {
        header.channels ().insert ("ZBack", Imf::Channel (Imf::FLOAT));
    }

    // Tell header to be deep!
    header.setType (Imf::DEEPSCANLINE);

    // Create output file, and fill it up!
    {
        Imf::DeepScanLineOutputFile outputFile (i_outFileName.c_str (), header);

        // Process deep pixels.
        if (numDtexChans < 4)
        {
            if (params.discrete)
            {
                if (params.deepOpacity)
                {
                    OneChanDeepOpacityDiscrete<RGBA_T> op (
                        dtexFile, numDtexChans, params);
                    op.processDeepBox (outputFile, dataWindow);
                }
                else
                {
                    OneChanDeepAlphaDiscrete<RGBA_T> op (
                        dtexFile, numDtexChans, params);
                    op.processDeepBox (outputFile, dataWindow);
                }
            }
            else
            {
                if (params.deepOpacity)
                {
                    OneChanDeepOpacityContinuous<RGBA_T> op (
                        dtexFile, numDtexChans, params);
                    op.processDeepBox (outputFile, dataWindow);
                }
                else
                {
                    OneChanDeepAlphaContinuous<RGBA_T> op (
                        dtexFile, numDtexChans, params);
                    op.processDeepBox (outputFile, dataWindow);
                }
            }
        }
        else
        {
            if (params.discrete)
            {
                FourChanDeepRgbaDiscrete<RGBA_T> op (
                    dtexFile, numDtexChans, params);
                op.processDeepBox (outputFile, dataWindow);
            }
            else
            {
                FourChanDeepRgbaContinuous<RGBA_T> op (
                    dtexFile, numDtexChans, params);
                op.processDeepBox (outputFile, dataWindow);
            }
        }
    } // Output file has gone out of scope, and should be closed!
    std::cout << "Wrote file: " << i_outFileName << std::endl;

    // Close it up!
    if (dtexFile) { DtexClose (dtexFile); }

    if (dtexCache) { DtexDestroyCache (dtexCache); }
}

} // End namespace PxDeep

//-*****************************************************************************
// Print Usage.
void
PrintUsage (const std::string& i_cmd, std::ostream& ostr)
{
    ostr << "DtexToExr: USAGE: " << i_cmd << std::endl
         << std::endl
         << "\t <inFileName.dtex>" << std::endl
         << std::endl
         << "\t <outFileName.exr>" << std::endl
         << std::endl
         << "\t --deepOpacity (DEFAULT) "
         << "\n\t\t (corresponds to output channels \'deepopacity\')"
         << std::endl
         << std::endl
         << "\t --deepAlpha "
         << "\n\t\t (corresponds to output channels \'a\' or \'rgba\')"
         << std::endl
         << std::endl
         << "\t --discrete (DEFAULT) "
         << "\n\t\t (corresponds to \'volumeinterpretation discrete\')"
         << std::endl
         << std::endl
         << "\t --continuous "
         << "\n\t\t (corresponds to \'volumeinterpretation continuous\')"
         << std::endl
         << std::endl
         << "\t --full "
         << "\n\t\t (use full 32-bit precision for non-depth (RGBA) data)"
         << std::endl
         << std::endl
         << "\t --half (DEFAULT) "
         << "\n\t\t (use half 16-bit precision for non-depth (RGBA) data)"
         << std::endl
         << std::endl
         << "\t --multRgb "
         << "\n\t\t (multiply RGB data by Alpha, "
         << "implying that source data is unpremultiplied)" << std::endl
         << std::endl
         << "\t --sideways [true|false]"
         << "\n\t\t (rotate image 90 deg clockwise)" << std::endl
         << std::endl
         << "\t --compressionError <float> (DEFAULT: 0.0f) "
         << "\n\t\t (compress dtex data before converting to deep exr)"
         << std::endl
         << std::endl
         << "\t --keepZeroAlpha "
         << "\n\t\t (don\'t discard samples with zero alpha)" << std::endl
         << std::endl
         << "\t --discardZeroAlpha (DEFAULT) "
         << "\n\t\t (discard samples with zero alpha)" << std::endl
         << std::endl
         << "\t -h,--h,--help "
         << "\n\t\t (print this message and exit)" << std::endl
         << std::endl;
}

//-*****************************************************************************
inline bool
GoodFileName (const std::string& i_fileName)
{
    return !((
        i_fileName.length () == 0 || i_fileName == "" || i_fileName[0] == '-'));
}

//-*****************************************************************************
// Argument parsing. So inelegant, but libarg is not widely supported, and
// boost::program_options is, well, boost. Also - not particularly awesome.
void
ParseArguments (
    int                 argc,
    char*               argv[],
    bool&               o_full,
    std::string&        o_dtexFileName,
    std::string&        o_exrFileName,
    PxDeep::Parameters& o_params)
{
    if (argc < 3)
    {
        PrintUsage (argv[0], std::cerr);
        exit (-1);
    }

    // Make our params match what the usage string says by default.
    o_full                           = false;
    o_dtexFileName                   = "";
    o_exrFileName                    = "";
    o_params.deepOpacity             = true;
    o_params.discrete                = true;
    o_params.multiplyColorByAlpha    = false;
    o_params.sideways                = false;
    o_params.discardZeroAlphaSamples = true;
    o_params.doDeepBack              = true;
    o_params.doRGB                   = false;
    o_params.compressionError        = 0.0f;

    // Eat up the args!
    for (int argi = 1; argi < argc; ++argi)
    {
        std::string arg = argv[argi];

        if (arg == "-h" || arg == "-help" || arg == "--help")
        {
            PrintUsage (argv[0], std::cerr);
            exit (-1);
        }
        else if (argi == 1)
        {
            if (!GoodFileName (arg))
            {
                PrintUsage (argv[0], std::cerr);
                PXDU_THROW ("Bad file name: " << arg);
            }
            o_dtexFileName = arg;
        }
        else if (argi == 2)
        {
            if (!GoodFileName (arg))
            {
                PrintUsage (argv[0], std::cerr);
                PXDU_THROW ("Bad file name: " << arg);
            }
            o_exrFileName = arg;
        }
        else if (arg == "--deepOpacity") { o_params.deepOpacity = true; }
        else if (arg == "--deepAlpha") { o_params.deepOpacity = false; }
        else if (arg == "--discrete") { o_params.discrete = true; }
        else if (arg == "--continuous") { o_params.discrete = false; }
        else if (arg == "--full") { o_full = true; }
        else if (arg == "--half") { o_full = false; }
        else if (arg == "--multRgb") { o_params.multiplyColorByAlpha = true; }
        else if (arg == "--sideways")
        {
            if (argi <= argc - 1)
            {
                std::cout << argv[argi + 1] << std::endl;
                if (strcmp (argv[argi + 1], "true") == 0)
                {
                    o_params.sideways = true;
                    ++argi;
                }
                else if (strcmp (argv[argi + 1], "false") == 0)
                {
                    o_params.sideways = false;
                    ++argi;
                }
                else if (argv[argi + 1][0] == '-')
                {
                    // found another arg. sideways has no parameter, which means sideways is on.
                    o_params.sideways = true;
                }
                else
                {
                    // sideways must be followed by "true", "false", another arg, or nothing.
                    PXDU_THROW (
                        "Invalid parameter for --sideways: " << argv[argi + 1]);
                }
            }
            else { o_params.sideways = true; }
        }
        else if (arg == "--compressionError")
        {
            if (argi >= argc - 1)
            {
                PrintUsage (argv[0], std::cerr);
                PXDU_THROW ("Unspecified compression error.");
            }

            o_params.compressionError = atof (argv[argi + 1]);
            ++argi;
        }
        else if (arg == "--keepZeroAlpha")
        {
            o_params.discardZeroAlphaSamples = false;
        }
        else if (arg == "--discardZeroAlpha")
        {
            o_params.discardZeroAlphaSamples = true;
        }
        else
        {
            PrintUsage (argv[0], std::cerr);
            PXDU_THROW ("Unknown command line argument: " << arg);
        }
    }
}

//-*****************************************************************************
// MAIN FUNCTION
//-*****************************************************************************
int
main (int argc, char* argv[])
{
    try
    {
        PxDeep::Parameters params;
        std::string        dtexFileName;
        std::string        exrFileName;
        bool               full;

        ParseArguments (argc, argv, full, dtexFileName, exrFileName, params);

        if (full)
        {
            PxDeep::ConvertDtexFile<float> (dtexFileName, exrFileName, params);
        }
        else
        {
            PxDeep::ConvertDtexFile<half> (dtexFileName, exrFileName, params);
        }
    }
    catch (std::exception& exc)
    {
        std::cerr << "ERROR EXCEPTION: " << exc.what () << std::endl;
        exit (-1);
    }
    catch (...)
    {
        std::cerr << "UNKNOWN EXCEPTION." << std::endl;
        exit (-1);
    }

    return 0;
}
