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

#include "../../graphics/graphicslayer.h"
#include "../../graphics/graphicslayerlist.h"
#include "../../graphics/slintgraphicsview.h"
#include "../../guiapplication.h"
#include "../../rulecheck/rulecheckmessagesmodel.h"
#include "../../undostack.h"
#include "../../utils/editortoolbox.h"
#include "../../utils/imagehelpers.h"
#include "../../utils/slinthelpers.h"
#include "../../utils/uihelpers.h"
#include "../../workspace/desktopservices.h"
#include "../projecteditor.h"
#include "fsm/schematiceditorfsm.h"
#include "fsm/schematiceditorstate_addcomponent.h"
#include "fsm/schematiceditorstate_addtext.h"
#include "fsm/schematiceditorstate_drawbus.h"
#include "fsm/schematiceditorstate_drawpolygon.h"
#include "graphicsitems/sgi_symbol.h"
#include "schematiceditor.h"
#include "schematicgraphicsscene.h"

#include <librepcb/core/attribute/attributetype.h>
#include <librepcb/core/attribute/attributeunit.h>
#include <librepcb/core/project/erc/electricalrulecheckmessages.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/schematic/items/si_symbol.h>
#include <librepcb/core/project/schematic/schematic.h>
#include <librepcb/core/project/schematic/schematicpainter.h>
#include <librepcb/core/utils/toolbox.h>
#include <librepcb/core/workspace/theme.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <librepcb/core/workspace/workspacesettings.h>

#include <QtCore>
#include <QtWidgets>

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

static ui::WireMode l2s(SchematicEditorState_DrawWire::WireMode v) noexcept {
  if (v == SchematicEditorState_DrawWire::WireMode::HV) {
    return ui::WireMode::HV;
  } else if (v == SchematicEditorState_DrawWire::WireMode::VH) {
    return ui::WireMode::VH;
  } else if (v == SchematicEditorState_DrawWire::WireMode::Deg9045) {
    return ui::WireMode::Deg9045;
  } else if (v == SchematicEditorState_DrawWire::WireMode::Deg4590) {
    return ui::WireMode::Deg4590;
  } else if (v == SchematicEditorState_DrawWire::WireMode::Straight) {
    return ui::WireMode::Straight;
  } else {
    return ui::WireMode::HV;
  }
}

static SchematicEditorState_DrawWire::WireMode s2l(ui::WireMode v) noexcept {
  if (v == ui::WireMode::HV) {
    return SchematicEditorState_DrawWire::WireMode::HV;
  } else if (v == ui::WireMode::VH) {
    return SchematicEditorState_DrawWire::WireMode::VH;
  } else if (v == ui::WireMode::Deg9045) {
    return SchematicEditorState_DrawWire::WireMode::Deg9045;
  } else if (v == ui::WireMode::Deg4590) {
    return SchematicEditorState_DrawWire::WireMode::Deg4590;
  } else if (v == ui::WireMode::Straight) {
    return SchematicEditorState_DrawWire::WireMode::Straight;
  } else {
    return SchematicEditorState_DrawWire::WireMode::HV;
  }
}

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

SchematicTab::SchematicTab(GuiApplication& app, SchematicEditor& editor,
                           QObject* parent) noexcept
  : WindowTab(app, parent),
    onDerivedUiDataChanged(*this),
    mProjectEditor(editor.getProjectEditor()),
    mProject(mProjectEditor.getProject()),
    mSchematicEditor(editor),
    mSchematic(mSchematicEditor.getSchematic()),
    mLayers(
        GraphicsLayerList::schematicLayers(&app.getWorkspace().getSettings())),
    mPinNumbersLayer(mLayers->get(Theme::Color::sSchematicPinNumbers)),
    mView(new SlintGraphicsView(SlintGraphicsView::defaultSchematicSceneRect(),
                                SlintGraphicsView::defaultEditorMargins(),
                                this)),
    mMsgInstallLibraries(app.getWorkspace(), "EMPTY_SCHEMATIC_NO_LIBRARIES"),
    mMsgAddDrawingFrame(app.getWorkspace(), "EMPTY_SCHEMATIC_ADD_FRAME"),
    mGridStyle(mApp.getWorkspace()
                   .getSettings()
                   .themes.getActive()
                   .getSchematicGridStyle()),
    mIgnorePlacementLocks(false),
    mFrameIndex(0),
    mToolFeatures(),
    mTool(ui::EditorTool::Select),
    mToolCursorShape(Qt::ArrowCursor),
    mToolWireMode(SchematicEditorState_DrawWire::WireMode::HV),
    mToolLayers(std::make_shared<slint::VectorModel<slint::SharedString>>()),
    mToolLayer(nullptr),
    mToolLineWidth(app.getWorkspace().getSettings()),
    mToolSize(app.getWorkspace().getSettings()),
    mToolFilled(false),
    mToolValueSuggestions(
        std::make_shared<slint::VectorModel<slint::SharedString>>()),
    mToolAttributeUnits(
        std::make_shared<slint::VectorModel<slint::SharedString>>()),
    mToolAttributeUnit(nullptr) {
  Q_ASSERT(&mSchematic.getProject() == &mProject);

  // Setup graphics view.
  mView->setUseOpenGl(mApp.getWorkspace().getSettings().useOpenGl.get());
  mView->setEventHandler(this);
  connect(
      &mApp.getWorkspace().getSettings().useOpenGl,
      &WorkspaceSettingsItem_GenericValue<bool>::edited, this, [this]() {
        mView->setUseOpenGl(mApp.getWorkspace().getSettings().useOpenGl.get());
      });
  connect(mView.get(), &SlintGraphicsView::transformChanged, this,
          &SchematicTab::requestRepaint);
  connect(mView.get(), &SlintGraphicsView::stateChanged, this,
          [this]() { onDerivedUiDataChanged.notify(); });

  // Connect schematic editor.
  connect(&mSchematicEditor, &SchematicEditor::uiIndexChanged, this,
          [this]() { onDerivedUiDataChanged.notify(); });
  connect(&mSchematicEditor, &SchematicEditor::aboutToBeDestroyed, this,
          &SchematicTab::closeEnforced);

  // Connect project editor.
  connect(&mProjectEditor, &ProjectEditor::uiIndexChanged, this,
          [this]() { onDerivedUiDataChanged.notify(); });
  connect(&mProjectEditor, &ProjectEditor::abortBlockingToolsInOtherEditors,
          this, [this](const void* source) {
            if (source != this) {
              // Not so nice...
              mFsm->processAbortCommand();
              mFsm->processAbortCommand();
              mFsm->processAbortCommand();
            }
          });
  connect(&mProjectEditor, &ProjectEditor::ercMarkersInvalidated, this,
          &SchematicTab::clearErcMarker);

  // Connect undo stack.
  connect(&mProjectEditor.getUndoStack(), &UndoStack::stateModified, this,
          [this]() { onUiDataChanged.notify(); });
  connect(&mProjectEditor, &ProjectEditor::manualModificationsMade, this,
          [this]() { onUiDataChanged.notify(); });

  // Connect tool values.
  connect(&mToolLineWidth, &LengthEditContext::uiDataChanged, this,
          [this]() { onDerivedUiDataChanged.notify(); });

  // Connect search context.
  connect(&mSearchContext, &SearchContext::goToTriggered, this,
          &SchematicTab::goToSymbol);

  // Setup messages.
  connect(&mApp.getWorkspace().getLibraryDb(),
          &WorkspaceLibraryDb::scanLibraryListUpdated, this,
          &SchematicTab::updateMessages);
  connect(&mSchematic, &Schematic::symbolAdded, this,
          &SchematicTab::updateMessages);
  connect(&mSchematic, &Schematic::symbolRemoved, this,
          &SchematicTab::updateMessages);
  connect(&mMsgInstallLibraries, &DismissableMessageContext::visibilityChanged,
          this, [this]() { onDerivedUiDataChanged.notify(); });
  connect(&mMsgAddDrawingFrame, &DismissableMessageContext::visibilityChanged,
          this, [this]() { onDerivedUiDataChanged.notify(); });
  updateMessages();

  // Build the whole schematic editor finite state machine.
  SchematicEditorFsm::Context fsmContext{
      mApp.getWorkspace(),           mProject, mSchematic,
      mProjectEditor.getUndoStack(), *this,
  };
  mFsm.reset(new SchematicEditorFsm(fsmContext));

  // Apply theme whenever it has been modified.
  connect(&mApp.getWorkspace().getSettings().themes,
          &WorkspaceSettingsItem_Themes::edited, this,
          &SchematicTab::applyTheme);
  applyTheme();

  // Restore client settings.
  QSettings cs;
  if (mPinNumbersLayer) {
    mPinNumbersLayer->setVisible(
        cs.value("schematic_editor/show_pin_numbers", true).toBool());
  }
}

SchematicTab::~SchematicTab() noexcept {
  clearErcMarker();  // Avoid dangling pointers.
  deactivate();
  mView->setEventHandler(nullptr);

  // Delete FSM as it may trigger some other methods during destruction.
  mFsm.reset();
}

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

int SchematicTab::getProjectIndex() const noexcept {
  return mProjectEditor.getUiIndex();
}

int SchematicTab::getProjectObjectIndex() const noexcept {
  return mProject.getSchematicIndex(mSchematic);
}

ui::TabData SchematicTab::getUiData() const noexcept {
  ui::TabFeatures features = {};
  features.save = toFs(mProject.getDirectory().isWritable());
  features.undo = toFs(mProjectEditor.getUndoStack().canUndo());
  features.redo = toFs(mProjectEditor.getUndoStack().canRedo());
  features.grid = toFs(mProject.getDirectory().isWritable());
  features.zoom = toFs(true);
  features.export_graphics = toFs(mTool == ui::EditorTool::Select);
  features.select = toFs(mToolFeatures.testFlag(Feature::Select));
  features.cut = toFs(mToolFeatures.testFlag(Feature::Cut));
  features.copy = toFs(mToolFeatures.testFlag(Feature::Copy));
  features.paste = toFs(mToolFeatures.testFlag(Feature::Paste));
  features.remove = toFs(mToolFeatures.testFlag(Feature::Remove));
  features.rotate = toFs(mToolFeatures.testFlag(Feature::Rotate));
  features.mirror = toFs(mToolFeatures.testFlag(Feature::Mirror));
  features.snap_to_grid = toFs(mToolFeatures.testFlag(Feature::SnapToGrid));
  features.reset_texts = toFs(mToolFeatures.testFlag(Feature::ResetTexts));
  features.edit_properties = toFs(mToolFeatures.testFlag(Feature::Properties));
  features.find = toFs(true);

  return ui::TabData{
      ui::TabType::Schematic,  // Type
      q2s(*mSchematic.getName()),  // Title
      features,  // Features
      !mProject.getDirectory().isWritable(),  // Read-only
      mProjectEditor.hasUnsavedChanges(),  // Unsaved changes
      q2s(mProjectEditor.getUndoStack().getUndoCmdText()),  // Undo text
      q2s(mProjectEditor.getUndoStack().getRedoCmdText()),  // Redo text
      q2s(mSearchContext.getTerm()),  // Find term
      mSearchContext.getSuggestions(),  // Find suggestions
      nullptr,  // Layers
  };
}

void SchematicTab::setUiData(const ui::TabData& data) noexcept {
  mSearchContext.setTerm(s2q(data.find_term));
  WindowTab::setUiData(data);
  onUiDataChanged.notify();
}

ui::SchematicTabData SchematicTab::getDerivedUiData() const noexcept {
  const Theme& theme = mApp.getWorkspace().getSettings().themes.getActive();
  const QColor bgColor =
      theme.getColor(Theme::Color::sSchematicBackground).getPrimaryColor();
  const QColor fgColor = (bgColor.lightnessF() >= 0.5) ? Qt::black : Qt::white;

  return ui::SchematicTabData{
      mProjectEditor.getUiIndex(),  // Project index
      mSchematicEditor.getUiIndex(),  // Schematic index
      q2s(bgColor),  // Background color
      q2s(fgColor),  // Foreground color
      q2s(theme.getColor(Theme::Color::sSchematicInfoBox)
              .getPrimaryColor()),  // Overlay color
      q2s(theme.getColor(Theme::Color::sSchematicInfoBox)
              .getSecondaryColor()),  // Overlay text color
      l2s(mGridStyle),  // Grid style
      l2s(*mSchematic.getGridInterval()),  // Grid interval
      l2s(mSchematic.getGridUnit()),  // Length unit
      mPinNumbersLayer && mPinNumbersLayer->isVisible(),  // Show pin numbers
      mIgnorePlacementLocks,  // Ignore placement locks
      mMsgInstallLibraries.getUiData(),  // Message "install libraries"
      mMsgAddDrawingFrame.getUiData(),  // Message "add schematic frame"
      mTool,  // Tool
      q2s(mView->isPanning() ? Qt::ClosedHandCursor
                             : mToolCursorShape),  // Tool cursor
      q2s(mToolOverlayText),  // Tool overlay text
      l2s(mToolWireMode),  // Tool wire mode
      ui::ComboBoxData{
          // Tool layer
          mToolLayers,  // Items
          static_cast<int>(mToolLayersQt.indexOf(mToolLayer)),  // Current index
      },
      mToolLineWidth.getUiData(),  // Tool line width
      mToolSize.getUiData(),  // // Tool size
      mToolFilled,  // Tool filled
      ui::LineEditData{
          // Tool value
          true,  // Enabled
          q2s(EditorToolbox::toSingleLine(mToolValue)),  // Text
          slint::SharedString(),  // Placeholder
          mToolValueSuggestions,  // Suggestions
      },
      ui::LineEditData{
          // Tool attribute value
          mToolAttributeValue.has_value(),  // Enabled
          mToolAttributeValue
              ? q2s(EditorToolbox::toSingleLine(*mToolAttributeValue))
              : slint::SharedString(),  // Text
          q2s(mToolAttributeValuePlaceholder),  // Placeholder
          mToolValueSuggestions,  // Suggestions
      },
      ui::ComboBoxData{
          // Tool attribute unit
          mToolAttributeUnits,  // Items
          static_cast<int>(mToolAttributeUnitsQt.indexOf(
              mToolAttributeUnit)),  // Current index
      },
      q2s(mSceneImagePos),  // Scene image position
      mFrameIndex,  // Frame index
      slint::SharedString(),  // New bus UUID
  };
}

void SchematicTab::setDerivedUiData(const ui::SchematicTabData& data) noexcept {
  mSceneImagePos = s2q(data.scene_image_pos);

  mGridStyle = s2l(data.grid_style);
  const std::optional<PositiveLength> interval = s2plength(data.grid_interval);
  if (interval && (*interval != mSchematic.getGridInterval())) {
    mSchematic.setGridInterval(*interval);
    mProjectEditor.setManualModificationsMade();
  }
  if (mScene) {
    mScene->setGridStyle(mGridStyle);
    mScene->setGridInterval(mSchematic.getGridInterval());
  }
  const LengthUnit unit = s2l(data.unit);
  if (unit != mSchematic.getGridUnit()) {
    mSchematic.setGridUnit(unit);
    mProjectEditor.setManualModificationsMade();
  }
  if (mPinNumbersLayer &&
      (mPinNumbersLayer->isVisible() != data.show_pin_numbers)) {
    mPinNumbersLayer->setVisible(data.show_pin_numbers);
    QSettings cs;
    cs.setValue("schematic_editor/show_pin_numbers", data.show_pin_numbers);
  }

  // Placement locks
  mIgnorePlacementLocks = data.ignore_placement_locks;

  // Messages
  mMsgInstallLibraries.setUiData(data.install_libraries_msg);
  mMsgAddDrawingFrame.setUiData(data.add_drawing_frame_msg);

  if (const Layer* layer = mToolLayersQt.value(data.tool_layer.current_index)) {
    emit layerRequested(*layer);
  }
  emit wireModeRequested(s2l(data.tool_wire_mode));
  emit filledRequested(data.tool_filled);
  mToolLineWidth.setUiData(data.tool_line_width);
  mToolSize.setUiData(data.tool_size);
  emit valueRequested(EditorToolbox::toMultiLine(s2q(data.tool_value.text)));
  // Unit must be set before value, because value may override the unit!
  emit attributeUnitRequested(
      mToolAttributeUnitsQt.value(data.tool_attribute_unit.current_index));
  emit attributeValueRequested(
      EditorToolbox::toMultiLine(s2q(data.tool_attribute_value.text)));

  // Actions
  if (!data.new_bus_uuid.empty()) {
    if (auto uuid = Uuid::tryFromString(s2q(data.new_bus_uuid))) {
      mFsm->processDrawBus(*uuid);
    }
  }

  requestRepaint();
}

void SchematicTab::highlightErcMessage(
    const std::shared_ptr<const ErcMsgBase>& msg, bool zoomTo) noexcept {
  if ((!msg) || msg->getLocations().isEmpty()) {
    // Position on schematic not known.
    clearErcMarker();
  } else if (mScene) {
    const ThemeColor& color =
        mApp.getWorkspace().getSettings().themes.getActive().getColor(
            Theme::Color::sSchematicOverlays);
    QPainterPath path = Path::toQPainterPathPx(msg->getLocations(), true);
    mErcLocationGraphicsItem.reset(new QGraphicsPathItem());
    mErcLocationGraphicsItem->setZValue(
        SchematicGraphicsScene::ZValue_ErcLocation);
    mErcLocationGraphicsItem->setPen(QPen(color.getPrimaryColor(), 0));
    mErcLocationGraphicsItem->setBrush(color.getSecondaryColor());
    mErcLocationGraphicsItem->setPath(path);
    mScene->addItem(*mErcLocationGraphicsItem.get());

    const QRectF rect = path.boundingRect();
    const qreal boxMargin = Length(1000000).toPx();
    mScene->setSceneRectMarker(
        rect.adjusted(-boxMargin, -boxMargin, boxMargin, boxMargin));
    if (zoomTo) {
      const qreal zoomMargin = Length(20000000).toPx();
      mView->zoomToSceneRect(
          rect.adjusted(-zoomMargin, -zoomMargin, zoomMargin, zoomMargin));
    }
  }
}

void SchematicTab::activate() noexcept {
  mScene.reset(new SchematicGraphicsScene(
      mSchematic, *mLayers, mProjectEditor.getHighlightedNetSignals(),
      mIgnorePlacementLocks, this));
  mScene->setGridInterval(mSchematic.getGridInterval());
  connect(&mProjectEditor, &ProjectEditor::highlightedNetSignalsChanged,
          mScene.get(), &SchematicGraphicsScene::updateHighlightedNetSignals);
  connect(mScene.get(), &GraphicsScene::changed, this,
          &SchematicTab::requestRepaint);

  // Initialize search context.
  mSearchContext.init();

  applyTheme();
  mProjectEditor.registerActiveSchematicTab(this);
  requestRepaint();
}

void SchematicTab::deactivate() noexcept {
  mProjectEditor.unregisterActiveSchematicTab(this);
  mSearchContext.deinit();
  mErcLocationGraphicsItem.reset();
  mScene.reset();
}

void SchematicTab::trigger(ui::TabAction a) noexcept {
  switch (a) {
    case ui::TabAction::Print: {
      execGraphicsExportDialog(GraphicsExportDialog::Output::Print, "print");
      break;
    }
    case ui::TabAction::ExportImage: {
      execGraphicsExportDialog(GraphicsExportDialog::Output::Image,
                               "image_export");
      break;
    }
    case ui::TabAction::ExportPdf: {
      execGraphicsExportDialog(GraphicsExportDialog::Output::Pdf, "pdf_export");
      break;
    }
    case ui::TabAction::SelectAll: {
      mFsm->processSelectAll();
      break;
    }
    case ui::TabAction::Abort: {
      if (mErcLocationGraphicsItem) {
        clearErcMarker();
      } else {
        mFsm->processAbortCommand();
      }
      break;
    }
    case ui::TabAction::Save: {
      mProjectEditor.saveProject();
      break;
    }
    case ui::TabAction::Undo: {
      mProjectEditor.undo();
      break;
    }
    case ui::TabAction::Redo: {
      mProjectEditor.redo();
      break;
    }
    case ui::TabAction::Cut: {
      mFsm->processCut();
      break;
    }
    case ui::TabAction::Copy: {
      mFsm->processCopy();
      break;
    }
    case ui::TabAction::Paste: {
      QByteArray data;
      QString basename;
      QString format;
      if (ImageHelpers::getImageFromClipboard(data, format, basename)) {
        mFsm->processAddImage(data, format, basename);
      } else {
        mFsm->processPaste();
      }
      break;
    }
    case ui::TabAction::Delete: {
      mFsm->processRemove();
      break;
    }
    case ui::TabAction::RotateCcw: {
      mFsm->processRotate(Angle::deg90());
      break;
    }
    case ui::TabAction::RotateCw: {
      mFsm->processRotate(-Angle::deg90());
      break;
    }
    case ui::TabAction::MirrorHorizontally: {
      mFsm->processMirror(Qt::Horizontal);
      break;
    }
    case ui::TabAction::MirrorVertically: {
      mFsm->processMirror(Qt::Vertical);
      break;
    }
    case ui::TabAction::MoveLeft: {
      if (!mFsm->processMove(Point(-mSchematic.getGridInterval(), 0))) {
        mView->scrollLeft();
      }
      break;
    }
    case ui::TabAction::MoveRight: {
      if (!mFsm->processMove(Point(*mSchematic.getGridInterval(), 0))) {
        mView->scrollRight();
      }
      break;
    }
    case ui::TabAction::MoveUp: {
      if (!mFsm->processMove(Point(0, *mSchematic.getGridInterval()))) {
        mView->scrollUp();
      }
      break;
    }
    case ui::TabAction::MoveDown: {
      if (!mFsm->processMove(Point(0, -mSchematic.getGridInterval()))) {
        mView->scrollDown();
      }
      break;
    }
    case ui::TabAction::SnapToGrid: {
      mFsm->processSnapToGrid();
      break;
    }
    case ui::TabAction::ResetTexts: {
      mFsm->processResetAllTexts();
      break;
    }
    case ui::TabAction::EditProperties: {
      mFsm->processEditProperties();
      break;
    }
    case ui::TabAction::GridIntervalIncrease: {
      mSchematic.setGridInterval(
          PositiveLength(mSchematic.getGridInterval() * 2));
      if (mScene) {
        mScene->setGridInterval(mSchematic.getGridInterval());
        requestRepaint();
      }
      break;
    }
    case ui::TabAction::GridIntervalDecrease: {
      if ((*mSchematic.getGridInterval() % 2) == 0) {
        mSchematic.setGridInterval(
            PositiveLength(mSchematic.getGridInterval() / 2));
        if (mScene) {
          mScene->setGridInterval(mSchematic.getGridInterval());
          requestRepaint();
        }
      }
      break;
    }
    case ui::TabAction::ZoomIn: {
      mView->zoomIn();
      break;
    }
    case ui::TabAction::ZoomOut: {
      mView->zoomOut();
      break;
    }
    case ui::TabAction::ZoomFit: {
      if (mScene) mView->zoomToSceneRect(mScene->itemsBoundingRect());
      break;
    }
    case ui::TabAction::FindRefreshSuggestions: {
      QStringList names;
      for (const SI_Symbol* sym : mSchematic.getSymbols()) {
        names.append(sym->getName());
      }
      Toolbox::sortNumeric(names);
      mSearchContext.setSuggestions(names);
      break;
    }
    case ui::TabAction::FindNext: {
      mSearchContext.findNext();
      break;
    }
    case ui::TabAction::FindPrevious: {
      mSearchContext.findPrevious();
      break;
    }
    case ui::TabAction::ToolSelect: {
      mFsm->processSelect();
      break;
    }
    case ui::TabAction::ToolWire: {
      mFsm->processDrawWire();
      break;
    }
    case ui::TabAction::ToolBus: {
      mFsm->processDrawBus();
      break;
    }
    case ui::TabAction::ToolLabel: {
      mFsm->processAddNetLabel();
      break;
    }
    case ui::TabAction::ToolPolygon: {
      mFsm->processDrawPolygon();
      break;
    }
    case ui::TabAction::ToolText: {
      mFsm->processAddText();
      break;
    }
    case ui::TabAction::ToolImage: {
      mFsm->processAddImage();
      break;
    }
    case ui::TabAction::ToolComponent: {
      mFsm->processAddComponent();
      break;
    }
    case ui::TabAction::ToolComponentFrame: {
      mFsm->processAddComponent("schematic frame");
      break;
    }
    case ui::TabAction::ToolComponentResistor: {
      mFsm->processAddComponent(
          Uuid::fromString("ef80cd5e-2689-47ee-8888-31d04fc99174"),
          Uuid::fromString(mProjectEditor.getUseIeee315Symbols()
                               ? "d16e1f44-16af-4773-a310-de370f744548"
                               : "a5995314-f535-45d4-8bd8-2d0b8a0dc42a"));
      break;
    }
    case ui::TabAction::ToolComponentInductor: {
      mFsm->processAddComponent(
          Uuid::fromString("506bd124-6062-400e-9078-b38bd7e1aaee"),
          Uuid::fromString(mProjectEditor.getUseIeee315Symbols()
                               ? "4245d515-6f6d-48cb-9958-a4ea23d0187f"
                               : "62a7598c-17fe-41cf-8fa1-4ed274c3adc2"));
      break;
    }
    case ui::TabAction::ToolComponentCapacitorBipolar: {
      mFsm->processAddComponent(
          Uuid::fromString("d167e0e3-6a92-4b76-b013-77b9c230e5f1"),
          Uuid::fromString(mProjectEditor.getUseIeee315Symbols()
                               ? "6e639ff1-4e81-423b-9d0e-b28b35693a61"
                               : "8cd7b37f-e5fa-4af5-a8dd-d78830bba3af"));
      break;
    }
    case ui::TabAction::ToolComponentCapacitorUnipolar: {
      mFsm->processAddComponent(
          Uuid::fromString("c54375c5-7149-4ded-95c5-7462f7301ee7"),
          Uuid::fromString(mProjectEditor.getUseIeee315Symbols()
                               ? "20a01a81-506e-4fee-9dc0-8b50e6537cd4"
                               : "5412add2-af9c-44b8-876d-a0fb7c201897"));
      break;
    }
    case ui::TabAction::ToolComponentGnd: {
      mFsm->processAddComponent(
          Uuid::fromString("8076f6be-bfab-4fc1-9772-5d54465dd7e1"),
          Uuid::fromString("f09ad258-595b-4ee9-a1fc-910804a203ae"));
      break;
    }
    case ui::TabAction::ToolComponentVcc: {
      mFsm->processAddComponent(
          Uuid::fromString("58c3c6cd-11eb-4557-aa3f-d3e05874afde"),
          Uuid::fromString("afb86b45-68ec-47b6-8d96-153d73567228"));
      break;
    }
    case ui::TabAction::ToolMeasure: {
      mFsm->processMeasure();
      break;
    }
    default: {
      WindowTab::trigger(a);
      break;
    }
  }
}

slint::Image SchematicTab::renderScene(float width, float height,
                                       int scene) noexcept {
  Q_UNUSED(scene);
  if (mScene) {
    return mView->render(*mScene, width, height);
  }
  return slint::Image();
}

bool SchematicTab::processScenePointerEvent(
    const QPointF& pos, slint::private_api::PointerEvent e) noexcept {
  return mView->pointerEvent(pos, e);
}

bool SchematicTab::processSceneScrolled(
    const QPointF& pos, slint::private_api::PointerScrollEvent e) noexcept {
  return mView->scrollEvent(pos, e);
}

bool SchematicTab::processSceneKeyEvent(
    const slint::private_api::KeyEvent& e) noexcept {
  return mView->keyEvent(e);
}

/*******************************************************************************
 *  IF_GraphicsViewEventHandler Methods
 ******************************************************************************/

bool SchematicTab::graphicsSceneKeyPressed(
    const GraphicsSceneKeyEvent& e) noexcept {
  return mFsm->processKeyPressed(e);
}

bool SchematicTab::graphicsSceneKeyReleased(
    const GraphicsSceneKeyEvent& e) noexcept {
  return mFsm->processKeyReleased(e);
}

bool SchematicTab::graphicsSceneMouseMoved(
    const GraphicsSceneMouseEvent& e) noexcept {
  emit cursorCoordinatesChanged(e.scenePos, mSchematic.getGridUnit());
  return mFsm->processGraphicsSceneMouseMoved(e);
}

bool SchematicTab::graphicsSceneLeftMouseButtonPressed(
    const GraphicsSceneMouseEvent& e) noexcept {
  return mFsm->processGraphicsSceneLeftMouseButtonPressed(e);
}

bool SchematicTab::graphicsSceneLeftMouseButtonReleased(
    const GraphicsSceneMouseEvent& e) noexcept {
  return mFsm->processGraphicsSceneLeftMouseButtonReleased(e);
}

bool SchematicTab::graphicsSceneLeftMouseButtonDoubleClicked(
    const GraphicsSceneMouseEvent& e) noexcept {
  return mFsm->processGraphicsSceneLeftMouseButtonDoubleClicked(e);
}

bool SchematicTab::graphicsSceneRightMouseButtonReleased(
    const GraphicsSceneMouseEvent& e) noexcept {
  return mFsm->processGraphicsSceneRightMouseButtonReleased(e);
}

/*******************************************************************************
 *  SchematicEditorFsmAdapter Methods
 ******************************************************************************/

SchematicGraphicsScene* SchematicTab::fsmGetGraphicsScene() noexcept {
  return mScene.get();
}

bool SchematicTab::fsmGetIgnoreLocks() const noexcept {
  return mIgnorePlacementLocks;
}

void SchematicTab::fsmSetViewCursor(
    const std::optional<Qt::CursorShape>& shape) noexcept {
  if (shape) {
    mToolCursorShape = *shape;
  } else {
    mToolCursorShape = Qt::ArrowCursor;
  }
  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmSetViewGrayOut(bool grayOut) noexcept {
  if (mScene) {
    mScene->setGrayOut(grayOut);
  }
}

void SchematicTab::fsmSetViewInfoBoxText(const QString& text) noexcept {
  QString t = text;
  t.replace("&nbsp;", " ");
  t.replace("<br>", "\n");
  t.replace("<b>", "");
  t.replace("</b>", "");

  if (t != mToolOverlayText) {
    mToolOverlayText = t;
    onDerivedUiDataChanged.notify();
  }
}

void SchematicTab::fsmSetViewRuler(
    const std::optional<std::pair<Point, Point>>& pos) noexcept {
  if (mScene) {
    mScene->setRulerPositions(pos);
  }
}

void SchematicTab::fsmSetSceneCursor(const Point& pos, bool cross,
                                     bool circle) noexcept {
  if (mScene) {
    mScene->setSceneCursor(pos, cross, circle);
  }
}

QPainterPath SchematicTab::fsmCalcPosWithTolerance(
    const Point& pos, qreal multiplier) const noexcept {
  return mView->calcPosWithTolerance(pos, multiplier);
}

Point SchematicTab::fsmMapGlobalPosToScenePos(
    const QPoint& pos) const noexcept {
  if (QWidget* win = qApp->activeWindow()) {
    return mView->mapToScenePos(win->mapFromGlobal(pos) - mSceneImagePos);
  } else {
    qWarning() << "Failed to map global position to scene position.";
    return Point();
  }
}

void SchematicTab::fsmZoomToSceneRect(const QRectF& r) noexcept {
  mView->zoomToSceneRect(r);
}

void SchematicTab::fsmSetHighlightedNetSignals(
    const QSet<const NetSignal*>& sigs) noexcept {
  mProjectEditor.setHighlightedNetSignals(sigs);
}

void SchematicTab::fsmAbortBlockingToolsInOtherEditors() noexcept {
  mProjectEditor.abortBlockingToolsInOtherEditors(this);
}

void SchematicTab::fsmSetStatusBarMessage(const QString& message,
                                          int timeoutMs) noexcept {
  emit statusBarMessageChanged(message, timeoutMs);
}

void SchematicTab::fsmSetFeatures(Features features) noexcept {
  if (features != mToolFeatures) {
    mToolFeatures = features;
    onUiDataChanged.notify();
  }
}

void SchematicTab::fsmToolLeave() noexcept {
  while (!mFsmStateConnections.isEmpty()) {
    disconnect(mFsmStateConnections.takeLast());
  }
  mTool = ui::EditorTool::Select;
  fsmSetFeatures(Features());
  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_Select& state) noexcept {
  Q_UNUSED(state);

  mTool = ui::EditorTool::Select;
  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_DrawWire& state) noexcept {
  mTool = ui::EditorTool::Wire;

  // Wire mode
  auto setWireMode = [this](SchematicEditorState_DrawWire::WireMode m) {
    mToolWireMode = m;
    onDerivedUiDataChanged.notify();
  };
  setWireMode(state.getWireMode());
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_DrawWire::wireModeChanged, this,
              setWireMode));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::wireModeRequested, &state,
              &SchematicEditorState_DrawWire::setWireMode));

  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_DrawBus& state) noexcept {
  mTool = ui::EditorTool::Bus;

  // Wire mode
  auto setWireMode = [this](SchematicEditorState_DrawWire::WireMode m) {
    mToolWireMode = m;
    onDerivedUiDataChanged.notify();
  };
  setWireMode(state.getWireMode());
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_DrawBus::wireModeChanged, this,
              setWireMode));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::wireModeRequested, &state,
              &SchematicEditorState_DrawBus::setWireMode));

  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_AddLabel& state) noexcept {
  Q_UNUSED(state);

  mTool = ui::EditorTool::Label;
  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(
    SchematicEditorState_AddComponent& state) noexcept {
  mTool = ui::EditorTool::Component;

  // Value
  auto setValue = [this](const QString& value) {
    mToolValue = value;
    onDerivedUiDataChanged.notify();
  };
  setValue(state.getValue());
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_AddComponent::valueChanged, this,
              setValue));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::valueRequested, &state,
              &SchematicEditorState_AddComponent::setValue));

  // Value suggestions
  auto setValueSuggestions = [this](const QStringList& suggestions) {
    mToolValueSuggestions->clear();
    for (const QString& v : suggestions) {
      mToolValueSuggestions->push_back(q2s(v));
    }
    onDerivedUiDataChanged.notify();
  };
  setValueSuggestions(state.getValueSuggestions());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddComponent::valueSuggestionsChanged, this,
      setValueSuggestions));

  // Attribute key
  auto setAttributeKey = [this](const std::optional<AttributeKey>& key) {
    mToolAttributeValuePlaceholder = key ? **key : QString();
    onDerivedUiDataChanged.notify();
  };
  setAttributeKey(state.getValueAttributeKey());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddComponent::valueAttributeKeyChanged,
      this, setAttributeKey));

  // Attribute value
  auto setAttributeValue = [this](const std::optional<QString>& value) {
    mToolAttributeValue = value;
    onDerivedUiDataChanged.notify();
  };
  setAttributeValue(state.getValueAttributeValue());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddComponent::valueAttributeValueChanged,
      this, setAttributeValue));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::attributeValueRequested, &state,
              [&state](QString value) {
                if (const AttributeType* type = state.getValueAttributeType()) {
                  if (const AttributeUnit* unit =
                          type->tryExtractUnitFromValue(value)) {
                    state.setValueAttributeUnit(unit);
                  }
                  state.setValueAttributeValue(value);
                }
              }));

  // Attribute units
  auto setAttributeUnits = [this](const AttributeType* type) {
    mToolAttributeUnitsQt =
        type ? type->getAvailableUnits() : QList<const AttributeUnit*>{};
    mToolAttributeUnits->clear();
    for (const AttributeUnit* unit : mToolAttributeUnitsQt) {
      mToolAttributeUnits->push_back(q2s(unit->getSymbolTr()));
    }
    onDerivedUiDataChanged.notify();
  };
  setAttributeUnits(state.getValueAttributeType());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddComponent::valueAttributeTypeChanged,
      this, setAttributeUnits));

  // Attribute unit
  auto setAttributeUnit = [this](const AttributeUnit* unit) {
    mToolAttributeUnit = unit;
    onDerivedUiDataChanged.notify();
  };
  setAttributeUnit(state.getValueAttributeUnit());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddComponent::valueAttributeUnitChanged,
      this, setAttributeUnit));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::attributeUnitRequested, &state,
              &SchematicEditorState_AddComponent::setValueAttributeUnit));

  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(
    SchematicEditorState_DrawPolygon& state) noexcept {
  mTool = ui::EditorTool::Polygon;

  // Layers
  mToolLayersQt = Layer::sorted(state.getAvailableLayers());
  mToolLayers->clear();
  for (const Layer* layer : mToolLayersQt) {
    mToolLayers->push_back(q2s(layer->getNameTr()));
  }

  // Layer
  auto setLayer = [this](const Layer& layer) {
    mToolLayer = &layer;
    onDerivedUiDataChanged.notify();
  };
  setLayer(state.getLayer());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_DrawPolygon::layerChanged, this, setLayer));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::layerRequested, &state,
              &SchematicEditorState_DrawPolygon::setLayer));

  // Line width
  mToolLineWidth.configure(state.getLineWidth(),
                           LengthEditContext::Steps::generic(),
                           "schematic_editor/draw_polygon/line_width");
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_DrawPolygon::lineWidthChanged,
              &mToolLineWidth, &LengthEditContext::setValueUnsigned));
  mFsmStateConnections.append(
      connect(&mToolLineWidth, &LengthEditContext::valueChangedUnsigned, &state,
              &SchematicEditorState_DrawPolygon::setLineWidth));

  // Filled
  auto setFilled = [this](bool filled) {
    mToolFilled = filled;
    onDerivedUiDataChanged.notify();
  };
  setFilled(state.getFilled());
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_DrawPolygon::filledChanged, this,
              setFilled));
  mFsmStateConnections.append(
      connect(this, &SchematicTab::filledRequested, &state,
              &SchematicEditorState_DrawPolygon::setFilled));

  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_AddText& state) noexcept {
  mTool = ui::EditorTool::Text;

  // Layers
  mToolLayersQt = Layer::sorted(state.getAvailableLayers());
  mToolLayers->clear();
  for (const Layer* layer : mToolLayersQt) {
    mToolLayers->push_back(q2s(layer->getNameTr()));
  }

  // Layer
  auto setLayer = [this](const Layer& layer) {
    mToolLayer = &layer;
    onDerivedUiDataChanged.notify();
  };
  setLayer(state.getLayer());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddText::layerChanged, this, setLayer));
  mFsmStateConnections.append(connect(this, &SchematicTab::layerRequested,
                                      &state,
                                      &SchematicEditorState_AddText::setLayer));

  // Height
  mToolSize.configure(state.getHeight(), LengthEditContext::Steps::textHeight(),
                      "schematic_editor/add_text/size");
  mFsmStateConnections.append(
      connect(&state, &SchematicEditorState_AddText::heightChanged, &mToolSize,
              &LengthEditContext::setValuePositive));
  mFsmStateConnections.append(
      connect(&mToolSize, &LengthEditContext::valueChangedPositive, &state,
              &SchematicEditorState_AddText::setHeight));

  // Text
  auto setText = [this](const QString& text) {
    mToolValue = text;
    onDerivedUiDataChanged.notify();
  };
  setText(state.getText());
  mFsmStateConnections.append(connect(
      &state, &SchematicEditorState_AddText::textChanged, this, setText));
  mFsmStateConnections.append(connect(this, &SchematicTab::valueRequested,
                                      &state,
                                      &SchematicEditorState_AddText::setText));

  // Text suggestions
  mToolValueSuggestions->clear();
  for (const QString& v : state.getTextSuggestions()) {
    mToolValueSuggestions->push_back(q2s(v));
  }

  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_AddImage& state) noexcept {
  Q_UNUSED(state);

  mTool = ui::EditorTool::Image;
  onDerivedUiDataChanged.notify();
}

void SchematicTab::fsmToolEnter(SchematicEditorState_Measure& state) noexcept {
  Q_UNUSED(state);

  mTool = ui::EditorTool::Measure;
  onDerivedUiDataChanged.notify();
}

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

void SchematicTab::updateMessages() noexcept {
  try {
    const bool empty = mSchematic.getSymbols().isEmpty();
    const bool noLibs = empty
        ? mApp.getWorkspace().getLibraryDb().getAll<Library>().isEmpty()
        : false;
    mMsgInstallLibraries.setActive(empty && noLibs);
    mMsgAddDrawingFrame.setActive(empty && (!noLibs));
  } catch (const Exception& e) {
  }
}

void SchematicTab::clearErcMarker() noexcept {
  mErcLocationGraphicsItem.reset();
  if (mScene) {
    mScene->setSceneRectMarker(QRectF());
  }
}

void SchematicTab::execGraphicsExportDialog(
    GraphicsExportDialog::Output output, const QString& settingsKey) noexcept {
  try {
    // Determine default file path.
    const QString projectName = FilePath::cleanFileName(
        *mProject.getName(), FilePath::ReplaceSpaces | FilePath::KeepCase);
    const QString projectVersion = FilePath::cleanFileName(
        *mProject.getVersion(), FilePath::ReplaceSpaces | FilePath::KeepCase);
    const QString relativePath =
        QString("output/%1/%2_Schematics").arg(projectVersion, projectName);
    const FilePath defaultFilePath = mProject.getPath().getPathTo(relativePath);

    // Copy all schematic pages to allow processing them in worker threads.
    const int count = mProject.getSchematics().count();
    QProgressDialog progress(tr("Preparing schematics..."), tr("Cancel"), 0,
                             count, qApp->activeWindow());
    progress.setWindowModality(Qt::WindowModal);
    progress.setMinimumDuration(100);
    QList<std::shared_ptr<GraphicsPagePainter>> pages;
    for (int i = 0; i < count; ++i) {
      pages.append(std::make_shared<SchematicPainter>(
          *mProject.getSchematicByIndex(i), nullptr));
      progress.setValue(i + 1);
      if (progress.wasCanceled()) {
        return;
      }
    }

    // Show dialog, which will do all the work.
    GraphicsExportDialog dialog(
        GraphicsExportDialog::Mode::Schematic, output, pages,
        mProject.getSchematicIndex(mSchematic), *mProject.getName(), 0,
        defaultFilePath,
        mApp.getWorkspace().getSettings().defaultLengthUnit.get(),
        mApp.getWorkspace().getSettings().themes.getActive(),
        "schematic_editor/" % settingsKey, qApp->activeWindow());
    connect(&dialog, &GraphicsExportDialog::requestOpenFile, this,
            [this](const FilePath& fp) {
              DesktopServices ds(mApp.getWorkspace().getSettings());
              ds.openLocalPath(fp);
            });
    dialog.exec();
  } catch (const Exception& e) {
    QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.getMsg());
  }
}

void SchematicTab::goToSymbol(const QString& name, int index) noexcept {
  QList<SI_Symbol*> symbolCandidates;
  foreach (SI_Symbol* symbol, mSchematic.getSymbols().values()) {
    if (symbol->getName().startsWith(name, Qt::CaseInsensitive)) {
      symbolCandidates.append(symbol);
    }
  }

  // Sort by name for a natural order of results.
  Toolbox::sortNumeric(
      symbolCandidates,
      [](const QCollator& cmp, const SI_Symbol* lhs, const SI_Symbol* rhs) {
        return cmp(lhs->getName(), rhs->getName());
      },
      Qt::CaseInsensitive, false);

  if (symbolCandidates.count()) {
    while (index < 0) {
      index += symbolCandidates.count();
    }
    index %= symbolCandidates.count();
    SI_Symbol* symbol = symbolCandidates[index];
    if (mScene) {
      mScene->clearSelection();
      if (auto item = mScene->getSymbols().value(symbol)) {
        item->setSelected(true);
        QRectF rect = item->mapRectToScene(item->childrenBoundingRect());
        // Zoom to a rectangle relative to the maximum graphics item dimension,
        // occupying 1/4th of the screen, but limiting the margin to 10mm.
        const qreal margin =
            std::min(1.5f * std::max(rect.size().width(), rect.size().height()),
                     Length::fromMm(10).toPx());
        rect.adjust(-margin, -margin, margin, margin);
        mView->zoomToSceneRect(rect);
      }
    }
  }
}

void SchematicTab::applyTheme() noexcept {
  const Theme& theme = mApp.getWorkspace().getSettings().themes.getActive();

  if (mScene) {
    mScene->setBackgroundColors(
        theme.getColor(Theme::Color::sSchematicBackground).getPrimaryColor(),
        theme.getColor(Theme::Color::sSchematicBackground).getSecondaryColor());
    mScene->setOverlayColors(
        theme.getColor(Theme::Color::sSchematicOverlays).getPrimaryColor(),
        theme.getColor(Theme::Color::sSchematicOverlays).getSecondaryColor());
    mScene->setSelectionRectColors(
        theme.getColor(Theme::Color::sSchematicSelection).getPrimaryColor(),
        theme.getColor(Theme::Color::sSchematicSelection).getSecondaryColor());
    mScene->setGridStyle(mGridStyle);
  }

  onDerivedUiDataChanged.notify();
}

void SchematicTab::requestRepaint() noexcept {
  ++mFrameIndex;
  onDerivedUiDataChanged.notify();
}

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

}  // namespace editor
}  // namespace librepcb
