/****************************************************************************
**
** This file is part of the LibreCAD project, a 2D CAD program
**
** Copyright (C) 2018 A. Stebich (librecad@mail.lordofbikes.de)
** Copyright (C) 2010 R. van Twisk (librecad@rvt.dds.nl)
** Copyright (C) 2001-2003 RibbonSoft. All rights reserved.
**
**
** This file may be distributed and/or modified under the terms of the
** GNU General Public License version 2 as published by the Free Software
** Foundation and appearing in the file gpl-2.0.txt included in the
** packaging of this file.
**
** This program 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 this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
** This copyright notice MUST APPEAR in all copies of the script!
**
**********************************************************************/

#include <cmath>
#include <QAction>
#include <QMouseEvent>
#include "rs_actiondimangular.h"
#include "rs_dimangular.h"

#include "rs_dialogfactory.h"
#include "rs_graphicview.h"
#include "rs_commandevent.h"
#include "rs_information.h"
#include "rs_coordinateevent.h"
#include "rs_preview.h"
#include "rs_debug.h"
#include "rs_math.h"

RS_ActionDimAngular::RS_ActionDimAngular(RS_EntityContainer& container,
                                         RS_GraphicView& graphicView) :
    RS_ActionDimension( "Draw Angular Dimensions", container, graphicView)
{
    reset();
}

RS_ActionDimAngular::~RS_ActionDimAngular() = default;

void RS_ActionDimAngular::reset()
{
    RS_ActionDimension::reset();

    actionType = RS2::ActionDimAngular;
    edata = std::make_unique<RS_DimAngularData>( RS_Vector( false),
                                        RS_Vector( false),
                                        RS_Vector( false),
                                        RS_Vector( false));
    RS_DIALOGFACTORY->requestOptions( this, true, true);
}

void RS_ActionDimAngular::trigger()
{
    RS_PreviewActionInterface::trigger();

    if (line1.getStartpoint().valid && line2.getStartpoint().valid) {
        RS_DimAngular* newEntity {new RS_DimAngular( container,
                                                     *data,
                                                     *edata)};

        newEntity->setLayerToActive();
        newEntity->setPenToActive();
        newEntity->update();
        container->addEntity(newEntity);

        // upd. undo list:
        if (document) {
            document->startUndoCycle();
            document->addUndoable(newEntity);
            document->endUndoCycle();
        }

        RS_Vector rz {graphicView->getRelativeZero()};
        setStatus( SetLine1);
        graphicView->redraw( RS2::RedrawDrawing);
        graphicView->moveRelativeZero( rz);
        RS_Snapper::finish();
    }
    else {
        RS_DEBUG->print( "RS_ActionDimAngular::trigger: Entity is nullptr\n");
    }
}

void RS_ActionDimAngular::mouseMoveEvent(QMouseEvent* e)
{
    RS_DEBUG->print( "RS_ActionDimAngular::mouseMoveEvent begin");

    switch (getStatus()) {
    case SetPos:
        if( setData( snapPoint(e))) {
            RS_DimAngular *d {new RS_DimAngular( preview.get(), *data, *edata)};

            deletePreview();
            preview->addEntity(d);
            d->update();
            drawPreview();
        }
        break;

    default:
        break;
    }

    RS_DEBUG->print("RS_ActionDimAngular::mouseMoveEvent end");
}

void RS_ActionDimAngular::mouseReleaseEvent(QMouseEvent* e)
{
    if (Qt::LeftButton == e->button()) {
        switch (getStatus()) {
        case SetLine1: {
            RS_Entity *en {catchEntity( e, RS2::ResolveAll)};
            if (en && RS2::EntityLine == en->rtti()) {
                line1 = *dynamic_cast<RS_Line*>(en);
                click1 = line1.getNearestPointOnEntity( graphicView->toGraph( e->x(), e->y()));
                setStatus(SetLine2);
            }
            break; }

        case SetLine2: {
            RS_Entity *en{catchEntity(e, RS2::ResolveAll)};
            if (en && en->rtti()==RS2::EntityLine) {
                line2 = *dynamic_cast<RS_Line*>(en);
                click2 = line2.getNearestPointOnEntity( graphicView->toGraph( e->x(), e->y()));
                if( setData( click2, true)) {
                    graphicView->moveRelativeZero( center);
                    setStatus(SetPos);
                }
            }
            break; }

        case SetPos: {
            RS_CoordinateEvent ce( snapPoint( e));
            coordinateEvent( &ce);
            break; }
        }
    }
    else if (Qt::RightButton == e->button()) {
        deletePreview();
        init( getStatus() - 1);
    }
}

void RS_ActionDimAngular::coordinateEvent(RS_CoordinateEvent* e)
{
    if ( ! e) {
        return;
    }

    switch (getStatus()) {
    case SetPos:
        if( setData( e->getCoordinate())) {
            trigger();
            reset();
            setStatus( SetLine1);
        }
        break;

    default:
        break;
    }
}

void RS_ActionDimAngular::commandEvent(RS_CommandEvent* e)
{
    QString c( e->getCommand().toLower());

    if (checkCommand( QStringLiteral( "help"), c)) {
        RS_DIALOGFACTORY->commandMessage( msgAvailableCommands()
                                          + getAvailableCommands().join(", "));
        return;
    }

    // setting new text label:
    if (SetText == getStatus()) {
        setText( c);
        RS_DIALOGFACTORY->requestOptions( this, true, true);
        graphicView->enableCoordinateInput();
        setStatus( lastStatus);
        return;
    }

    // command: text
    if (checkCommand( QStringLiteral( "text"), c)) {
        lastStatus = static_cast<Status>(getStatus());
        graphicView->disableCoordinateInput();
        setStatus( SetText);
    }
}

QStringList RS_ActionDimAngular::getAvailableCommands()
{
    QStringList cmd;

    switch (getStatus()) {
    case SetLine1:
    case SetLine2:
    case SetPos:
        cmd += command( QStringLiteral( "text"));
        break;

    default:
        break;
    }

    return cmd;
}

void RS_ActionDimAngular::showOptions()
{
    RS_ActionInterface::showOptions();

    RS_DIALOGFACTORY->requestOptions( this, true);
}

void RS_ActionDimAngular::hideOptions()
{
    RS_ActionInterface::hideOptions();

    RS_DIALOGFACTORY->requestOptions( this, false);
}

void RS_ActionDimAngular::updateMouseButtonHints()
{
    switch (getStatus()) {
    case SetLine1:
        RS_DIALOGFACTORY->updateMouseWidget( tr("Select first line"),
                                             tr("Cancel"));
        break;

    case SetLine2:
        RS_DIALOGFACTORY->updateMouseWidget( tr("Select second line"),
                                             tr("Cancel"));
        break;

    case SetPos:
        RS_DIALOGFACTORY->updateMouseWidget( tr("Specify dimension arc line location"),
                                             tr("Cancel"));
        break;

    case SetText:
        RS_DIALOGFACTORY->updateMouseWidget( tr("Enter dimension text:"), "");
        break;

    default:
        RS_DIALOGFACTORY->updateMouseWidget();
        break;
    }
}

/**
 * Justify one of the angle lines to ensure that the starting point
 * of the line has the same angle from the intersection point as the
 * selection click point and it is further away than the line end point
 *
 * @param line A selected line for the dimension
 * @param click The click pos which selected the line
 * @param center The intersection of the 2 lines to dimension
 */
void RS_ActionDimAngular::justify(RS_Line &line, const RS_Vector &click)
{
    RS_Vector vStartPoint( line.getStartpoint());

    if( ! RS_Math::equal( vStartPoint.angleTo(center), click.angleTo( center))
        || vStartPoint.distanceTo( center) < click.distanceTo( center)) {
        line.reverse();
    }
}

/**
 * Create a sorted array with angles from the lines intersection point
 * to the starting points and their revers angles.
 * Ensure, that line1 and line2 are in CCW order.
 * Compute an offset for quadrant() method.
 *
 * @param line A selected line for the dimension
 * @param click The click pos which selected the line
 * @param center The intersection of the 2 lines to dimension
 */
void RS_ActionDimAngular::lineOrder(const RS_Vector &dimPos)
{
    if( ! center.valid) {
        return;
    }

    // starting point angles and selection point angle from intersection point
    double  a0  {(dimPos - center).angle()};
    double  a1  {(line1.getStartpoint() - center).angle()};
    double  a2  {(line2.getStartpoint() - center).angle()};

    // swap lines if necessary to ensure CCW order
    if( RS_Math::correctAngle2( a1 - a0) > RS_Math::correctAngle2( a2 - a0)) {
        RS_Line swapLines( line1);
        line1 = line2;
        line2 = swapLines;
        double  swapAngle {a1};
        a1 = a2;
        a2 = swapAngle;
    }

    // sorted array with starting point and reverse angles
    angles.clear();
    angles.push_back( a1);
    angles.push_back( RS_Math::correctAngle( a1 + M_PI));
    angles.push_back( a2);
    angles.push_back( RS_Math::correctAngle( a2 + M_PI));
    std::sort( angles.begin(), angles.end());

    // find starting quadrant and compute the offset for quadrant() method
    int startQuadrant = 0;
    for( auto angle : angles) {
        if( RS_Math::equal( a1, angle)) {
            break;
        }
        ++startQuadrant;
    }
    quadrantOffset = 0x03 & (4 - startQuadrant);
}

/**
 * Find the quadrant of \p angle relative to 1st quadrant.
 * When the angle lines are selected, the starting quadrant
 * is shifted to become 0 by \p quadrantOffset.
 * This is the criterion how the angles dimension is drawn.
 *
 * @param angle The angle, e.g. mouse or coordinate position
 * @return The quadrant of \p angle, relative to the 1st selection quadrant
 */
int RS_ActionDimAngular::quadrant(const double angle)
{
    if( 1 > angles.size()) {
        return 0;
    }

    double a1 {RS_Math::correctAngle2( angles.at(0) - angle)};
    double a2 {RS_Math::correctAngle2( angles.at(1) - angle)};

    int angleQuadrant {0};
    if( 0.0 < a1 && 0.0 < a2) {
        angleQuadrant = 3;
    }
    else if( 0.0 >= a1 && 0.0 >= a2) {
        angleQuadrant = 1;
    }
    else if( 0.0 < a1 && 0.0 >= a2) {
        angleQuadrant = 2;
    }

    return (0x03 & (angleQuadrant + quadrantOffset));
}

/**
 * On \p mouseMoveEvent, \p mouseReleaseEvent and \p coordinateEvent
 * this method sets the dimension data appropriate to the mouse
 * cursor/coordinate in \p dimPos.
 * When \p calcCenter is true, the intersection point and other static
 * values are computed. This is only necessary, when line selection changes,
 * e.g. on \p mouseReleaseEvent. For \p mouseMoveEvent calcCenter is false.
 *
 * @param dimPos The mouse/coordinate position
 * @param calcCenter If true, the center and corresponding values are calculated
 * @return true If the dimension data were set, false is a parameter is invalid
 */
bool RS_ActionDimAngular::setData(const RS_Vector &dimPos, const bool calcCenter /*= false*/)
{
    if( ! line1.getStartpoint().valid || ! line2.getStartpoint().valid) {
        return false;
    }

    if ( ! center.valid || calcCenter) {
        RS_VectorSolutions sol = RS_Information::getIntersectionLineLine( &line1, &line2);
        center = sol.get(0);
    }
    if ( ! center.valid) {
        return false;
    }

    if( calcCenter) {
        justify( line1, click1);
        justify( line2, click2);
        lineOrder( dimPos);
    }

    edata->definitionPoint4 = dimPos;
    switch( quadrant( (dimPos - center).angle())) {
    default:
    case 0:
        edata->definitionPoint1 = line1.getEndpoint();
        edata->definitionPoint2 = line1.getStartpoint();
        edata->definitionPoint3 = line2.getEndpoint();
        data->definitionPoint   = line2.getStartpoint();
        break;

    case 1:
        edata->definitionPoint1 = line2.getEndpoint();
        edata->definitionPoint2 = line2.getStartpoint();
        edata->definitionPoint3 = line1.getStartpoint();
        data->definitionPoint   = line1.getEndpoint();
        break;

    case 2:
        edata->definitionPoint1 = line2.getEndpoint();
        edata->definitionPoint2 = line2.getStartpoint();
        edata->definitionPoint3 = line1.getEndpoint();
        data->definitionPoint   = line1.getStartpoint();
        break;

    case 3:
        edata->definitionPoint1 = line2.getStartpoint();
        edata->definitionPoint2 = line2.getEndpoint();
        edata->definitionPoint3 = line1.getEndpoint();
        data->definitionPoint   = line1.getStartpoint();
        break;
    }

    return true;
}

// EOF
