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

#include "../../dialogs/filedialog.h"
#include "../../dialogs/gridsettingsdialog.h"
#include "../../editorcommandset.h"
#include "../../project/cmd/cmdschematicadd.h"
#include "../../project/cmd/cmdschematicedit.h"
#include "../../project/cmd/cmdschematicremove.h"
#include "../../undostack.h"
#include "../../utils/exclusiveactiongroup.h"
#include "../../utils/menubuilder.h"
#include "../../utils/standardeditorcommandhandler.h"
#include "../../utils/toolbarproxy.h"
#include "../../utils/undostackactiongroup.h"
#include "../../widgets/searchtoolbar.h"
#include "../../workspace/desktopservices.h"
#include "../bomgeneratordialog.h"
#include "../erc/ercmsgdock.h"
#include "../projecteditor.h"
#include "../projectpropertieseditordialog.h"
#include "fsm/schematiceditorfsm.h"
#include "schematicpagesdock.h"

#include <librepcb/core/application.h>
#include <librepcb/core/project/circuit/circuit.h>
#include <librepcb/core/project/circuit/componentinstance.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/projectmetadata.h>
#include <librepcb/core/project/projectsettings.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/types/gridproperties.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <librepcb/core/workspace/workspacesettings.h>

#include <QSvgGenerator>
#include <QtCore>
#include <QtPrintSupport>
#include <QtWidgets>

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

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

SchematicEditor::SchematicEditor(ProjectEditor& projectEditor, Project& project)
  : QMainWindow(0),
    mProjectEditor(projectEditor),
    mProject(project),
    mUi(new Ui::SchematicEditor),
    mCommandToolBarProxy(new ToolBarProxy(this)),
    mStandardCommandHandler(new StandardEditorCommandHandler(
        mProjectEditor.getWorkspace().getSettings(), this)),
    mActiveSchematicIndex(-1),
    mFsm() {
  mUi->setupUi(this);

  // Setup graphics view.
  mUi->graphicsView->setUseOpenGl(
      mProjectEditor.getWorkspace().getSettings().useOpenGl.get());
  mUi->graphicsView->setEventHandlerObject(this);
  connect(mUi->graphicsView, &GraphicsView::cursorScenePositionChanged,
          mUi->statusbar, &StatusBar::setAbsoluteCursorPosition);

  // Setup status bar.
  mUi->statusbar->setFields(StatusBar::AbsolutePosition |
                            StatusBar::ProgressBar);
  mUi->statusbar->setProgressBarTextFormat(tr("Scanning libraries (%p%)"));
  connect(&mProjectEditor.getWorkspace().getLibraryDb(),
          &WorkspaceLibraryDb::scanProgressUpdate, mUi->statusbar,
          &StatusBar::setProgressBarPercent, Qt::QueuedConnection);
  mUi->statusbar->setProgressBarPercent(
      mProjectEditor.getWorkspace().getLibraryDb().getScanProgressPercent());

  // Set window title.
  QString filenameStr = mProject.getFilepath().getFilename();
  if (!mProject.getDirectory().isWritable()) {
    filenameStr.append(QStringLiteral(" [Read-Only]"));
  }
  setWindowTitle(tr("%1 - LibrePCB Schematic Editor").arg(filenameStr));

  // Build the whole schematic editor finite state machine.
  SchematicEditorFsm::Context fsmContext{mProjectEditor.getWorkspace(),
                                         mProject,
                                         *this,
                                         *mUi->graphicsView,
                                         *mCommandToolBarProxy,
                                         mProjectEditor.getUndoStack()};
  mFsm.reset(new SchematicEditorFsm(fsmContext));
  connect(mFsm.data(), &SchematicEditorFsm::statusBarMessageChanged, this,
          [this](const QString& message, int timeoutMs) {
            if (timeoutMs < 0) {
              mUi->statusbar->setPermanentMessage(message);
            } else {
              mUi->statusbar->showMessage(message, timeoutMs);
            }
          });

  // Create all actions, window menus, toolbars and dock widgets.
  createActions();
  createToolBars();
  createDockWidgets();
  createMenus();  // Depends on dock widgets!

  // Setup "empty schematic" message.
  mUi->msgEmptySchematic->init(
      mProjectEditor.getWorkspace(), "SCHEMATIC_HAS_NO_SYMBOLS",
      tr("This schematic doesn't contain any components yet. Use the "
         "<a href='%1'>Add Component</a> dialog to populate it. A good idea "
         "is to <a href='%2'>add a schematic frame</a> first.")
          .arg("dialog")
          .arg("frame"),
      false);
  connect(mUi->msgEmptySchematic, &MessageWidget::linkActivated, this,
          [this](const QString& link) {
            if (mFsm) {
              if (link == "frame") {
                mFsm->processAddComponent("schematic frame");
              } else {
                mFsm->processAddComponent();
              }
            }
          });

  // Restore window geometry.
  QSettings clientSettings;
  restoreGeometry(
      clientSettings.value("schematic_editor/window_geometry").toByteArray());
  restoreState(
      clientSettings.value("schematic_editor/window_state_v2").toByteArray());

  // Load first schematic page
  if (mProject.getSchematics().count() > 0) setActiveSchematicIndex(0);

  // Set focus to graphics view (avoid having the focus in some arbitrary
  // widget).
  mUi->graphicsView->setFocus();

  // mGraphicsView->zoomAll(); does not work properly here, should be executed
  // later in the event loop (ugly, but seems to work...)
  QTimer::singleShot(200, mUi->graphicsView, &GraphicsView::zoomAll);
}

SchematicEditor::~SchematicEditor() {
  // Save window geometry.
  QSettings clientSettings;
  clientSettings.setValue("schematic_editor/window_geometry", saveGeometry());
  clientSettings.setValue("schematic_editor/window_state_v2", saveState());

  // Important: Release command toolbar proxy since otherwise the actions will
  // be deleted first.
  mCommandToolBarProxy->setToolBar(nullptr);
}

/*******************************************************************************
 *  Getters
 ******************************************************************************/

Schematic* SchematicEditor::getActiveSchematic() const noexcept {
  return mProject.getSchematicByIndex(mActiveSchematicIndex);
}

/*******************************************************************************
 *  Setters
 ******************************************************************************/

bool SchematicEditor::setActiveSchematicIndex(int index) noexcept {
  if (index == mActiveSchematicIndex) return true;

  // "Ask" the FSM if changing the scene is allowed at the moment.
  // If the FSM accepts the event, we can switch to the specified schematic
  // page.
  if (!mFsm->processSwitchToSchematicPage(index)) {
    return false;  // changing the schematic page is not allowed!
  }

  // event accepted --> change the schematic page
  Schematic* schematic = getActiveSchematic();
  if (schematic) {
    // save current view scene rect
    schematic->saveViewSceneRect(mUi->graphicsView->getVisibleSceneRect());
  }
  while (!mSchematicConnections.isEmpty()) {
    disconnect(mSchematicConnections.takeLast());
  }
  schematic = mProject.getSchematicByIndex(index);
  if (schematic) {
    // show scene, restore view scene rect, set grid properties
    mUi->graphicsView->setScene(&schematic->getGraphicsScene());
    mUi->graphicsView->setVisibleSceneRect(schematic->restoreViewSceneRect());
    mUi->graphicsView->setGridProperties(schematic->getGridProperties());
    mUi->statusbar->setLengthUnit(schematic->getGridProperties().getUnit());
    mSchematicConnections.append(
        connect(schematic, &Schematic::symbolAdded, this,
                &SchematicEditor::updateEmptySchematicMessage));
    mSchematicConnections.append(
        connect(schematic, &Schematic::symbolRemoved, this,
                &SchematicEditor::updateEmptySchematicMessage));
  } else {
    mUi->graphicsView->setScene(nullptr);
  }

  // update toolbars
  mActionGridProperties->setEnabled(schematic != nullptr);
  mActionGridIncrease->setEnabled(schematic != nullptr);
  mActionGridDecrease->setEnabled(schematic != nullptr);

  // schematic page has changed!
  mActiveSchematicIndex = index;
  emit activeSchematicChanged(mActiveSchematicIndex);
  updateEmptySchematicMessage();
  return true;
}

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

void SchematicEditor::abortAllCommands() noexcept {
  // ugly... ;-)
  mFsm->processAbortCommand();
  mFsm->processAbortCommand();
  mFsm->processAbortCommand();
}

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

/*******************************************************************************
 *  Inherited Methods
 ******************************************************************************/

void SchematicEditor::closeEvent(QCloseEvent* event) {
  if (!mProjectEditor.windowIsAboutToClose(*this))
    event->ignore();
  else
    QMainWindow::closeEvent(event);
}

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

void SchematicEditor::createActions() noexcept {
  const EditorCommandSet& cmd = EditorCommandSet::instance();

  mActionAboutLibrePcb.reset(cmd.aboutLibrePcb.createAction(
      this, mStandardCommandHandler.data(),
      &StandardEditorCommandHandler::aboutLibrePcb));
  mActionAboutQt.reset(
      cmd.aboutQt.createAction(this, qApp, &QApplication::aboutQt));
  mActionOnlineDocumentation.reset(cmd.documentationOnline.createAction(
      this, mStandardCommandHandler.data(),
      &StandardEditorCommandHandler::onlineDocumentation));
  mActionKeyboardShortcutsReference.reset(
      cmd.keyboardShortcutsReference.createAction(
          this, mStandardCommandHandler.data(),
          &StandardEditorCommandHandler::shortcutsReference));
  mActionWebsite.reset(
      cmd.website.createAction(this, mStandardCommandHandler.data(),
                               &StandardEditorCommandHandler::website));
  mActionSaveProject.reset(cmd.projectSave.createAction(
      this, &mProjectEditor, &ProjectEditor::saveProject));
  mActionSaveProject->setEnabled(mProject.getDirectory().isWritable());
  mActionCloseProject.reset(cmd.projectClose.createAction(
      this, this, [this]() { mProjectEditor.closeAndDestroy(true, this); }));
  mActionCloseWindow.reset(
      cmd.windowClose.createAction(this, this, &SchematicEditor::close));
  mActionQuit.reset(cmd.applicationQuit.createAction(
      this, qApp, &Application::quitTriggered));
  mActionFileManager.reset(cmd.fileManager.createAction(this, this, [this]() {
    mStandardCommandHandler->fileManager(mProject.getPath());
  }));
  mActionBoardEditor.reset(cmd.boardEditor.createAction(
      this, &mProjectEditor, &ProjectEditor::showBoardEditor));
  mActionControlPanel.reset(cmd.controlPanel.createAction(
      this, &mProjectEditor, &ProjectEditor::showControlPanelClicked));
  mActionProjectProperties.reset(
      cmd.projectProperties.createAction(this, this, [this]() {
        abortBlockingToolsInOtherEditors();  // Release undo stack.
        ProjectPropertiesEditorDialog dialog(
            mProject.getMetadata(), mProjectEditor.getUndoStack(), this);
        dialog.exec();
      }));
  mActionProjectSettings.reset(cmd.projectSettings.createAction(
      this, this,
      [this]() { mProjectEditor.execProjectSettingsDialog(this); }));
  mActionNetClasses.reset(cmd.netClasses.createAction(this, this, [this]() {
    mProjectEditor.execNetClassesEditorDialog(this);
  }));
  mActionUpdateLibrary.reset(
      cmd.projectLibraryUpdate.createAction(this, this, [this]() {
        // Ugly hack until we have a *real* project library updater...
        emit mProjectEditor.openProjectLibraryUpdaterClicked(
            mProject.getFilepath());
      }));
  mActionExportLppz.reset(cmd.exportLppz.createAction(
      this, this, [this]() { mProjectEditor.execLppzExportDialog(this); }));
  mActionExportImage.reset(cmd.exportImage.createAction(this, this, [this]() {
    execGraphicsExportDialog(GraphicsExportDialog::Output::Image,
                             "image_export");
  }));
  mActionExportPdf.reset(cmd.exportPdf.createAction(this, this, [this]() {
    execGraphicsExportDialog(GraphicsExportDialog::Output::Pdf, "pdf_export");
  }));
  mActionPrint.reset(cmd.print.createAction(this, this, [this]() {
    execGraphicsExportDialog(GraphicsExportDialog::Output::Print, "print");
  }));
  mActionGenerateBom.reset(cmd.generateBom.createAction(this, this, [this]() {
    const Board* board = mProject.getBoards().count() == 1
        ? mProject.getBoardByIndex(0)
        : nullptr;
    BomGeneratorDialog dialog(mProjectEditor.getWorkspace().getSettings(),
                              mProject, board, this);
    dialog.exec();
  }));
  mActionOrderPcb.reset(cmd.orderPcb.createAction(this, this, [this]() {
    mProjectEditor.execOrderPcbDialog(nullptr, this);
  }));
  mActionNewSheet.reset(
      cmd.sheetNew.createAction(this, this, &SchematicEditor::addSchematic));
  mActionRenameSheet.reset(cmd.sheetRename.createAction(
      this, this, [this]() { renameSchematic(mActiveSchematicIndex); }));
  mActionRemoveSheet.reset(cmd.sheetRemove.createAction(
      this, this, [this]() { removeSchematic(mActiveSchematicIndex); }));
  mActionNextPage.reset(cmd.pageNext.createAction(this, this, [this]() {
    const int newIndex = mActiveSchematicIndex + 1;
    if (newIndex < mProject.getSchematics().count()) {
      setActiveSchematicIndex(newIndex);
    }
  }));
  addAction(mActionNextPage.data());
  mActionPreviousPage.reset(cmd.pagePrevious.createAction(this, this, [this]() {
    const int newIndex = mActiveSchematicIndex - 1;
    if (newIndex >= 0) {
      setActiveSchematicIndex(newIndex);
    }
  }));
  addAction(mActionPreviousPage.data());
  mActionFind.reset(cmd.find.createAction(this));
  mActionFindNext.reset(cmd.findNext.createAction(this));
  mActionFindPrevious.reset(cmd.findPrevious.createAction(this));
  mActionSelectAll.reset(cmd.selectAll.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processSelectAll));
  mActionGridProperties.reset(cmd.gridProperties.createAction(
      this, this, &SchematicEditor::execGridPropertiesDialog));
  mActionGridIncrease.reset(cmd.gridIncrease.createAction(this, this, [this]() {
    if (const Schematic* schematic = getActiveSchematic()) {
      GridProperties grid = schematic->getGridProperties();
      grid.setInterval(PositiveLength(grid.getInterval() * 2));
      setGridProperties(grid, true);
    }
  }));
  mActionGridDecrease.reset(cmd.gridDecrease.createAction(this, this, [this]() {
    if (const Schematic* schematic = getActiveSchematic()) {
      GridProperties grid = schematic->getGridProperties();
      if ((*grid.getInterval()) % 2 == 0) {
        grid.setInterval(PositiveLength(grid.getInterval() / 2));
        setGridProperties(grid, true);
      }
    }
  }));
  mActionZoomFit.reset(cmd.zoomFitContent.createAction(this, mUi->graphicsView,
                                                       &GraphicsView::zoomAll));
  mActionZoomIn.reset(
      cmd.zoomIn.createAction(this, mUi->graphicsView, &GraphicsView::zoomIn));
  mActionZoomOut.reset(cmd.zoomOut.createAction(this, mUi->graphicsView,
                                                &GraphicsView::zoomOut));
  mActionUndo.reset(cmd.undo.createAction(this));
  mActionRedo.reset(cmd.redo.createAction(this));
  mActionCut.reset(cmd.clipboardCut.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processCut));
  mActionCopy.reset(cmd.clipboardCopy.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processCopy));
  mActionPaste.reset(cmd.clipboardPaste.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processPaste));
  mActionMoveLeft.reset(cmd.moveLeft.createAction(this, this, [this]() {
    if (!mFsm->processMove(
            Point(-mUi->graphicsView->getGridProperties().getInterval(), 0))) {
      // Workaround for consumed keyboard shortcuts for scrolling.
      mUi->graphicsView->horizontalScrollBar()->triggerAction(
          QScrollBar::SliderSingleStepSub);
    }
  }));
  addAction(mActionMoveLeft.data());
  mActionMoveRight.reset(cmd.moveRight.createAction(this, this, [this]() {
    if (!mFsm->processMove(
            Point(*mUi->graphicsView->getGridProperties().getInterval(), 0))) {
      // Workaround for consumed keyboard shortcuts for scrolling.
      mUi->graphicsView->horizontalScrollBar()->triggerAction(
          QScrollBar::SliderSingleStepAdd);
    }
  }));
  addAction(mActionMoveRight.data());
  mActionMoveUp.reset(cmd.moveUp.createAction(this, this, [this]() {
    if (!mFsm->processMove(
            Point(0, *mUi->graphicsView->getGridProperties().getInterval()))) {
      // Workaround for consumed keyboard shortcuts for scrolling.
      mUi->graphicsView->verticalScrollBar()->triggerAction(
          QScrollBar::SliderSingleStepSub);
    }
  }));
  addAction(mActionMoveUp.data());
  mActionMoveDown.reset(cmd.moveDown.createAction(this, this, [this]() {
    if (!mFsm->processMove(
            Point(0, -mUi->graphicsView->getGridProperties().getInterval()))) {
      // Workaround for consumed keyboard shortcuts for scrolling.
      mUi->graphicsView->verticalScrollBar()->triggerAction(
          QScrollBar::SliderSingleStepAdd);
    }
  }));
  addAction(mActionMoveDown.data());
  mActionRotateCcw.reset(cmd.rotateCcw.createAction(
      this, this, [this]() { mFsm->processRotate(Angle::deg90()); }));
  mActionRotateCw.reset(cmd.rotateCw.createAction(
      this, this, [this]() { mFsm->processRotate(-Angle::deg90()); }));
  mActionMirrorHorizontal.reset(cmd.mirrorHorizontal.createAction(
      this, this, [this]() { mFsm->processMirror(Qt::Horizontal); }));
  mActionMirrorVertical.reset(cmd.mirrorVertical.createAction(
      this, this, [this]() { mFsm->processMirror(Qt::Vertical); }));
  mActionProperties.reset(cmd.properties.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processEditProperties));
  mActionRemove.reset(cmd.remove.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processRemove));
  mActionAbort.reset(cmd.abort.createAction(
      this, mFsm.data(), &SchematicEditorFsm::processAbortCommand));
  mActionToolSelect.reset(cmd.toolSelect.createAction(this));
  mActionToolWire.reset(cmd.toolWire.createAction(this));
  mActionToolNetLabel.reset(cmd.toolNetLabel.createAction(this));
  mActionToolComponent.reset(cmd.toolComponent.createAction(this));
  mActionToolMeasure.reset(cmd.toolMeasure.createAction(this));
  mActionComponentResistor.reset(
      cmd.componentResistor.createAction(this, this, [this]() {
        Uuid componentUuid =
            Uuid::fromString("ef80cd5e-2689-47ee-8888-31d04fc99174");
        Uuid symbVarUuid = Uuid::fromString(
            useIeee315Symbols() ? "d16e1f44-16af-4773-a310-de370f744548"
                                : "a5995314-f535-45d4-8bd8-2d0b8a0dc42a");
        mFsm->processAddComponent(componentUuid, symbVarUuid);
      }));
  mActionComponentInductor.reset(
      cmd.componentInductor.createAction(this, this, [this]() {
        Uuid componentUuid =
            Uuid::fromString("506bd124-6062-400e-9078-b38bd7e1aaee");
        Uuid symbVarUuid = Uuid::fromString(
            useIeee315Symbols() ? "4245d515-6f6d-48cb-9958-a4ea23d0187f"
                                : "62a7598c-17fe-41cf-8fa1-4ed274c3adc2");
        mFsm->processAddComponent(componentUuid, symbVarUuid);
      }));
  mActionComponentCapacitorBipolar.reset(
      cmd.componentCapacitorBipolar.createAction(this, this, [this]() {
        Uuid componentUuid =
            Uuid::fromString("d167e0e3-6a92-4b76-b013-77b9c230e5f1");
        Uuid symbVarUuid = Uuid::fromString(
            useIeee315Symbols() ? "6e639ff1-4e81-423b-9d0e-b28b35693a61"
                                : "8cd7b37f-e5fa-4af5-a8dd-d78830bba3af");
        mFsm->processAddComponent(componentUuid, symbVarUuid);
      }));
  mActionComponentCapacitorUnipolar.reset(
      cmd.componentCapacitorUnipolar.createAction(this, this, [this]() {
        Uuid componentUuid =
            Uuid::fromString("c54375c5-7149-4ded-95c5-7462f7301ee7");
        Uuid symbVarUuid = Uuid::fromString(
            useIeee315Symbols() ? "20a01a81-506e-4fee-9dc0-8b50e6537cd4"
                                : "5412add2-af9c-44b8-876d-a0fb7c201897");
        mFsm->processAddComponent(componentUuid, symbVarUuid);
      }));
  mActionComponentGnd.reset(cmd.componentGnd.createAction(this, this, [this]() {
    Uuid componentUuid =
        Uuid::fromString("8076f6be-bfab-4fc1-9772-5d54465dd7e1");
    Uuid symbVarUuid = Uuid::fromString("f09ad258-595b-4ee9-a1fc-910804a203ae");
    mFsm->processAddComponent(componentUuid, symbVarUuid);
  }));
  mActionComponentVcc.reset(cmd.componentVcc.createAction(this, this, [this]() {
    Uuid componentUuid =
        Uuid::fromString("58c3c6cd-11eb-4557-aa3f-d3e05874afde");
    Uuid symbVarUuid = Uuid::fromString("afb86b45-68ec-47b6-8d96-153d73567228");
    mFsm->processAddComponent(componentUuid, symbVarUuid);
  }));
  mActionDockPages.reset(cmd.dockPages.createAction(this, this, [this]() {
    mDockPages->show();
    mDockPages->raise();
    mDockPages->setFocus();
  }));
  mActionDockErc.reset(cmd.dockErc.createAction(this, this, [this]() {
    mDockErc->show();
    mDockErc->raise();
    mDockErc->setFocus();
  }));

  // Widget shortcuts.
  mUi->graphicsView->addAction(cmd.commandToolBarFocus.createAction(
      this, this,
      [this]() {
        mCommandToolBarProxy->startTabFocusCycle(*mUi->graphicsView);
      },
      EditorCommand::ActionFlag::WidgetShortcut));

  // Undo stack action group.
  mUndoStackActionGroup.reset(
      new UndoStackActionGroup(*mActionUndo, *mActionRedo, nullptr,
                               &mProjectEditor.getUndoStack(), this));

  // Tools action group.
  mToolsActionGroup.reset(new ExclusiveActionGroup());
  mToolsActionGroup->addAction(SchematicEditorFsm::State::SELECT,
                               mActionToolSelect.data());
  mToolsActionGroup->addAction(SchematicEditorFsm::State::DRAW_WIRE,
                               mActionToolWire.data());
  mToolsActionGroup->addAction(SchematicEditorFsm::State::ADD_NETLABEL,
                               mActionToolNetLabel.data());
  mToolsActionGroup->addAction(SchematicEditorFsm::State::ADD_COMPONENT,
                               mActionToolComponent.data());
  mToolsActionGroup->addAction(SchematicEditorFsm::State::MEASURE,
                               mActionToolMeasure.data());
  mToolsActionGroup->setCurrentAction(mFsm->getCurrentState());
  connect(mFsm.data(), &SchematicEditorFsm::stateChanged,
          mToolsActionGroup.data(), &ExclusiveActionGroup::setCurrentAction);
  connect(mToolsActionGroup.data(),
          &ExclusiveActionGroup::changeRequestTriggered, this,
          &SchematicEditor::toolActionGroupChangeTriggered);
}

void SchematicEditor::createToolBars() noexcept {
  // File.
  mToolBarFile.reset(new QToolBar(tr("File"), this));
  mToolBarFile->setObjectName("toolBarFile");
  mToolBarFile->addAction(mActionCloseProject.data());
  mToolBarFile->addSeparator();
  mToolBarFile->addAction(mActionNewSheet.data());
  mToolBarFile->addAction(mActionSaveProject.data());
  mToolBarFile->addAction(mActionPrint.data());
  mToolBarFile->addAction(mActionExportPdf.data());
  mToolBarFile->addAction(mActionOrderPcb.data());
  mToolBarFile->addSeparator();
  mToolBarFile->addAction(mActionControlPanel.data());
  mToolBarFile->addAction(mActionBoardEditor.data());
  mToolBarFile->addSeparator();
  mToolBarFile->addAction(mActionUndo.data());
  mToolBarFile->addAction(mActionRedo.data());
  addToolBar(Qt::TopToolBarArea, mToolBarFile.data());

  // Edit.
  mToolBarEdit.reset(new QToolBar(tr("Edit"), this));
  mToolBarEdit->setObjectName("toolBarEdit");
  mToolBarEdit->addAction(mActionCut.data());
  mToolBarEdit->addAction(mActionCopy.data());
  mToolBarEdit->addAction(mActionPaste.data());
  mToolBarEdit->addAction(mActionRemove.data());
  mToolBarEdit->addAction(mActionRotateCcw.data());
  mToolBarEdit->addAction(mActionRotateCw.data());
  mToolBarEdit->addAction(mActionMirrorHorizontal.data());
  mToolBarEdit->addAction(mActionMirrorVertical.data());
  addToolBar(Qt::TopToolBarArea, mToolBarEdit.data());

  // View.
  mToolBarView.reset(new QToolBar(tr("View"), this));
  mToolBarView->setObjectName("toolBarView");
  mToolBarView->addAction(mActionGridProperties.data());
  mToolBarView->addAction(mActionZoomIn.data());
  mToolBarView->addAction(mActionZoomOut.data());
  mToolBarView->addAction(mActionZoomFit.data());
  addToolBar(Qt::TopToolBarArea, mToolBarView.data());

  // Search.
  mToolBarSearch.reset(new SearchToolBar(this));
  mToolBarSearch->setObjectName("toolBarSearch");
  mToolBarSearch->setPlaceholderText(tr("Find symbol..."));
  mToolBarSearch->setCompleterListFunction(
      std::bind(&SchematicEditor::getSearchToolBarCompleterList, this));
  connect(mActionFind.data(), &QAction::triggered, mToolBarSearch.data(),
          &SearchToolBar::selectAllAndSetFocus);
  connect(mActionFindNext.data(), &QAction::triggered, mToolBarSearch.data(),
          &SearchToolBar::findNext);
  connect(mActionFindPrevious.data(), &QAction::triggered,
          mToolBarSearch.data(), &SearchToolBar::findPrevious);
  addToolBar(Qt::TopToolBarArea, mToolBarSearch.data());
  connect(mToolBarSearch.data(), &SearchToolBar::goToTriggered, this,
          &SchematicEditor::goToSymbol);

  // Command.
  mToolBarCommand.reset(new QToolBar(tr("Command"), this));
  mToolBarCommand->setObjectName("toolBarCommand");
  mToolBarCommand->addAction(mActionAbort.data());
  mToolBarCommand->addSeparator();
  addToolBarBreak(Qt::TopToolBarArea);
  addToolBar(Qt::TopToolBarArea, mToolBarCommand.data());
  mCommandToolBarProxy->setToolBar(mToolBarCommand.data());

  // Tools.
  mToolBarTools.reset(new QToolBar(tr("Tools"), this));
  mToolBarTools->setObjectName("toolBarTools");
  mToolBarTools->addAction(mActionToolSelect.data());
  mToolBarTools->addAction(mActionToolWire.data());
  mToolBarTools->addAction(mActionToolNetLabel.data());
  mToolBarTools->addAction(mActionToolComponent.data());
  mToolBarTools->addSeparator();
  mToolBarTools->addAction(mActionToolMeasure.data());
  addToolBar(Qt::LeftToolBarArea, mToolBarTools.data());

  // Components.
  mToolBarComponents.reset(new QToolBar(tr("Components"), this));
  mToolBarComponents->setObjectName("toolBarComponents");
  mToolBarComponents->addAction(mActionComponentResistor.data());
  mToolBarComponents->addAction(mActionComponentInductor.data());
  mToolBarComponents->addAction(mActionComponentCapacitorBipolar.data());
  mToolBarComponents->addAction(mActionComponentCapacitorUnipolar.data());
  mToolBarComponents->addAction(mActionComponentGnd.data());
  mToolBarComponents->addAction(mActionComponentVcc.data());
  addToolBarBreak(Qt::LeftToolBarArea);
  addToolBar(Qt::LeftToolBarArea, mToolBarComponents.data());
  updateComponentToolbarIcons();  // Load icons according workspace settings.
  connect(&mProject.getSettings(), &ProjectSettings::settingsChanged, this,
          &SchematicEditor::updateComponentToolbarIcons);
}

void SchematicEditor::createDockWidgets() noexcept {
  // Pages.
  mDockPages.reset(new SchematicPagesDock(mProject, this));
  connect(this, &SchematicEditor::activeSchematicChanged, mDockPages.data(),
          &SchematicPagesDock::setSelectedSchematic);
  connect(mDockPages.data(), &SchematicPagesDock::selectedSchematicChanged,
          this, &SchematicEditor::setActiveSchematicIndex);
  connect(mDockPages.data(), &SchematicPagesDock::addSchematicTriggered, this,
          &SchematicEditor::addSchematic);
  connect(mDockPages.data(), &SchematicPagesDock::removeSchematicTriggered,
          this, &SchematicEditor::removeSchematic);
  connect(mDockPages.data(), &SchematicPagesDock::renameSchematicTriggered,
          this, &SchematicEditor::renameSchematic);
  addDockWidget(Qt::LeftDockWidgetArea, mDockPages.data(), Qt::Vertical);

  // ERC Messages.
  mDockErc.reset(new ErcMsgDock(mProject));
  addDockWidget(Qt::RightDockWidgetArea, mDockErc.data(), Qt::Vertical);

  // Set reasonable default dock size.
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
  resizeDocks(
      {
          mDockPages.data(),
          mDockErc.data(),
      },
      {
          120,
          150,
      },
      Qt::Horizontal);
#endif
}

void SchematicEditor::createMenus() noexcept {
  MenuBuilder mb(mUi->menuBar);

  // File.
  mb.newMenu(&MenuBuilder::createFileMenu);
  mb.addAction(mActionSaveProject);
  mb.addAction(mActionFileManager);
  mb.addSeparator();
  {
    MenuBuilder smb(mb.addSubMenu(&MenuBuilder::createExportMenu));
    smb.addAction(mActionExportPdf);
    smb.addAction(mActionExportImage);
    smb.addAction(mActionExportLppz);
  }
  {
    MenuBuilder smb(mb.addSubMenu(&MenuBuilder::createProductionDataMenu));
    smb.addAction(mActionGenerateBom);
  }
  mb.addSeparator();
  mb.addAction(mActionPrint);
  mb.addAction(mActionOrderPcb);
  mb.addSeparator();
  mb.addAction(mActionCloseWindow);
  mb.addAction(mActionCloseProject);
  mb.addSeparator();
  mb.addAction(mActionQuit);

  // Edit.
  mb.newMenu(&MenuBuilder::createEditMenu);
  mb.addAction(mActionUndo);
  mb.addAction(mActionRedo);
  mb.addSeparator();
  mb.addAction(mActionSelectAll);
  mb.addSeparator();
  mb.addAction(mActionCut);
  mb.addAction(mActionCopy);
  mb.addAction(mActionPaste);
  mb.addAction(mActionRemove);
  mb.addSeparator();
  mb.addAction(mActionRotateCcw);
  mb.addAction(mActionRotateCw);
  mb.addAction(mActionMirrorHorizontal);
  mb.addAction(mActionMirrorVertical);
  mb.addSeparator();
  mb.addAction(mActionFind);
  mb.addAction(mActionFindNext);
  mb.addAction(mActionFindPrevious);
  mb.addSeparator();
  mb.addAction(mActionProperties);

  // View.
  mb.newMenu(&MenuBuilder::createViewMenu);
  mb.addAction(mActionGridProperties);
  mb.addAction(mActionGridIncrease.data());
  mb.addAction(mActionGridDecrease.data());
  mb.addSeparator();
  mb.addAction(mActionZoomIn);
  mb.addAction(mActionZoomOut);
  mb.addAction(mActionZoomFit);
  mb.addSeparator();
  {
    MenuBuilder smb(mb.addSubMenu(&MenuBuilder::createGoToDockMenu));
    smb.addAction(mActionDockPages);
    smb.addAction(mActionDockErc);
  }
  {
    MenuBuilder smb(mb.addSubMenu(&MenuBuilder::createDocksVisibilityMenu));
    smb.addAction(mDockPages->toggleViewAction());
    smb.addAction(mDockErc->toggleViewAction());
  }

  // Schematic.
  mb.newMenu(&MenuBuilder::createSchematicMenu);
  mb.addAction(mActionNewSheet);
  mb.addAction(mActionRenameSheet);
  mb.addAction(mActionRemoveSheet);

  // Project.
  mb.newMenu(&MenuBuilder::createProjectMenu);
  mb.addAction(mActionNetClasses);
  mb.addAction(mActionProjectProperties);
  mb.addAction(mActionProjectSettings);
  mb.addSeparator();
  mb.addAction(mActionUpdateLibrary);

  // Tools.
  mb.newMenu(&MenuBuilder::createToolsMenu);
  mb.addAction(mActionToolSelect);
  mb.addAction(mActionToolWire);
  mb.addAction(mActionToolNetLabel);
  mb.addAction(mActionToolComponent);
  mb.addSeparator();
  mb.addAction(mActionToolMeasure);

  // Help.
  mb.newMenu(&MenuBuilder::createHelpMenu);
  mb.addAction(mActionOnlineDocumentation);
  mb.addAction(mActionKeyboardShortcutsReference);
  mb.addAction(mActionWebsite);
  mb.addSeparator();
  mb.addAction(mActionAboutLibrePcb);
  mb.addAction(mActionAboutQt);
}

bool SchematicEditor::graphicsViewEventHandler(QEvent* event) {
  Q_ASSERT(event);
  switch (event->type()) {
    case QEvent::GraphicsSceneMouseMove: {
      auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
      Q_ASSERT(e);
      mFsm->processGraphicsSceneMouseMoved(*e);
      break;
    }

    case QEvent::GraphicsSceneMousePress: {
      auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
      Q_ASSERT(e);
      switch (e->button()) {
        case Qt::LeftButton: {
          mFsm->processGraphicsSceneLeftMouseButtonPressed(*e);
          break;
        }
        default: { break; }
      }
      break;
    }

    case QEvent::GraphicsSceneMouseRelease: {
      auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
      Q_ASSERT(e);
      switch (e->button()) {
        case Qt::LeftButton: {
          mFsm->processGraphicsSceneLeftMouseButtonReleased(*e);
          break;
        }
        case Qt::RightButton: {
          mFsm->processGraphicsSceneRightMouseButtonReleased(*e);
          break;
        }
        default: { break; }
      }
      break;
    }

    case QEvent::GraphicsSceneMouseDoubleClick: {
      auto* e = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
      Q_ASSERT(e);
      switch (e->button()) {
        case Qt::LeftButton: {
          mFsm->processGraphicsSceneLeftMouseButtonDoubleClicked(*e);
          break;
        }
        default: { break; }
      }
      break;
    }

    case QEvent::KeyPress: {
      auto* e = dynamic_cast<QKeyEvent*>(event);
      Q_ASSERT(e);
      if (mFsm->processKeyPressed(*e)) {
        return true;
      }
      switch (e->key()) {
        case Qt::Key_Left:
        case Qt::Key_Right:
        case Qt::Key_Up:
        case Qt::Key_Down:
          // Allow handling these keys by the graphics view for scrolling.
          return false;
        default:
          break;
      }
      break;
    }

    case QEvent::KeyRelease: {
      auto* e = dynamic_cast<QKeyEvent*>(event);
      Q_ASSERT(e);
      mFsm->processKeyReleased(*e);
      break;
    }

    default: { break; }
  }

  // Always accept graphics scene events, even if we do not react on some of
  // the events! This will give us the full control over the graphics scene.
  // Otherwise, the graphics scene can react on some events and disturb our
  // state machine. Only the wheel event is ignored because otherwise the
  // view will not allow to zoom with the mouse wheel.
  return (event->type() != QEvent::GraphicsSceneWheel);
}

void SchematicEditor::toolActionGroupChangeTriggered(
    const QVariant& newTool) noexcept {
  // Note: Converting the QVariant to SchematicEditorFsm::State doesn't work
  // with some Qt versions, thus we convert to int instead. Fixed in:
  // https://codereview.qt-project.org/c/qt/qtbase/+/159277/
  switch (newTool.value<int>()) {
    case SchematicEditorFsm::State::SELECT:
      mFsm->processSelect();
      break;
    case SchematicEditorFsm::State::DRAW_WIRE:
      mFsm->processDrawWire();
      break;
    case SchematicEditorFsm::State::ADD_NETLABEL:
      mFsm->processAddNetLabel();
      break;
    case SchematicEditorFsm::State::ADD_COMPONENT:
      mFsm->processAddComponent();
      break;
    case SchematicEditorFsm::State::MEASURE:
      mFsm->processMeasure();
      break;
    default:
      qCritical() << "Unhandled switch-case in "
                     "SchematicEditor::toolActionGroupChangeTriggered():"
                  << newTool;
      break;
  }
}

void SchematicEditor::addSchematic() noexcept {
  bool ok = false;
  QString name = QInputDialog::getText(this, tr("Add schematic page"),
                                       tr("Choose a name:"), QLineEdit::Normal,
                                       tr("New Page"), &ok);
  if (!ok) return;

  try {
    abortBlockingToolsInOtherEditors();  // Release undo stack.
    CmdSchematicAdd* cmd =
        new CmdSchematicAdd(mProject, ElementName(name));  // can throw
    mProjectEditor.getUndoStack().execCmd(cmd);
    setActiveSchematicIndex(mProject.getSchematics().count() - 1);
  } catch (Exception& e) {
    QMessageBox::critical(this, tr("Error"), e.getMsg());
  }
}

void SchematicEditor::removeSchematic(int index) noexcept {
  Schematic* schematic = mProject.getSchematicByIndex(index);
  if (!schematic) return;

  try {
    abortBlockingToolsInOtherEditors();  // Release undo stack.
    CmdSchematicRemove* cmd = new CmdSchematicRemove(mProject, *schematic);
    mProjectEditor.getUndoStack().execCmd(cmd);
  } catch (Exception& e) {
    QMessageBox::critical(this, tr("Error"), e.getMsg());
  }
}

void SchematicEditor::renameSchematic(int index) noexcept {
  Schematic* schematic = mProject.getSchematicByIndex(index);
  if (!schematic) return;

  bool ok = false;
  QString name =
      QInputDialog::getText(this, tr("Rename sheet"), tr("Choose new name:"),
                            QLineEdit::Normal, *schematic->getName(), &ok);
  if (!ok) return;

  try {
    abortBlockingToolsInOtherEditors();  // Release undo stack.
    QScopedPointer<CmdSchematicEdit> cmd(new CmdSchematicEdit(*schematic));
    cmd->setName(ElementName(cleanElementName(name)));  // can throw
    mProjectEditor.getUndoStack().execCmd(cmd.take());
  } catch (Exception& e) {
    QMessageBox::critical(this, tr("Error"), e.getMsg());
  }
}

QList<SI_Symbol*> SchematicEditor::getSearchCandidates() noexcept {
  QList<SI_Symbol*> candidates;
  foreach (const Schematic* schematic, mProject.getSchematics()) {
    Q_ASSERT(schematic);
    candidates += schematic->getSymbols();
  }
  return candidates;
}

QStringList SchematicEditor::getSearchToolBarCompleterList() noexcept {
  QStringList list;
  foreach (SI_Symbol* symbol, getSearchCandidates()) {
    list.append(symbol->getName());
  }
  return list;
}

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

  // Sort by name for a natural order of results.
  QCollator collator;
  collator.setCaseSensitivity(Qt::CaseInsensitive);
  collator.setIgnorePunctuation(false);
  collator.setNumericMode(true);
  std::sort(symbolCandidates.begin(), symbolCandidates.end(),
            [&collator](const SI_Symbol* a, const SI_Symbol* b) {
              return collator(a->getName(), b->getName());
            });

  if (symbolCandidates.count()) {
    while (index < 0) {
      index += symbolCandidates.count();
    }
    index %= symbolCandidates.count();
    SI_Symbol* symbol = symbolCandidates[index];
    Schematic& schematic = symbol->getSchematic();
    if (setActiveSchematicIndex(mProject.getSchematics().indexOf(&schematic))) {
      schematic.clearSelection();
      symbol->setSelected(true);
      QRectF rect = symbol->getBoundingRect();
      // Zoom to a rectangle relative to the maximum symbol dimension. The
      // symbol is 1/4th of the screen.
      qreal margin = 1.5f * std::max(rect.size().width(), rect.size().height());
      rect.adjust(-margin, -margin, margin, margin);
      mUi->graphicsView->zoomToRect(rect);
    }
  }
}

void SchematicEditor::updateEmptySchematicMessage() noexcept {
  Schematic* schematic = getActiveSchematic();
  mUi->msgEmptySchematic->setActive(schematic &&
                                    schematic->getSymbols().isEmpty());
}

void SchematicEditor::updateComponentToolbarIcons() noexcept {
  QString suffix = useIeee315Symbols() ? "us" : "eu";
  mActionComponentResistor->setIcon(
      QIcon(":/img/library/resistor_" % suffix % ".png"));
  mActionComponentInductor->setIcon(
      QIcon(":/img/library/inductor_" % suffix % ".png"));
  mActionComponentCapacitorBipolar->setIcon(
      QIcon(":/img/library/bipolar_capacitor_" % suffix % ".png"));
  mActionComponentCapacitorUnipolar->setIcon(
      QIcon(":/img/library/unipolar_capacitor_" % suffix % ".png"));
}

void SchematicEditor::setGridProperties(const GridProperties& grid,
                                        bool applyToSchematics) noexcept {
  mUi->graphicsView->setGridProperties(grid);
  mUi->statusbar->setLengthUnit(grid.getUnit());

  if (applyToSchematics) {
    foreach (Schematic* schematic, mProject.getSchematics()) {
      schematic->setGridProperties(grid);
    }
  }
}

void SchematicEditor::execGridPropertiesDialog() noexcept {
  if (const Schematic* activeSchematic = getActiveSchematic()) {
    GridSettingsDialog dialog(activeSchematic->getGridProperties(), this);
    connect(
        &dialog, &GridSettingsDialog::gridPropertiesChanged,
        [this](const GridProperties& grid) { setGridProperties(grid, false); });
    if (dialog.exec()) {
      setGridProperties(dialog.getGrid(), true);
    }
  }
}

void SchematicEditor::execGraphicsExportDialog(
    GraphicsExportDialog::Output output, const QString& settingsKey) noexcept {
  try {
    // Determine default file path.
    QString projectName =
        FilePath::cleanFileName(*mProject.getMetadata().getName(),
                                FilePath::ReplaceSpaces | FilePath::KeepCase);
    QString projectVersion =
        FilePath::cleanFileName(mProject.getMetadata().getVersion(),
                                FilePath::ReplaceSpaces | FilePath::KeepCase);
    QString relativePath =
        QString("output/%1/%2_Schematics").arg(projectVersion, projectName);
    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, this);
    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)));
      progress.setValue(i + 1);
      if (progress.wasCanceled()) {
        return;
      }
    }

    // Show dialog, which will do all the work.
    GraphicsExportDialog dialog(
        GraphicsExportDialog::Mode::Schematic, output, pages,
        getActiveSchematicIndex(), *mProject.getMetadata().getName(), 0,
        defaultFilePath,
        mProjectEditor.getWorkspace().getSettings().defaultLengthUnit.get(),
        "schematic_editor/" % settingsKey, this);
    connect(&dialog, &GraphicsExportDialog::requestOpenFile, this,
            [this](const FilePath& fp) {
              DesktopServices services(
                  mProjectEditor.getWorkspace().getSettings(), this);
              services.openLocalPath(fp);
            });
    dialog.exec();
  } catch (const Exception& e) {
    QMessageBox::warning(this, tr("Error"), e.getMsg());
  }
}

bool SchematicEditor::useIeee315Symbols() const noexcept {
  foreach (const QString& norm, mProject.getSettings().getNormOrder()) {
    if (norm.toLower() == "ieee 315") {
      return true;
    } else if (norm.toLower() == "iec 60617") {
      return false;
    }
  }
  return false;
}

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

}  // namespace editor
}  // namespace librepcb
