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

#include "../dialogs/directorylockhandlerdialog.h"
#include "../undostack.h"
#include "../utils/exclusiveactiongroup.h"
#include "../utils/toolbarproxy.h"
#include "../utils/undostackactiongroup.h"
#include "../widgets/statusbar.h"

#include <librepcb/core/library/librarybaseelement.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacesettings.h>

#include <QtCore>
#include <QtWidgets>

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

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

EditorWidgetBase::EditorWidgetBase(const Context& context, const FilePath& fp,
                                   QWidget* parent)
  : QWidget(parent),
    mContext(context),
    mFilePath(fp),
    mFileSystem(TransactionalFileSystem::open(
        fp, !context.readOnly, &askForRestoringBackup,
        DirectoryLockHandlerDialog::createDirectoryLockCallback())),  // can
                                                                      // throw
    mUndoStackActionGroup(nullptr),
    mToolsActionGroup(nullptr),
    mStatusBar(nullptr),
    mManualModificationsMade(false),
    mIsInterfaceBroken(false),
    mStatusBarMessage(),
    mSupportedApprovals(),
    mDisappearedApprovals() {
  mUndoStack.reset(new UndoStack());
  connect(mUndoStack.data(), &UndoStack::cleanChanged, this,
          &EditorWidgetBase::undoStackCleanChanged);
  connect(mUndoStack.data(), &UndoStack::stateModified, this,
          &EditorWidgetBase::undoStackStateModified);

  mCommandToolBarProxy.reset(new ToolBarProxy());

  // Run checks, but delay it because the subclass is not loaded yet!
  scheduleLibraryElementChecks();
}

EditorWidgetBase::~EditorWidgetBase() noexcept {
}

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

void EditorWidgetBase::connectEditor(UndoStackActionGroup& undoStackActionGroup,
                                     ExclusiveActionGroup& toolsActionGroup,
                                     QToolBar& commandToolBar,
                                     StatusBar& statusBar) noexcept {
  mUndoStackActionGroup = &undoStackActionGroup;
  mUndoStackActionGroup->setUndoStack(mUndoStack.data());

  mToolsActionGroup = &toolsActionGroup;
  mToolsActionGroup->reset();
  connect(mToolsActionGroup, &ExclusiveActionGroup::actionTriggered, this,
          &EditorWidgetBase::toolRequested);

  mCommandToolBarProxy->setToolBar(&commandToolBar);

  mStatusBar = &statusBar;
  mStatusBar->setPermanentMessage(mStatusBarMessage);
}

void EditorWidgetBase::disconnectEditor() noexcept {
  mUndoStackActionGroup->setUndoStack(nullptr);
  mUndoStackActionGroup = nullptr;

  disconnect(mToolsActionGroup, &ExclusiveActionGroup::actionTriggered, this,
             &EditorWidgetBase::toolRequested);
  mToolsActionGroup->reset();

  mCommandToolBarProxy->setToolBar(nullptr);

  mStatusBar->clearMessage();
  mStatusBar->clearPermanentMessage();
  mStatusBar = nullptr;
}

/*******************************************************************************
 *  Public Methods
 ******************************************************************************/

bool EditorWidgetBase::save() noexcept {
  mManualModificationsMade = false;
  mIsInterfaceBroken = false;
  mUndoStack->setClean();
  emit dirtyChanged(false);
  emit interfaceBrokenChanged(false);
  emit elementEdited(mFilePath);
  return true;
}

bool EditorWidgetBase::exportImage() noexcept {
  return execGraphicsExportDialog(GraphicsExportDialog::Output::Image,
                                  "image_export");
}
bool EditorWidgetBase::exportPdf() noexcept {
  return execGraphicsExportDialog(GraphicsExportDialog::Output::Pdf,
                                  "pdf_export");
}
bool EditorWidgetBase::print() noexcept {
  return execGraphicsExportDialog(GraphicsExportDialog::Output::Print, "print");
}

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

void EditorWidgetBase::setupInterfaceBrokenWarningWidget(
    QWidget& widget) noexcept {
  widget.setVisible(false);
  widget.setStyleSheet(
      "background-color: rgb(255, 255, 127); "
      "color: rgb(170, 0, 0);");
  QLabel* label = new QLabel(&widget);
  QFont font = label->font();
  font.setBold(true);
  label->setFont(font);
  label->setWordWrap(true);
  label->setText(
      tr("WARNING: You have changed some important properties of this "
         "library element. This breaks all other elements which depend on "
         "this one! Maybe you want to create a new library element instead "
         "of modifying this one?"));
  QHBoxLayout* layout = new QHBoxLayout(&widget);
  layout->addWidget(label);
  connect(this, &EditorWidgetBase::interfaceBrokenChanged, &widget,
          &QWidget::setVisible);
}

void EditorWidgetBase::setupErrorNotificationWidget(QWidget& widget) noexcept {
  widget.setVisible(false);
  widget.setStyleSheet(
      "background-color: rgb(255, 255, 127); "
      "color: rgb(170, 0, 0);");
  QLabel* label = new QLabel(&widget);
  QFont font = label->font();
  font.setBold(true);
  label->setFont(font);
  label->setWordWrap(true);
  label->setText(
      tr("WARNING: This library element contains errors, see exact messages "
         "below. You should fix these errors before saving it, otherwise the "
         "library element may not work as expected."));
  QHBoxLayout* layout = new QHBoxLayout(&widget);
  layout->addWidget(label);
  connect(this, &EditorWidgetBase::errorsAvailableChanged, &widget,
          &QWidget::setVisible);
}

void EditorWidgetBase::setMessageApproved(
    LibraryBaseElement& element, std::shared_ptr<const RuleCheckMessage> msg,
    bool approve) noexcept {
  if (msg) {
    QSet<SExpression> approvals = element.getMessageApprovals();
    if (approve) {
      approvals.insert(msg->getApproval());
    } else {
      approvals.remove(msg->getApproval());
    }
    element.setMessageApprovals(approvals);
    mManualModificationsMade = true;
    emit dirtyChanged(true);
  }
}

void EditorWidgetBase::undoStackStateModified() noexcept {
  if (!mContext.elementIsNewlyCreated) {
    bool broken = isInterfaceBroken();
    if (broken != mIsInterfaceBroken) {
      mIsInterfaceBroken = broken;
      emit interfaceBrokenChanged(mIsInterfaceBroken);
    }
  }
  scheduleLibraryElementChecks();
}

void EditorWidgetBase::setStatusBarMessage(const QString& message,
                                           int timeoutMs) noexcept {
  if (mStatusBar) {
    if (timeoutMs < 0) {
      mStatusBar->setPermanentMessage(message);
    } else {
      mStatusBar->showMessage(message, timeoutMs);
    }
  }
  if (timeoutMs < 0) {
    mStatusBarMessage = message;
  }
}

const QStringList& EditorWidgetBase::getLibLocaleOrder() const noexcept {
  return mContext.workspace.getSettings().libraryLocaleOrder.get();
}

QString EditorWidgetBase::getWorkspaceSettingsUserName() noexcept {
  QString u = mContext.workspace.getSettings().userName.get();
  if (u.isEmpty()) {
    QMessageBox::warning(
        this, tr("User name not set"),
        tr("No user name defined in workspace settings. Please open "
           "workspace settings to set the default user name."));
  }
  return u;
}

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

bool EditorWidgetBase::askForRestoringBackup(const FilePath& dir) {
  Q_UNUSED(dir);
  QMessageBox::StandardButton btn = QMessageBox::question(
      0, tr("Restore autosave backup?"),
      tr("It seems that the application crashed the last time you opened this "
         "library element. Do you want to restore the last autosave backup?"),
      QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
      QMessageBox::Cancel);
  switch (btn) {
    case QMessageBox::Yes:
      return true;
    case QMessageBox::No:
      return false;
    default:
      throw UserCanceled(__FILE__, __LINE__);
  }
}

void EditorWidgetBase::toolRequested(int tool, const QVariant& mode) noexcept {
  toolChangeRequested(static_cast<Tool>(tool), mode);
}

void EditorWidgetBase::undoStackCleanChanged(bool clean) noexcept {
  Q_UNUSED(clean);
  emit dirtyChanged(isDirty());
}

void EditorWidgetBase::scheduleLibraryElementChecks() noexcept {
  // Don't run check immediately when requested. Sometimes when the undo stack
  // reports changes, it's just in the middle of a bigger change, so the whole
  // change is not done yet. In that case, running checks would lead to wrong
  // results. Instead, just delay checks for some time to get more stable
  // messages. But also don't wait too long, otherwise it would feel like a
  // lagging user interface.
  QTimer::singleShot(50, this, &EditorWidgetBase::updateCheckMessages);
}

void EditorWidgetBase::updateCheckMessages() noexcept {
  try {
    RuleCheckMessageList msgs;
    if (runChecks(msgs)) {  // can throw
      const QSet<SExpression> approvals =
          RuleCheckMessage::getAllApprovals(msgs);
      mSupportedApprovals |= approvals;
      mDisappearedApprovals = mSupportedApprovals - approvals;

      int errors = 0;
      foreach (const auto& msg, msgs) {
        if (msg->getSeverity() == RuleCheckMessage::Severity::Error) {
          ++errors;
        }
      }
      emit errorsAvailableChanged(errors > 0);
    } else {
      // Failed to run checks (for example because a command is active), try it
      // later again.
      scheduleLibraryElementChecks();
    }
  } catch (const Exception& e) {
    qCritical() << "Failed to run library element checks:" << e.getMsg();
  }
}

bool EditorWidgetBase::ruleCheckFixAvailable(
    std::shared_ptr<const RuleCheckMessage> msg) noexcept {
  try {
    return processRuleCheckMessage(msg,
                                   false);  // can throw, but should really not
  } catch (const Exception&) {
    return false;
  }
}

void EditorWidgetBase::ruleCheckFixRequested(
    std::shared_ptr<const RuleCheckMessage> msg) noexcept {
  try {
    processRuleCheckMessage(msg, true);  // can throw
  } catch (const Exception& e) {
    QMessageBox::critical(this, tr("Error"), e.getMsg());
  }
}

void EditorWidgetBase::ruleCheckDescriptionRequested(
    std::shared_ptr<const RuleCheckMessage> msg) noexcept {
  if (msg) {
    QMessageBox::information(this, msg->getMessage(), msg->getDescription());
  }
}

void EditorWidgetBase::ruleCheckMessageSelected(
    std::shared_ptr<const RuleCheckMessage> msg) noexcept {
  Q_UNUSED(msg);
}

void EditorWidgetBase::ruleCheckMessageDoubleClicked(
    std::shared_ptr<const RuleCheckMessage> msg) noexcept {
  ruleCheckDescriptionRequested(msg);
}

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

}  // namespace editor
}  // namespace librepcb
