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

#include "../fileio/transactionaldirectory.h"
#include "../fileio/versionfile.h"
#include "../geometry/path.h"
#include "../types/alignment.h"
#include "../types/angle.h"
#include "../types/length.h"
#include "../types/point.h"
#include "../types/simplestring.h"
#include "../types/uuid.h"
#include "sexpression.h"

#include <QtCore>

#include <algorithm>

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

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

FileFormatMigrationV01::FileFormatMigrationV01(QObject* parent) noexcept
  : FileFormatMigration(Version::fromString("0.1"), Version::fromString("1"),
                        parent) {
}

FileFormatMigrationV01::~FileFormatMigrationV01() noexcept {
}

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

void FileFormatMigrationV01::upgradeComponentCategory(
    TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-cmpcat");
}

void FileFormatMigrationV01::upgradePackageCategory(
    TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-pkgcat");
}

void FileFormatMigrationV01::upgradeSymbol(TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-sym");

  // Content File.
  {
    const QString fp = "symbol.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    root->appendChild("generated_by", QString());

    // Various strings.
    upgradeStrings(*root);

    // Layers.
    upgradeLayers(*root);

    // Pins.
    upgradeInversionCharacters(*root, "pin", "name/@0");
    for (SExpression* pinNode : root->getChildren("pin")) {
      const UnsignedLength length =
          deserialize<UnsignedLength>(pinNode->getChild("length/@0"));
      const Point namePos(length + Length(1270000), 0);
      const Alignment nameAlign(HAlign::left(), VAlign::center());
      namePos.serialize(pinNode->appendList("name_position"));
      pinNode->appendChild("name_rotation", Angle::deg0());
      pinNode->appendChild("name_height", PositiveLength(2500000));
      nameAlign.serialize(pinNode->appendList("name_align"));
    }

    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV01::upgradePackage(TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-pkg");

  // Content File.
  {
    const QString fp = "package.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    root->appendChild("generated_by", QString());

    // Various strings.
    upgradeStrings(*root);

    // Layers.
    upgradeLayers(*root);

    // Assembly type.
    root->appendChild("assembly_type", SExpression::createToken("auto"));

    // Footprints.
    for (SExpression* fptNode : root->getChildren("footprint")) {
      // Add 3D model position.
      SExpression& modelPosition = fptNode->appendList("3d_position");
      modelPosition.appendChild(SExpression::createToken("0.0"));
      modelPosition.appendChild(SExpression::createToken("0.0"));
      modelPosition.appendChild(SExpression::createToken("0.0"));

      // Add 3D model rotation.
      SExpression& modelRotation = fptNode->appendList("3d_rotation");
      modelRotation.appendChild(SExpression::createToken("0.0"));
      modelRotation.appendChild(SExpression::createToken("0.0"));
      modelRotation.appendChild(SExpression::createToken("0.0"));

      // Pads.
      for (SExpression* padNode : fptNode->getChildren("pad")) {
        // In the file format 0.1, footprint pads did not have their own UUID,
        // but only the UUID of the package pad they were connected to. To get
        // a deterministic UUID when upgrading a v0.1 footprint pad to v1,
        // we simply use the package pad UUID as the footprint pad UUID too.
        // See https://github.com/LibrePCB/LibrePCB/issues/445
        const Uuid uuid = deserialize<Uuid>(padNode->getChild("@0"));
        padNode->appendChild("package_pad", uuid);

        // Convert shape & corner radius.
        SExpression& padShape = padNode->getChild("shape/@0");
        const bool isRoundShape = (padShape.getValue() == "round");
        const bool isRectShape = (padShape.getValue() == "rect");
        padNode->appendChild(
            "radius", SExpression::createToken(isRoundShape ? "1.0" : "0.0"));
        if (isRoundShape || isRectShape) {
          std::unique_ptr<const SExpression> sexpr =
              SExpression::createToken("roundrect");
          padShape = *sexpr;
        }

        // Convert holes.
        // Note: In the Gerber export, drills on SMT pads were ignored thus
        // we delete such drills now to keep the same behavior.
        // To get a deterministic UUID, the pad's UUID is reused for the hole.
        SExpression& boardSideNode = padNode->getChild("side/@0");
        const UnsignedLength drill =
            deserialize<UnsignedLength>(padNode->getChild("drill/@0"));
        if ((boardSideNode.getValue() == "tht") && (drill > 0)) {
          SExpression& holeNode = padNode->appendList("hole");
          holeNode.appendChild(uuid);
          holeNode.appendChild("diameter", drill);
          SExpression& vertexNode = holeNode.appendList("vertex");
          SExpression& positionNode = vertexNode.appendList("position");
          positionNode.appendChild(Length(0));  // X
          positionNode.appendChild(Length(0));  // Y
          vertexNode.appendChild("angle", Angle::deg0());
        }
        if (boardSideNode.getValue() == "tht") {
          // THT is no longer a valid value. Since footprints are always drawn
          // from the top view, it should be safe to set it to "top" now.
          std::unique_ptr<const SExpression> sexpr =
              SExpression::createToken("top");
          boardSideNode = *sexpr;
        }

        // Add mask configs.
        padNode->appendChild("stop_mask", SExpression::createToken("auto"));
        padNode->appendChild(
            "solder_paste",
            SExpression::createToken((drill > 0) ? "off" : "auto"));

        // Add function.
        padNode->appendChild("function",
                             SExpression::createToken("unspecified"));

        // Add copper clearance.
        padNode->appendChild("clearance", SExpression::createToken("0.0"));
      }

      // Polygons.
      for (SExpression* polygonNode : fptNode->getChildren("polygon")) {
        if (polygonNode->getChild("layer/@0")
                .getValue()
                .endsWith("_courtyard")) {
          SExpression& widthNode = polygonNode->getChild("width/@0");
          widthNode.setValue("0.0");
        }
      }

      // Circles.
      for (SExpression* circleNode : fptNode->getChildren("circle")) {
        if (circleNode->getChild("layer/@0")
                .getValue()
                .endsWith("_courtyard")) {
          SExpression& widthNode = circleNode->getChild("width/@0");
          widthNode.setValue("0.0");
        }
      }

      // Stroke texts.
      for (SExpression* txtNode : fptNode->getChildren("stroke_text")) {
        if (deserialize<bool>(txtNode->getChild("mirror/@0"))) {
          SExpression& rotNode = txtNode->getChild("rotation/@0");
          const std::unique_ptr<const SExpression> sexpr =
              serialize(-deserialize<Angle>(rotNode));
          rotNode = *sexpr;
        }
      }

      // Holes.
      upgradeHoles(*fptNode, false);

      // Move cutouts from the board outlines layer to the new cutouts layer.
      upgradeCutouts(*fptNode, nullptr);
    }

    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV01::upgradeComponent(TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-cmp");

  // Content File.
  {
    const QString fp = "component.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    root->appendChild("generated_by", QString());

    // Signals.
    upgradeInversionCharacters(*root, "signal", "name/@0");

    // Various strings.
    upgradeStrings(*root);

    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV01::upgradeDevice(TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-dev");

  // Content File.
  {
    const QString fp = "device.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    root->appendChild("generated_by", QString());

    // Various strings.
    upgradeStrings(*root);

    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV01::upgradeOrganization(TransactionalDirectory& dir) {
  Q_UNUSED(dir);
  // Didn't exist yet.
}

void FileFormatMigrationV01::upgradeLibrary(TransactionalDirectory& dir) {
  // Version File.
  upgradeVersionFile(dir, ".librepcb-lib");

  // Content File.
  {
    const QString fp = "library.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    root->appendChild("manufacturer", QString());
    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV01::upgradeProject(TransactionalDirectory& dir,
                                            QList<Message>& messages) {
  // ATTENTION: Do not actually perform any upgrade in this method! Instead,
  // just call virtual protected methods which do the upgrade. This allows
  // FileFormatMigrationUnstable to override them with partial upgrades.

  ProjectContext context;

  // Version File.
  upgradeVersionFile(dir, ".librepcb-project");

  // Symbols.
  foreach (const QString& dirName, dir.getDirs("library/sym")) {
    TransactionalDirectory subDir(dir, "library/sym/" % dirName);
    if (subDir.fileExists(".librepcb-sym")) {
      const QString fp = "symbol.lp";
      std::unique_ptr<SExpression> root =
          SExpression::parse(subDir.read(fp), subDir.getAbsPath(fp));
      const Uuid uuid = deserialize<Uuid>(root->getChild("@0"));
      Symbol sym;

      // Texts.
      for (SExpression* textNode : root->getChildren("text")) {
        sym.texts.append(Text{
            deserialize<Uuid>(textNode->getChild("@0")),
            textNode->getChild("layer/@0").getValue(),
            textNode->getChild("value/@0").getValue(),
            Point(textNode->getChild("position")),
            deserialize<Angle>(textNode->getChild("rotation/@0")),
            deserialize<PositiveLength>(textNode->getChild("height/@0")),
            Alignment(textNode->getChild("align")),
        });
      }

      context.symbols.insert(uuid, sym);

      upgradeSymbol(subDir);
    }
  }

  // Packages.
  foreach (const QString& dirName, dir.getDirs("library/pkg")) {
    TransactionalDirectory subDir(dir, "library/pkg/" % dirName);
    if (subDir.fileExists(".librepcb-pkg")) {
      const QString fp = "package.lp";
      std::unique_ptr<SExpression> root =
          SExpression::parse(subDir.read(fp), subDir.getAbsPath(fp));

      // Footprints.
      for (SExpression* fptNode : root->getChildren("footprint")) {
        context.holesCount += fptNode->getChildren("hole").count();
        foreach (
            const SExpression* geometryNode,
            fptNode->getChildren("polygon") + fptNode->getChildren("circle")) {
          if (geometryNode->getChild("layer/@0").getValue() == "brd_outlines") {
            ++context.footprintBoardOutlinesObjectCount;
          }
        }
      }

      upgradePackage(subDir);
    }
  }

  // Components.
  foreach (const QString& dirName, dir.getDirs("library/cmp")) {
    TransactionalDirectory subDir(dir, "library/cmp/" % dirName);
    if (subDir.fileExists(".librepcb-cmp")) {
      const QString fp = "component.lp";
      std::unique_ptr<SExpression> root =
          SExpression::parse(subDir.read(fp), subDir.getAbsPath(fp));
      const Uuid uuid = deserialize<Uuid>(root->getChild("@0"));
      Component cmp;
      cmp.schematicOnly =
          deserialize<bool>(root->getChild("schematic_only/@0"));

      // Symbol variants.
      for (SExpression* varNode : root->getChildren("variant")) {
        ComponentSymbolVariant symbVar{
            deserialize<Uuid>(varNode->getChild("@0")),
            {},
        };

        // Gates.
        for (SExpression* gateNode : varNode->getChildren("gate")) {
          symbVar.gates.append(Gate{
              deserialize<Uuid>(gateNode->getChild("@0")),
              deserialize<Uuid>(gateNode->getChild("symbol/@0")),
          });
        }

        cmp.symbolVariants.append(symbVar);
      }

      context.components.insert(uuid, cmp);

      upgradeComponent(subDir);
    }
  }

  // Devices.
  foreach (const QString& dirName, dir.getDirs("library/dev")) {
    TransactionalDirectory subDir(dir, "library/dev/" % dirName);
    if (subDir.fileExists(".librepcb-dev")) {
      upgradeDevice(subDir);
    }
  }

  // Get schematics list.
  // This is important to upgrade only the used schematics. If there are unused
  // schematic files left over in the project, they could cause the upgrade to
  // fail. It's better to just ignore the unused files (if any).
  QStringList schematicFiles;  // Relative file paths
  {
    const QString fp = "schematics/schematics.lp";
    const std::unique_ptr<const SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    foreach (const SExpression* child, root->getChildren("schematic")) {
      schematicFiles.append(child->getChild("@0").getValue());
    }
  }

  // Get boards list.
  // This is important to upgrade only the used boards. If there are unused
  // board files left over in the project, they could cause the upgrade to
  // fail. It's better to just ignore the unused files (if any).
  QStringList boardFiles;  // Relative file paths
  {
    const QString fp = "boards/boards.lp";
    const std::unique_ptr<const SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    foreach (const SExpression* child, root->getChildren("board")) {
      boardFiles.append(child->getChild("@0").getValue());
    }
  }

  // Scan boards.
  foreach (const QString& fp, boardFiles) {
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    foreach (const SExpression* devNode, root->getChildren("device")) {
      const Uuid cmpUuid = deserialize<Uuid>(devNode->getChild("@0"));
      const Uuid libDevUuid =
          deserialize<Uuid>(devNode->getChild("lib_device/@0"));
      context.devicesUsedInBoards[cmpUuid].insert(libDevUuid);
    }
  }

  // Output jobs.
  createOutputJobs(dir);

  // Metadata.
  {
    const QString fp = "project/metadata.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    context.projectUuid = root->getChild("@0").getValue();
    upgradeMetadata(*root);
    dir.write(fp, root->toByteArray());
  }

  // Settings.
  {
    const QString fp = "project/settings.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeSettings(*root);
    dir.write(fp, root->toByteArray());
  }

  // Circuit.
  {
    const QString fp = "circuit/circuit.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeCircuit(*root, context);
    dir.write(fp, root->toByteArray());

    // Component instances.
    for (SExpression* cmpNode : root->getChildren("component")) {
      const Uuid uuid = deserialize<Uuid>(cmpNode->getChild("@0"));
      ComponentInstance cmpInst{
          deserialize<Uuid>(cmpNode->getChild("lib_component/@0")),
          deserialize<Uuid>(cmpNode->getChild("lib_variant/@0")),
      };
      context.componentInstances.insert(uuid, cmpInst);
    }
  }

  // ERC.
  {
    const QString fp = "circuit/erc.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeErc(*root, context);
    dir.write(fp, root->toByteArray());
  }

  // Schematics.
  foreach (const QString& fp, schematicFiles) {
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeSchematic(*root, context);
    dir.write(fp, root->toByteArray());
  }

  // Boards.
  foreach (QString fp, boardFiles) {
    // Board content.
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeBoard(*root, context);
    dir.write(fp, root->toByteArray());

    // User settings.
    fp.replace("/board.lp", "/settings.user.lp");
    if (dir.fileExists(fp)) {
      std::unique_ptr<SExpression> root =
          SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
      upgradeBoardUserSettings(*root);
      dir.write(fp, root->toByteArray());
    }
  }

  // Emit messages at the very end to avoid duplicate messages caused my
  // multiple schematics/boards.
  if (context.componentsWithAssemblyOptions > 0) {
    messages.append(buildMessage(
        Message::Severity::Note,
        tr("Components were automatically populated with assembly information "
           "required for the new, built-in MPN management and assembly variant "
           "mechanism. If the BOM or PnP export is used, please review the "
           "output and correct MPNs and attributes manually in the component "
           "properties dialog where needed."),
        context.componentsWithAssemblyOptions));
  }
  if (context.removedErcApprovals > 0) {
    messages.append(buildMessage(
        Message::Severity::Note,
        tr("Some particular ERC message approvals cannot be migrated and "
           "therefore have been removed. Please check the remaining ERC "
           "messages and approve them if desired."),
        context.removedErcApprovals));
  }
  if (context.holesCount > 0) {
    messages.append(buildMessage(
        Message::Severity::Note,
        tr("All non-plated holes (NPTH) now have automatic solder resist "
           "openings added on both board sides. The expansion value is "
           "configured in the board design rules but can be overridden in the "
           "hole properties dialog."),
        context.holesCount));
  }
  if (context.nonRoundViaCount > 0) {
    messages.append(
        buildMessage(Message::Severity::Warning,
                     tr("Non-circular via shapes are no longer supported, "
                        "all vias were changed to circular now."),
                     context.nonRoundViaCount));
  }
  if (context.planeCount > 0) {
    messages.append(buildMessage(
        Message::Severity::Note,
        tr("Plane area calculations have been adjusted, manual review and "
           "running the DRC is recommended."),
        context.planeCount));
  }
  if (context.planeConnectNoneCount > 0) {
    messages.append(buildMessage(
        Message::Severity::Warning,
        tr("Vias within planes with connect style 'None' are now fully "
           "connected to the planes since the connect style is no longer "
           "respected for vias. You might want to remove traces now which are "
           "no longer needed to connect these vias."),
        context.planeConnectNoneCount));
  }
  if ((context.footprintBoardOutlinesObjectCount > 0) ||
      (context.topLevelBoardOutlinesObjectCount > 1)) {
    messages.append(buildMessage(
        Message::Severity::Warning,
        tr("Board cutouts now have a dedicated layer, thus nested board "
           "outline polygons and circles have automatically been moved to the "
           "cutouts layer. As the auto-detection is not perfect, please "
           "check if each cutout has been converted correctly. The easiest way "
           "is to review the PCB in the 3D viewer."),
        context.footprintBoardOutlinesObjectCount +
            context.topLevelBoardOutlinesObjectCount));
  }
}

void FileFormatMigrationV01::upgradeWorkspaceData(TransactionalDirectory& dir) {
  // Create version file.
  dir.write(".librepcb-data", VersionFile(mToVersion).toByteArray());

  // Remove legacy files.
  const QStringList filesToRemove = {
      "cache",
      "cache_v1",
      "cache_v2",
      "library_cache",
  };
  TransactionalDirectory librariesDir(dir, "libraries");
  foreach (const QString fileName, librariesDir.getFiles()) {
    if (filesToRemove.contains(fileName.split(".").first())) {
      qInfo() << "Removing legacy file:"
              << librariesDir.getAbsPath(fileName).toNative();
      librariesDir.removeFile(fileName);
    }
  }

  // Upgrade settings.
  const QString settingsFp = "settings.lp";
  if (dir.fileExists(settingsFp)) {
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(settingsFp), dir.getAbsPath(settingsFp));
    if (SExpression* node = root->tryGetChild("repositories")) {
      foreach (SExpression* child, node->getChildren("repository")) {
        child->setName("url");
      }
      node->setName("api_endpoints");
    }
    {
      const std::unique_ptr<const SExpression> search =
          SExpression::createToken("board_placement_top");
      const std::unique_ptr<const SExpression> replace =
          SExpression::createToken("board_legend_top");
      root->replaceRecursive(*search, *replace);
    }
    {
      const std::unique_ptr<const SExpression> search =
          SExpression::createToken("board_placement_bottom");
      const std::unique_ptr<const SExpression> replace =
          SExpression::createToken("board_legend_bottom");
      root->replaceRecursive(*search, *replace);
    }
    dir.write(settingsFp, root->toByteArray());
  }
}

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

void FileFormatMigrationV01::createOutputJobs(TransactionalDirectory& dir) {
  const std::unique_ptr<const SExpression> root =
      SExpression::createList("librepcb_jobs");
  dir.write("project/jobs.lp", root->toByteArray());
}

void FileFormatMigrationV01::upgradeMetadata(SExpression& root) {
  SExpression& versionNode = root.getChild("version/@0");
  versionNode.setValue(toFileProofName(versionNode.getValue(), "latest"));
}

void FileFormatMigrationV01::upgradeSettings(SExpression& root) {
  upgradeStrings(root);
  root.appendList("custom_bom_attributes");
  root.appendChild("output_directory", QString("./output/{{VERSION}}/"));
  root.appendChild("default_lock_component_assembly",
                   SExpression::createToken("false"));
}

void FileFormatMigrationV01::upgradeCircuit(SExpression& root,
                                            ProjectContext& context) {
  upgradeStrings(root);

  // Add default assembly variant. Use the project's UUID as assembly variant
  // UUID to make the migration deterministic.
  {
    SExpression& node = root.appendList("variant");
    node.appendChild(SExpression::createToken(context.projectUuid));
    node.appendChild("name", SExpression::createString("Std"));
    node.appendChild("description",
                     SExpression::createString("Standard assembly"));
  }

  // Add assembly options & parts to components.
  foreach (SExpression* cmpNode, root.getChildren("component")) {
    const Uuid cmpUuid = deserialize<Uuid>(cmpNode->getChild("@0"));
    const Uuid libCmpUuid =
        deserialize<Uuid>(cmpNode->getChild("lib_component/@0"));
    const bool isLogo =
        (libCmpUuid.toStr() == "b91cf23a-4f07-4b99-8f52-0b42304aef20");
    const bool addToAssemblyVariant =
        (!context.components.value(libCmpUuid).schematicOnly) && (!isLogo);
    const SExpression& libDevNode = cmpNode->getChild("lib_device");
    QSet<Uuid> libDeviceUuids = context.devicesUsedInBoards.value(cmpUuid);
    if (auto u = deserialize<std::optional<Uuid>>(libDevNode.getChild("@0"))) {
      libDeviceUuids.insert(*u);
    }

    if (!libDeviceUuids.isEmpty()) {
      QString mpn, manufacturer;
      QVector<SExpression*> consumedAttributes;
      const QList<SExpression*> attributes = cmpNode->getChildren("attribute");
      for (int i = attributes.count() - 1; i >= 0; --i) {
        const QString key = attributes.at(i)->getChild("@0").getValue();
        if (key == "MPN") {
          mpn = *cleanSimpleString(
              attributes.at(i)->getChild("value/@0").getValue());
          consumedAttributes.append(attributes.at(i));
        } else if (key == "MANUFACTURER") {
          manufacturer = *cleanSimpleString(
              attributes.at(i)->getChild("value/@0").getValue());
          consumedAttributes.append(attributes.at(i));
        }
      }
      foreach (SExpression* attrNode, consumedAttributes) {
        cmpNode->removeChild(*attrNode);
      }

      foreach (const Uuid& devUuid, libDeviceUuids) {
        SExpression& devNode = cmpNode->appendList("device");
        devNode.appendChild(SExpression::createToken(devUuid.toStr()));
        if ((!mpn.isEmpty()) || (!manufacturer.isEmpty())) {
          SExpression& partNode = devNode.appendList("part");
          partNode.appendChild(mpn);
          partNode.appendChild("manufacturer", manufacturer);
        }
        if (addToAssemblyVariant) {
          devNode.appendChild("variant",
                              SExpression::createToken(context.projectUuid));
        }
      }
      ++context.componentsWithAssemblyOptions;
    }

    cmpNode->removeChild(libDevNode);
    cmpNode->appendChild("lock_assembly", SExpression::createToken("false"));
  }
}

void FileFormatMigrationV01::upgradeErc(SExpression& root,
                                        ProjectContext& context) {
  std::unique_ptr<SExpression> newRoot =
      SExpression::createList(root.getName());
  for (const SExpression* node : root.getChildren("approved")) {
    const QString msgClass = node->getChild("class/@0").getValue();
    const QString instance = node->getChild("instance/@0").getValue();
    const QString message = node->getChild("message/@0").getValue();
    if ((msgClass == "NetClass") && (message == "Unused")) {
      SExpression& child = newRoot->appendList("approved");
      child.appendChild(SExpression::createToken("unused_netclass"));
      child.appendChild("netclass", SExpression::createToken(instance));
    } else if ((msgClass == "NetSignal") && (message == "Unused")) {
      SExpression& child = newRoot->appendList("approved");
      child.appendChild(SExpression::createToken("open_net"));
      child.appendChild("net", SExpression::createToken(instance));
    } else if ((msgClass == "NetSignal") &&
               (message == "ConnectedToLessThanTwoPins")) {
      SExpression& child = newRoot->appendList("approved");
      child.appendChild(SExpression::createToken("open_net"));
      child.appendChild("net", SExpression::createToken(instance));
    } else if (message == "UnconnectedRequiredSignal") {
      SExpression& child = newRoot->appendList("approved");
      child.appendChild(
          SExpression::createToken("unconnected_required_signal"));
      child.ensureLineBreak();
      child.appendChild("component",
                        SExpression::createToken(instance.split("/").first()));
      child.ensureLineBreak();
      child.appendChild("signal",
                        SExpression::createToken(instance.split("/").last()));
      child.ensureLineBreak();
    } else if (message == "ForcedNetSignalNameConflict") {
      SExpression& child = newRoot->appendList("approved");
      child.appendChild(
          SExpression::createToken("unconnected_required_signal"));
      child.ensureLineBreak();
      child.appendChild("component",
                        SExpression::createToken(instance.split("/").first()));
      child.ensureLineBreak();
      child.appendChild("signal",
                        SExpression::createToken(instance.split("/").last()));
      child.ensureLineBreak();
    } else {
      ++context.removedErcApprovals;
    }
  }
  root = *newRoot;
}

void FileFormatMigrationV01::upgradeSchematic(SExpression& root,
                                              ProjectContext& context) {
  upgradeStrings(root);
  upgradeGrid(root);
  upgradeLayers(root);

  const QString contactUs =
      " If the project could be opened with older releases, this might be a "
      "bug - please contact us then.";

  // Symbols.
  for (SExpression* symNode : root.getChildren("symbol")) {
    const Uuid cmpUuid = deserialize<Uuid>(symNode->getChild("component/@0"));
    const Uuid gateUuid = deserialize<Uuid>(symNode->getChild("lib_gate/@0"));
    auto cmpInstIt = context.componentInstances.find(cmpUuid);
    if (cmpInstIt == context.componentInstances.end()) {
      throw RuntimeError(__FILE__, __LINE__,
                         QString("Failed to find component instance '%1'.")
                                 .arg(cmpUuid.toStr()) +
                             contactUs);
    }
    auto libCmpIt = context.components.find(cmpInstIt->libCmpUuid);
    if (libCmpIt == context.components.end()) {
      throw RuntimeError(__FILE__, __LINE__,
                         QString("Failed to find component '%1'. This looks "
                                 "like a bug, please contact us.")
                                 .arg(cmpInstIt->libCmpUuid.toStr()) +
                             contactUs);
    }
    const ComponentSymbolVariant* cmpSymbVar = nullptr;
    foreach (const auto& var, libCmpIt->symbolVariants) {
      if (var.uuid == cmpInstIt->libSymbVarUuid) {
        cmpSymbVar = &var;
        break;
      }
    }
    if (!cmpSymbVar) {
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("Failed to find component symbol variant '%1'.")
                  .arg(cmpInstIt->libSymbVarUuid.toStr()) +
              contactUs);
    }
    const Gate* gate = nullptr;
    foreach (const auto& g, cmpSymbVar->gates) {
      if (g.uuid == gateUuid) {
        gate = &g;
        break;
      }
    }
    if (!gate) {
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("Failed to find gate '%1'.").arg(gateUuid.toStr()) +
              contactUs);
    }
    auto symIt = context.symbols.find(gate->symbolUuid);
    if (symIt == context.symbols.end()) {
      throw RuntimeError(
          __FILE__, __LINE__,
          QString("Failed to find symbol '%1'.").arg(gate->symbolUuid.toStr()) +
              contactUs);
    }
    const Point symPos(symNode->getChild("position"));
    const Angle symRot = deserialize<Angle>(symNode->getChild("rotation/@0"));
    const bool symMirror = deserialize<bool>(symNode->getChild("mirror/@0"));
    foreach (const Text& text, symIt->texts) {
      Point position = text.position.rotated(symRot);
      if (symMirror) {
        position.mirror(Qt::Horizontal);
      }
      position += symPos;
      Angle rotation = symMirror ? (Angle::deg180() - symRot - text.rotation)
                                 : (symRot + text.rotation);
      Alignment align = text.align;
      if (symMirror) {
        align.mirrorV();
      }

      SExpression& textNode = symNode->appendList("text");
      textNode.appendChild(text.uuid);
      textNode.appendChild("layer", text.layerName);
      textNode.appendChild("value", text.text);
      align.serialize(textNode.appendList("align"));
      textNode.appendChild("height", text.height);
      position.serialize(textNode.appendList("position"));
      textNode.appendChild("rotation", rotation);
    }

    // Swap transformation order of mirror/rotate.
    if (symMirror) {
      SExpression& rotNode = symNode->getChild("rotation/@0");
      const std::unique_ptr<const SExpression> sexpr =
          serialize(-deserialize<Angle>(rotNode));
      rotNode = *sexpr;
    }
  }

  // Net segments.
  for (SExpression* segNode : root.getChildren("netsegment")) {
    // Net labels.
    for (SExpression* lblNode : segNode->getChildren("label")) {
      lblNode->appendChild("mirror", false);
    }
  }
}

void FileFormatMigrationV01::upgradeBoard(SExpression& root,
                                          ProjectContext& context) {
  upgradeStrings(root);
  upgradeGrid(root);
  upgradeBoardDesignRules(root);
  upgradeBoardDrcSettings(root);
  upgradeLayers(root);
  upgradeCutouts(root, &context);

  // Board setup.
  root.appendChild("thickness", SExpression::createToken("1.6"));
  root.appendChild("solder_resist", SExpression::createToken("green"));
  root.appendChild("silkscreen", SExpression::createToken("white"));

  // Fabrication output settings.
  {
    SExpression& node = root.getChild("fabrication_output_settings");
    SExpression& drillNode = node.getChild("drills");
    drillNode.appendChild("g85_slots", false);
    if (drillNode.getChild("suffix_merged/@0").getValue().contains("DRILLS")) {
      // Default suffixes.
      drillNode.appendChild(
          "suffix_buried",
          QString("_DRILLS-PLATED-{{START_LAYER}}-{{END_LAYER}}.drl"));
    } else {
      // Protel suffixes.
      drillNode.appendChild("suffix_buried",
                            QString("_L{{START_NUMBER}}-L{{END_NUMBER}}.drl"));
    }

    SExpression& silkTop = node.getChild("silkscreen_top");
    SExpression& silkLayersTop = silkTop.getChild("layers");
    root.appendChild(SExpression(silkLayersTop))
        .setName("silkscreen_layers_top");
    silkTop.removeChild(silkLayersTop);

    SExpression& silkBot = node.getChild("silkscreen_bot");
    SExpression& silkLayersBot = silkBot.getChild("layers");
    root.appendChild(SExpression(silkLayersBot))
        .setName("silkscreen_layers_bot");
    silkBot.removeChild(silkLayersBot);
  }

  // Devices.
  for (SExpression* devNode : root.getChildren("device")) {
    if (deserialize<bool>(devNode->getChild("mirror/@0"))) {
      SExpression& rotNode = devNode->getChild("rotation/@0");
      const std::unique_ptr<const SExpression> sexpr =
          serialize(-deserialize<Angle>(rotNode));
      rotNode = *sexpr;
    }
    devNode->appendChild("lock", SExpression::createToken("false"));
    for (SExpression* txtNode : devNode->getChildren("stroke_text")) {
      if (deserialize<bool>(txtNode->getChild("mirror/@0"))) {
        SExpression& rotNode = txtNode->getChild("rotation/@0");
        const std::unique_ptr<const SExpression> sexpr =
            serialize(-deserialize<Angle>(rotNode));
        rotNode = *sexpr;
      }
      txtNode->appendChild("lock", SExpression::createToken("false"));
    }
    devNode->getChild("mirror").setName("flip");
    devNode->appendChild("lib_3d_model", SExpression::createToken("none"));
  }

  // Net segments.
  const UnsignedLength stopMaskMaxViaDiameter = deserialize<UnsignedLength>(
      root.getChild("design_rules/stopmask_max_via_drill_diameter/@0"));
  for (SExpression* segNode : root.getChildren("netsegment")) {
    // Vias.
    for (SExpression* viaNode : segNode->getChildren("via")) {
      SExpression& shapeNode = viaNode->getChild("shape");
      if (shapeNode.getChild("@0").getValue() != "round") {
        ++context.nonRoundViaCount;
      }
      viaNode->removeChild(shapeNode);
      viaNode->appendChild("from", SExpression::createToken("top_cu"));
      viaNode->appendChild("to", SExpression::createToken("bot_cu"));
      const PositiveLength drill =
          deserialize<PositiveLength>(viaNode->getChild("drill/@0"));
      if (drill > stopMaskMaxViaDiameter) {
        viaNode->appendChild("exposure", SExpression::createToken("auto"));
      } else {
        viaNode->appendChild("exposure", SExpression::createToken("off"));
      }
    }
  }

  // Polygons.
  for (SExpression* polyNode : root.getChildren("polygon")) {
    polyNode->appendChild("lock", SExpression::createToken("false"));
  }

  // Stroke texts.
  for (SExpression* txtNode : root.getChildren("stroke_text")) {
    if (deserialize<bool>(txtNode->getChild("mirror/@0"))) {
      SExpression& rotNode = txtNode->getChild("rotation/@0");
      const std::unique_ptr<const SExpression> sexpr =
          serialize(-deserialize<Angle>(rotNode));
      rotNode = *sexpr;
    }
    txtNode->appendChild("lock", SExpression::createToken("false"));
  }

  // Holes.
  context.holesCount += root.getChildren("hole").count();
  upgradeHoles(root, true);

  // Planes.
  for (SExpression* planeNode : root.getChildren("plane")) {
    Q_UNUSED(planeNode);
    ++context.planeCount;
    if (planeNode->getChild("connect_style/@0").getValue() == "none") {
      ++context.planeConnectNoneCount;
    }
    planeNode->appendChild("thermal_gap",
                           planeNode->getChild("min_clearance/@0"));
    planeNode->appendChild("thermal_spoke",
                           planeNode->getChild("min_width/@0"));
    planeNode->appendChild("lock", SExpression::createToken("false"));
    planeNode->getChild("keep_orphans").setName("keep_islands");
  }
}

void FileFormatMigrationV01::upgradeBoardUserSettings(SExpression& root) {
  upgradeLayers(root);

  // Layer colors.
  foreach (SExpression* node, root.getChildren("layer")) {
    for (const auto tagName : {"color", "color_hl"}) {
      if (SExpression* child = node->tryGetChild(tagName)) {
        node->removeChild(*child);
      }
    }
  }
}

void FileFormatMigrationV01::upgradeBoardDesignRules(SExpression& root) {
  SExpression& node = root.getChild("design_rules");
  node.removeChild(node.getChild("name"));
  node.removeChild(node.getChild("description"));
  for (SExpression* child : node.getChildren(SExpression::Type::List)) {
    QString name = child->getName();
    name.replace("restring_pad_", "pad_annular_ring_");
    name.replace("restring_via_", "via_annular_ring_");
    name.replace("creammask_", "solderpaste_");
    child->setName(name);
  }
  for (const QString param : {"stopmask_clearance", "solderpaste_clearance",
                              "pad_annular_ring", "via_annular_ring"}) {
    SExpression& newChild = node.appendList(param);
    for (const QString property : {"ratio", "min", "max"}) {
      SExpression& oldChild = node.getChild(param % "_" % property);
      newChild.appendChild(property, oldChild.getChild("@0"));
      node.removeChild(oldChild);
    }
  }
  {
    SExpression& child = node.getChild("pad_annular_ring");
    child.appendChild("outer", SExpression::createToken("full"));
    child.appendChild("inner", SExpression::createToken("full"));
  }
}

void FileFormatMigrationV01::upgradeBoardDrcSettings(SExpression& root) {
  SExpression& node = root.appendList("design_rule_check");
  node.appendChild("min_copper_copper_clearance",
                   SExpression::createToken("0.2"));
  node.appendChild("min_copper_board_clearance",
                   SExpression::createToken("0.3"));
  node.appendChild("min_copper_npth_clearance",
                   SExpression::createToken("0.25"));
  node.appendChild("min_drill_drill_clearance",
                   SExpression::createToken("0.35"));
  node.appendChild("min_drill_board_clearance",
                   SExpression::createToken("0.5"));
  node.appendChild("min_silkscreen_stopmask_clearance",
                   SExpression::createToken("0.127"));
  node.appendChild("min_copper_width", SExpression::createToken("0.2"));
  node.appendChild("min_annular_ring", SExpression::createToken("0.2"));
  node.appendChild("min_npth_drill_diameter", SExpression::createToken("0.3"));
  node.appendChild("min_pth_drill_diameter", SExpression::createToken("0.3"));
  node.appendChild("min_npth_slot_width", SExpression::createToken("1.0"));
  node.appendChild("min_pth_slot_width", SExpression::createToken("0.7"));
  node.appendChild("min_silkscreen_width", SExpression::createToken("0.15"));
  node.appendChild("min_silkscreen_text_height",
                   SExpression::createToken("0.8"));
  node.appendChild("min_outline_tool_diameter",
                   SExpression::createToken("2.0"));
  node.appendChild("blind_vias_allowed", SExpression::createToken("false"));
  node.appendChild("buried_vias_allowed", SExpression::createToken("false"));
  node.appendChild("allowed_npth_slots",
                   SExpression::createToken("single_segment_straight"));
  node.appendChild("allowed_pth_slots",
                   SExpression::createToken("single_segment_straight"));
  node.appendChild("approvals_version", SExpression::createToken("0.2"));
}

void FileFormatMigrationV01::upgradeGrid(SExpression& node) {
  SExpression& gridNode = node.getChild("grid");
  gridNode.removeChild(gridNode.getChild("type"));
}

void FileFormatMigrationV01::upgradeCutouts(SExpression& node,
                                            ProjectContext* context) {
  // Collect all outline objects.
  struct OutlineObject {
    SExpression* node;
    Path outline;
    qreal lengthMm;
  };
  QVector<OutlineObject> outlineObjects;
  foreach (SExpression* child, node.getChildren("polygon")) {
    if (child->getChild("layer/@0").getValue() == "brd_outlines") {
      OutlineObject obj{child, Path(*child), 0};
      obj.lengthMm = obj.outline.getTotalStraightLength()->toMm();
      outlineObjects.append(obj);
    }
  }
  foreach (SExpression* child, node.getChildren("circle")) {
    if (child->getChild("layer/@0").getValue() == "brd_outlines") {
      const Point position(*child);
      const PositiveLength diameter =
          deserialize<PositiveLength>(child->getChild("diameter/@0"));
      OutlineObject obj{child, Path::circle(diameter).translated(position),
                        diameter->toMm() * M_PI};
      outlineObjects.append(obj);
    }
  }

  // Sort by outline length ascending.
  std::sort(outlineObjects.begin(), outlineObjects.end(),
            [](const OutlineObject& a, const OutlineObject& b) {
              return a.lengthMm < b.lengthMm;
            });

  // Discard the outline which is considered as the outer most board outline.
  if (!outlineObjects.isEmpty()) {
    if (context) {
      // In boards, the longest outline is considered as the board outlines.
      outlineObjects.removeLast();
      context->topLevelBoardOutlinesObjectCount += outlineObjects.count();
    } else {
      // In footprints, the longest outline is only considered as the board
      // outlines if there is any pad located *within* the outline.
      foreach (const SExpression* padNode, node.getChildren("pad")) {
        const Point padPosition(padNode->getChild("position"));
        if (outlineObjects.last().outline.toQPainterPathPx().contains(
                padPosition.toPxQPointF())) {
          outlineObjects.removeLast();
          break;
        }
      }
    }
  }

  // Move all remaining outlines to the new cutouts layer.
  foreach (const OutlineObject& obj, outlineObjects) {
    obj.node->getChild("layer/@0").setValue("brd_cutouts");
  }
}

void FileFormatMigrationV01::upgradeHoles(SExpression& node, bool isBoardHole) {
  for (SExpression* holeNode : node.getChildren("hole")) {
    holeNode->appendChild("stop_mask", SExpression::createToken("auto"));
    const Point pos(holeNode->getChild("position"));
    SExpression& vertexNode = holeNode->appendList("vertex");
    pos.serialize(vertexNode.appendList("position"));
    vertexNode.appendChild("angle", Angle::deg0());
    if (isBoardHole) {
      holeNode->appendChild("lock", SExpression::createToken("false"));
    }
  }
}

void FileFormatMigrationV01::upgradeLayers(SExpression& node) {
  // Rename "sch_scheet_frames" to "sch_frames".
  {
    const std::unique_ptr<const SExpression> search =
        SExpression::createToken("sch_scheet_frames");
    const std::unique_ptr<const SExpression> replace =
        SExpression::createToken("sch_frames");
    node.replaceRecursive(*search, *replace);
  }

  // Rename "brd_sheet_frames" to "brd_frames".
  {
    const std::unique_ptr<const SExpression> search =
        SExpression::createToken("brd_sheet_frames");
    const std::unique_ptr<const SExpression> replace =
        SExpression::createToken("brd_frames");
    node.replaceRecursive(*search, *replace);
  }

  // Rename "brd_milling_pth" to "brd_plated_cutouts".
  {
    const std::unique_ptr<const SExpression> search =
        SExpression::createToken("brd_milling_pth");
    const std::unique_ptr<const SExpression> replace =
        SExpression::createToken("brd_plated_cutouts");
    node.replaceRecursive(*search, *replace);
  }

  // Remove nodes on never officially existing layer "brd_keepout".
  {
    std::unique_ptr<SExpression> search = SExpression::createList("layer");
    search->appendChild(SExpression::createToken("brd_keepout"));
    node.removeChildrenWithNodeRecursive(*search);
  }

  // Rename "top_placement" to "top_legend".
  {
    const std::unique_ptr<const SExpression> search =
        SExpression::createToken("top_placement");
    const std::unique_ptr<const SExpression> replace =
        SExpression::createToken("top_legend");
    node.replaceRecursive(*search, *replace);
  }

  // Rename "bot_placement" to "bot_legend".
  {
    const std::unique_ptr<const SExpression> search =
        SExpression::createToken("bot_placement");
    const std::unique_ptr<const SExpression> replace =
        SExpression::createToken("bot_legend");
    node.replaceRecursive(*search, *replace);
  }
}

void FileFormatMigrationV01::upgradeInversionCharacters(
    SExpression& root, const QString& childName, const QString& valuePath) {
  QSet<QString> reservedValues;
  foreach (SExpression* child, root.getChildren(childName)) {
    reservedValues.insert(child->getChild(valuePath).getValue());
  }
  foreach (SExpression* child, root.getChildren(childName)) {
    SExpression& node = child->getChild(valuePath);
    const QString newValue = "!" % node.getValue().mid(1);
    if (node.getValue().startsWith("/") &&
        (!reservedValues.contains(newValue))) {
      node.setValue(newValue);
    }
  }
}

void FileFormatMigrationV01::upgradeStrings(SExpression& root) {
  QMap<QString, QString> replacements = {
      {"MODIFIED_DATE", "DATE"},
      {"MODIFIED_TIME", "TIME"},
      {"PARTNUMBER", "MPN"},
  };
  replaceStrings(root, replacements);
}

void FileFormatMigrationV01::replaceStrings(
    SExpression& root, const QMap<QString, QString>& replacements) {
  foreach (SExpression* child, root.getChildren(SExpression::Type::List)) {
    replaceStrings(*child, replacements);
  }
  foreach (SExpression* child, root.getChildren(SExpression::Type::String)) {
    QString s = child->getValue();
    for (auto it = replacements.begin(); it != replacements.end(); it++) {
      s.replace(it.key(), it.value());
    }
    child->setValue(s);
  }
}

QString FileFormatMigrationV01::toFileProofName(
    const QString& name, const QString& fallback) noexcept {
  // perform compatibility decomposition (NFKD)
  QString ret = name.normalized(QString::NormalizationForm_KD);
  // remove leading and trailing spaces
  ret = ret.trimmed();
  // replace remaining spaces with replacement
  ret.replace(" ", "-");
  // remove all invalid characters
  ret.remove(QRegularExpression("[^-a-zA-Z0-9_+().]"));
  // truncate to maximum allowed length
  ret.truncate(20);
  // if there are leading or trailing spaces, remove them again ;)
  ret = ret.trimmed();
  // if the result is not valid, return the fallback
  if (ret.isEmpty()) ret = fallback;
  return ret;
}

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

}  // namespace librepcb
