/*
 *  This file is part of RawTherapee.
 */
#include "gradient.h"

#include "editwidgets.h"
#include "rtimage.h"

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

using namespace rtengine;
using namespace rtengine::procparams;

namespace
{

enum GeometryIndex {
    H_LINE,
    V_LINE,
    FEATHER_LINE_1,
    FEATHER_LINE_2,
    CENTER_CIRCLE,
};

}

const Glib::ustring Gradient::TOOL_NAME = "gradient";

Gradient::Gradient () : FoldableToolPanel(this, TOOL_NAME, M("TP_GRADIENT_LABEL"), false, true), EditSubscriber(ET_OBJECTS), lastObject(-1), draggedPointOldAngle(-1000.)
{

    editHBox = Gtk::manage (new Gtk::Box());
    edit = Gtk::manage (new Gtk::ToggleButton());
    edit->get_style_context()->add_class("independent");
    edit->add (*Gtk::manage (new RTImage ("crosshair-adjust", Gtk::ICON_SIZE_BUTTON)));
    edit->set_tooltip_text(M("EDIT_OBJECT_TOOLTIP"));
    editConn = edit->signal_toggled().connect( sigc::mem_fun(*this, &Gradient::editToggled) );
    editHBox->pack_start(*edit, Gtk::PACK_SHRINK, 0);
    pack_start (*editHBox, Gtk::PACK_SHRINK, 0);

    strength = Gtk::manage (new Adjuster (M("TP_GRADIENT_STRENGTH"), -5, 5, 0.01, 0));
    strength->set_tooltip_text (M("TP_GRADIENT_STRENGTH_TOOLTIP"));
    strength->setAdjusterListener (this);

    degree = Gtk::manage (new Adjuster (M("TP_GRADIENT_DEGREE"), -180, 180, 1, 0));
    degree->set_tooltip_text (M("TP_GRADIENT_DEGREE_TOOLTIP"));
    degree->setAdjusterListener (this);

    feather = Gtk::manage (new Adjuster (M("TP_GRADIENT_FEATHER"), 0, 100, 1, 25));
    feather->set_tooltip_text (M("TP_GRADIENT_FEATHER_TOOLTIP"));
    feather->setAdjusterListener (this);

    centerX = Gtk::manage (new Adjuster (M("TP_GRADIENT_CENTER_X"), -100, 100, 1, 0));
    centerX->set_tooltip_text (M("TP_GRADIENT_CENTER_X_TOOLTIP"));
    centerX->setAdjusterListener (this);

    centerY = Gtk::manage (new Adjuster (M("TP_GRADIENT_CENTER_Y"), -100, 100, 1, 0));
    centerY->set_tooltip_text (M("TP_GRADIENT_CENTER_Y_TOOLTIP"));
    centerY->setAdjusterListener (this);

    pack_start (*strength, Gtk::PACK_SHRINK, 0);
    pack_start (*degree, Gtk::PACK_SHRINK, 0);
    pack_start (*feather, Gtk::PACK_SHRINK, 0);
    pack_start (*centerX, Gtk::PACK_SHRINK, 0);
    pack_start (*centerY, Gtk::PACK_SHRINK, 0);

    // Instantiating the Editing geometry; positions will be initialized later
    Line *hLine, *vLine, *featherLine[2];
    Circle *centerCircle;

    // Visible geometry
    hLine = new Line();
    hLine->innerLineWidth = 2;
    vLine = new Line();
    hLine->datum = vLine->datum = Geometry::IMAGE;

    featherLine[0] = new Line();
    featherLine[0]->innerLineWidth = 2;
    featherLine[1] = new Line();
    featherLine[1]->innerLineWidth = 2;
    featherLine[0]->datum = featherLine[1]->datum = Geometry::IMAGE;

    centerCircle = new Circle();
    centerCircle->datum = Geometry::IMAGE;
    centerCircle->radiusInImageSpace = false;
    centerCircle->radius = 6;
    centerCircle->filled = true;

    EditSubscriber::visibleGeometry.push_back( hLine );
    EditSubscriber::visibleGeometry.push_back( vLine );
    EditSubscriber::visibleGeometry.push_back( featherLine[0] );
    EditSubscriber::visibleGeometry.push_back( featherLine[1] );
    EditSubscriber::visibleGeometry.push_back( centerCircle );

    // MouseOver geometry
    hLine = new Line();
    hLine->innerLineWidth = 2;
    vLine = new Line();
    hLine->datum = vLine->datum = Geometry::IMAGE;

    featherLine[0] = new Line();
    featherLine[0]->innerLineWidth = 2;
    featherLine[1] = new Line();
    featherLine[1]->innerLineWidth = 2;
    featherLine[0]->datum = featherLine[1]->datum = Geometry::IMAGE;

    centerCircle = new Circle();
    centerCircle->datum = Geometry::IMAGE;
    centerCircle->radiusInImageSpace = false;
    centerCircle->radius = 12;
    centerCircle->filled = true;

    EditSubscriber::mouseOverGeometry.push_back( hLine );
    EditSubscriber::mouseOverGeometry.push_back( vLine );
    EditSubscriber::mouseOverGeometry.push_back( featherLine[0] );
    EditSubscriber::mouseOverGeometry.push_back( featherLine[1] );
    EditSubscriber::mouseOverGeometry.push_back( centerCircle );

    show_all();
}

Gradient::~Gradient()
{
    for (std::vector<Geometry*>::const_iterator i = visibleGeometry.begin(); i != visibleGeometry.end(); ++i) {
        delete *i;
    }

    for (std::vector<Geometry*>::const_iterator i = mouseOverGeometry.begin(); i != mouseOverGeometry.end(); ++i) {
        delete *i;
    }
}

void Gradient::read (const ProcParams* pp, const ParamsEdited* pedited)
{
    disableListener ();

    if (pedited) {
        degree->setEditedState (pedited->gradient.degree ? Edited : UnEdited);
        feather->setEditedState (pedited->gradient.feather ? Edited : UnEdited);
        strength->setEditedState (pedited->gradient.strength ? Edited : UnEdited);
        centerX->setEditedState (pedited->gradient.centerX ? Edited : UnEdited);
        centerY->setEditedState (pedited->gradient.centerY ? Edited : UnEdited);
        set_inconsistent (multiImage && !pedited->gradient.enabled);
    }

    setEnabled(pp->gradient.enabled);
    degree->setValue (pp->gradient.degree);
    feather->setValue (pp->gradient.feather);
    strength->setValue (pp->gradient.strength);
    centerX->setValue (pp->gradient.centerX);
    centerY->setValue (pp->gradient.centerY);

    updateGeometry (pp->gradient.centerX, pp->gradient.centerY, pp->gradient.feather, pp->gradient.degree);

    enableListener ();
}

void Gradient::updateGeometry(const int centerX, const int centerY, const double feather, const double degree, const int fullWidth, const int fullHeight)
{
    EditDataProvider* dataProvider = getEditProvider();

    if (!dataProvider) {
        return;
    }

    int imW=0;
    int imH=0;


    if (fullWidth != -1 && fullHeight != -1) {
        imW = fullWidth;
        imH = fullHeight;
    } else {
        dataProvider->getImageSize(imW, imH);
        if (!imW || !imH) {
            return;
        }
    }

    const auto decay = feather * rtengine::norm2<double> (imW, imH) / 200.0;
    rtengine::Coord origin (imW / 2 + centerX * imW / 200, imH / 2 + centerY * imH / 200);

    const auto updateLine = [&](Geometry* geometry, const double radius, const double begin, const double end)
    {
        const auto line = static_cast<Line*>(geometry);
        line->begin = PolarCoord(radius, -degree + begin);
        line->begin += origin;
        line->end = PolarCoord(radius, -degree + end);
        line->end += origin;
    };

    const auto updateLineWithDecay = [&](Geometry* geometry, const double radius, const double offSetAngle)
    {
        const auto line = static_cast<Line*>(geometry);
        line->begin = PolarCoord (radius, -degree + 180.) + PolarCoord (decay, -degree + offSetAngle);
        line->begin += origin;
        line->end = PolarCoord (radius, -degree) + PolarCoord (decay, -degree + offSetAngle);
        line->end += origin;
    };

    const auto updateCircle = [&](Geometry* geometry)
    {
        const auto circle = static_cast<Circle*>(geometry);
        circle->center = origin;
    };

    // update horizontal line
    updateLine (visibleGeometry.at(H_LINE), 1500., 0., 180.);
    updateLine (mouseOverGeometry.at(H_LINE), 1500., 0., 180.);

    // update vertical line
    updateLine (visibleGeometry.at(V_LINE), 700., 90., 270.);
    updateLine (mouseOverGeometry.at(V_LINE), 700., 90., 270.);

    // update upper feather line
    updateLineWithDecay (visibleGeometry.at(FEATHER_LINE_1), 350., 270.);
    updateLineWithDecay (mouseOverGeometry.at(FEATHER_LINE_1), 350., 270.);

    // update lower feather line
    updateLineWithDecay (visibleGeometry.at(FEATHER_LINE_2), 350., 90.);
    updateLineWithDecay (mouseOverGeometry.at(FEATHER_LINE_2), 350., 90.);

    // update circle's position
    updateCircle (visibleGeometry.at(CENTER_CIRCLE));
    updateCircle (mouseOverGeometry.at(CENTER_CIRCLE));
}

void Gradient::write (ProcParams* pp, ParamsEdited* pedited)
{
    pp->gradient.degree = degree->getValue ();
    pp->gradient.feather = feather->getIntValue ();
    pp->gradient.strength = strength->getValue ();
    pp->gradient.centerX = centerX->getIntValue ();
    pp->gradient.centerY = centerY->getIntValue ();
    pp->gradient.enabled = getEnabled();

    if (pedited) {
        pedited->gradient.degree = degree->getEditedState ();
        pedited->gradient.feather = feather->getEditedState ();
        pedited->gradient.strength = strength->getEditedState ();
        pedited->gradient.centerX = centerX->getEditedState ();
        pedited->gradient.centerY = centerY->getEditedState ();
        pedited->gradient.enabled = !get_inconsistent();
    }
}

void Gradient::setDefaults (const ProcParams* defParams, const ParamsEdited* pedited)
{
    degree->setDefault (defParams->gradient.degree);
    feather->setDefault (defParams->gradient.feather);
    strength->setDefault (defParams->gradient.strength);
    centerX->setDefault (defParams->gradient.centerX);
    centerY->setDefault (defParams->gradient.centerY);

    if (pedited) {
        degree->setDefaultEditedState (pedited->gradient.degree ? Edited : UnEdited);
        feather->setDefaultEditedState (pedited->gradient.feather ? Edited : UnEdited);
        strength->setDefaultEditedState (pedited->gradient.strength ? Edited : UnEdited);
        centerX->setDefaultEditedState (pedited->gradient.centerX ? Edited : UnEdited);
        centerY->setDefaultEditedState (pedited->gradient.centerY ? Edited : UnEdited);
    } else {
        degree->setDefaultEditedState (Irrelevant);
        feather->setDefaultEditedState (Irrelevant);
        strength->setDefaultEditedState (Irrelevant);
        centerX->setDefaultEditedState (Irrelevant);
        centerY->setDefaultEditedState (Irrelevant);
    }
}

void Gradient::adjusterChanged(Adjuster* a, double newval)
{
    updateGeometry(int(centerX->getValue()), int(centerY->getValue()), feather->getValue(), degree->getValue());

    if (listener && getEnabled()) {

        if (a == degree) {
            listener->panelChanged (EvGradientDegree, degree->getTextValue());
        } else if (a == feather) {
            listener->panelChanged (EvGradientFeather, feather->getTextValue());
        } else if (a == strength) {
            listener->panelChanged (EvGradientStrength, strength->getTextValue());
        } else if (a == centerX || a == centerY) {
            listener->panelChanged (EvGradientCenter, Glib::ustring::compose ("X=%1\nY=%2", centerX->getTextValue(), centerY->getTextValue()));
        }
    }
}

void Gradient::enabledChanged ()
{

    if (listener) {
        if (get_inconsistent()) {
            listener->panelChanged (EvGradientEnabled, M("GENERAL_UNCHANGED"));
        } else if (getEnabled()) {
            listener->panelChanged (EvGradientEnabled, M("GENERAL_ENABLED"));
        } else {
            listener->panelChanged (EvGradientEnabled, M("GENERAL_DISABLED"));
        }
    }
}

void Gradient::setAdjusterBehavior (bool degreeadd, bool featheradd, bool strengthadd, bool centeradd)
{
    degree->setAddMode(degreeadd);
    feather->setAddMode(featheradd);
    strength->setAddMode(strengthadd);
    centerX->setAddMode(centeradd);
    centerY->setAddMode(centeradd);
}

void Gradient::trimValues (rtengine::procparams::ProcParams* pp)
{
    degree->trimValue(pp->gradient.degree);
    feather->trimValue(pp->gradient.feather);
    strength->trimValue(pp->gradient.strength);
    centerX->trimValue(pp->gradient.centerX);
    centerY->trimValue(pp->gradient.centerY);
}

void Gradient::setBatchMode (bool batchMode)
{
    editConn.disconnect();
    removeIfThere(this, editHBox, false);
    ToolPanel::setBatchMode (batchMode);
    degree->showEditedCB ();
    feather->showEditedCB ();
    strength->showEditedCB ();
    centerX->showEditedCB ();
    centerY->showEditedCB ();
}

void Gradient::setEditProvider (EditDataProvider* provider)
{
    EditSubscriber::setEditProvider(provider);
}

void Gradient::editToggled ()
{
    if (edit->get_active()) {
        subscribe();
    } else {
        releaseEdit();
        unsubscribe();
    }
}

CursorShape Gradient::getCursor(int objectID, int xPos, int yPos) const
{
    switch (objectID) {
    case (H_LINE):
    case (V_LINE):
        return CSMoveRotate;

    case (FEATHER_LINE_1):
    case (FEATHER_LINE_2): {
        int angle = degree->getIntValue();

        if (angle < -135 || (angle >= -45 && angle <= 45) || angle > 135) {
            return CSMove1DV;
        }

        return CSMove1DH;
    }

    case (CENTER_CIRCLE):
        return CSMove2D;

    default:
        return CSHandOpen;
    }
}

bool Gradient::mouseOver(int modifierKey)
{
    EditDataProvider* editProvider = getEditProvider();

    if (editProvider && editProvider->getObject() != lastObject) {
        if (lastObject > -1) {
            if (lastObject == FEATHER_LINE_1 || lastObject == FEATHER_LINE_2) {
                EditSubscriber::visibleGeometry.at(FEATHER_LINE_1)->state = Geometry::NORMAL;
                EditSubscriber::visibleGeometry.at(FEATHER_LINE_2)->state = Geometry::NORMAL;
            } else {
                EditSubscriber::visibleGeometry.at(lastObject)->state = Geometry::NORMAL;
            }
        }

        if (editProvider->getObject() > -1) {
            if (editProvider->getObject() == FEATHER_LINE_1 || editProvider->getObject() == FEATHER_LINE_2) {
                EditSubscriber::visibleGeometry.at(FEATHER_LINE_1)->state = Geometry::PRELIGHT;
                EditSubscriber::visibleGeometry.at(FEATHER_LINE_2)->state = Geometry::PRELIGHT;
            } else {
                EditSubscriber::visibleGeometry.at(editProvider->getObject())->state = Geometry::PRELIGHT;
            }
        }

        lastObject = editProvider->getObject();
        return true;
    }

    return false;
}

bool Gradient::button1Pressed(int modifierKey)
{
    if (lastObject < 0) {
        return false;
    }

    EditDataProvider *provider = getEditProvider();

    if (!(modifierKey & GDK_CONTROL_MASK)) {
        // button press is valid (no modifier key)
        PolarCoord pCoord;
        int imW, imH;
        provider->getImageSize(imW, imH);
        double halfSizeW = imW / 2.;
        double halfSizeH = imH / 2.;
        draggedCenter.set(int(halfSizeW + halfSizeW * (centerX->getValue() / 100.)), int(halfSizeH + halfSizeH * (centerY->getValue() / 100.)));

        // trick to get the correct angle (clockwise/counter-clockwise)
        rtengine::Coord p1 = draggedCenter;
        rtengine::Coord p2 = provider->posImage;
        int p = p1.y;
        p1.y = p2.y;
        p2.y = p;

        pCoord = p2 - p1;
        draggedPointOldAngle = pCoord.angle;
        //printf("\ndraggedPointOldAngle=%.3f\n\n", draggedPointOldAngle);
        draggedPointAdjusterAngle = degree->getValue();

        if (lastObject == FEATHER_LINE_1 || lastObject == FEATHER_LINE_2) {
            // Dragging a line to change the angle
            PolarCoord draggedPoint;
            rtengine::Coord currPos;
            currPos = provider->posImage;
            rtengine::Coord centerPos = draggedCenter;

            double diagonal = sqrt(double(imW) * double(imW) + double(imH) * double(imH));

            // trick to get the correct angle (clockwise/counter-clockwise)
            std::swap(centerPos.y, currPos.y);

            draggedPoint = currPos - centerPos;
            // compute the projected value of the dragged point
            draggedFeatherOffset = draggedPoint.radius * sin((draggedPoint.angle - degree->getValue()) / 180.*rtengine::RT_PI);

            if (lastObject == FEATHER_LINE_2) {
                draggedFeatherOffset = -draggedFeatherOffset;
            }

            draggedFeatherOffset -= (feather->getValue() / 200. * diagonal);
        }

        EditSubscriber::action = EditSubscriber::Action::DRAGGING;
        return false;
    } else { // should theoretically always be true
        // this will let this class ignore further drag events
        if (lastObject == FEATHER_LINE_1 || lastObject == FEATHER_LINE_2) {
            EditSubscriber::visibleGeometry.at(FEATHER_LINE_1)->state = Geometry::NORMAL;
            EditSubscriber::visibleGeometry.at(FEATHER_LINE_2)->state = Geometry::NORMAL;
        } else {
            EditSubscriber::visibleGeometry.at(lastObject)->state = Geometry::NORMAL;
        }

        lastObject = -1;
        return true;
    }

    return false;
}

bool Gradient::button1Released()
{
    draggedPointOldAngle = -1000.;
    EditSubscriber::action = EditSubscriber::Action::NONE;
    return true;
}

bool Gradient::drag1(int modifierKey)
{
    // compute the polar coordinate of the mouse position
    EditDataProvider *provider = getEditProvider();
    int imW, imH;
    provider->getImageSize(imW, imH);
    double halfSizeW = imW / 2.;
    double halfSizeH = imH / 2.;

    if (lastObject == H_LINE || lastObject == V_LINE) {

        // Dragging a line to change the angle
        PolarCoord draggedPoint;
        rtengine::Coord currPos;
        currPos = provider->posImage + provider->deltaImage;
        rtengine::Coord centerPos = draggedCenter;

        // trick to get the correct angle (clockwise/counter-clockwise)
        int p = centerPos.y;
        centerPos.y = currPos.y;
        currPos.y = p;

        draggedPoint = currPos - centerPos;
        double deltaAngle = draggedPoint.angle - draggedPointOldAngle;

        if (deltaAngle > 180.) { // crossing the boundary (0->360)
            deltaAngle -= 360.;
        } else if (deltaAngle < -180.) { // crossing the boundary (360->0)
            deltaAngle += 360.;
        }

        draggedPointOldAngle = draggedPoint.angle;

        draggedPointAdjusterAngle += deltaAngle;

        if (draggedPointAdjusterAngle > 180.) {
            draggedPointAdjusterAngle = -360. + draggedPointAdjusterAngle;
        } else if (draggedPointAdjusterAngle < -180.) {
            draggedPointAdjusterAngle = 360. - draggedPointAdjusterAngle;
        }

        //printf("draggedPointOldAngle: %.3f /  From %d,%d to %d,%d -> angle = %.3f  /  ", draggedPointAdjusterAngle, centerPos.x, centerPos.y, currPos.x, currPos.y, draggedPoint.angle);
        //printf("currAngle: %.3f = degree: %.3f + deltaAngle: %.3f %s /  draggedPointOldAngle: %.3f\n", draggedPointAdjusterAngle, degree->getValue(), deltaAngle, degree->getValue()>180.?">180":degree->getValue()<180.?"<180":"", draggedPointOldAngle);
        if (int(draggedPointAdjusterAngle) != degree->getIntValue()) {
            degree->setValue(draggedPointAdjusterAngle);
            updateGeometry (int(centerX->getValue()), int(centerY->getValue()), feather->getValue(), degree->getValue());

            if (listener) {
                listener->panelChanged (EvGradientDegree, degree->getTextValue());
            }

            return true;
        }
    } else if (lastObject == FEATHER_LINE_1 || lastObject == FEATHER_LINE_2) {
        // Dragging the upper or lower feather bar
        PolarCoord draggedPoint;
        rtengine::Coord currPos;
        currPos = provider->posImage + provider->deltaImage;
        rtengine::Coord centerPos = draggedCenter;

        double diagonal = sqrt(double(imW) * double(imW) + double(imH) * double(imH));

        // trick to get the correct angle (clockwise/counter-clockwise)
        int p = centerPos.y;
        centerPos.y = currPos.y;
        currPos.y = p;

        draggedPoint = currPos - centerPos;
        double currDraggedFeatherOffset = draggedPoint.radius * sin((draggedPoint.angle - degree->getValue()) / 180.*rtengine::RT_PI);

        if (lastObject == FEATHER_LINE_1)
            // Dragging the upper feather bar
        {
            currDraggedFeatherOffset -= draggedFeatherOffset;
        } else if (lastObject == FEATHER_LINE_2)
            // Dragging the lower feather bar
        {
            currDraggedFeatherOffset = -currDraggedFeatherOffset + draggedFeatherOffset;
        }

        currDraggedFeatherOffset = currDraggedFeatherOffset * 200. / diagonal;

        if (int(currDraggedFeatherOffset) != feather->getIntValue()) {
            feather->setValue(double(int(currDraggedFeatherOffset)));
            updateGeometry (centerX->getValue(), centerY->getValue(), feather->getValue(), degree->getValue());

            if (listener) {
                listener->panelChanged (EvGradientFeather, feather->getTextValue());
            }

            return true;
        }
    } else if (lastObject == CENTER_CIRCLE) {
        // Dragging the circle to change the center
        rtengine::Coord currPos;
        draggedCenter += provider->deltaPrevImage;
        currPos = draggedCenter;
        currPos.clip(imW, imH);
        int newCenterX = int((double(currPos.x) - halfSizeW) / halfSizeW * 100.);
        int newCenterY = int((double(currPos.y) - halfSizeH) / halfSizeH * 100.);

        if (newCenterX != centerX->getIntValue() || newCenterY != centerY->getIntValue()) {
            centerX->setValue(newCenterX);
            centerY->setValue(newCenterY);
            updateGeometry (newCenterX, newCenterY, feather->getValue(), degree->getValue());

            if (listener) {
                listener->panelChanged (EvGradientCenter, Glib::ustring::compose ("X=%1\nY=%2", centerX->getTextValue(), centerY->getTextValue()));
            }

            return true;
        }
    }

    return false;
}

void Gradient::releaseEdit()
{
    if (lastObject >= 0) {
        if (lastObject == FEATHER_LINE_1 || lastObject == FEATHER_LINE_2) {
            EditSubscriber::visibleGeometry.at(FEATHER_LINE_1)->state = Geometry::NORMAL;
            EditSubscriber::visibleGeometry.at(FEATHER_LINE_2)->state = Geometry::NORMAL;
        } else {
            EditSubscriber::visibleGeometry.at(lastObject)->state = Geometry::NORMAL;
        }
    }
    action = Action::NONE;
}

void Gradient::switchOffEditMode ()
{
    if (edit->get_active()) {
        // switching off the toggle button
        bool wasBlocked = editConn.block(true);
        edit->set_active(false);

        if (!wasBlocked) {
            editConn.block(false);
        }
    }

    EditSubscriber::switchOffEditMode();  // disconnect
}

