/*
 * 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 "boardeditorstate.h"

#include "../../../graphics/graphicslayer.h"
#include "../../../graphics/graphicslayerlist.h"
#include "../../../undostack.h"
#include "../../../widgets/graphicsview.h"
#include "../boardgraphicsscene.h"
#include "../graphicsitems/bgi_device.h"
#include "../graphicsitems/bgi_hole.h"
#include "../graphicsitems/bgi_netline.h"
#include "../graphicsitems/bgi_netpoint.h"
#include "../graphicsitems/bgi_pad.h"
#include "../graphicsitems/bgi_plane.h"
#include "../graphicsitems/bgi_polygon.h"
#include "../graphicsitems/bgi_stroketext.h"
#include "../graphicsitems/bgi_via.h"
#include "../graphicsitems/bgi_zone.h"

#include <librepcb/core/geometry/polygon.h>
#include <librepcb/core/project/board/board.h>
#include <librepcb/core/project/board/items/bi_device.h>
#include <librepcb/core/project/board/items/bi_hole.h>
#include <librepcb/core/project/board/items/bi_netline.h>
#include <librepcb/core/project/board/items/bi_netpoint.h>
#include <librepcb/core/project/board/items/bi_netsegment.h>
#include <librepcb/core/project/board/items/bi_pad.h>
#include <librepcb/core/project/board/items/bi_plane.h>
#include <librepcb/core/project/board/items/bi_polygon.h>
#include <librepcb/core/project/board/items/bi_stroketext.h>
#include <librepcb/core/project/board/items/bi_via.h>
#include <librepcb/core/project/board/items/bi_zone.h>
#include <librepcb/core/utils/toolbox.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacesettings.h>

#include <QtCore>

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

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

BoardEditorState::BoardEditorState(const Context& context,
                                   QObject* parent) noexcept
  : QObject(parent), mContext(context), mAdapter(context.adapter) {
}

BoardEditorState::~BoardEditorState() noexcept {
}

/*******************************************************************************
 *  Protected Methods
 ******************************************************************************/

BoardGraphicsScene* BoardEditorState::getActiveBoardScene() noexcept {
  return mAdapter.fsmGetGraphicsScene();
}

bool BoardEditorState::getIgnoreLocks() const noexcept {
  return mAdapter.fsmGetIgnoreLocks();
}

PositiveLength BoardEditorState::getGridInterval() const noexcept {
  return mContext.board.getGridInterval();
}

const LengthUnit& BoardEditorState::getLengthUnit() const noexcept {
  return mContext.board.getGridUnit();
}

QSet<const Layer*> BoardEditorState::getAllowedGeometryLayers() noexcept {
  static const QSet<const Layer*> commonLayers = {
      &Layer::boardSheetFrames(),
      &Layer::boardOutlines(),
      &Layer::boardCutouts(),
      &Layer::boardPlatedCutouts(),
      &Layer::boardMeasures(),
      &Layer::boardAlignment(),
      &Layer::boardDocumentation(),
      &Layer::boardComments(),
      &Layer::boardGuide(),
      &Layer::topNames(),
      &Layer::topValues(),
      &Layer::topLegend(),
      &Layer::topDocumentation(),
      // &Layer::topPackageOutlines(), -> makes no sense in boards
      // &Layer::topCourtyard(), -> makes no sense in boards
      // &Layer::topHiddenGrabAreas(), -> makes no sense in boards
      &Layer::topCopper(),
      &Layer::topGlue(),
      &Layer::topSolderPaste(),
      &Layer::topStopMask(),
      &Layer::botNames(),
      &Layer::botValues(),
      &Layer::botLegend(),
      &Layer::botDocumentation(),
      // &Layer::botPackageOutlines(), -> makes no sense in boards
      // &Layer::botCourtyard(), -> makes no sense in boards
      // &Layer::botHiddenGrabAreas(), -> makes no sense in boards
      &Layer::botCopper(),
      &Layer::botGlue(),
      &Layer::botSolderPaste(),
      &Layer::botStopMask(),
  };
  return commonLayers | mContext.board.getCopperLayers();
}

void BoardEditorState::makeLayerVisible(const QString& layer) noexcept {
  if (std::shared_ptr<GraphicsLayer> l = mContext.layers.get(layer)) {
    if (l->isEnabled()) {
      l->setVisible(true);
    }
  }
}

void BoardEditorState::abortBlockingToolsInOtherEditors() noexcept {
  mAdapter.fsmAbortBlockingToolsInOtherEditors();
}

bool BoardEditorState::execCmd(UndoCommand* cmd) {
  return mContext.undoStack.execCmd(cmd);
}

QWidget* BoardEditorState::parentWidget() noexcept {
  return mAdapter.fsmGetParentWidget();
}

QList<std::shared_ptr<QGraphicsItem>> BoardEditorState::findItemsAtPos(
    const Point& pos, FindFlags flags, const Layer* cuLayer,
    const QSet<const NetSignal*>& netsignals,
    const QVector<std::shared_ptr<QGraphicsItem>>& except) noexcept {
  BoardGraphicsScene* scene = getActiveBoardScene();
  if (!scene) {
    return QList<std::shared_ptr<QGraphicsItem>>();
  }

  const QPointF posExact = pos.toPxQPointF();
  const QPointF posOnGrid = pos.mappedToGrid(getGridInterval()).toPxQPointF();
  const QPainterPath posArea = mAdapter.fsmCalcPosWithTolerance(pos, 1);
  const QPainterPath posAreaLarge = mAdapter.fsmCalcPosWithTolerance(pos, 1.5);

  // Note: The order of adding the items is very important (the top most item
  // must appear as the first item in the list)! For that, we work with
  // priorities (0 = highest priority):
  //
  //     0: vias
  //     1: pads THT
  //     5: holes
  //    50: polygons/texts board layer
  //   110: netpoints top
  //   120: netlines top
  //   130: planes/zones top
  //   140: footprints top
  //   150: pads top
  //   160: polygons/texts top
  //   210: netpoints inner
  //   220: netlines inner
  //   230: planes/zones inner
  //   240: polygons/texts inner
  //   310: netpoints bottom
  //   320: netlines bottom
  //   330: planes/zones bottom
  //   340: footprints bottom
  //   350: pads bottom
  //   360: polygons/texts bottom
  //
  // So the system is:
  //      0 for vias
  //      5 for holes
  //     10 for netpoints
  //     20 for netlines
  //     30 for planes/zones
  //     40 for footprints
  //     50 for pads
  //     60 for polygons/texts
  //   +100 for top layer items
  //   +200 for inner layer items
  //   +300 for bottom layer items
  //
  // And for items not directly under the cursor, but very close to the cursor,
  // add +1000. For items not under the cursor, but on the next grid interval,
  // add +2000.
  QMultiMap<std::pair<int, int>, std::shared_ptr<QGraphicsItem>> items;
  std::optional<std::pair<int, int>> lowestPriority;
  auto addItem = [&items, &lowestPriority](
                     const std::pair<int, int>& prio,
                     std::shared_ptr<QGraphicsItem> item) {
    if ((!lowestPriority) || (prio < (*lowestPriority))) {
      lowestPriority = prio;
    }
    items.insert(prio, item);
  };
  auto canSkip = [&lowestPriority, flags](const std::pair<int, int>& prio) {
    return flags.testFlag(FindFlag::SkipLowerPriorityMatches) &&
        lowestPriority && (prio > (*lowestPriority));
  };
  auto priorityFromLayer = [](const Layer& layer) {
    if (layer.isTop()) {
      return 100;
    } else if (layer.isInner()) {
      return 200;
    } else if (layer.isBottom()) {
      return 300;
    } else {
      return 0;
    }
  };
  auto processItem = [&pos, &posExact, &posOnGrid, &posArea, &posAreaLarge,
                      flags, &except, &addItem, &canSkip](
                         std::shared_ptr<QGraphicsItem> itemToCheck,
                         std::shared_ptr<QGraphicsItem> itemToAdd,
                         const Point& nearestPos, int priority, bool large) {
    if (except.contains(itemToAdd)) {
      return;
    }
    auto prio = std::make_pair(priority, 0);
    if (canSkip(prio)) {
      return;
    }
    const QPainterPath grabArea = itemToCheck->mapToScene(itemToCheck->shape());
    if (grabArea.isEmpty()) {
      return;
    }
    const int distance = qRound((nearestPos - pos).getLength()->toPx());
    prio = std::make_pair(priority, distance);
    if (canSkip(prio)) {
      return;
    }
    if (grabArea.contains(posExact)) {
      addItem(prio, itemToAdd);
      return;
    }
    prio = std::make_pair(priority + 1000, distance);
    if (canSkip(prio)) {
      return;
    }
    if ((flags & (FindFlag::AcceptNearMatch | FindFlag::AcceptNextGridMatch)) &&
        grabArea.intersects(large ? posAreaLarge : posArea)) {
      addItem(prio, itemToAdd);
      return;
    }
    prio = std::make_pair(distance + 2000, priority);  // Swapped order!
    if (canSkip(prio)) {
      return;
    }
    if ((flags & FindFlag::AcceptNextGridMatch) && (posOnGrid != posExact) &&
        grabArea.contains(posOnGrid)) {
      addItem(prio, itemToAdd);
      return;
    }
  };

  if (flags.testFlag(FindFlag::Holes)) {
    for (auto it = scene->getHoles().begin(); it != scene->getHoles().end();
         it++) {
      processItem(it.value(), it.value(),
                  it.key()->getData().getPath()->getVertices().first().getPos(),
                  5, false);
    }
  }

  if (flags.testFlag(FindFlag::Vias)) {
    for (auto it = scene->getVias().begin(); it != scene->getVias().end();
         it++) {
      if (netsignals.isEmpty() ||
          netsignals.contains(it.key()->getNetSegment().getNetSignal())) {
        if ((!cuLayer) || (it.key()->getVia().isOnLayer(*cuLayer))) {
          processItem(it.value(), it.value(), it.key()->getPosition(), 0,
                      false);
        }
      }
    }
  }

  if (flags.testFlag(FindFlag::NetPoints)) {
    for (auto it = scene->getNetPoints().begin();
         it != scene->getNetPoints().end(); it++) {
      if (netsignals.isEmpty() ||
          netsignals.contains(it.key()->getNetSegment().getNetSignal())) {
        const Layer* layer = it.key()->getLayerOfTraces();
        if ((!cuLayer) || (&*cuLayer == layer)) {
          processItem(it.value(), it.value(), it.key()->getPosition(),
                      10 + (layer ? priorityFromLayer(*layer) : 0), false);
        }
      }
    }
  }

  if (flags.testFlag(FindFlag::NetLines)) {
    for (auto it = scene->getNetLines().begin();
         it != scene->getNetLines().end(); it++) {
      if (netsignals.isEmpty() ||
          netsignals.contains(it.key()->getNetSegment().getNetSignal())) {
        const Layer& layer = it.key()->getLayer();
        if ((!cuLayer) || (*cuLayer == layer)) {
          processItem(
              it.value(), it.value(),
              Toolbox::nearestPointOnLine(pos.mappedToGrid(getGridInterval()),
                                          it.key()->getP1().getPosition(),
                                          it.key()->getP2().getPosition()),
              20 + priorityFromLayer(layer), false);
        }
      }
    }
  }

  if (flags.testFlag(FindFlag::Planes)) {
    for (auto it = scene->getPlanes().begin(); it != scene->getPlanes().end();
         it++) {
      if (netsignals.isEmpty() ||
          netsignals.contains(it.key()->getNetSignal())) {
        if ((!cuLayer) || (*cuLayer == it.key()->getLayer())) {
          processItem(
              it.value(), it.value(),
              it.key()->getOutline().calcNearestPointBetweenVertices(pos),
              30 + priorityFromLayer(it.key()->getLayer()),
              true);  // Probably large grab area makes sense?
        }
      }
    }
  }

  if (flags.testFlag(FindFlag::Zones)) {
    for (auto it = scene->getZones().begin(); it != scene->getZones().end();
         it++) {
      if ((!cuLayer) || (it.key()->getData().getLayers().contains(&*cuLayer))) {
        const QVector<const Layer*> layers =
            Layer::sorted(it.key()->getData().getLayers());
        int priority = 30;
        if (!layers.isEmpty()) {
          priority += priorityFromLayer(*layers.first());
        }
        processItem(
            it.value(), it.value(),
            it.key()->getData().getOutline().calcNearestPointBetweenVertices(
                pos),
            priority,
            true);  // Probably large grab area makes sense?
      }
    }
  }

  if (flags.testFlag(FindFlag::Devices)) {
    for (auto it = scene->getDevices().begin(); it != scene->getDevices().end();
         it++) {
      processItem(it.value(), it.value(), it.key()->getPosition(),
                  40 + (it.key()->getMirrored() ? 300 : 100), false);
    }
  }

  if (flags.testFlag(FindFlag::FootprintPads) ||
      flags.testFlag(FindFlag::BoardPads)) {
    for (auto it = scene->getPads().begin(); it != scene->getPads().end();
         it++) {
      if (((it.key()->getDevice() && flags.testFlag(FindFlag::FootprintPads)) ||
           (it.key()->getNetSegment() &&
            flags.testFlag(FindFlag::BoardPads))) &&
          (netsignals.isEmpty() ||
           netsignals.contains(it.key()->getNetSignal()))) {
        if ((!cuLayer) || (it.key()->isOnLayer(*cuLayer))) {
          // Give THT pads high priority to fix
          // https://github.com/LibrePCB/LibrePCB/issues/1073.
          const int priority = it.key()->getProperties().isTht()
              ? 1
              : (50 + (it.key()->getMirrored() ? 300 : 100));
          std::shared_ptr<QGraphicsItem> itemToAdd;
          if (flags.testFlag(FindFlag::DevicesOfPads) &&
              it.key()->getDevice()) {
            // See https://github.com/LibrePCB/LibrePCB/issues/1531.
            itemToAdd = it.value()->getDeviceGraphicsItem().lock();
          } else {
            itemToAdd = it.value();
          }
          if (itemToAdd) {
            processItem(it.value(), itemToAdd, it.key()->getPosition(),
                        priority, false);
          }
        }
      }
    }
  }

  if (flags.testFlag(FindFlag::Polygons)) {
    for (auto it = scene->getPolygons().begin();
         it != scene->getPolygons().end(); it++) {
      processItem(
          it.value(), it.value(),
          it.key()->getData().getPath().calcNearestPointBetweenVertices(pos),
          60 + priorityFromLayer(it.key()->getData().getLayer()),
          true);  // Probably large grab area makes sense?
    }
  }

  if (flags.testFlag(FindFlag::StrokeTexts)) {
    for (auto it = scene->getStrokeTexts().begin();
         it != scene->getStrokeTexts().end(); it++) {
      processItem(it.value(), it.value(), it.key()->getData().getPosition(),
                  60 + priorityFromLayer(it.key()->getData().getLayer()),
                  false);
    }
  }

  return items.values();
}

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

}  // namespace editor
}  // namespace librepcb
