/*
 *  This file is part of RawTherapee.
 *
 *  Copyright (c) 2004-2010 Gabor Horvath <hgabor@rawtherapee.com>
 *
 *  RawTherapee is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  RawTherapee is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with RawTherapee.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "crophandler.h"
#undef THREAD_PRIORITY_NORMAL

#include <cstring>
#include "guiutils.h"
#include "cropwindow.h"
#include "imagearea.h"

#include "../rtengine/dcrop.h"
#include "../rtengine/procparams.h"
#include "../rtengine/refreshmap.h"
#include "../rtengine/rt_math.h"

using namespace rtengine;

CropHandler::CropHandler() :
    cropParams(new procparams::CropParams),
    colorParams(new procparams::ColorManagementParams),
    zoom(100),
    ww(0),
    wh(0),
    cax(-1),
    cay(-1),
    cx(0),
    cy(0),
    cw(0),
    ch(0),
    cropX(0),
    cropY(0),
    cropW(0),
    cropH(0),
    enabled(false),
    cropimg_width(0),
    cropimg_height(0),
    cix(0),
    ciy(0),
    ciw(0),
    cih(0),
    cis(1),
    isLowUpdatePriority(false),
    ipc(nullptr),
    crop(nullptr),
    displayHandler(nullptr),
    redraw_needed(false),
    initial(false)
{
}

CropHandler::~CropHandler ()
{
    idle_register.destroy();

    if (ipc) {
        ipc->delSizeListener (this);
    }

    setEnabled (false);

    if (crop) {
        //crop->destroy ();
        delete crop; // will do the same than destroy, plus delete the object
        crop = nullptr;
    }
}

void CropHandler::setEditSubscriber (EditSubscriber* newSubscriber)
{
    (static_cast<rtengine::Crop *>(crop))->setEditSubscriber(newSubscriber);
}

void CropHandler::newImage (StagedImageProcessor* ipc_, bool isDetailWindow)
{

    ipc = ipc_;
    cx = 0;
    cy = 0;

    if (!ipc) {
        return;
    }

    EditDataProvider *editDataProvider = nullptr;
    CropWindow *cropWin = displayHandler ? static_cast<CropWindow*>(displayHandler) : nullptr;

    if (cropWin) {
        editDataProvider = cropWin->getImageArea();
    }

    crop = ipc->createCrop (editDataProvider, isDetailWindow);
    ipc->setSizeListener (this);
    crop->setListener (enabled ? this : nullptr);
    initial = true;
}

void CropHandler::sizeChanged(int x, int y, int ow, int oh)    // the ipc notifies it to keep track size changes like rotation
{
    compDim ();
}

bool CropHandler::isFullDisplay ()
{
    int w, h;
    getFullImageSize(w, h);
    if (!w) {
        return false;
    }
    return cropW == w && cropH == h;
}

double CropHandler::getFitCropZoom ()
{
    double z1 = (double) wh / cropParams->h;
    double z2 = (double) ww / cropParams->w;
    return z1 < z2 ? z1 : z2;
}

double CropHandler::getFitZoom ()
{

    if (ipc) {
        double z1 = (double) wh / ipc->getFullHeight ();
        double z2 = (double) ww / ipc->getFullWidth ();
        return z1 < z2 ? z1 : z2;
    } else {
        return 1.0;
    }
}

void CropHandler::setZoom (int z, int centerx, int centery)
{
    assert (ipc);

    int oldZoom = zoom;
    float oldScale = zoom >= 1000 ? float(zoom / 1000) : 10.f / float(zoom);
    float newScale = z >= 1000 ? float(z / 1000) : 10.f / float(z);

    int oldcax = cax;
    int oldcay = cay;

    if (centerx == -1) {
        cax = ipc->getFullWidth () / 2;
    } else {
        float distToAnchor = float(cax - centerx);
        distToAnchor = distToAnchor / newScale * oldScale;
        cax = centerx + int(distToAnchor);
    }

    if (centery == -1) {
        cay = ipc->getFullHeight () / 2;
    } else {
        float distToAnchor = float(cay - centery);
        distToAnchor = distToAnchor / newScale * oldScale;
        cay = centery + int(distToAnchor);
    }

    // maybe demosaic etc. if we cross the border to >100%
    bool needsFullRefresh = (z >= 1000 && zoom < 1000);

    zoom = z;

    if (zoom >= 1000) {
        cw = ww * 1000 / zoom;
        ch = wh * 1000 / zoom;
    } else {
        cw = ww * (zoom / 10);
        ch = wh * (zoom / 10);
    }

    cx = cax - cw / 2;
    cy = cay - ch / 2;


    int oldCropX = cropX;
    int oldCropY = cropY;
    int oldCropW = cropW;
    int oldCropH = cropH;

    compDim ();

    if (enabled && (oldZoom != zoom || oldcax != cax || oldcay != cay || oldCropX != cropX || oldCropY != cropY || oldCropW != cropW || oldCropH != cropH)) {
        if (needsFullRefresh && !ipc->getHighQualComputed()) {
            cropPixbuf.clear ();
            ipc->startProcessing(M_HIGHQUAL);
            ipc->setHighQualComputed();
        } else {
            update ();
        }
    }
}

float CropHandler::getZoomFactor ()
{
    if (zoom >= 1000) {
        return zoom / 1000;
    } else {
        return 10.f / (float)zoom;
    }
}


void CropHandler::setWSize (int w, int h)
{

    ww = w;
    wh = h;

    if (zoom >= 1000) {
        cw = ww * 1000 / zoom;
        ch = wh * 1000 / zoom;
    } else {
        cw = ww * (zoom / 10);
        ch = wh * (zoom / 10);
    }

    compDim ();

    if (enabled) {
        update ();
    }
}

void CropHandler::getWSize (int& w, int &h)
{

    w = ww;
    h = wh;
}

void CropHandler::getAnchorPosition (int& x, int& y)
{
    x = cax;
    y = cay;
}

void CropHandler::setAnchorPosition (int x, int y, bool update_)
{
    cax = x;
    cay = y;

    compDim ();

    if (enabled && update_) {
        update ();
    }
}

void CropHandler::moveAnchor (int deltaX, int deltaY, bool update_)
{
    cax += deltaX;
    cay += deltaY;

    compDim ();

    if (enabled && update_) {
        update ();
    }
}

void CropHandler::centerAnchor (bool update_)
{
    assert (ipc);

    // Computes the crop's size and position given the anchor's position and display size

    cax = ipc->getFullWidth() / 2;
    cay = ipc->getFullHeight() / 2;

    compDim ();

    if (enabled && update_) {
        update ();
    }
}

void CropHandler::getPosition (int& x, int& y)
{

    x = cropX;
    y = cropY;
}


void CropHandler::setDetailedCrop(
    IImage8* im,
    IImage8* imtrue,
    const rtengine::procparams::ColorManagementParams& cmp,
    const rtengine::procparams::CropParams& cp,
    int ax,
    int ay,
    int aw,
    int ah,
    int askip
)
{
    if (!enabled) {
        return;
    }

    cimg.lock ();

    *cropParams = cp;
    *colorParams = cmp;

    if (!cropimg.empty()) {
        cropimg.clear();
    }

    if (!cropimgtrue.empty()) {
        cropimgtrue.clear();
    }

    if (ax == cropX && ay == cropY && aw == cropW && ah == cropH && askip == (zoom >= 1000 ? 1 : zoom / 10)) {
        cropimg_width = im->getWidth ();
        cropimg_height = im->getHeight ();
        const std::size_t cropimg_size = 3 * cropimg_width * cropimg_height;
        cropimg.assign(im->getData(), im->getData() + cropimg_size);
        cropimgtrue.assign(imtrue->getData(), imtrue->getData() + cropimg_size);
        cix = ax;
        ciy = ay;
        ciw = aw;
        cih = ah;
        cis = askip;

        bool expected = false;

        if (redraw_needed.compare_exchange_strong(expected, true)) {
            idle_register.add(
                [this]() -> bool
                {
                    cimg.lock ();

                    if (redraw_needed.exchange(false)) {
                        cropPixbuf.clear ();

                        if (!enabled) {
                            cropimg.clear();
                            cropimgtrue.clear();
                            cimg.unlock ();
                            return false;
                        }

                        if (!cropimg.empty()) {
                            if (cix == cropX && ciy == cropY && ciw == cropW && cih == cropH && cis == (zoom >= 1000 ? 1 : zoom / 10)) {
                                // calculate final image size
                                float czoom = zoom >= 1000 ?
                                    zoom / 1000.f :
                                    float((zoom/10) * 10) / float(zoom);
                                int imw = cropimg_width * czoom;
                                int imh = cropimg_height * czoom;

                                if (imw > ww) {
                                    imw = ww;
                                }

                                if (imh > wh) {
                                    imh = wh;
                                }

                                Glib::RefPtr<Gdk::Pixbuf> tmpPixbuf = Gdk::Pixbuf::create_from_data (cropimg.data(), Gdk::COLORSPACE_RGB, false, 8, cropimg_width, cropimg_height, 3 * cropimg_width);
                                cropPixbuf = Gdk::Pixbuf::create (Gdk::COLORSPACE_RGB, false, 8, imw, imh);
                                tmpPixbuf->scale (cropPixbuf, 0, 0, imw, imh, 0, 0, czoom, czoom, Gdk::INTERP_TILES);
                                tmpPixbuf.clear ();

                                Glib::RefPtr<Gdk::Pixbuf> tmpPixbuftrue = Gdk::Pixbuf::create_from_data (cropimgtrue.data(), Gdk::COLORSPACE_RGB, false, 8, cropimg_width, cropimg_height, 3 * cropimg_width);
                                cropPixbuftrue = Gdk::Pixbuf::create (Gdk::COLORSPACE_RGB, false, 8, imw, imh);
                                tmpPixbuftrue->scale (cropPixbuftrue, 0, 0, imw, imh, 0, 0, czoom, czoom, Gdk::INTERP_TILES);
                                tmpPixbuftrue.clear ();
                            }

                            cropimg.clear();
                            cropimgtrue.clear();
                        }

                        cimg.unlock ();

                        if (displayHandler) {
                            displayHandler->cropImageUpdated ();

                            if (initial.exchange(false)) {
                                displayHandler->initialImageArrived ();
                            }
                        }
                    } else {
                        cimg.unlock();
                    }

                    return false;
                }
            );
        }
    }

    cimg.unlock ();
}

void CropHandler::getWindow(int& cwx, int& cwy, int& cww, int& cwh, int& cskip)
{
    cwx = cropX;
    cwy = cropY;
    cww = cropW;
    cwh = cropH;

    // hack: if called before first size allocation the size will be 0
    if (cww == 0) {
        cww = 10;
    }

    if (cwh == 0) {
        cwh = 32;
    }

    cskip = zoom >= 1000 ? 1 : zoom/10;
}

void CropHandler::update ()
{

    if (crop && enabled) {
//        crop->setWindow (cropX, cropY, cropW, cropH, zoom>=1000 ? 1 : zoom); --> we use the "getWindow" hook instead of setting the size before
        crop->setListener (this);
        cropPixbuf.clear ();

        // To save threads, try to mark "needUpdate" without a thread first
        if (crop->tryUpdate()) {
            if (isLowUpdatePriority) {
                Glib::Thread::create(sigc::mem_fun(*crop, &DetailedCrop::fullUpdate), 0, false, true, Glib::THREAD_PRIORITY_LOW);
            } else {
                Glib::Thread::create(sigc::mem_fun(*crop, &DetailedCrop::fullUpdate), false );
            }
        }
    }
}

void CropHandler::setEnabled (bool e)
{

    enabled = e;

    if (!enabled) {
        if (crop) {
            crop->setListener (nullptr);
        }

        cimg.lock();
        cropimg.clear();
        cropimgtrue.clear();
        cropPixbuf.clear();
        cimg.unlock();
    } else {
        update ();
    }
}

bool CropHandler::getEnabled ()
{

    return enabled;
}

void CropHandler::colorPick (const rtengine::Coord &pickerPos, float &r, float &g, float &b, float &rpreview, float &gpreview, float &bpreview, LockableColorPicker::Size size)
{

    if (!cropPixbuf || !cropPixbuftrue) {
        r = g = b = 0.f;
        rpreview = gpreview = bpreview = 0.f;
        return;
    }

    int xSize = (int)size;
    int ySize = (int)size;
    int pixbufW = cropPixbuftrue->get_width();
    int pixbufH = cropPixbuftrue->get_height();
    rtengine::Coord topLeftPos(pickerPos.x - xSize/2, pickerPos.y - ySize/2);

    if (topLeftPos.x > pixbufW || topLeftPos.y > pixbufH || topLeftPos.x + xSize < 0 || topLeftPos.y + ySize < 0) {
        return;
    }

    // Store the position of the center of the picker
    int radius = (int)size / 2;

    // X/Width clip
    if (topLeftPos.x < 0) {
        xSize += topLeftPos.x;
        topLeftPos.x = 0;
    }
    if (topLeftPos.x + xSize > pixbufW) {
        xSize = pixbufW - topLeftPos.x;
    }
    // Y/Height clip
    if (topLeftPos.y < 0) {
        ySize += topLeftPos.y;
        topLeftPos.y = 0;
    }
    if (topLeftPos.y + ySize > pixbufH) {
        ySize = pixbufH - topLeftPos.y;
    }

    // Accumulating the data
    std::uint32_t r2=0, g2=0, b2=0;
    std::uint32_t count = 0;
    const guint8* data = cropPixbuftrue->get_pixels();
    for (int j = topLeftPos.y ; j < topLeftPos.y + ySize ; ++j) {
        const guint8* data2 = data + cropPixbuftrue->get_rowstride()*j;
        for (int i = topLeftPos.x ; i < topLeftPos.x + xSize ; ++i) {
            const guint8* data3 = data2 + i*3;
            rtengine::Coord currPos(i, j);
            rtengine::Coord delta = pickerPos - currPos;
            rtengine::PolarCoord p(delta);
            if (p.radius <= radius) {
                r2 += *data3;
                g2 += *(data3+1);
                b2 += *(data3+2);
                ++count;
            }
        }
    }

    count = std::max(1u, count);
    // Averaging
    r = (float)r2 / (float)count / 255.f;
    g = (float)g2 / (float)count / 255.f;
    b = (float)b2 / (float)count / 255.f;

    // Accumulating the data
    r2=0, g2=0, b2=0;
    count = 0;
    data = cropPixbuf->get_pixels();
    for (int j = topLeftPos.y ; j < topLeftPos.y + ySize ; ++j) {
        const guint8* data2 = data + cropPixbuf->get_rowstride()*j;
        for (int i = topLeftPos.x ; i < topLeftPos.x + xSize ; ++i) {
            const guint8* data3 = data2 + i*3;
            rtengine::Coord currPos(i, j);
            rtengine::Coord delta = pickerPos - currPos;
            rtengine::PolarCoord p(delta);
            if (p.radius <= radius) {
                r2 += *data3;
                g2 += *(data3+1);
                b2 += *(data3+2);
                ++count;
            }
        }
    }

    count = std::max(1u, count);
    // Averaging
    rpreview = (float)r2 / (float)count / 255.f;
    gpreview = (float)g2 / (float)count / 255.f;
    bpreview = (float)b2 / (float)count / 255.f;
}

void CropHandler::getSize (int& w, int& h)
{

    w = cropW;
    h = cropH;
}

void CropHandler::getFullImageSize (int& w, int& h)
{
    if (ipc) {
        w = ipc->getFullWidth ();
        h = ipc->getFullHeight ();
    } else {
        w = h = 0;
    }
}

void CropHandler::compDim ()
{
    assert (ipc && displayHandler);

    // Computes the crop's size and position given the anchor's position and display size

    int fullW = ipc->getFullWidth();
    int fullH = ipc->getFullHeight();
    int imgX = -1, imgY = -1;
    //int scaledFullW, scaledFullH;
    int scaledCAX, scaledCAY;
    int wwImgSpace;
    int whImgSpace;

    cax = rtengine::LIM(cax, 0, fullW-1);
    cay = rtengine::LIM(cay, 0, fullH-1);

    if (zoom >= 1000) {
        wwImgSpace = int(float(ww) / float(zoom/1000) + 0.5f);
        whImgSpace = int(float(wh) / float(zoom/1000) + 0.5f);
        //scaledFullW = fullW * (zoom/1000);
        //scaledFullH = fullH * (zoom/1000);
        scaledCAX = cax * (zoom/1000);
        scaledCAY = cay * (zoom/1000);
    } else {
        wwImgSpace = int(float(ww) * (float(zoom)/10.f) + 0.5f);
        whImgSpace = int(float(wh) * (float(zoom)/10.f) + 0.5f);
        //scaledFullW = fullW / zoom;
        //scaledFullH = fullH / zoom;
        scaledCAX = int(float(cax) / (float(zoom)/10.f));
        scaledCAY = int(float(cay) / (float(zoom)/10.f));
    }

    imgX = ww / 2 - scaledCAX;
    if (imgX < 0) {
        imgX = 0;
    }
    imgY = wh / 2 - scaledCAY;
    if (imgY < 0) {
        imgY = 0;
    }

    cropX = cax - (wwImgSpace/2);
    cropY = cay - (whImgSpace/2);
    cropW = wwImgSpace;
    cropH = whImgSpace;

    if (cropX + cropW > fullW) {
        cropW = fullW - cropX;
    }

    if (cropY + cropH > fullH) {
        cropH = fullH - cropY;
    }

    if (cropX < 0) {
        cropW += cropX;
        cropX = 0;
    }

    if (cropY < 0) {
        cropH += cropY;
        cropY = 0;
    }

    // Should be good already, but this will correct eventual rounding error

    if (cropW > fullW) {
        cropW = fullW;
    }

    if (cropH > fullH) {
        cropH = fullH;
    }

    displayHandler->setDisplayPosition(imgX, imgY);
}
