#include "tracepolar.h"

#include "Marker/marker.h"
#include "Util/util.h"

#include <QFileDialog>

using namespace std;

TracePolar::TracePolar(TraceModel &model, QWidget *parent)
    : TracePlot(model, parent)
{
    limitToSpan = true;
    limitToEdge = true;
    manualFrequencyRange = false;
    fmin = 0;
    fmax = 6000000000;
    edgeReflection = 1.0;
    dx = 0;
    initializeTraceInfo();
}

nlohmann::json TracePolar::toJSON()
{
    nlohmann::json j;
    j["limit_to_span"] = limitToSpan;
    j["limit_to_edge"] = limitToEdge;
    j["edge_reflection"] = edgeReflection;
    j["offset_axis_x"] = dx;
    j["frequency_override"] = manualFrequencyRange;
    j["override_min"] = fmin;
    j["override_max"] = fmax;
    nlohmann::json jtraces;
    for(auto t : traces) {
        if(t.second) {
            jtraces.push_back(t.first->toHash());
        }
    }
    j["traces"] = jtraces;
    return j;
}

void TracePolar::fromJSON(nlohmann::json j)
{
    limitToSpan = j.value("limit_to_span", true);
    limitToEdge = j.value("limit_to_edge", false);
    edgeReflection = j.value("edge_reflection", 1.0);
    manualFrequencyRange = j.value("frequency_override", false);
    fmin = j.value("override_min", 0.0);
    fmax = j.value("override_max", 6000000000.0);
    dx = j.value("offset_axis_x", 0.0);
    for(unsigned int hash : j["traces"]) {
        // attempt to find the traces with this hash
        bool found = false;
        for(auto t : model.getTraces()) {
            if(t->toHash() == hash) {
                enableTrace(t, true);
                found = true;
                break;
            }
        }
        if(!found) {
            qWarning() << "Unable to find trace with hash" << hash;
        }
    }
}

void TracePolar::wheelEvent(QWheelEvent *event)
{
    // most mousewheel have 15 degree increments, the reported delta is in 1/8th degree -> 120
    auto increment = event->angleDelta().y() / 120.0;
    // round toward bigger step in case of special higher resolution mousewheel
    int steps = increment > 0 ? ceil(increment) : floor(increment);

    constexpr double zoomfactor = 1.1;
    auto zoom = pow(zoomfactor, steps);
    edgeReflection /= zoom;

    auto incrementX = event->angleDelta().x() / 120.0;
    dx += incrementX/10;

    triggerReplot();
}

QPoint TracePolar::dataToPixel(std::complex<double> d)
{
    return transform.map(QPoint(d.real() * polarCoordMax * (1.0 / edgeReflection), -d.imag() * polarCoordMax * (1.0 / edgeReflection)));
}

QPoint TracePolar:: dataToPixel(Trace::Data d)
{
    return dataToPixel(d.y);
}

std::complex<double> TracePolar::dataAddDx(std::complex<double> d)
{
    auto dataShift = complex<double>(dx, 0);
    d = d + dataShift;
    return d;
}

Trace::Data TracePolar::dataAddDx(Trace::Data d)
{
    d.y = dataAddDx(d.y);
    return d;
}

std::complex<double> TracePolar::pixelToData(QPoint p)
{
    auto data = transform.inverted().map(QPointF(p));
    return complex<double>(data.x() / polarCoordMax * edgeReflection, -data.y() / polarCoordMax * edgeReflection);
}

QPoint TracePolar::markerToPixel(Marker *m)
{
    QPoint ret = QPoint();
//    if(!m->isTimeDomain()) {
        if(m->getPosition() >= sweep_fmin && m->getPosition() <= sweep_fmax) {
            auto d = m->getData();
            d = dataAddDx(d);
            ret = dataToPixel(d);
        }
//    }
    return ret;
}

double TracePolar::nearestTracePoint(Trace *t, QPoint pixel, double *distance)
{
    double closestDistance = numeric_limits<double>::max();
    double closestXpos = 0;
    unsigned int closestIndex = 0;
    auto samples = t->size();
    for(unsigned int i=0;i<samples;i++) {
        auto data = t->sample(i);
        data = dataAddDx(data);
        auto plotPoint = dataToPixel(data);
        if (plotPoint.isNull()) {
            // destination point outside of currently displayed range
            continue;
        }
        auto diff = plotPoint - pixel;
        unsigned int distance = diff.x() * diff.x() + diff.y() * diff.y();
        if(distance < closestDistance) {
            closestDistance = distance;
            closestXpos = t->sample(i).x;
            closestIndex = i;
        }
    }
    closestDistance = sqrt(closestDistance);

    if(closestIndex > 0) {
        auto l1 = dataToPixel(dataAddDx(t->sample(closestIndex-1)));
        auto l2 = dataToPixel(dataAddDx(t->sample(closestIndex)));
        double ratio;
        auto distance = Util::distanceToLine(pixel, l1, l2, nullptr, &ratio);
        if(distance < closestDistance) {
            closestDistance = distance;
            closestXpos = t->sample(closestIndex-1).x + (t->sample(closestIndex).x - t->sample(closestIndex-1).x) * ratio;
        }
    }
    if(closestIndex < t->size() - 1) {
        auto l1 = dataToPixel(dataAddDx(t->sample(closestIndex)));
        auto l2 = dataToPixel(dataAddDx(t->sample(closestIndex+1)));
        double ratio;
        auto distance = Util::distanceToLine(pixel, l1, l2, nullptr, &ratio);
        if(distance < closestDistance) {
            closestDistance = distance;
            closestXpos = t->sample(closestIndex).x + (t->sample(closestIndex+1).x - t->sample(closestIndex).x) * ratio;
        }
    }
    if(distance) {
        *distance = closestDistance;
    }
    return closestXpos;
}

bool TracePolar::markerVisible(double x)
{
    if(limitToSpan) {
        if(x >= sweep_fmin && x <= sweep_fmax) {
            return true;
        } else {
            return false;
        }
    } else {
        // complete traces visible
        return true;
    }
}

void TracePolar::updateContextMenu()
{
    contextmenu->clear();
    auto setup = new QAction("Setup...", contextmenu);
    connect(setup, &QAction::triggered, this, &TracePolar::axisSetupDialog, Qt::UniqueConnection);
    contextmenu->addAction(setup);

    contextmenu->addSeparator();
    auto image = new QAction("Save image...", contextmenu);
    contextmenu->addAction(image);
    connect(image, &QAction::triggered, [=]() {
        auto filename = QFileDialog::getSaveFileName(nullptr, "Save plot image", "", "PNG image files (*.png)", nullptr, QFileDialog::DontUseNativeDialog);
        if(filename.isEmpty()) {
            // aborted selection
            return;
        }
        if(filename.endsWith(".png")) {
            filename.chop(4);
        }
        filename += ".png";
        grab().save(filename);
    });

    auto createMarker = contextmenu->addAction("Add marker here");
    bool activeTraces = false;
    for(auto t : traces) {
        if(t.second) {
            activeTraces = true;
            break;
        }
    }
    if(!activeTraces) {
        createMarker->setEnabled(false);
    }
    connect(createMarker, &QAction::triggered, [=](){
        createMarkerAtPosition(contextmenuClickpoint);
    });

    contextmenu->addSection("Traces");
    // Populate context menu
    for(auto t : orderedTraces()) {
        if(!supported(t)) {
            continue;
        }
        auto action = new QAction(t->name(), contextmenu);
        action->setCheckable(true);
        if(traces[t]) {
            action->setChecked(true);
        }
        connect(action, &QAction::toggled, [=](bool active) {
            enableTrace(t, active);
        });
        contextmenu->addAction(action);
    }

    finishContextMenu();
}

bool TracePolar::constrainLineToCircle(QPointF &a, QPointF &b, QPointF center, double radius)
{
    auto distance = [](const QPointF &a, const QPointF &b) {
        auto dx = b.x() - a.x();
        auto dy = b.y() - a.y();
        return sqrt(dx*dx + dy*dy);
    };

    if(distance(a, center) <= radius && distance(b, center) <= radius) {
        // both points are completely contained within the circle, no adjustment necessary
        return true;
    }

    // shift points, the formulas assume center = (0,0)
    a -= center;
    b -= center;

    // according to https://mathworld.wolfram.com/Circle-LineIntersection.html
    auto dx = b.x() - a.x();
    auto dy = b.y() - a.y();
    auto dr = sqrt(dx*dx+dy*dy);
    auto D = a.x()*b.y() - b.x()*a.y();
    // check intersection
    auto delta = radius*radius * dr*dr - D*D;
    if(delta <= 0) {
        // line does not intersect the circle
        return false;
    }
    // line intersects the circle, calculate intersection points
    auto x1 = (D*dy+copysign(1.0, dy) * dx*sqrt(delta)) / (dr*dr);
    auto x2 = (D*dy-copysign(1.0, dy) * dx*sqrt(delta)) / (dr*dr);
    auto y1 = (-D*dx+abs(dy)*sqrt(delta)) / (dr*dr);
    auto y2 = (-D*dx-abs(dy)*sqrt(delta)) / (dr*dr);

    auto inter1 = QPointF(x1, y1);
    auto inter2 = QPointF(x2, y2);

    bool inter1betweenPoints = false;
    bool inter2betweenPoints = false;
    if(abs(distance(a, inter1) + distance(b, inter1) - distance(a, b)) < 0.000001) {
        inter1betweenPoints = true;
    }
    if(abs(distance(a, inter2) + distance(b, inter2) - distance(a, b)) < 0.000001) {
        inter2betweenPoints = true;
    }
    if(inter1betweenPoints && inter2betweenPoints) {
        // adjust both points, order does not matter
        a = inter1;
        b = inter2;
    } else if(!inter1betweenPoints && !inter2betweenPoints) {
        // the line intersect the circle but outside of the segment defined by the points -> ignore
        a += center;
        b += center;
        return false;
    } else {
        // exactly one intersection point must lie between the two line points, otherwise we would have returned already
        auto inter = inter1betweenPoints ? inter1 : inter2;
        if(distance(a, QPointF(0,0)) < radius) {
            // point is in the circle and can remain unchanged. Use inter as new point b
            b = inter;
        } else {
            // the other way around
            a = inter;
        }
    }
    a += center;
    b += center;
    return true;
}

PolarArc::PolarArc(QPointF center, double radius,  double startAngle, double spanAngle)
    : center(center),
      radius(radius),
      startAngle(startAngle),
      spanAngle(spanAngle)
{

}

void PolarArc::constrainToCircle(QPointF center, double radius)
{
    // check if arc/circle intersect
    auto centerDiff = this->center - center;
    auto centerDistSquared = centerDiff.x() * centerDiff.x() + centerDiff.y() * centerDiff.y();
    if (centerDistSquared >= (radius + this->radius) * (radius + this->radius)) {
        // arc completely outside of constraining circle
        spanAngle = 0.0;
        return;
    } else if (centerDistSquared <= (radius - this->radius) * (radius - this->radius)) {
        if (radius >= this->radius) {
            // arc completely in constraining circle, do nothing
            return;
        } else {
            // arc completely outside of circle
            spanAngle = 0.0;
            return;
        }
    } else {
        // there are intersections between the arc and the circle. Calculate points according to https://stackoverflow.com/questions/3349125/circle-circle-intersection-points
        auto distance = sqrt(centerDistSquared);
        auto a = (this->radius*this->radius-radius*radius+distance*distance) / (2*distance);
        auto h = sqrt(this->radius*this->radius - a*a);
        auto intersectMiddle = this->center + a*(center - this->center) / distance;
        auto rotatedCenterDiff = center - this->center;
        swap(rotatedCenterDiff.rx(), rotatedCenterDiff.ry());
        rotatedCenterDiff.setY(-rotatedCenterDiff.y());
        auto intersect1 = intersectMiddle + h * rotatedCenterDiff / distance;
        auto intersect2 = intersectMiddle - h * rotatedCenterDiff / distance;

        // got intersection points, convert into angles from arc center
        auto wrapAngle = [](double angle) -> double {
            double ret = fmod(angle, 2*M_PI);
            if(ret < 0) {
                ret += 2*M_PI;
            }
            return ret;
        };

        auto angle1 = wrapAngle(atan2((intersect1 - this->center).y(), (intersect1 - this->center).x()));
        auto angle2 = wrapAngle(atan2((intersect2 - this->center).y(), (intersect2 - this->center).x()));

        auto angleDiff = wrapAngle(angle2 - angle1);
        if ((angleDiff >= M_PI) ^ (a > 0.0)) {
            // allowed angles go from intersect1 to intersect2
            if(startAngle < angle1) {
                startAngle = angle1;
            }
            auto maxSpan = wrapAngle(angle2 - startAngle);
            if(spanAngle > maxSpan) {
                spanAngle = maxSpan;
            }
        } else {
            // allowed angles go from intersect2 to intersect1
            if(startAngle < angle2) {
                startAngle = angle2;
            }
            auto maxSpan = wrapAngle(angle1 - startAngle);
            if(spanAngle > maxSpan) {
                spanAngle = maxSpan;
            }
        }
    }
}