/**********************************************************************************************
    Copyright (C) 2014-2015 Oliver Eichler <oliver.eichler@gmx.de>
    Copyright (C) 2017 Norbert Truchsess <norbert.truchsess@t-online.de>

    This program 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.

    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, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#include "mouse/line/ILineOp.h"

#include <QtWidgets>

#include "canvas/CCanvas.h"
#include "gis/CGisDraw.h"
#include "gis/CGisWorkspace.h"
#include "gis/GeoMath.h"
#include "gis/rte/router/CRouterSetup.h"
#include "mouse/line/IMouseEditLine.h"
#include "theme/CUiTheme.h"

/**
   Identifies the two polyline segments (by index) whose perpendicular projections are closest
   to two query points pt1 and pt2, as computed by GPS_Math_SubPolyline().
 */
struct segment_t {
  qint32 seg1Start = NOIDX; /**< first index of the polyline segment nearest to pt1 */
  qint32 seg1End = NOIDX;   /**< second index of that segment (always seg1Start + 1) */
  qint32 seg2Start = NOIDX; /**< first index of the polyline segment nearest to pt2 */

  /**
     @brief Extracts the polyline vertices that lie between the two nearest segments.
   */
  void apply(const QPolygonF& coords, const QPolygonF& pixel, QPolygonF& segCoord, QPolygonF& segPixel) const {
    if (seg1Start == NOIDX || seg2Start == NOIDX) {
      return;
    }
    if (seg1End == seg2Start) {
      segPixel.push_back(pixel[seg1End]);
      segCoord.push_back(coords[seg1End]);
    } else if (seg1Start < seg2Start) {
      for (int i = seg1End; i <= seg2Start; i++) {
        segPixel.push_back(pixel[i]);
        segCoord.push_back(coords[i]);
      }
    } else {
      for (int i = seg1Start; i > seg2Start; i--) {
        segPixel.push_back(pixel[i]);
        segCoord.push_back(coords[i]);
      }
    }
  }
};

static inline qreal distance(const QPointF& pa, const QPointF& pb) {
  const qreal& dx = pa.x() - pb.x();
  const qreal& dy = pa.y() - pb.y();
  return qSqrt(dx * dx + dy * dy);
}

/**
   @brief Find the sub-polyline in @p pixel that lies between two query points.

   For each query point, finds the segment in @p pixel whose perpendicular projection is closest
   (within @p threshold pixels). If no perpendicular projection lands within the threshold, falls
   back to the nearest endpoint (within 2 * @p threshold). The resulting segment indices allow
   segment_t::apply() to extract the vertices between the two query points.

   @param pt1       first query point in pixel coordinates
   @param pt2       second query point in pixel coordinates
   @param threshold maximum pixel distance for a segment projection to be considered
   @param pixel     the candidate polyline in pixel coordinates
   @param result    filled with the segment indices nearest to pt1 and pt2
 */
void GPS_Math_SubPolyline(const QPointF& pt1, const QPointF& pt2, qint32 threshold, const QPolygonF& pixel,
                          segment_t& result) {
  PJ_UV p1, p2;
  qreal dx, dy;
  qreal d_p1_p2;
  qreal x, y;
  qreal shortest1 = threshold;
  qreal shortest2 = threshold;
  qint32 seg1Start = NOIDX, seg1End = NOIDX, seg2Start = NOIDX;

  const qint32 len = pixel.size();
  for (qint32 i = 1; i < len; ++i) {
    p1.u = pixel[i - 1].x();
    p1.v = pixel[i - 1].y();
    p2.u = pixel[i].x();
    p2.v = pixel[i].y();

    dx = p2.u - p1.u;
    dy = p2.v - p1.v;
    d_p1_p2 = qSqrt(dx * dx + dy * dy);

    qreal u = ((pt1.x() - p1.u) * dx + (pt1.y() - p1.v) * dy) / (d_p1_p2 * d_p1_p2);

    if (u >= 0.0 && u <= 1.0) {
      x = p1.u + u * dx;
      y = p1.v + u * dy;
      qreal dist = qSqrt((x - pt1.x()) * (x - pt1.x()) + (y - pt1.y()) * (y - pt1.y()));
      if (dist < shortest1) {
        seg1Start = i - 1;
        seg1End = i;
        shortest1 = dist;
      }
    }

    u = ((pt2.x() - p1.u) * dx + (pt2.y() - p1.v) * dy) / (d_p1_p2 * d_p1_p2);

    if (u >= 0.0 && u <= 1.0) {
      x = p1.u + u * dx;
      y = p1.v + u * dy;
      qreal dist = qSqrt((x - pt2.x()) * (x - pt2.x()) + (y - pt2.y()) * (y - pt2.y()));
      if (dist < shortest2) {
        seg2Start = i - 1;
        shortest2 = dist;
      }
    }
  }

  // fall back to nearest endpoint if no segment projection was found
  if (seg1Start == NOIDX) {
    QPointF px = pixel.first();
    qreal dist = distance(px, pt1);
    if (dist < (threshold << 1)) {
      seg1Start = 0;
      seg1End = 1;
    } else {
      px = pixel.last();
      dist = distance(px, pt1);
      if (dist < (threshold << 1)) {
        seg1Start = pixel.size() - 2;
        seg1End = pixel.size() - 1;
      }
    }
  }

  if (seg2Start == NOIDX) {
    QPointF px = pixel.first();
    qreal dist = distance(px, pt2);
    if (dist < (threshold << 1)) {
      seg2Start = 0;
    } else {
      px = pixel.last();
      dist = distance(px, pt2);
      if (dist < (threshold << 1)) {
        seg2Start = pixel.size() - 2;
      }
    }
  }

  result.seg1Start = seg1Start;
  result.seg1End = seg1End;
  result.seg2Start = seg2Start;
}

ILineOp::ILineOp(SGisLine& points, CGisDraw* gis, CCanvas* canvas, IMouseEditLine* parent)
    : QObject(parent), parentHandler(parent), points(points), canvas(canvas), gis(gis) {
  timerRouting = new QTimer(this);
  timerRouting->setSingleShot(true);
  timerRouting->setInterval(400);
  connect(timerRouting, &QTimer::timeout, this, &ILineOp::slotTimeoutRouting);
}

ILineOp::~ILineOp() { canvas->reportStatus("Routino", QString()); }

void ILineOp::cancelDelayedRouting() { timerRouting->stop(); }

void ILineOp::startDelayedRouting() {
  if (parentHandler->useAutoRouting()) {
    timerRouting->start();
  } else if (parentHandler->useVectorRouting() || parentHandler->useTrackRouting()) {
    slotTimeoutRouting();
  }
}

void ILineOp::slotTimeoutRouting() {
  cancelDelayedRouting();
  isRouting = true;
  finalizeOperation(idxFocus);
  isRouting = false;
  canvas->slotTriggerCompleteUpdate(CCanvas::eRedrawMouse);
}

void ILineOp::drawBg(QPainter& p) {
  drawLeadLine(leadLinePixel1, p);
  drawLeadLine(leadLinePixel2, p);
}

void ILineOp::drawSinglePointSmall(const QPointF& pt, QPainter& p) {
  QRect r(0, 0, 3, 3);
  r.moveCenter(pt.toPoint());

  p.setPen(QPen(Qt::white, 2));
  p.setBrush(Qt::white);
  p.drawRect(r);

  p.setPen(Qt::black);
  p.setBrush(Qt::black);
  p.drawRect(r);
}

void ILineOp::drawSinglePointLarge(const QPointF& pt, QPainter& p) {
  rectPoint.moveCenter(pt.toPoint());

  p.setPen(penBgPoint);
  p.setBrush(brushBgPoint);
  p.drawRect(rectPoint);

  p.setPen(penFgPoint);
  p.setBrush(brushFgPoint);
  p.drawRect(rectPoint);
}

void ILineOp::drawLeadLine(const QPolygonF& line, QPainter& p) const {
  p.setPen(QPen(Qt::yellow, 7, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
  p.drawPolyline(line);
}

void ILineOp::mouseMove(const QPoint& pos) { updateLeadLines(idxFocus); }

void ILineOp::leftButtonDown(const QPoint& pos) {
  cancelDelayedRouting();
  showRoutingErrorMessage(QString());
}

void ILineOp::scaleChanged() { cancelDelayedRouting(); }

void ILineOp::startMouseMove(const QPointF& point) {
  /** After finalizeOperation() we reposition the virtual mouse cursor onto the just-placed
      point so that the next mouse move is measured from there, preventing an immediate
      re-trigger of on-the-fly routing before the cursor has actually moved. */
  parentHandler->startMouseMove(point.toPoint());
  cancelDelayedRouting();
}

void ILineOp::updateLeadLines(qint32 idx) {
  leadLinePixel1.clear();
  leadLinePixel2.clear();
  subLinePixel1.clear();
  subLinePixel2.clear();

  if ((parentHandler->useVectorRouting() || parentHandler->useTrackRouting()) && (idx != NOIDX)) {
    leadLineCoord1.clear();
    leadLineCoord2.clear();
    subLineCoord1.clear();
    subLineCoord2.clear();

    if (idx > 0) {
      const IGisLine::point_t& pt1 = points[idx - 1];
      const IGisLine::point_t& pt2 = points[idx];

      bool res = parentHandler->useVectorRouting()
                     ? canvas->findPolylineCloseBy(pt2.pixel, pt2.pixel, 10, leadLineCoord1)
                     : CGisWorkspace::self().findPolylineCloseBy(pt2.pixel, pt2.pixel, 10, leadLineCoord1);
      if (res) {
        leadLinePixel1 = leadLineCoord1;
        gis->convertRad2Px(leadLinePixel1);

        segment_t result;
        GPS_Math_SubPolyline(pt1.pixel, pt2.pixel, 10, leadLinePixel1, result);
        result.apply(leadLineCoord1, leadLinePixel1, subLineCoord1, subLinePixel1);
      }
    }

    if (idx < points.size() - 1) {
      const IGisLine::point_t& pt1 = points[idx];
      const IGisLine::point_t& pt2 = points[idx + 1];

      bool res = parentHandler->useVectorRouting()
                     ? canvas->findPolylineCloseBy(pt1.pixel, pt1.pixel, 10, leadLineCoord2)
                     : CGisWorkspace::self().findPolylineCloseBy(pt1.pixel, pt1.pixel, 10, leadLineCoord2);

      if (res) {
        leadLinePixel2 = leadLineCoord2;
        gis->convertRad2Px(leadLinePixel2);

        segment_t result;
        GPS_Math_SubPolyline(pt1.pixel, pt2.pixel, 10, leadLinePixel2, result);
        result.apply(leadLineCoord2, leadLinePixel2, subLineCoord2, subLinePixel2);
      }
    }
  }
}

void ILineOp::showRoutingErrorMessage(const QString& msg) const {
  if (msg.isEmpty()) {
    canvas->reportStatus("Routino", QString());
  } else {
    canvas->reportStatus("Routino", CUiTheme::Role::eError, QString("<b>%1</b><br />%2").arg(tr("Routing"), msg));
  }
}

void ILineOp::tryRouting(qint32 idx) const {
  // Copy coords before calcRoute: the router shows a progress dialog that runs an event loop,
  // during which the user can abort (right-click), causing restoreFromHistory() to reallocate
  // points and invalidate any references/pointers into it.
  QPointF coord1 = points[idx].coord;
  QPointF coord2 = points[idx + 1].coord;
  QPolygonF subs;

  try {
    if (CRouterSetup::self().calcRoute(coord1, coord2, subs) >= 0) {
      // Re-check after calcRoute's event loop: abort (right-click) may have
      // called restoreFromHistory(), shrinking points or changing what idx refers to.
      if (idx + 1 >= points.size() || idxFocus == NOIDX) {
        return;
      }
      points[idx].subpts.clear();
      for (const QPointF& sub : std::as_const(subs)) {
        points[idx].subpts << IGisLine::subpt_t(sub);
      }
    }
    showRoutingErrorMessage(QString());
  } catch (const QString& msg) {
    showRoutingErrorMessage(msg);
  }
  // workaround for canvas losing mouse tracking caused by CProgressDialog being modal:
  canvas->setMouseTracking(true);
}

void ILineOp::finalizeOperation(qint32 idx) {
  if (idx == NOIDX) {
    return;
  }

  if (parentHandler->useAutoRouting()) {
    CCanvasCursorLock cursorLock(Qt::WaitCursor, __func__);
    if (idx > 0 && idx < points.size()) {
      tryRouting(idx - 1);
    }
    if (idxFocus == NOIDX) {
      return;
    }
    if (idx < (points.size() - 1)) {
      tryRouting(idx);
    }
  } else if (parentHandler->useVectorRouting() || parentHandler->useTrackRouting()) {
    if (idx > 0) {
      IGisLine::point_t& pt1 = points[idx - 1];
      pt1.subpts.clear();
      for (const QPointF& pt : std::as_const(subLineCoord1)) {
        pt1.subpts << IGisLine::subpt_t(pt);
      }
    }

    if (idx < (points.size() - 1)) {
      IGisLine::point_t& pt1 = points[idx];
      pt1.subpts.clear();
      for (const QPointF& pt : std::as_const(subLineCoord2)) {
        pt1.subpts << IGisLine::subpt_t(pt);
      }
    }
  }

  // need to move the mouse away by some pixels to trigger next routing event
  // idx may be out of bounds if points was modified during calcRoute's event loop (e.g. abort)
  if (idx >= points.size()) {
    return;
  }
  startMouseMove(points[idx].pixel);

  parentHandler->updateStatus();
}

bool ILineOp::runRoutingAndPin(const QPointF& coord) {
  points[idxFocus].coord = coord;
  slotTimeoutRouting();
  return idxFocus != NOIDX && idxFocus < points.size();
}

qint32 ILineOp::isCloseTo(const QPoint& pos) const {
  qint32 min = 30;
  qint32 idx = NOIDX;
  const int N = points.size();
  for (int i = 0; i < N; i++) {
    const IGisLine::point_t& pt = points[i];

    qint32 d = (pos - pt.pixel).manhattanLength();
    if (d < min) {
      min = d;
      idx = i;
    }
  }

  return idx;
}

qint32 ILineOp::isCloseToLine(const QPoint& pos) const {
  qint32 idx = NOIDX;
  qreal dist = 60;

  for (int i = 0; i < points.size() - 1; i++) {
    QPolygonF line;
    const IGisLine::point_t& pt1 = points[i];
    const IGisLine::point_t& pt2 = points[i + 1];

    if (pt1.subpts.isEmpty()) {
      line << pt1.pixel << pt2.pixel;
    } else {
      line << pt1.pixel;
      for (const IGisLine::subpt_t& pt : pt1.subpts) {
        line << pt.pixel;
      }
      line << pt2.pixel;
    }

    qreal d = GPS_Math_DistPointPolyline(line, pos);
    if (d < dist) {
      dist = d;
      idx = i;
    }
  }

  return idx;
}
