/*
 * LibrePCB - Professional EDA for everyone!
 * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
 * https://librepcb.org/
 *
 * 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/>.
 */

/*******************************************************************************
 *  Includes
 ******************************************************************************/
#include "schematicnetsegmentsplitter.h"

#include "../../utils/toolbox.h"

#include <QtCore>
#include <QtWidgets>

/*******************************************************************************
 *  Namespace
 ******************************************************************************/
namespace librepcb {

/*******************************************************************************
 *  Constructors / Destructor
 ******************************************************************************/

SchematicNetSegmentSplitter::SchematicNetSegmentSplitter() noexcept
  : mJunctions(), mNetLines(), mNetLabels() {
}

SchematicNetSegmentSplitter::~SchematicNetSegmentSplitter() noexcept {
}

/*******************************************************************************
 *  General Methods
 ******************************************************************************/

void SchematicNetSegmentSplitter::addFixedAnchor(
    const NetLineAnchor& anchor, const Point& pos,
    bool replaceByJunction) noexcept {
  if (replaceByJunction) {
    std::shared_ptr<Junction> newJunction =
        std::make_shared<Junction>(Uuid::createRandom(), pos);
    mJunctions.append(newJunction);
    NetLineAnchor newAnchor(NetLineAnchor::junction(newJunction->getUuid()));
    mFixedAnchorsToReplace.insert(anchor, newAnchor);
  } else {
    mFixedAnchorPositions[anchor] = pos;
  }
}

void SchematicNetSegmentSplitter::addJunction(
    const Junction& junction) noexcept {
  mJunctions.append(std::make_shared<Junction>(junction));
}

void SchematicNetSegmentSplitter::addNetLine(const NetLine& netline) noexcept {
  std::shared_ptr<NetLine> copy = std::make_shared<NetLine>(netline);
  copy->setAnchors(replaceFixedAnchor(copy->getP1()),
                   replaceFixedAnchor(copy->getP2()));
  mNetLines.append(copy);
}

void SchematicNetSegmentSplitter::addNetLabel(
    const NetLabel& netlabel) noexcept {
  mNetLabels.append(std::make_shared<NetLabel>(netlabel));
}

QList<SchematicNetSegmentSplitter::Segment>
    SchematicNetSegmentSplitter::split() noexcept {
  QList<Segment> segments;

  // Split netsegment by anchors and lines.
  // IMPORTANT: Make shallow copies to keep all references valid even though
  // findConnectedLinesAndPoints() removes items from this list.
  QList<std::shared_ptr<NetLine>> availableNetLines = mNetLines.values();
  while (!availableNetLines.isEmpty()) {
    Segment segment;
    findConnectedLinesAndPoints(availableNetLines.first()->getP1(),
                                availableNetLines, segment);
    segments.append(segment);
  }
  Q_ASSERT(availableNetLines.isEmpty());

  // Add netlabels to their nearest netsegment
  for (NetLabel& netlabel : mNetLabels) {
    addNetLabelToNearestNetSegment(netlabel, segments);
  }

  return segments;
}

/*******************************************************************************
 *  Private Methods
 ******************************************************************************/

NetLineAnchor SchematicNetSegmentSplitter::replaceFixedAnchor(
    const NetLineAnchor& anchor) noexcept {
  return mFixedAnchorsToReplace.value(anchor, anchor);
}

void SchematicNetSegmentSplitter::findConnectedLinesAndPoints(
    const NetLineAnchor& anchor,
    QList<std::shared_ptr<NetLine>>& availableNetLines,
    Segment& segment) noexcept {
  if (std::optional<Uuid> junctionUuid = anchor.tryGetJunction()) {
    if (std::shared_ptr<Junction> junction = mJunctions.find(*junctionUuid)) {
      if (!segment.junctions.contains(junction->getUuid())) {
        segment.junctions.append(junction);
      }
    }
  }
  for (int i = 0; i < mNetLines.count(); ++i) {
    std::shared_ptr<NetLine> netline = mNetLines.value(i);
    if (((netline->getP1() == anchor) || (netline->getP2() == anchor)) &&
        availableNetLines.contains(netline) &&
        (!segment.netlines.contains(netline->getUuid()))) {
      segment.netlines.append(netline);
      availableNetLines.removeOne(netline);
      findConnectedLinesAndPoints(netline->getP1(), availableNetLines, segment);
      findConnectedLinesAndPoints(netline->getP2(), availableNetLines, segment);
    }
  }
}

void SchematicNetSegmentSplitter::addNetLabelToNearestNetSegment(
    const NetLabel& netlabel, QList<Segment>& segments) const noexcept {
  int nearestIndex = -1;
  Length nearestDistance;
  for (int i = 0; i < segments.count(); ++i) {
    Length distance =
        getDistanceBetweenNetLabelAndNetSegment(netlabel, segments.at(i));
    if ((distance < nearestDistance) || (nearestIndex < 0)) {
      nearestIndex = i;
      nearestDistance = distance;
    }
  }
  if (nearestIndex >= 0) {
    segments[nearestIndex].netlabels.append(
        std::make_shared<NetLabel>(netlabel));
  }
}

Length SchematicNetSegmentSplitter::getDistanceBetweenNetLabelAndNetSegment(
    const NetLabel& netlabel, const Segment& netsegment) const noexcept {
  bool firstRun = true;
  Length nearestDistance;
  for (const NetLine& netline : netsegment.netlines) {
    UnsignedLength distance = Toolbox::shortestDistanceBetweenPointAndLine(
        netlabel.getPosition(), getAnchorPosition(netline.getP1()),
        getAnchorPosition(netline.getP2()));
    if ((distance < nearestDistance) || firstRun) {
      nearestDistance = *distance;
      firstRun = false;
    }
  }
  return nearestDistance;
}

Point SchematicNetSegmentSplitter::getAnchorPosition(
    const NetLineAnchor& anchor) const noexcept {
  if (mFixedAnchorPositions.contains(anchor)) {
    return mFixedAnchorPositions[anchor];
  } else if (std::optional<Uuid> junctionUuid = anchor.tryGetJunction()) {
    if (mJunctions.contains(*junctionUuid)) {
      return mJunctions.get(*junctionUuid)->getPosition();
    }
  }
  qWarning() << "Failed to determine position of net label anchor while "
                "splitting segments!";
  return Point();
}

/*******************************************************************************
 *  End of File
 ******************************************************************************/

}  // namespace librepcb
