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

#include "../application.h"
#include "../fileio/versionfile.h"
#include "../library/cmp/component.h"
#include "../library/dev/device.h"
#include "../library/dev/part.h"
#include "../library/pkg/package.h"
#include "../library/sym/symbol.h"
#include "../serialization/fileformatmigration.h"
#include "../types/pcbcolor.h"
#include "board/board.h"
#include "board/boarddesignrules.h"
#include "board/boardfabricationoutputsettings.h"
#include "board/drc/boarddesignrulechecksettings.h"
#include "board/items/bi_device.h"
#include "board/items/bi_hole.h"
#include "board/items/bi_netline.h"
#include "board/items/bi_netpoint.h"
#include "board/items/bi_netsegment.h"
#include "board/items/bi_pad.h"
#include "board/items/bi_plane.h"
#include "board/items/bi_polygon.h"
#include "board/items/bi_stroketext.h"
#include "board/items/bi_via.h"
#include "board/items/bi_zone.h"
#include "circuit/assemblyvariant.h"
#include "circuit/bus.h"
#include "circuit/circuit.h"
#include "circuit/componentinstance.h"
#include "circuit/componentsignalinstance.h"
#include "circuit/netclass.h"
#include "circuit/netsignal.h"
#include "erc/electricalrulecheck.h"
#include "project.h"
#include "projectlibrary.h"
#include "schematic/items/si_busjunction.h"
#include "schematic/items/si_buslabel.h"
#include "schematic/items/si_busline.h"
#include "schematic/items/si_bussegment.h"
#include "schematic/items/si_image.h"
#include "schematic/items/si_netlabel.h"
#include "schematic/items/si_netline.h"
#include "schematic/items/si_netpoint.h"
#include "schematic/items/si_netsegment.h"
#include "schematic/items/si_polygon.h"
#include "schematic/items/si_symbol.h"
#include "schematic/items/si_symbolpin.h"
#include "schematic/items/si_text.h"
#include "schematic/schematic.h"

#include <QtCore>

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

static const char* HTML_LOG_HEADER = R"(<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{TITLE}}</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
    table { border-collapse: collapse; width: 100%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    th, td { padding: 12px; border-bottom: 1px solid #ddd; width: auto; white-space: nowrap; text-align: center; vertical-align: middle; }
    th { background: #e0e0e0; font-weight: bold; }
    th:nth-child(3), td:nth-child(4) { text-align: left; width: 100%; white-space: normal; }
    .footer { font-size: 10pt; font-style: italic; color: #505050; }
    .note { background: #e6f3ff; }
    .warning { background: #fff3cd; }
    .critical { background: #f8d7da; }
    .note td:nth-child(3)::before { content: "ℹ️"; }
    .warning td:nth-child(3)::before { content: "⚠️"; }
    .critical td:nth-child(3)::before { content: "❌"; }
  </style>
</head>
<body>
  <h1>🚀 {{TITLE}}</h1>
)";
static const char* HTML_LOG_TABLE_HEADER = R"(  <table>
    <thead>
      <tr>
        <th>{{HEADER_VERSION}}</th>
        <th>{{HEADER_OCCURRENCES}}</th>
        <th colspan="2">{{HEADER_MESSAGE}}</th>
      </tr>
    </thead>
    <tbody>
)";
static const char* HTML_LOG_FOOTER = R"(    </tbody>
  </table>
  <p class="footer">{{GENERATED_AT}}</p>
</body>
</html>)";

QByteArray ProjectLoader::MigrationLog::toHtml(
    bool isTemporary) const noexcept {
  const QHash<FileFormatMigration::Message::Severity, QString> classes = {
      {FileFormatMigration::Message::Severity::Note, "note"},
      {FileFormatMigration::Message::Severity::Warning, "warning"},
      {FileFormatMigration::Message::Severity::Critical, "critical"},
  };
  QString html = HTML_LOG_HEADER;
  if (isTemporary) {
    html +=
        "  <div style=\"background:#d0d0d0;border: 1px solid "
        "black;padding:8px;margin-bottom:15px;\">\n";
    html += "    <span style=\"color:blue;\">" %
        tr("Note: This is a temporary file since the project has not been "
           "saved to disk yet. When you save the project, this log will be "
           "written to the projects '%1' directory.")
            .arg("logs")
            .toHtmlEscaped() %
        "</span></br>\n";
    html += "    <span style=\"color:red;font-weight:bold;\">" %
        tr("Attention: After saving the project, the file format migration "
           "cannot be reverted. Check if everything looks good before saving "
           "the project.")
            .toHtmlEscaped() %
        "</span>\n";
    html += "  </div>\n";
  }
  html += "  <p>" %
      tr("The project has been migrated from file format <strong>%1 to "
         "%2</strong>, which emitted the following messages:")
          .arg("v" % fromVersion.toStr(), "v" % toVersion.toStr()) %
      "</p>\n";
  html += HTML_LOG_TABLE_HEADER;
  if (messages.isEmpty()) {
    html += QString("      <tr>\n");
    html +=
        QString(
            "        <td colspan=\"4\" style=\"text-align:left;\">%1</td>\n")
            .arg(tr("No messages emitted.").toHtmlEscaped());
    html += "      </tr>\n";
  }
  for (const FileFormatMigration::Message& m : messages) {
    const QStringList cells = {
        m.fromVersion.toStr() % " ⇨ " % m.toVersion.toStr(),
        (m.affectedItems > 0) ? QString::number(m.affectedItems) : QString(),
        QString(),  // Severity icon from CSS
        m.message,
    };
    html += QString("      <tr class=\"%1\">\n").arg(classes.value(m.severity));
    for (const QString& s : cells) {
      html += QString("        <td>%1</td>\n").arg(s.toHtmlEscaped());
    }
    html += "      </tr>\n";
  }
  html += HTML_LOG_FOOTER;
  html.replace("{{HEADER_VERSION}}", tr("Version"));
  html.replace("{{HEADER_OCCURRENCES}}", tr("Occurrences"));
  html.replace("{{HEADER_MESSAGE}}", tr("Message"));
  html.replace("{{TITLE}}",
               QString("Migration Log: %1").arg(projectName).toHtmlEscaped());
  html.replace(
      "{{GENERATED_AT}}",
      QString("Generated by LibrePCB %1 at %2")
          .arg(Application::getVersion())
          .arg(dateTime.date().toString() % ", " % dateTime.time().toString())
          .toHtmlEscaped());
  return html.toUtf8();
}

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

ProjectLoader::ProjectLoader(QObject* parent) noexcept
  : QObject(parent), mAutoAssignDeviceModels(false) {
}

ProjectLoader::~ProjectLoader() noexcept {
}

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

std::unique_ptr<Project> ProjectLoader::open(
    std::unique_ptr<TransactionalDirectory> directory,
    const QString& filename) {
  Q_ASSERT(directory);
  mMigrationLog = std::nullopt;

  QElapsedTimer timer;
  timer.start();
  const FilePath fp = directory->getAbsPath(filename);
  qDebug().nospace() << "Open project " << fp.toNative() << "...";

  // Check if the project file exists.
  if (!directory->fileExists(filename)) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("File does not exist: '%1'").arg(fp.toNative()));
  }

  // Read the file format version.
  if (!directory->fileExists(".librepcb-project")) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Directory does not contain a LibrePCB project: '%1'")
                           .arg(directory->getAbsPath().toNative()));
  }
  const Version fileFormat =
      VersionFile::fromByteArray(directory->read(".librepcb-project"))
          .getVersion();
  qDebug().noquote() << "Detected project file format:" << fileFormat.toStr();

  // Check file format version.
  if (fileFormat > Application::getFileFormatVersion()) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("This project was created with a newer application version.\n"
           "You need at least LibrePCB %1 to open it.\n\n%2")
            .arg(fileFormat.toPrettyStr(3))
            .arg(fp.toNative()));
  }

  // Upgrate file format, if needed.
  for (auto migration : FileFormatMigration::getMigrations(fileFormat)) {
    if (!mMigrationLog) {
      mMigrationLog = MigrationLog{filename,
                                   QDateTime::currentDateTime(),
                                   fileFormat,
                                   Application::getFileFormatVersion(),
                                   {}};
    }
    qInfo().nospace().noquote()
        << "Project file format is outdated, upgrading from v"
        << migration->getFromVersion().toStr() << " to v"
        << migration->getToVersion().toStr() << "...";
    migration->upgradeProject(*directory, mMigrationLog->messages);
  }

  // Sort & save migration messages.
  if (mMigrationLog) {
    std::sort(mMigrationLog->messages.begin(), mMigrationLog->messages.end(),
              [](const FileFormatMigration::Message& a,
                 const FileFormatMigration::Message& b) {
                // It is most intuitive to have the oldest messages at top, and
                // the newest messages at bottom since they might "depend" on
                // each other.
                if (a.toVersion != b.toVersion) {
                  return a.toVersion < b.toVersion;
                } else if (a.severity != b.severity) {
                  return a.severity > b.severity;
                } else {
                  return a.message < b.message;
                }
              });

    const QByteArray html = mMigrationLog->toHtml(false);
    const QString logFileName =
        QString("logs/%1_migration_to_v%2.html")
            .arg(mMigrationLog->dateTime.date().toString("yyyy-MM-dd"))
            .arg(Application::getFileFormatVersion().toStr());
    directory->write(logFileName, html);
  }

  // Load project.
  std::unique_ptr<Project> p(new Project(std::move(directory), filename));
  loadMetadata(*p);
  loadSettings(*p);
  loadOutputJobs(*p);
  loadLibrary(*p);
  loadCircuit(*p);
  loadErc(*p);
  loadSchematics(*p);
  loadBoards(*p);
  loadProjectUserSettings(*p);

  // If the file format was migrated, clean up obsolete ERC messages.
  if (mMigrationLog) {
    qInfo() << "Running ERC to clean up obsolete message approvals...";
    ElectricalRuleCheck erc(*p);
    const RuleCheckMessageList msgs = erc.runChecks();
    const QSet<SExpression> approvals = RuleCheckMessage::getAllApprovals(msgs);
    p->setErcMessageApprovals(p->getErcMessageApprovals() & approvals);
  }

  // Make sure the files are formatted correctly. Also handle possible errors
  // during serialization now instead of later.
  if (mMigrationLog) {
    p->save();
  }

  // Done!
  qDebug() << "Successfully opened project in" << timer.elapsed() << "ms.";
  return p;
}

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

void ProjectLoader::loadMetadata(Project& p) {
  qDebug() << "Load project metadata...";
  const QString fp = "project/metadata.lp";
  const std::unique_ptr<const SExpression> root = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));

  p.setUuid(deserialize<Uuid>(root->getChild("@0")));
  p.setName(deserialize<ElementName>(root->getChild("name/@0")));
  p.setAuthor(root->getChild("author/@0").getValue());
  p.setVersion(deserialize<FileProofName>(root->getChild("version/@0")));
  p.setCreated(deserialize<QDateTime>(root->getChild("created/@0")));
  p.setAttributes(AttributeList(*root));

  qDebug() << "Successfully loaded project metadata.";
}

void ProjectLoader::loadSettings(Project& p) {
  qDebug() << "Load project settings...";
  const QString fp = "project/settings.lp";
  const std::unique_ptr<const SExpression> root = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));

  {
    QStringList l;
    foreach (const SExpression* node,
             root->getChild("library_locale_order").getChildren("locale")) {
      l.append(node->getChild("@0").getValue());
    }
    p.setLocaleOrder(l);
  }

  {
    QStringList l;
    foreach (const SExpression* node,
             root->getChild("library_norm_order").getChildren("norm")) {
      l.append(node->getChild("@0").getValue());
    }
    p.setNormOrder(l);
  }

  {
    QStringList l;
    foreach (const SExpression* node,
             root->getChild("custom_bom_attributes").getChildren("attribute")) {
      l.append(node->getChild("@0").getValue());
    }
    p.setCustomBomAttributes(l);
  }

  p.setDefaultLockComponentAssembly(
      deserialize<bool>(root->getChild("default_lock_component_assembly/@0")));

  qDebug() << "Successfully loaded project settings.";
}

void ProjectLoader::loadOutputJobs(Project& p) {
  qDebug() << "Load output jobs...";
  const QString fp = "project/jobs.lp";
  const std::unique_ptr<const SExpression> root = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));
  p.getOutputJobs() = deserialize<OutputJobList>(*root);
  qDebug() << "Successfully loaded output jobs.";
}

void ProjectLoader::loadLibrary(Project& p) {
  qDebug() << "Load project library...";

  loadLibraryElements<Symbol>(p, "sym", "symbols", &ProjectLibrary::addSymbol);
  loadLibraryElements<Package>(p, "pkg", "packages",
                               &ProjectLibrary::addPackage);
  loadLibraryElements<Component>(p, "cmp", "components",
                                 &ProjectLibrary::addComponent);
  loadLibraryElements<Device>(p, "dev", "devices", &ProjectLibrary::addDevice);

  qDebug() << "Successfully loaded project library.";
}

template <typename ElementType>
void ProjectLoader::loadLibraryElements(
    Project& p, const QString& dirname, const QString& type,
    void (ProjectLibrary::*addFunction)(ElementType&)) {
  // Search all subdirectories which have a valid UUID as directory name.
  int count = 0;
  foreach (const QString& sub, p.getLibrary().getDirectory().getDirs(dirname)) {
    std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
        p.getLibrary().getDirectory(), dirname % "/" % sub));

    // Check if directory is a valid library element.
    if (!LibraryBaseElement::isValidElementDirectory<ElementType>(*dir, "")) {
      qWarning() << "Invalid directory in project library, ignoring it:"
                 << dir->getAbsPath().toNative();
      continue;
    }

    // Load the library element.
    ElementType* element =
        ElementType::open(std::move(dir)).release();  // can throw
    (p.getLibrary().*addFunction)(*element);
    ++count;
  }

  qDebug().nospace().noquote()
      << "Successfully loaded " << count << " " << type << ".";
}

void ProjectLoader::loadCircuit(Project& p) {
  qDebug() << "Load circuit...";
  const QString fp = "circuit/circuit.lp";
  const std::unique_ptr<const SExpression> root = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));

  // Load assembly variants.
  foreach (const SExpression* node, root->getChildren("variant")) {
    auto av = std::make_shared<AssemblyVariant>(*node);
    p.getCircuit().addAssemblyVariant(av);
  }
  if (p.getCircuit().getAssemblyVariants().isEmpty()) {
    throw RuntimeError(__FILE__, __LINE__, "Project has no assembly variants.");
  }

  // Load net classes.
  foreach (const SExpression* node, root->getChildren("netclass")) {
    NetClass* netclass = new NetClass(p.getCircuit(), *node);
    p.getCircuit().addNetClass(*netclass);
  }

  // Load net signals.
  foreach (const SExpression* node, root->getChildren("net")) {
    const Uuid netclassUuid = deserialize<Uuid>(node->getChild("netclass/@0"));
    NetClass* netclass = p.getCircuit().getNetClasses().value(netclassUuid);
    if (!netclass) {
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("Inexistent net class: '%1'").arg(netclassUuid.toStr()));
    }
    NetSignal* netsignal = new NetSignal(
        p.getCircuit(), deserialize<Uuid>(node->getChild("@0")), *netclass,
        deserialize<CircuitIdentifier>(node->getChild("name/@0")),
        deserialize<bool>(node->getChild("auto/@0")));
    p.getCircuit().addNetSignal(*netsignal);
  }

  // Load buses.
  foreach (const SExpression* node, root->getChildren("bus")) {
    Bus* bus = new Bus(p.getCircuit(), deserialize<Uuid>(node->getChild("@0")),
                       deserialize<BusName>(node->getChild("name/@0")),
                       deserialize<bool>(node->getChild("auto/@0")),
                       deserialize<bool>(node->getChild("prefix_nets/@0")),
                       deserialize<std::optional<UnsignedLength>>(
                           node->getChild("max_trace_length_difference/@0")));
    p.getCircuit().addBus(*bus);
  }

  // Load component instances.
  foreach (const SExpression* node, root->getChildren("component")) {
    const Uuid cmpUuid = deserialize<Uuid>(node->getChild("lib_component/@0"));
    const Component* libCmp = p.getLibrary().getComponent(cmpUuid);
    if (!libCmp) {
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("The component '%1' does not exist in the project's library.")
              .arg(cmpUuid.toStr()));
    }
    ComponentInstance* cmp = new ComponentInstance(
        p.getCircuit(), deserialize<Uuid>(node->getChild("@0")), *libCmp,
        deserialize<Uuid>(node->getChild("lib_variant/@0")),
        deserialize<CircuitIdentifier>(node->getChild("name/@0")));
    cmp->setValue(node->getChild("value/@0").getValue());
    cmp->setAttributes(AttributeList(*node));
    cmp->setAssemblyOptions(ComponentAssemblyOptionList(*node));
    cmp->setLockAssembly(deserialize<bool>(node->getChild("lock_assembly/@0")));
    p.getCircuit().addComponentInstance(*cmp);

    QSet<Uuid> loadedSignals;
    foreach (const SExpression* child, node->getChildren("signal")) {
      const Uuid cmpSigUuid = deserialize<Uuid>(child->getChild("@0"));
      ComponentSignalInstance* cmpSig = cmp->getSignalInstance(cmpSigUuid);
      if (!cmpSig) {
        throw RuntimeError(__FILE__, __LINE__,
                           QString("Inexistent component signal: '%1'")
                               .arg(cmpSigUuid.toStr()));
      }
      if (loadedSignals.contains(cmpSigUuid)) {
        throw RuntimeError(__FILE__, __LINE__,
                           QString("The signal '%1' is defined multiple times.")
                               .arg(cmpSigUuid.toStr()));
      }
      loadedSignals.insert(cmpSigUuid);
      if (const std::optional<Uuid> netSignalUuid =
              deserialize<std::optional<Uuid>>(child->getChild("net/@0"))) {
        NetSignal* netSignal =
            p.getCircuit().getNetSignals().value(*netSignalUuid);
        if (!netSignal) {
          throw RuntimeError(__FILE__, __LINE__,
                             QString("Inexistent net signal: '%1'")
                                 .arg(netSignalUuid->toStr()));
        }
        cmpSig->setNetSignal(netSignal);
      }
    }
    if (loadedSignals.count() != cmp->getSignals().count()) {
      qCritical() << "Signal count mismatch:" << loadedSignals.count()
                  << "!=" << cmp->getSignals().count();
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("The signal count of the component instance '%1' does "
                  "not match with the signal count of the component '%2'.")
              .arg(cmp->getUuid().toStr())
              .arg(libCmp->getUuid().toStr()));
    }
  }

  qDebug() << "Successfully loaded circuit.";
}

void ProjectLoader::loadErc(Project& p) {
  qDebug() << "Load ERC approvals...";
  const QString fp = "circuit/erc.lp";
  const std::unique_ptr<const SExpression> root = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));

  // Load approvals.
  QSet<SExpression> approvals;
  foreach (const SExpression* node, root->getChildren("approved")) {
    approvals.insert(*node);
  }
  p.setErcMessageApprovals(approvals);

  qDebug() << "Successfully loaded ERC approvals.";
}

void ProjectLoader::loadSchematics(Project& p) {
  qDebug() << "Load schematics...";
  const QString fp = "schematics/schematics.lp";
  const std::unique_ptr<const SExpression> indexRoot = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));
  foreach (const SExpression* indexNode, indexRoot->getChildren("schematic")) {
    loadSchematic(p, indexNode->getChild("@0").getValue());
  }
  qDebug() << "Successfully loaded" << p.getSchematics().count()
           << "schematics.";
}

void ProjectLoader::loadSchematic(Project& p, const QString& relativeFilePath) {
  const FilePath fp = FilePath::fromRelative(p.getPath(), relativeFilePath);
  std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
      p.getDirectory(), fp.getParentDir().toRelative(p.getPath())));
  const std::unique_ptr<const SExpression> root =
      SExpression::parse(dir->read(fp.getFilename()), fp);

  Schematic* schematic =
      new Schematic(p, std::move(dir), fp.getParentDir().getFilename(),
                    deserialize<Uuid>(root->getChild("@0")),
                    deserialize<ElementName>(root->getChild("name/@0")));
  schematic->setGridInterval(
      deserialize<PositiveLength>(root->getChild("grid/interval/@0")));
  schematic->setGridUnit(
      deserialize<LengthUnit>(root->getChild("grid/unit/@0")));
  p.addSchematic(*schematic);

  foreach (const SExpression* node, root->getChildren("symbol")) {
    loadSchematicSymbol(*schematic, *node);
  }
  foreach (const SExpression* node, root->getChildren("bussegment")) {
    loadSchematicBusSegment(*schematic, *node);
  }
  foreach (const SExpression* node, root->getChildren("netsegment")) {
    loadSchematicNetSegment(*schematic, *node);
  }
  foreach (const SExpression* node, root->getChildren("polygon")) {
    SI_Polygon* polygon = new SI_Polygon(*schematic, Polygon(*node));
    schematic->addPolygon(*polygon);
  }
  foreach (const SExpression* node, root->getChildren("text")) {
    SI_Text* text = new SI_Text(*schematic, Text(*node));
    schematic->addText(*text);
  }
  foreach (const SExpression* node, root->getChildren("image")) {
    SI_Image* image = new SI_Image(*schematic, Image(*node));
    schematic->addImage(*image);
  }

  // Load user settings.
  loadSchematicUserSettings(*schematic);
}

void ProjectLoader::loadSchematicSymbol(Schematic& s, const SExpression& node) {
  const Uuid cmpUuid = deserialize<Uuid>(node.getChild("component/@0"));
  ComponentInstance* cmp =
      s.getProject().getCircuit().getComponentInstanceByUuid(cmpUuid);
  if (!cmp) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("The component '%1' does not exist in the circuit.")
            .arg(cmpUuid.toStr()));
  }
  SI_Symbol* symbol =
      new SI_Symbol(s, deserialize<Uuid>(node.getChild("@0")), *cmp,
                    deserialize<Uuid>(node.getChild("lib_gate/@0")),
                    Point(node.getChild("position")),
                    deserialize<Angle>(node.getChild("rotation/@0")),
                    deserialize<bool>(node.getChild("mirror/@0")), false);
  foreach (const SExpression* child, node.getChildren("text")) {
    symbol->addText(*new SI_Text(s, Text(*child)));
  }
  s.addSymbol(*symbol);
}

void ProjectLoader::loadSchematicBusSegment(Schematic& s,
                                            const SExpression& node) {
  const Uuid busUuid = deserialize<Uuid>(node.getChild("bus/@0"));
  Bus* bus = s.getProject().getCircuit().getBuses().value(busUuid);
  if (!bus) {
    throw RuntimeError(__FILE__, __LINE__,
                       QString("Inexistent bus: '%1'").arg(busUuid.toStr()));
  }
  SI_BusSegment* segment =
      new SI_BusSegment(s, deserialize<Uuid>(node.getChild("@0")), *bus);
  s.addBusSegment(*segment);

  // Load junctions.
  QList<SI_BusJunction*> junctions;
  foreach (const SExpression* child, node.getChildren("junction")) {
    SI_BusJunction* j =
        new SI_BusJunction(*segment, deserialize<Uuid>(child->getChild("@0")),
                           Point(child->getChild("position")));
    junctions.append(j);
  }

  // Load lines.
  QList<SI_BusLine*> lines;
  foreach (const SExpression* child, node.getChildren("line")) {
    auto parseAnchor = [&](const SExpression& aNode) {
      const Uuid junctionUuid =
          deserialize<Uuid>(aNode.getChild("junction/@0"));
      foreach (SI_BusJunction* j, junctions) {
        if (j->getUuid() == junctionUuid) {
          return j;
        }
      }
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("Bus junction '%1:%2' does not exist in schematic.")
              .arg(segment->getUuid().toStr(), junctionUuid.toStr()));
    };
    SI_BusLine* line = new SI_BusLine(
        *segment, deserialize<Uuid>(child->getChild("@0")),
        *parseAnchor(child->getChild("from")),
        *parseAnchor(child->getChild("to")),
        deserialize<UnsignedLength>(child->getChild("width/@0")));
    lines.append(line);
  }

  // Add junctions & lines.
  segment->addJunctionsAndLines(junctions, lines);

  // Load labels.
  foreach (const SExpression* child, node.getChildren("label")) {
    SI_BusLabel* l = new SI_BusLabel(*segment, NetLabel(*child));
    segment->addLabel(*l);
  }
}

void ProjectLoader::loadSchematicNetSegment(Schematic& s,
                                            const SExpression& node) {
  const Uuid netSignalUuid = deserialize<Uuid>(node.getChild("net/@0"));
  NetSignal* netSignal =
      s.getProject().getCircuit().getNetSignals().value(netSignalUuid);
  if (!netSignal) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("Inexistent net signal: '%1'").arg(netSignalUuid.toStr()));
  }
  SI_NetSegment* netSegment =
      new SI_NetSegment(s, deserialize<Uuid>(node.getChild("@0")), *netSignal);
  s.addNetSegment(*netSegment);

  // Load net points.
  QList<SI_NetPoint*> netPoints;
  foreach (const SExpression* child, node.getChildren("junction")) {
    SI_NetPoint* netpoint =
        new SI_NetPoint(*netSegment, deserialize<Uuid>(child->getChild("@0")),
                        Point(child->getChild("position")));
    netPoints.append(netpoint);
  }

  // Load net lines.
  QList<SI_NetLine*> netLines;
  foreach (const SExpression* child, node.getChildren("line")) {
    auto parseAnchor = [&](const SExpression& aNode) {
      const NetLineAnchor anchor(aNode);
      if (const std::optional<Uuid>& obj = anchor.tryGetJunction()) {
        foreach (SI_NetPoint* netPoint, netPoints) {
          if (netPoint->getUuid() == *obj) {
            return static_cast<SI_NetLineAnchor*>(netPoint);
          }
        }
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Net point '%1' does not exist in schematic.")
                .arg(obj->toStr()));
      } else if (const std::optional<NetLineAnchor::BusAnchor>& obj =
                     anchor.tryGetBusJunction()) {
        SI_BusSegment* segment = s.getBusSegments().value(obj->segment);
        if (!segment) {
          throw RuntimeError(
              __FILE__, __LINE__,
              QString("Bus segment '%1' does not exist in schematic.")
                  .arg(obj->segment.toStr()));
        }
        SI_NetLineAnchor* anchor = segment->getJunctions().value(obj->junction);
        if (!anchor) {
          throw RuntimeError(
              __FILE__, __LINE__,
              QString("Bus junction '%1:%2' does not exist in schematic.")
                  .arg(obj->segment.toStr(), obj->junction.toStr()));
        }
        return anchor;
      } else if (const std::optional<NetLineAnchor::PinAnchor>& obj =
                     anchor.tryGetPin()) {
        SI_Symbol* symbol = s.getSymbols().value(obj->symbol);
        if (!symbol) {
          throw RuntimeError(__FILE__, __LINE__,
                             QString("Symbol '%1' does not exist in schematic.")
                                 .arg(obj->symbol.toStr()));
        }
        SI_NetLineAnchor* anchor = symbol->getPin(obj->pin);
        if (!anchor) {
          throw RuntimeError(
              __FILE__, __LINE__,
              QString("Symbol pin '%1:%2' does not exist in schematic.")
                  .arg(obj->symbol.toStr(), obj->pin.toStr()));
        }
        return anchor;
      } else {
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Unknown net line anchor in net segment '%1'.")
                .arg(netSegment->getUuid().toStr()));
      }
    };
    SI_NetLine* netLine = new SI_NetLine(
        *netSegment, deserialize<Uuid>(child->getChild("@0")),
        *parseAnchor(child->getChild("from")),
        *parseAnchor(child->getChild("to")),
        deserialize<UnsignedLength>(child->getChild("width/@0")));
    netLines.append(netLine);
  }

  // Add net points & net lines.
  netSegment->addNetPointsAndNetLines(netPoints, netLines);

  // Load net labels.
  foreach (const SExpression* child, node.getChildren("label")) {
    SI_NetLabel* netLabel = new SI_NetLabel(*netSegment, NetLabel(*child));
    netSegment->addNetLabel(*netLabel);
  }
}

void ProjectLoader::loadSchematicUserSettings(Schematic& s) {
  try {
    const QString fp = "settings.user.lp";
    const std::unique_ptr<const SExpression> root = SExpression::parse(
        s.getDirectory().read(fp), s.getDirectory().getAbsPath(fp));
  } catch (const Exception&) {
    // Project user settings are normally not put under version control and
    // thus the likelyhood of parse errors is higher (e.g. when switching to
    // an older, now incompatible revision). To avoid frustration, we just
    // ignore these errors and load the default settings instead...
    qCritical() << "Could not load schematic user settings, defaults will be "
                   "used instead.";
  }
}

void ProjectLoader::loadBoards(Project& p) {
  qDebug() << "Load boards...";
  const QString fp = "boards/boards.lp";
  const std::unique_ptr<const SExpression> indexRoot = SExpression::parse(
      p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));
  foreach (const SExpression* node, indexRoot->getChildren("board")) {
    loadBoard(p, node->getChild("@0").getValue());
  }
  qDebug() << "Successfully loaded" << p.getBoards().count() << "boards.";
}

void ProjectLoader::loadBoard(Project& p, const QString& relativeFilePath) {
  const FilePath fp = FilePath::fromRelative(p.getPath(), relativeFilePath);
  std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
      p.getDirectory(), fp.getParentDir().toRelative(p.getPath())));
  const std::unique_ptr<const SExpression> root =
      SExpression::parse(dir->read(fp.getFilename()), fp);

  Board* board = new Board(p, std::move(dir), fp.getParentDir().getFilename(),
                           deserialize<Uuid>(root->getChild("@0")),
                           deserialize<ElementName>(root->getChild("name/@0")));
  board->setDefaultFontName(root->getChild("default_font/@0").getValue());
  board->setGridInterval(
      deserialize<PositiveLength>(root->getChild("grid/interval/@0")));
  board->setGridUnit(deserialize<LengthUnit>(root->getChild("grid/unit/@0")));
  board->setPreferredFootprintTags(Board::PreferredFootprintTags(
      root->getChild("preferred_footprint_tags")));
  board->setInnerLayerCount(
      deserialize<uint>(root->getChild("layers/inner/@0")));
  board->setPcbThickness(
      deserialize<PositiveLength>(root->getChild("thickness/@0")));
  board->setSolderResist(
      deserialize<const PcbColor*>(root->getChild("solder_resist/@0")));
  board->setSilkscreenColor(
      deserialize<const PcbColor&>(root->getChild("silkscreen/@0")));
  {
    QVector<const Layer*> layers;
    foreach (const SExpression* child,
             root->getChild("silkscreen_layers_top")
                 .getChildren(SExpression::Type::Token)) {
      layers.append(deserialize<const Layer*>(*child));
    }
    board->setSilkscreenLayersTop(layers);
  }
  {
    QVector<const Layer*> layers;
    foreach (const SExpression* child,
             root->getChild("silkscreen_layers_bot")
                 .getChildren(SExpression::Type::Token)) {
      layers.append(deserialize<const Layer*>(*child));
    }
    board->setSilkscreenLayersBot(layers);
  }
  board->setDesignRules(BoardDesignRules(root->getChild("design_rules")));
  {
    const SExpression& node = root->getChild("design_rule_check");
    const Version approvalsVersion =
        deserialize<Version>(node.getChild("approvals_version/@0"));
    QSet<SExpression> approvals;
    foreach (const SExpression* child, node.getChildren("approved")) {
      approvals.insert(*child);
    }
    board->setDrcSettings(BoardDesignRuleCheckSettings(node));
    board->loadDrcMessageApprovals(approvalsVersion, approvals);
  }
  board->getFabricationOutputSettings() = BoardFabricationOutputSettings(
      root->getChild("fabrication_output_settings"));
  p.addBoard(*board);

  foreach (const SExpression* node, root->getChildren("device")) {
    loadBoardDeviceInstance(*board, *node);
  }
  foreach (const SExpression* node, root->getChildren("netsegment")) {
    loadBoardNetSegment(*board, *node);
  }
  foreach (const SExpression* node, root->getChildren("plane")) {
    loadBoardPlane(*board, *node);
  }
  foreach (const SExpression* node, root->getChildren("zone")) {
    BI_Zone* zone = new BI_Zone(*board, BoardZoneData(*node));
    board->addZone(*zone);
  }
  foreach (const SExpression* node, root->getChildren("polygon")) {
    BI_Polygon* polygon = new BI_Polygon(*board, BoardPolygonData(*node));
    board->addPolygon(*polygon);
  }
  foreach (const SExpression* node, root->getChildren("stroke_text")) {
    BI_StrokeText* text = new BI_StrokeText(*board, BoardStrokeTextData(*node));
    board->addStrokeText(*text);
  }
  foreach (const SExpression* node, root->getChildren("hole")) {
    BI_Hole* hole = new BI_Hole(*board, BoardHoleData(*node));
    board->addHole(*hole);
  }

  // Load user settings.
  loadBoardUserSettings(*board);
}

void ProjectLoader::loadBoardDeviceInstance(Board& b, const SExpression& node) {
  const Uuid cmpUuid = deserialize<Uuid>(node.getChild("@0"));
  ComponentInstance* cmp =
      b.getProject().getCircuit().getComponentInstanceByUuid(cmpUuid);
  if (!cmp) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("The component instace '%1' does not exist in the circuit.")
            .arg(cmpUuid.toStr()));
  }
  BI_Device* device =
      new BI_Device(b, *cmp, deserialize<Uuid>(node.getChild("lib_device/@0")),
                    deserialize<Uuid>(node.getChild("lib_footprint/@0")),
                    Point(node.getChild("position")),
                    deserialize<Angle>(node.getChild("rotation/@0")),
                    deserialize<bool>(node.getChild("flip/@0")),
                    deserialize<bool>(node.getChild("lock/@0")),
                    deserialize<bool>(node.getChild("glue/@0")), false);
  device->setAttributes(AttributeList(node));
  {
    // Load 3D package model. But after upgrading the project library, assign
    // the default package model if no valid model is set on this device.
    std::optional<Uuid> model =
        deserialize<std::optional<Uuid>>(node.getChild("lib_3d_model/@0"));
    if (mAutoAssignDeviceModels &&
        ((!model) || (!device->getLibPackage().getModels().contains(*model)) ||
         (!device->getLibFootprint().getModels().contains(*model)))) {
      model = device->getDefaultLibModelUuid();
    }
    device->setModel(model);
  }
  foreach (const SExpression* child, node.getChildren("stroke_text")) {
    device->addStrokeText(*new BI_StrokeText(b, BoardStrokeTextData(*child)));
  }
  b.addDeviceInstance(*device);
}

void ProjectLoader::loadBoardNetSegment(Board& b, const SExpression& node) {
  const std::optional<Uuid> netSignalUuid =
      deserialize<std::optional<Uuid>>(node.getChild("net/@0"));
  NetSignal* netSignal = netSignalUuid
      ? b.getProject().getCircuit().getNetSignals().value(*netSignalUuid)
      : nullptr;
  if (netSignalUuid && (!netSignal)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("Inexistent net signal: '%1'").arg(netSignalUuid->toStr()));
  }
  BI_NetSegment* netSegment =
      new BI_NetSegment(b, deserialize<Uuid>(node.getChild("@0")), netSignal);
  b.addNetSegment(*netSegment);

  // Load pads.
  QList<BI_Pad*> pads;
  foreach (const SExpression* child, node.getChildren("pad")) {
    BI_Pad* pad = new BI_Pad(*netSegment, BoardPadData(*child));
    pads.append(pad);
  }

  // Load vias.
  QList<BI_Via*> vias;
  foreach (const SExpression* child, node.getChildren("via")) {
    BI_Via* via = new BI_Via(*netSegment, Via(*child));
    vias.append(via);
  }

  // Load net points.
  QList<BI_NetPoint*> netPoints;
  foreach (const SExpression* child, node.getChildren("junction")) {
    BI_NetPoint* netPoint =
        new BI_NetPoint(*netSegment, deserialize<Uuid>(child->getChild("@0")),
                        Point(child->getChild("position")));
    netPoints.append(netPoint);
  }

  // Load net lines.
  QList<BI_NetLine*> netLines;
  foreach (const SExpression* child, node.getChildren("trace")) {
    auto parseAnchor = [&](const SExpression& aNode) {
      const TraceAnchor anchor(aNode);
      if (const std::optional<Uuid>& obj = anchor.tryGetJunction()) {
        foreach (BI_NetPoint* netPoint, netPoints) {
          if (netPoint->getUuid() == *obj) {
            return static_cast<BI_NetLineAnchor*>(netPoint);
          }
        }
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Net point '%1' does not exist in schematic.")
                .arg(obj->toStr()));
      } else if (const std::optional<Uuid>& obj = anchor.tryGetVia()) {
        foreach (BI_Via* via, vias) {
          if (via->getUuid() == *obj) {
            return static_cast<BI_NetLineAnchor*>(via);
          }
        }
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Via '%1' does not exist in board.").arg(obj->toStr()));
      } else if (const std::optional<TraceAnchor::PadAnchor>& obj =
                     anchor.tryGetFootprintPad()) {
        BI_Device* device = b.getDeviceInstanceByComponentUuid(obj->device);
        if (!device) {
          throw RuntimeError(
              __FILE__, __LINE__,
              QString("Device instance '%1' does not exist in board.")
                  .arg(obj->device.toStr()));
        }
        BI_NetLineAnchor* anchor = device->getPad(obj->pad);
        if (!anchor) {
          throw RuntimeError(
              __FILE__, __LINE__,
              QString("Footprint pad '%1:%2' does not exist in board.")
                  .arg(obj->device.toStr(), obj->pad.toStr()));
        }
        return anchor;
      } else if (const std::optional<Uuid>& obj = anchor.tryGetPad()) {
        foreach (BI_Pad* pad, pads) {
          if (pad->getUuid() == *obj) {
            return static_cast<BI_NetLineAnchor*>(pad);
          }
        }
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Pad '%1' does not exist in board.").arg(obj->toStr()));
      } else {
        throw RuntimeError(
            __FILE__, __LINE__,
            QString("Unknown trace anchor in trace segment '%1'.")
                .arg(netSegment->getUuid().toStr()));
      }
    };
    BI_NetLine* netLine = new BI_NetLine(
        *netSegment, deserialize<Uuid>(child->getChild("@0")),
        *parseAnchor(child->getChild("from")),
        *parseAnchor(child->getChild("to")),
        deserialize<const Layer&>(child->getChild("layer/@0")),
        deserialize<PositiveLength>(child->getChild("width/@0")));
    netLines.append(netLine);
  }

  // Add vias, net points & net lines.
  netSegment->addElements(pads, vias, netPoints, netLines);
}

void ProjectLoader::loadBoardPlane(Board& b, const SExpression& node) {
  const std::optional<Uuid> netSignalUuid =
      deserialize<std::optional<Uuid>>(node.getChild("net/@0"));
  NetSignal* netSignal = netSignalUuid
      ? b.getProject().getCircuit().getNetSignals().value(*netSignalUuid)
      : nullptr;
  if (netSignalUuid && (!netSignal)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("Inexistent net signal: '%1'").arg(netSignalUuid->toStr()));
  }
  BI_Plane* plane =
      new BI_Plane(b, deserialize<Uuid>(node.getChild("@0")),
                   deserialize<const Layer&>(node.getChild("layer/@0")),
                   netSignal, Path(node));
  plane->setMinWidth(
      deserialize<UnsignedLength>(node.getChild("min_width/@0")));
  plane->setMinClearanceToCopper(
      deserialize<UnsignedLength>(node.getChild("min_copper_clearance/@0")));
  plane->setMinClearanceToBoard(
      deserialize<UnsignedLength>(node.getChild("min_board_clearance/@0")));
  plane->setMinClearanceToNpth(
      deserialize<UnsignedLength>(node.getChild("min_npth_clearance/@0")));
  plane->setKeepIslands(deserialize<bool>(node.getChild("keep_islands/@0")));
  plane->setPriority(deserialize<int>(node.getChild("priority/@0")));
  plane->setConnectStyle(
      deserialize<BI_Plane::ConnectStyle>(node.getChild("connect_style/@0")));
  plane->setThermalGap(
      deserialize<PositiveLength>(node.getChild("thermal_gap/@0")));
  plane->setThermalSpokeWidth(
      deserialize<PositiveLength>(node.getChild("thermal_spoke/@0")));
  b.addPlane(*plane);
}

void ProjectLoader::loadBoardUserSettings(Board& b) {
  try {
    const QString fp = "settings.user.lp";
    const std::unique_ptr<const SExpression> root = SExpression::parse(
        b.getDirectory().read(fp), b.getDirectory().getAbsPath(fp));

    // Layers.
    QMap<QString, bool> layersVisibility;
    for (const SExpression* node : root->getChildren("layer")) {
      const QString name = node->getChild("@0").getValue();
      layersVisibility[name] = deserialize<bool>(node->getChild("visible/@0"));
    }
    b.setLayersVisibility(layersVisibility);

    // Planes visibility.
    foreach (const SExpression* node, root->getChildren("plane")) {
      const Uuid uuid = deserialize<Uuid>(node->getChild("@0"));
      if (BI_Plane* plane = b.getPlanes().value(uuid)) {
        plane->setVisible(deserialize<bool>(node->getChild("visible/@0")));
      } else {
        qWarning() << "Plane" << uuid.toStr()
                   << "doesn't exist, could not restore its visibility.";
      }
    }
  } catch (const Exception&) {
    // Project user settings are normally not put under version control and
    // thus the likelyhood of parse errors is higher (e.g. when switching to
    // an older, now incompatible revision). To avoid frustration, we just
    // ignore these errors and load the default settings instead...
    qCritical() << "Could not load board user settings, defaults will be "
                   "used instead.";
  }
}

void ProjectLoader::loadProjectUserSettings(Project& p) {
  try {
    const QString fp = "project/settings.user.lp";
    const std::unique_ptr<const SExpression> root = SExpression::parse(
        p.getDirectory().read(fp), p.getDirectory().getAbsPath(fp));
  } catch (const Exception&) {
    // Project user settings are normally not put under version control and
    // thus the likelyhood of parse errors is higher (e.g. when switching to
    // an older, now incompatible revision). To avoid frustration, we just
    // ignore these errors and load the default settings instead...
    qCritical() << "Could not load project user settings, defaults will be "
                   "used instead.";
  }
}

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

}  // namespace librepcb
