#include <cmath>
#include <QLocale>
#include "data/data.h"
#include "slideritem.h"
#include "sliderinfoitem.h"
#include "format.h"
#include "combinedgraphitem.h"
#include "combinedgraph.h"


CombinedGraph::GraphInfo CombinedGraph::graphInfo(DataType type) const
{
    GraphInfo gi;
    gi.unitScale = 1.0;
    gi.unitOffset = 0.0;

    switch (type) {
    case Elevation:
        gi.label = tr("Elevation");
        gi.color = QColor(0xd7, 0xd1, 0xdb);
        if (_units == Imperial || _units == Nautical) {
            gi.units = tr("ft");
            gi.unitScale = M2FT;
        } else {
            gi.units = tr("m");
        }
        break;
    case Speed:
        gi.label = tr("Speed");
        gi.color = QColor(0x31, 0x78, 0xc6);
        if (_units == Imperial) {
            gi.units = tr("mi/h");
            gi.unitScale = MS2MIH;
        } else if (_units == Nautical) {
            gi.units = tr("kn");
            gi.unitScale = MS2KN;
        } else {
            gi.units = tr("km/h");
            gi.unitScale = MS2KMH;
        }
        break;
    case HeartRate:
        gi.label = tr("Heart rate");
        gi.color = QColor(0xd1, 0x2d, 0x2d);
        gi.units = tr("bpm");
        break;
    case Temperature:
        gi.label = tr("Temperature");
        gi.color = QColor(0xd8, 0xdd, 0xd0);
        if (_units == Imperial || _units == Nautical) {
            gi.units = QChar(0x00B0) + tr("F");
            gi.unitScale = C2FS;
            gi.unitOffset = C2FO;
        } else {
            gi.units = QChar(0x00B0) + tr("C");
        }
        break;
    case Cadence:
        gi.label = tr("Cadence");
        gi.color = QColor(0x8e, 0x44, 0xad);
        gi.units = tr("rpm");
        break;
    case Power:
        gi.label = tr("Power");
        gi.color = QColor(0xf1, 0xc4, 0x0f);
        gi.units = tr("W");
        break;
    case GearRatio:
        gi.label = tr("Gear ratio");
        gi.color = QColor(0x7f, 0x8c, 0x8d);
        gi.units = QString();
        break;
    default:
        gi.label = tr("Unknown");
        gi.color = Qt::black;
        break;
    }

    return gi;
}

void CombinedGraph::graphMinMax(const Graph &graph, qreal &yMin, qreal &yMax)
{
    yMin = NAN;
    yMax = NAN;

    for (int i = 0; i < graph.size(); i++) {
        const GraphSegment &seg = graph.at(i);
        for (int j = 0; j < seg.size(); j++) {
            qreal y = seg.at(j).y();
            if (std::isnan(yMin) || y < yMin)
                yMin = y;
            if (std::isnan(yMax) || y > yMax)
                yMax = y;
        }
    }
}

qreal CombinedGraph::niceNum(qreal x, bool ceil)
{
    if (x == 0.0)
        return 0.0;

    bool neg = (x < 0);
    if (neg)
        x = -x;

    qreal exp = std::floor(std::log10(x));
    qreal frac = x / std::pow(10.0, exp);

    qreal nice;
    if (ceil) {
        if (frac <= 1.0)      nice = 1.0;
        else if (frac <= 2.0) nice = 2.0;
        else if (frac <= 5.0) nice = 5.0;
        else                  nice = 10.0;
    } else {
        if (frac >= 5.0)      nice = 5.0;
        else if (frac >= 2.0) nice = 2.0;
        else if (frac >= 1.0) nice = 1.0;
        else                  nice = 0.5;
    }

    qreal result = nice * std::pow(10.0, exp);
    return neg ? -result : result;
}

CombinedGraph::NiceRange CombinedGraph::niceRange(qreal min, qreal max)
{
    NiceRange nr;

    if (std::isnan(min) || std::isnan(max) || min >= max) {
        nr.min = 0;
        nr.max = 100;
        return nr;
    }

    qreal range = max - min;
    qreal step = niceNum(range / 5.0, true);

    nr.min = std::floor(min / step) * step;
    nr.max = std::ceil(max / step) * step;

    if (nr.min == nr.max)
        nr.max = nr.min + step;

    return nr;
}

Graph CombinedGraph::remapGraph(const Graph &graph, qreal srcMin,
                                qreal srcMax, qreal dstMin, qreal dstMax)
{
    if (graph.isEmpty())
        return graph;

    qreal srcRange = srcMax - srcMin;
    qreal dstRange = dstMax - dstMin;
    if (srcRange < 1e-9)
        srcRange = 1.0;

    Graph remapped;
    remapped.setColor(graph.color());

    for (int i = 0; i < graph.size(); i++) {
        const GraphSegment &seg = graph.at(i);
        GraphSegment ns(seg.size(), seg.start());

        for (int j = 0; j < seg.size(); j++) {
            const GraphPoint &p = seg.at(j);
            qreal t = (p.y() - srcMin) / srcRange;
            qreal ny = dstMin + t * dstRange;
            ns[j] = GraphPoint(p.s(), p.t(), ny);
        }

        remapped.append(ns);
    }

    return remapped;
}

CombinedGraph::CombinedGraph(QWidget *parent) : GraphTab(parent)
{
    _units = Metric;
    _axisType = Speed;
    _showTracks = false;
    _showRoutes = false;
    _loadGroup = 0;

    setSliderPrecision(1);
    setMinYRange(5.0);
}

CombinedGraph::~CombinedGraph()
{
    qDeleteAll(_tracks);
    qDeleteAll(_routes);
}

CombinedGraphItem *CombinedGraph::addItem(const Graph &graph, DataType type,
                                          bool isRoute, const NiceRange &axisRange, const GraphInfo &axisInfo)
{
    if (graph.isEmpty())
        return 0;

    GraphInfo gi = graphInfo(type);

    // Original data range in display units
    qreal origMin, origMax;
    graphMinMax(graph, origMin, origMax);
    qreal displayMin = origMin * gi.unitScale + gi.unitOffset;
    qreal displayMax = origMax * gi.unitScale + gi.unitOffset;

    // Compute average in display units using GraphItem::avg() approach
    // avg = weighted average of Y by distance
    qreal sum = 0, totalDist = 0;
    for (int i = 0; i < graph.size(); i++) {
        const GraphSegment &seg = graph.at(i);
        for (int j = 1; j < seg.size(); j++) {
            qreal ds = seg.at(j).s() - seg.at(j-1).s();
            sum += seg.at(j).y() * ds;
            totalDist += ds;
        }
    }
    qreal displayAvg = (totalDist > 0)
                           ? (sum / totalDist) * gi.unitScale + gi.unitOffset : 0;

    Graph renderGraph;
    qreal niceMin, niceMax;

    if (type == _axisType) {
        // Axis owner: scale to display units directly
        NiceRange nr = niceRange(displayMin, displayMax);
        niceMin = nr.min;
        niceMax = nr.max;

        renderGraph.setColor(graph.color());
        for (int i = 0; i < graph.size(); i++) {
            const GraphSegment &seg = graph.at(i);
            GraphSegment ns(seg.size(), seg.start());
            for (int j = 0; j < seg.size(); j++) {
                const GraphPoint &p = seg.at(j);
                ns[j] = GraphPoint(p.s(), p.t(),
                                   p.y() * axisInfo.unitScale + axisInfo.unitOffset);
            }
            renderGraph.append(ns);
        }
    } else {
        // Non-axis: nice-round, remap into axis range
        NiceRange srcNice = niceRange(displayMin, displayMax);
        niceMin = srcNice.min;
        niceMax = srcNice.max;

        Graph scaled;
        scaled.setColor(graph.color());
        for (int i = 0; i < graph.size(); i++) {
            const GraphSegment &seg = graph.at(i);
            GraphSegment ns(seg.size(), seg.start());
            for (int j = 0; j < seg.size(); j++) {
                const GraphPoint &p = seg.at(j);
                ns[j] = GraphPoint(p.s(), p.t(),
                                   p.y() * gi.unitScale + gi.unitOffset);
            }
            scaled.append(ns);
        }

        renderGraph = remapGraph(scaled, srcNice.min, srcNice.max,
                                 axisRange.min, axisRange.max);
    }

    CombinedGraphItem *item = new CombinedGraphItem(renderGraph, _graphType,
                                                    _width, gi.color, Qt::SolidLine, gi.label, gi.units,
                                                    displayMin, displayMax, displayAvg, niceMin, niceMax);

    if (isRoute) {
        _routes.append(item);
        if (_showRoutes)
            addGraph(item);
    } else {
        _tracks.append(item);
        if (_showTracks)
            addGraph(item);
    }

    return item;
}

void CombinedGraph::rebuildItems()
{
    for (int i = 0; i < _tracks.size(); i++)
        removeGraph(_tracks.at(i));
    for (int i = 0; i < _routes.size(); i++)
        removeGraph(_routes.at(i));
    qDeleteAll(_tracks);
    _tracks.clear();
    qDeleteAll(_routes);
    _routes.clear();

    if (_rawGraphs.isEmpty())
        return;

    // Determine axis owner
    bool hasSpeed = false, hasHR = false;
    for (int i = 0; i < _rawGraphs.size(); i++) {
        if (_rawGraphs.at(i).type == Speed)
            hasSpeed = true;
        if (_rawGraphs.at(i).type == HeartRate)
            hasHR = true;
    }

    if (hasSpeed)
        _axisType = Speed;
    else if (hasHR)
        _axisType = HeartRate;
    else
        _axisType = Speed;

    // Compute axis range
    GraphInfo axisInfo = graphInfo(_axisType);
    qreal axisDataMin = NAN, axisDataMax = NAN;

    for (int i = 0; i < _rawGraphs.size(); i++) {
        if (_rawGraphs.at(i).type != _axisType)
            continue;
        qreal mn, mx;
        graphMinMax(_rawGraphs.at(i).graph, mn, mx);
        if (!std::isnan(mn)) {
            qreal dMn = mn * axisInfo.unitScale + axisInfo.unitOffset;
            qreal dMx = mx * axisInfo.unitScale + axisInfo.unitOffset;
            if (std::isnan(axisDataMin) || dMn < axisDataMin)
                axisDataMin = dMn;
            if (std::isnan(axisDataMax) || dMx > axisDataMax)
                axisDataMax = dMx;
        }
    }

    NiceRange axisRange = niceRange(axisDataMin, axisDataMax);

    setYLabel(axisInfo.label);
    GraphView::setYUnits(axisInfo.units);
    setYScale(1.0);
    setYOffset(0.0);

    for (int i = 0; i < _rawGraphs.size(); i++) {
        const RawGraph &rg = _rawGraphs.at(i);
        addItem(rg.graph, rg.type, rg.isRoute, axisRange, axisInfo);
    }
}

QList<GraphItem*> CombinedGraph::loadData(const Data &data, Map *map)
{
    int currentGroup = _loadGroup++;

    for (int i = 0; i < data.tracks().count(); i++) {
        const Track &track = data.tracks().at(i);

        struct { Graph (Track::*fn)() const; DataType type; } simple[] = {
                      {&Track::heartRate, HeartRate},
                      {&Track::temperature, Temperature},
                      {&Track::cadence, Cadence},
                      {&Track::power, Power},
                      {&Track::ratio, GearRatio},
                      };

        const GraphPair &elev = track.elevation(map);
        if (!elev.primary().isEmpty()) {
            RawGraph rg;
            rg.graph = elev.primary();
            rg.type = Elevation;
            rg.isRoute = false;
            rg.trackIndex = i;
            rg.loadGroup = currentGroup;
            _rawGraphs.append(rg);
        }

        const GraphPair &spd = track.speed();
        if (!spd.primary().isEmpty()) {
            RawGraph rg;
            rg.graph = spd.primary();
            rg.type = Speed;
            rg.isRoute = false;
            rg.trackIndex = i;
            rg.loadGroup = currentGroup;
            _rawGraphs.append(rg);
        }

        for (int s = 0; s < 5; s++) {
            Graph g = (track.*(simple[s].fn))();
            if (!g.isEmpty()) {
                RawGraph rg;
                rg.graph = g;
                rg.type = simple[s].type;
                rg.isRoute = false;
                rg.trackIndex = i;
                rg.loadGroup = currentGroup;
                _rawGraphs.append(rg);
            }
        }
    }

    for (int i = 0; i < data.routes().count(); i++) {
        const Route &route = data.routes().at(i);
        const GraphPair &elev = route.elevation(map);
        if (!elev.primary().isEmpty()) {
            RawGraph rg;
            rg.graph = elev.primary();
            rg.type = Elevation;
            rg.isRoute = true;
            rg.trackIndex = i;
            rg.loadGroup = currentGroup;
            _rawGraphs.append(rg);
        }
    }

    rebuildItems();

    // Build return list: first item per track/route for map sync
    QList<GraphItem*> graphs;

    for (int i = 0; i < data.tracks().count(); i++) {
        GraphItem *first = 0;
        int itemIdx = 0;
        for (int r = 0; r < _rawGraphs.size(); r++) {
            const RawGraph &rg = _rawGraphs.at(r);
            if (rg.isRoute)
                continue;
            if (rg.loadGroup == currentGroup && rg.trackIndex == i) {
                if (itemIdx < _tracks.size()) {
                    first = _tracks.at(itemIdx);
                    break;
                }
            }
            itemIdx++;
        }
        graphs.append(first);
    }

    for (int i = 0; i < data.routes().count(); i++) {
        GraphItem *first = 0;
        int itemIdx = 0;
        for (int r = 0; r < _rawGraphs.size(); r++) {
            const RawGraph &rg = _rawGraphs.at(r);
            if (!rg.isRoute)
                continue;
            if (rg.loadGroup == currentGroup && rg.trackIndex == i) {
                if (itemIdx < _routes.size()) {
                    first = _routes.at(itemIdx);
                    break;
                }
            }
            itemIdx++;
        }
        graphs.append(first);
    }

    for (int i = 0; i < data.areas().count(); i++)
        _palette.nextColor();

    setInfo();
    redraw();

    return graphs;
}

void CombinedGraph::updateSliderInfo()
{
    QLocale l(QLocale::system());
    qreal pos = sliderPos();

    // Collect visible items
    QList<CombinedGraphItem *> visible;
    const QList<GraphItem*> &gl = graphList();
    for (int i = 0; i < gl.size(); i++) {
        CombinedGraphItem *ci = dynamic_cast<CombinedGraphItem*>(gl.at(i));
        if (ci)
            visible.append(ci);
    }

    if (visible.isEmpty()) {
        GraphView::updateSliderInfo();
        return;
    }

    // Build Y text lines — one per visible data type
    QStringList yLines;
    GraphInfo axisInfo = graphInfo(_axisType);

    for (int i = 0; i < visible.size(); i++) {
        CombinedGraphItem *ci = visible.at(i);
        qreal rawY = ci->yAtX(pos);
        if (std::isnan(rawY))
            continue;

        qreal realValue;
        if (ci->dataLabel() == axisInfo.label) {
            // Axis owner: rawY is already in display units
            realValue = rawY;
        } else {
            // Non-axis: rawY is in axis units, reverse-map to real units
            // rawY was mapped from [niceMin,niceMax] -> [axisMin,axisMax]
            // We need the axis range to reverse
            qreal axisMin = NAN, axisMax = NAN;
            for (int j = 0; j < visible.size(); j++) {
                if (visible.at(j)->dataLabel() == axisInfo.label) {
                    // Get the nice range of the axis owner
                    axisMin = visible.at(j)->niceMin();
                    axisMax = visible.at(j)->niceMax();
                    break;
                }
            }
            if (std::isnan(axisMin)) {
                // No axis owner visible, can't reverse-map
                realValue = rawY;
            } else {
                qreal axisRange = axisMax - axisMin;
                if (axisRange < 1e-9) axisRange = 1.0;
                qreal t = (rawY - axisMin) / axisRange;
                realValue = ci->niceMin() + t * (ci->niceMax() - ci->niceMin());
            }
        }

        QString line = ci->dataLabel() + ": "
                       + l.toString(realValue, 'f', 1)
                       + UNIT_SPACE + ci->originalUnits();
        yLines.append(line);
    }

    // X text
    QString xText(_graphType == Time
                      ? Format::timeSpan(pos, bounds().width() > 3600)
                      : l.toString(pos * yScale(), 'f', 1) + UNIT_SPACE + yUnits());

    // Use the multi-line setText overload
    sliderInfo()->setText(xText, yLines);

    // Position the slider info
    qreal sliderX = (pos / bounds().width()) * slider()->area().width();
    SliderInfoItem::Side s = (sliderX + sliderInfo()->boundingRect().width()
                              > slider()->area().right())
                                 ? SliderInfoItem::Left : SliderInfoItem::Right;
    sliderInfo()->setSide(s);
    sliderInfo()->setPos(QPointF(0, 0));
}

void CombinedGraph::setInfo()
{
    clearInfo();

    QList<CombinedGraphItem *> allItems;
    if (_showTracks)
        allItems.append(_tracks);
    if (_showRoutes)
        allItems.append(_routes);

    bool hasType[DataTypeCount] = {};

    for (int i = 0; i < allItems.size(); i++) {
        CombinedGraphItem *item = allItems.at(i);
        for (int t = 0; t < DataTypeCount; t++) {
            GraphInfo gi = graphInfo(static_cast<DataType>(t));
            if (item->dataLabel() == gi.label && !hasType[t])
                hasType[t] = true;
        }
    }

    for (int t = 0; t < DataTypeCount; t++) {
        if (hasType[t]) {
            GraphInfo gi = graphInfo(static_cast<DataType>(t));
            GraphView::addInfo(gi.label, gi.units);
        }
    }
}

void CombinedGraph::clear()
{
    qDeleteAll(_tracks);
    _tracks.clear();
    qDeleteAll(_routes);
    _routes.clear();
    _rawGraphs.clear();
    _loadGroup = 0;

    GraphTab::clear();
}

void CombinedGraph::setUnits(Units units)
{
    _units = units;

    rebuildItems();
    setInfo();
    redraw();

    GraphView::setUnits(units);
}

void CombinedGraph::setGraphType(GraphType type)
{
    GraphView::setGraphType(type);
}

void CombinedGraph::showTracks(bool show)
{
    _showTracks = show;

    for (int i = 0; i < _tracks.size(); i++) {
        if (show)
            addGraph(_tracks.at(i));
        else
            removeGraph(_tracks.at(i));
    }

    setInfo();
    redraw();
}

void CombinedGraph::showRoutes(bool show)
{
    _showRoutes = show;

    for (int i = 0; i < _routes.size(); i++) {
        if (show)
            addGraph(_routes.at(i));
        else
            removeGraph(_routes.at(i));
    }

    setInfo();
    redraw();
}
