/*
 *  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"

#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),
    cropGuideParams(new procparams::CropGuideParams),
    colorParams(new procparams::ColorManagementParams),
    zoom(100),
    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),
    deviceScale(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 ()
{
    ImageSize fullSize = getFullImageSize();
    if (fullSize.width == 0) {
        return false;
    }
    return cropW == fullSize.width && cropH == fullSize.height;
}

double CropHandler::getFitCropZoom ()
{
    hidpi::DeviceSize device = getDeviceWSize();

    double z1 = (double) device.height / cropParams->h;
    double z2 = (double) device.width / cropParams->w;
    return z1 < z2 ? z1 : z2;
}

double CropHandler::getFitZoom ()
{
    if (ipc) {
        hidpi::DeviceSize device = getDeviceWSize();
        ImageSize imageSize = getFullImageSize();
        double z1 = (double) device.height / imageSize.height;
        double z2 = (double) device.width / imageSize.width;
        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;

    ImageSize imageSize = getFullImageSize();
    if (centerx == -1) {
        cax = imageSize.width / 2;
    } else {
        float distToAnchor = float(cax - centerx);
        distToAnchor = distToAnchor / newScale * oldScale;
        cax = centerx + int(distToAnchor);
    }

    if (centery == -1) {
        cay = imageSize.height / 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;

    hidpi::DeviceSize output = getDeviceWSize();
    if (zoom >= 1000) {
        cw = output.width * 1000 / zoom;
        ch = output.height * 1000 / zoom;
    } else {
        cw = output.width * (zoom / 10);
        ch = output.height * (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(hidpi::LogicalSize newSize)
{
    windowSize = newSize;
    hidpi::DeviceSize output = getDeviceWSize();

    if (zoom >= 1000) {
        cw = output.width * 1000 / zoom;
        ch = output.height * 1000 / zoom;
    } else {
        cw = output.width * (zoom / 10);
        ch = output.height * (zoom / 10);
    }

    compDim ();

    if (enabled) {
        update ();
    }
}

hidpi::DeviceSize CropHandler::getDeviceWSize() const
{
    hidpi::DeviceSize device = {};
    device.width = windowSize.width * deviceScale;
    device.height = windowSize.height * deviceScale;
    return device;
}

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

    compDim();

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

void CropHandler::moveAnchor(ImageCoord delta, bool update_)
{
    cax += delta.x;
    cay += delta.y;

    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

    ImageSize size = getFullImageSize();
    cax = size.width / 2;
    cay = size.height / 2;

    compDim();

    if (update_) {
        update();
    }
}

void CropHandler::setDeviceScale(int scale) {
    deviceScale = scale;
    compDim();
    update();
}

bool CropHandler::acceptUpdate(const PendingUpdate& pending) const
{
    bool accept =
        pending.x == cropX &&
        pending.y == cropY &&
        pending.width == cropW &&
        pending.height == cropH &&
        pending.scale == (zoom >= 1000 ? 1 : zoom / 10);
    return accept;
}

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

    cimg.lock ();

    *cropParams = cp;
    *cropGuideParams = guideParams;
    *colorParams = cmp;

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

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

    PendingUpdate received = {};
    received.x = ax;
    received.y = ay;
    received.width = aw;
    received.height = ah;
    received.scale = askip;

    if (acceptUpdate(received)) {
        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);
        pendingUpdate = received;

        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 (acceptUpdate(pendingUpdate)) {
                                // 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;

                                hidpi::DeviceSize window = getDeviceWSize();
                                if (imw > window.width) {
                                    imw = window.width;
                                }
                                if (imh > window.height) {
                                    imh = window.height;
                                }

                                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;
    // These values already accounted for HiDPI scaling.
    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;
}

ImageSize CropHandler::getSize() const
{
    return ImageSize(cropW, cropH);
}

ImageSize CropHandler::getFullImageSize() const
{
    if (ipc) {
        return ImageSize(ipc->getFullWidth(), ipc->getFullHeight());
    } else {
        return ImageSize(0, 0);
    }
}

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

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

    ImageSize imageSize = getFullImageSize();
    int fullW = imageSize.width;
    int fullH = imageSize.height;

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

    // Convert GUI logical pixel coord space to image pixel coord space
    hidpi::DeviceSize output = getDeviceWSize();

    double scaledWidth;
    double scaledHeight;
    if (zoom >= 1000) {
        scaledWidth = static_cast<double>(output.width) / (zoom/1000.0) + 0.5;
        scaledHeight = static_cast<double>(output.height) / (zoom/1000.0) + 0.5;
    } else {
        scaledWidth = static_cast<double>(output.width) * (zoom/10.0) + 0.5;
        scaledHeight = static_cast<double>(output.height) * (zoom/10.0) + 0.5;
    }

    cropX = cax - (scaledWidth/2);
    cropY = cay - (scaledHeight/2);
    cropW = scaledWidth;
    cropH = scaledHeight;

    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;
    }

    // Update display position
    ImageCoord scaledAnchor;
    if (zoom >= 1000) {
        scaledAnchor.x = cax * (zoom/1000);
        scaledAnchor.y = cay * (zoom/1000);
    } else {
        scaledAnchor.x = int(float(cax) / (float(zoom)/10.f));
        scaledAnchor.y = int(float(cay) / (float(zoom)/10.f));
    }

    // Go from image pixel space back to GUI logical pixel space
    scaledAnchor.x /= deviceScale;
    scaledAnchor.y /= deviceScale;

    int imgX = windowSize.width / 2 - scaledAnchor.x;
    if (imgX < 0) {
        imgX = 0;
    }
    int imgY = windowSize.height / 2 - scaledAnchor.y;
    if (imgY < 0) {
        imgY = 0;
    }
    displayHandler->setDisplayPosition(hidpi::LogicalCoord(imgX, imgY));
}
