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

#ifdef NDEBUG
#    undef NDEBUG
#endif

#include "IlmThread.h"
#include "ImathRandom.h"
#include <ImfArray.h>
#include <ImfChannelList.h>
#include <ImfInputFile.h>
#include <ImfRgbaFile.h>
#include <ImfThreading.h>
#include <ImfTileDescriptionAttribute.h>
#include <ImfTiledInputFile.h>
#include <ImfTiledOutputFile.h>
#include <ImfTiledRgbaFile.h>
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string>
#include <vector>

namespace IMF = OPENEXR_IMF_NAMESPACE;
using namespace IMF;
using namespace std;
using namespace IMATH_NAMESPACE;

namespace
{

void
fillPixels (
    Array2D<unsigned int>& pi,
    Array2D<half>&         ph,
    Array2D<float>&        pf,
    int                    width,
    int                    height)
{
    for (int y = 0; y < height; ++y)
        for (int x = 0; x < width; ++x)
        {
            pi[y][x] = x % 100 + 100 * (y % 100);
            ph[y][x] = sin (double (x)) + sin (y * 0.5);
            pf[y][x] = sin (double (y)) + sin (x * 0.5);
        }
}

void
writeRead (
    const Array2D<unsigned int>& pi1,
    const Array2D<half>&         ph1,
    const Array2D<float>&        pf1,
    const char                   fileName[],
    LineOrder                    lorder,
    int                          width,
    int                          height,
    int                          xSize,
    int                          ySize,
    int                          xOffset,
    int                          yOffset,
    Compression                  comp,
    LevelMode                    mode,
    LevelRoundingMode            rmode,
    bool                         fillChannel)
{
    //
    // Write the pixel data in pi1, ph1 and ph2 to a tiled
    // image file using the specified parameters.
    // Read the pixel data back from the file using the scanline
    // interface one scanline at a time, and verify that the data did
    // not change.
    // For MIPMAP and RIPMAP_LEVELS, the lower levels of the images
    // are filled in cropped versions of the level(0,0) image,
    // i.e. no filtering is done.
    //

    cout << "levelMode " << mode << ", roundingMode " << rmode
         << ", line order " << lorder << ",\ntileSize " << xSize << "x" << ySize
         << ", xOffset " << xOffset << ", yOffset " << yOffset << endl;

    Header hdr (
        (Box2i (
            V2i (0, 0), // display window
            V2i (width - 1, height - 1))),
        (Box2i (
            V2i (xOffset, yOffset), // data window
            V2i (xOffset + width - 1, yOffset + height - 1))));
    hdr.lineOrder ()   = lorder;
    hdr.compression () = comp;

    hdr.channels ().insert ("I", Channel (IMF::UINT));
    hdr.channels ().insert ("H", Channel (IMF::HALF));
    hdr.channels ().insert ("F", Channel (IMF::FLOAT));

    hdr.setTileDescription (TileDescription (xSize, ySize, mode, rmode));
    {
        FrameBuffer fb;

        fb.insert (
            "I", // name
            Slice (
                IMF::UINT,                        // type
                (char*) &pi1[-yOffset][-xOffset], // base
                sizeof (pi1[0][0]),               // xStride
                sizeof (pi1[0][0]) * width)       // yStride
        );

        fb.insert (
            "H", // name
            Slice (
                IMF::HALF,                        // type
                (char*) &ph1[-yOffset][-xOffset], // base
                sizeof (ph1[0][0]),               // xStride
                sizeof (ph1[0][0]) * width)       // yStride
        );

        fb.insert (
            "F", // name
            Slice (
                IMF::FLOAT,                       // type
                (char*) &pf1[-yOffset][-xOffset], // base
                sizeof (pf1[0][0]),               // xStride
                sizeof (pf1[0][0]) * width)       // yStride
        );

        cout << " writing" << flush;

        remove (fileName);
        TiledOutputFile out (fileName, hdr);
        out.setFrameBuffer (fb);

        int startTileY = -1;
        int endTileY   = -1;
        int dy;

        switch (mode)
        {
            case ONE_LEVEL: {
                if (lorder == DECREASING_Y)
                {
                    startTileY = out.numYTiles () - 1;
                    endTileY   = -1;

                    dy = -1;
                }
                else
                {
                    startTileY = 0;
                    endTileY   = out.numYTiles ();

                    dy = 1;
                }

                for (int tileY = startTileY; tileY != endTileY; tileY += dy)
                    for (int tileX = 0; tileX < out.numXTiles (); ++tileX)
                        out.writeTile (tileX, tileY);
            }
            break;

            case MIPMAP_LEVELS: {
                if (lorder == DECREASING_Y)
                {
                    endTileY = -1;
                    dy       = -1;
                }
                else
                {
                    startTileY = 0;
                    dy         = 1;
                }

                for (int level = 0; level < out.numLevels (); ++level)
                {
                    if (lorder == DECREASING_Y)
                        startTileY = out.numYTiles (level) - 1;
                    else
                        endTileY = out.numYTiles (level);

                    for (int tileY = startTileY; tileY != endTileY; tileY += dy)
                        for (int tileX = 0; tileX < out.numXTiles (level);
                             ++tileX)
                            out.writeTile (tileX, tileY, level);
                }
            }
            break;

            case RIPMAP_LEVELS: {
                for (int ylevel = 0; ylevel < out.numYLevels (); ++ylevel)
                {
                    if (lorder == DECREASING_Y)
                    {
                        startTileY = out.numYTiles (ylevel) - 1;
                        endTileY   = -1;

                        dy = -1;
                    }
                    else
                    {
                        startTileY = 0;
                        endTileY   = out.numYTiles (ylevel);

                        dy = 1;
                    }

                    for (int xlevel = 0; xlevel < out.numXLevels (); ++xlevel)
                    {
                        for (int tileY = startTileY; tileY != endTileY;
                             tileY += dy)
                            for (int tileX = 0; tileX < out.numXTiles (xlevel);
                                 ++tileX)
                                out.writeTile (tileX, tileY, xlevel, ylevel);
                    }
                }
            }
            break;
            case NUM_LEVELMODES:
            default:
                std::cerr << "Invalid tile mode " << int (mode) << std::endl;
                break;
        }
    }

    {
        cout << " reading INCREASING_Y" << flush;

        InputFile in (fileName);

        const Box2i& dw  = in.header ().dataWindow ();
        int          w   = dw.max.x - dw.min.x + 1;
        int          h   = dw.max.y - dw.min.y + 1;
        int          dwx = dw.min.x;
        int          dwy = dw.min.y;

        Array2D<unsigned int> pi2 (h, w);
        Array2D<half>         ph2 (h, w);
        Array2D<float>        pf2 (h, w);

        Array2D<unsigned int> fi2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<half>         fh2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<float>        ff2 (fillChannel ? h : 1, fillChannel ? w : 1);

        const unsigned int fillInt   = 12;
        const half         fillHalf  = 4.5;
        const float        fillFloat = M_PI;

        FrameBuffer fb;

        fb.insert (
            "I", // name
            Slice (
                IMF::UINT,                // type
                (char*) &pi2[-dwy][-dwx], // base
                sizeof (pi2[0][0]),       // xStride
                sizeof (pi2[0][0]) * w)   // yStride
        );

        fb.insert (
            "H", // name
            Slice (
                IMF::HALF,                // type
                (char*) &ph2[-dwy][-dwx], // base
                sizeof (ph2[0][0]),       // xStride
                sizeof (ph2[0][0]) * w)   // yStride
        );

        fb.insert (
            "F", // name
            Slice (
                IMF::FLOAT,               // type
                (char*) &pf2[-dwy][-dwx], // base
                sizeof (pf2[0][0]),       // xStride
                sizeof (pf2[0][0]) * w)   // yStride
        );

        if (fillChannel)
        {
            fb.insert (
                "FI", // name
                Slice (
                    IMF::UINT,                // type
                    (char*) &fi2[-dwy][-dwx], // base
                    sizeof (fi2[0][0]),       // xStride
                    sizeof (fi2[0][0]) * w,
                    1,
                    1,
                    fillInt) // yStride
            );

            fb.insert (
                "FH", // name
                Slice (
                    IMF::HALF,                // type
                    (char*) &fh2[-dwy][-dwx], // base
                    sizeof (fh2[0][0]),       // xStride
                    sizeof (fh2[0][0]) * w,
                    1,
                    1,
                    fillHalf) // yStride
            );

            fb.insert (
                "FF", // name
                Slice (
                    IMF::FLOAT,               // type
                    (char*) &ff2[-dwy][-dwx], // base
                    sizeof (ff2[0][0]),       // xStride
                    sizeof (ff2[0][0]) * w,
                    1,
                    1,
                    fillFloat) // yStride
            );
        }

        in.setFrameBuffer (fb);
        for (int y = dw.min.y; y <= dw.max.y; ++y)
            in.readPixels (y);

        cout << " comparing" << flush;

        assert (in.header ().displayWindow () == hdr.displayWindow ());
        assert (in.header ().dataWindow () == hdr.dataWindow ());
        assert (in.header ().pixelAspectRatio () == hdr.pixelAspectRatio ());
        assert (
            in.header ().screenWindowCenter () == hdr.screenWindowCenter ());
        assert (in.header ().screenWindowWidth () == hdr.screenWindowWidth ());
        assert (in.header ().lineOrder () == hdr.lineOrder ());
        assert (in.header ().compression () == hdr.compression ());

        ChannelList::ConstIterator hi = hdr.channels ().begin ();
        ChannelList::ConstIterator ii = in.header ().channels ().begin ();

        while (hi != hdr.channels ().end ())
        {
            assert (!strcmp (hi.name (), ii.name ()));
            assert (hi.channel ().type == ii.channel ().type);
            assert (hi.channel ().xSampling == ii.channel ().xSampling);
            assert (hi.channel ().ySampling == ii.channel ().ySampling);

            ++hi;
            ++ii;
        }

        assert (ii == in.header ().channels ().end ());

        for (int y = 0; y < h; ++y)
        {
            for (int x = 0; x < w; ++x)
            {
                assert (pi1[y][x] == pi2[y][x]);
                assert (ph1[y][x] == ph2[y][x]);
                assert (pf1[y][x] == pf2[y][x]);

                if (fillChannel)
                {
                    assert (fi2[y][x] == fillInt);
                    assert (fh2[y][x] == fillHalf);
                    assert (ff2[y][x] == fillFloat);
                }
            }
        }
    }

    {
        cout << endl << "         reading DECREASING_Y" << flush;

        InputFile in (fileName);

        const Box2i& dw  = in.header ().dataWindow ();
        int          w   = dw.max.x - dw.min.x + 1;
        int          h   = dw.max.y - dw.min.y + 1;
        int          dwx = dw.min.x;
        int          dwy = dw.min.y;

        Array2D<unsigned int> pi2 (h, w);
        Array2D<half>         ph2 (h, w);
        Array2D<float>        pf2 (h, w);

        Array2D<unsigned int> fi2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<half>         fh2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<float>        ff2 (fillChannel ? h : 1, fillChannel ? w : 1);

        FrameBuffer fb;

        fb.insert (
            "I", // name
            Slice (
                IMF::UINT,                // type
                (char*) &pi2[-dwy][-dwx], // base
                sizeof (pi2[0][0]),       // xStride
                sizeof (pi2[0][0]) * w)   // yStride
        );

        fb.insert (
            "H", // name
            Slice (
                IMF::HALF,                // type
                (char*) &ph2[-dwy][-dwx], // base
                sizeof (ph2[0][0]),       // xStride
                sizeof (ph2[0][0]) * w)   // yStride
        );

        fb.insert (
            "F", // name
            Slice (
                IMF::FLOAT,               // type
                (char*) &pf2[-dwy][-dwx], // base
                sizeof (pf2[0][0]),       // xStride
                sizeof (pf2[0][0]) * w)   // yStride
        );
        const unsigned int fillInt   = 21;
        const half         fillHalf  = 42;
        const float        fillFloat = 2.8;

        if (fillChannel)
        {
            fb.insert (
                "FI", // name
                Slice (
                    IMF::UINT,                // type
                    (char*) &fi2[-dwy][-dwx], // base
                    sizeof (fi2[0][0]),       // xStride
                    sizeof (fi2[0][0]) * w,
                    1,
                    1,
                    fillInt) // yStride
            );

            fb.insert (
                "FH", // name
                Slice (
                    IMF::HALF,                // type
                    (char*) &fh2[-dwy][-dwx], // base
                    sizeof (fh2[0][0]),       // xStride
                    sizeof (fh2[0][0]) * w,
                    1,
                    1,
                    fillHalf) // yStride
            );

            fb.insert (
                "FF", // name
                Slice (
                    IMF::FLOAT,               // type
                    (char*) &ff2[-dwy][-dwx], // base
                    sizeof (ff2[0][0]),       // xStride
                    sizeof (ff2[0][0]) * w,
                    1,
                    1,
                    fillFloat) // yStride
            );
        }

        in.setFrameBuffer (fb);
        for (int y = dw.max.y; y >= dw.min.y; --y)
            in.readPixels (y);

        cout << " comparing" << flush;

        assert (in.header ().displayWindow () == hdr.displayWindow ());
        assert (in.header ().dataWindow () == hdr.dataWindow ());
        assert (in.header ().pixelAspectRatio () == hdr.pixelAspectRatio ());
        assert (
            in.header ().screenWindowCenter () == hdr.screenWindowCenter ());
        assert (in.header ().screenWindowWidth () == hdr.screenWindowWidth ());
        assert (in.header ().lineOrder () == hdr.lineOrder ());
        assert (in.header ().compression () == hdr.compression ());

        ChannelList::ConstIterator hi = hdr.channels ().begin ();
        ChannelList::ConstIterator ii = in.header ().channels ().begin ();

        while (hi != hdr.channels ().end ())
        {
            assert (!strcmp (hi.name (), ii.name ()));
            assert (hi.channel ().type == ii.channel ().type);
            assert (hi.channel ().xSampling == ii.channel ().xSampling);
            assert (hi.channel ().ySampling == ii.channel ().ySampling);

            ++hi;
            ++ii;
        }

        assert (ii == in.header ().channels ().end ());

        for (int y = 0; y < h; ++y)
        {
            for (int x = 0; x < w; ++x)
            {
                assert (pi1[y][x] == pi2[y][x]);
                assert (ph1[y][x] == ph2[y][x]);
                assert (pf1[y][x] == pf2[y][x]);
                if (fillChannel)
                {
                    assert (fi2[y][x] == fillInt);
                    assert (fh2[y][x] == fillHalf);
                    assert (ff2[y][x] == fillFloat);
                }
            }
        }
    }

    {
        cout << endl
             << "         reading INCREASING_Y "
                "(new frame buffer on every line)"
             << flush;

        InputFile in (fileName);

        const Box2i& dw  = in.header ().dataWindow ();
        int          w   = dw.max.x - dw.min.x + 1;
        int          h   = dw.max.y - dw.min.y + 1;
        int          dwx = dw.min.x;
        int          dwy = dw.min.y;

        Array2D<unsigned int> pi2 (h, w);
        Array2D<half>         ph2 (h, w);
        Array2D<float>        pf2 (h, w);

        Array2D<unsigned int> fi2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<half>         fh2 (fillChannel ? h : 1, fillChannel ? w : 1);
        Array2D<float>        ff2 (fillChannel ? h : 1, fillChannel ? w : 1);

        const unsigned int fillInt   = 81;
        const half         fillHalf  = 0.5;
        const float        fillFloat = 7.8;

        for (int y = dw.min.y; y <= dw.max.y; ++y)
        {
            FrameBuffer fb;

            fb.insert (
                "I", // name
                Slice (
                    IMF::UINT,                   // type
                    (char*) &pi2[y - dwy][-dwx], // base
                    sizeof (pi2[0][0]),          // xStride
                    0)                           // yStride
            );

            fb.insert (
                "H", // name
                Slice (
                    IMF::HALF,                   // type
                    (char*) &ph2[y - dwy][-dwx], // base
                    sizeof (ph2[0][0]),          // xStride
                    0)                           // yStride
            );

            fb.insert (
                "F", // name
                Slice (
                    IMF::FLOAT,                  // type
                    (char*) &pf2[y - dwy][-dwx], // base
                    sizeof (pf2[0][0]),          // xStride
                    0)                           // yStride
            );

            if (fillChannel)
            {
                fb.insert (
                    "FI", // name
                    Slice (
                        IMF::UINT,                   // type
                        (char*) &fi2[y - dwy][-dwx], // base
                        sizeof (fi2[0][0]),          // xStride
                        0,
                        1,
                        1,
                        fillInt) // yStride
                );

                fb.insert (
                    "FH", // name
                    Slice (
                        IMF::HALF,                   // type
                        (char*) &fh2[y - dwy][-dwx], // base
                        sizeof (fh2[0][0]),          // xStride
                        0,
                        1,
                        1,
                        fillHalf) // yStride
                );

                fb.insert (
                    "FF", // name
                    Slice (
                        IMF::FLOAT,                  // type
                        (char*) &ff2[y - dwy][-dwx], // base
                        sizeof (ff2[0][0]),          // xStride
                        0,
                        1,
                        1,
                        fillFloat) // yStride
                );
            }

            in.setFrameBuffer (fb);
            in.readPixels (y);
        }

        cout << " comparing" << flush;

        assert (in.header ().displayWindow () == hdr.displayWindow ());
        assert (in.header ().dataWindow () == hdr.dataWindow ());
        assert (in.header ().pixelAspectRatio () == hdr.pixelAspectRatio ());
        assert (
            in.header ().screenWindowCenter () == hdr.screenWindowCenter ());
        assert (in.header ().screenWindowWidth () == hdr.screenWindowWidth ());
        assert (in.header ().lineOrder () == hdr.lineOrder ());
        assert (in.header ().compression () == hdr.compression ());

        ChannelList::ConstIterator hi = hdr.channels ().begin ();
        ChannelList::ConstIterator ii = in.header ().channels ().begin ();

        while (hi != hdr.channels ().end ())
        {
            assert (!strcmp (hi.name (), ii.name ()));
            assert (hi.channel ().type == ii.channel ().type);
            assert (hi.channel ().xSampling == ii.channel ().xSampling);
            assert (hi.channel ().ySampling == ii.channel ().ySampling);

            ++hi;
            ++ii;
        }

        assert (ii == in.header ().channels ().end ());

        for (int y = 0; y < h; ++y)
        {
            for (int x = 0; x < w; ++x)
            {
                assert (pi1[y][x] == pi2[y][x]);
                assert (ph1[y][x] == ph2[y][x]);
                assert (pf1[y][x] == pf2[y][x]);
                if (fillChannel)
                {
                    assert (fi2[y][x] == fillInt);
                    assert (fh2[y][x] == fillHalf);
                    assert (ff2[y][x] == fillFloat);
                }
            }
        }
    }

    remove (fileName);
    cout << endl;
}

void
writeRead (
    const std::string&           tempDir,
    const Array2D<unsigned int>& pi,
    const Array2D<half>&         ph,
    const Array2D<float>&        pf,
    int                          W,
    int                          H,
    LineOrder                    lorder,
    Compression                  comp,
    LevelRoundingMode            rmode,
    int                          dx,
    int                          dy,
    int                          xSize,
    int                          ySize)
{
    std::string filename = tempDir + "imf_test_scanline_api.exr";

    writeRead (
        pi,
        ph,
        pf,
        filename.c_str (),
        lorder,
        W,
        H,
        xSize,
        ySize,
        dx,
        dy,
        comp,
        ONE_LEVEL,
        rmode,
        false);
    writeRead (
        pi,
        ph,
        pf,
        filename.c_str (),
        lorder,
        W,
        H,
        xSize,
        ySize,
        dx,
        dy,
        comp,
        MIPMAP_LEVELS,
        rmode,
        false);
    writeRead (
        pi,
        ph,
        pf,
        filename.c_str (),
        lorder,
        W,
        H,
        xSize,
        ySize,
        dx,
        dy,
        comp,
        RIPMAP_LEVELS,
        rmode,
        false);
    writeRead (
        pi,
        ph,
        pf,
        filename.c_str (),
        lorder,
        W,
        H,
        xSize,
        ySize,
        dx,
        dy,
        comp,
        ONE_LEVEL,
        rmode,
        true);
}

} // namespace

void
testScanLineApi (const std::string& tempDir)
{
    try
    {
        cout << "Testing the scanline API for tiled files" << endl;

        const int W  = 48;
        const int H  = 81;
        const int DX = -17;
        const int DY = -29;

        Array2D<unsigned int> pi (H, W);
        Array2D<half>         ph (H, W);
        Array2D<float>        pf (H, W);
        fillPixels (pi, ph, pf, W, H);

        int maxThreads = ILMTHREAD_NAMESPACE::supportsThreads () ? 3 : 0;

        for (int n = 0; n <= maxThreads; ++n)
        {
            if (ILMTHREAD_NAMESPACE::supportsThreads ())
            {
                setGlobalThreadCount (n);
                cout << "\nnumber of threads: " << globalThreadCount () << endl;
            }

            for (int lorder = 0; lorder < NUM_LINEORDERS; ++lorder)
            {
                for (int rmode = 0; rmode < NUM_ROUNDINGMODES; ++rmode)
                {
                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        0,
                        0,
                        1,
                        1);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        DX,
                        DY,
                        1,
                        1);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        0,
                        0,
                        24,
                        26);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        DX,
                        DY,
                        24,
                        26);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        0,
                        0,
                        48,
                        81);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        DX,
                        DY,
                        48,
                        81);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        0,
                        0,
                        128,
                        96);

                    writeRead (
                        tempDir,
                        pi,
                        ph,
                        pf,
                        W,
                        H,
                        LineOrder (lorder),
                        ZIP_COMPRESSION,
                        LevelRoundingMode (rmode),
                        DX,
                        DY,
                        128,
                        96);
                }
            }
        }

        cout << "ok\n" << endl;
    }
    catch (const std::exception& e)
    {
        cerr << "ERROR -- caught exception: " << e.what () << endl;
        assert (false);
    }
}
