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

#include "../fileio/transactionaldirectory.h"
#include "../fileio/versionfile.h"
#include "../types/length.h"
#include "sexpression.h"

#include <QtCore>

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

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

FileFormatMigrationV1::FileFormatMigrationV1(QObject* parent) noexcept
  : FileFormatMigration(Version::fromString("1"), Version::fromString("2"),
                        parent) {
}

FileFormatMigrationV1::~FileFormatMigrationV1() noexcept {
}

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

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

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

void FileFormatMigrationV1::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("grid_interval", SExpression::createToken("2.54"));
    upgradeTexts(*root, true);
    dir.write(fp, root->toByteArray());
  }
}

void FileFormatMigrationV1::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("grid_interval", SExpression::createToken("2.54"));
    root->appendChild("min_copper_clearance", SExpression::createToken("0.2"));

    // Footprints.
    for (SExpression* fptNode : root->getChildren("footprint")) {
      // Add tags depending on name.
      const QString name = fptNode->getChild("name/@0").getValue().toLower();
      if (name.contains("density level a")) {
        fptNode->appendChild("tag", QString("ipc-density-level-a"));
      }
      if (name.contains("density level b")) {
        fptNode->appendChild("tag", QString("ipc-density-level-b"));
      }
      if (name.contains("density level c")) {
        fptNode->appendChild("tag", QString("ipc-density-level-c"));
      }
      if (name.contains("hand")) {
        fptNode->appendChild("tag", QString("hand-soldering"));
      }
      if (name.contains("reflow")) {
        fptNode->appendChild("tag", QString("reflow-soldering"));
      }
      if (name.contains("wave")) {
        fptNode->appendChild("tag", QString("wave-soldering"));
      }
      if (name.contains("large")) {
        fptNode->appendChild("tag", QString("extra-large-pads"));
      }

      // Pads.
      for (SExpression* padNode : fptNode->getChildren("pad")) {
        // Revert possibly made manual change as a workaround for bug, see
        // https://librepcb.discourse.group/t/migrating-libraries-from-old-version-1-0-pressfit-problem/810
        SExpression& padFunction = padNode->getChild("function/@0");
        if (padFunction.getValue() == "press_fit") {
          padFunction.setValue("pressfit");
        }
      }

      // Stroke texts.
      for (SExpression* child : fptNode->getChildren("stroke_text")) {
        const QSet<QString> unlockedLayers = {"top_names", "top_values",
                                              "bot_names", "bot_values"};
        const QString layer = child->getChild("layer/@0").getValue();
        const bool lock = !unlockedLayers.contains(layer);
        child->appendChild("lock",
                           SExpression::createToken(lock ? "true" : "false"));
      }
    }

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

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

void FileFormatMigrationV1::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));

    // Pinout.
    for (SExpression* padNode : root->getChildren("pad")) {
      padNode->appendChild("optional", SExpression::createToken("false"));
    }

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

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

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

void FileFormatMigrationV1::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")) {
      upgradeSymbol(subDir);
    }
  }

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

  // Components.
  foreach (const QString& dirName, dir.getDirs("library/cmp")) {
    TransactionalDirectory subDir(dir, "library/cmp/" % dirName);
    if (subDir.fileExists(".librepcb-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());
    }
  }

  // Metadata.
  {
    const QString fp = "project/metadata.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeMetadata(*root, messages);
    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, messages);
    dir.write(fp, root->toByteArray());
  }

  // Output Jobs.
  {
    const QString fp = "project/jobs.lp";
    std::unique_ptr<SExpression> root =
        SExpression::parse(dir.read(fp), dir.getAbsPath(fp));
    upgradeOutputJobs(*root, context);
    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, messages);
    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);
    dir.write(fp, root->toByteArray());
  }

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

  // Emit messages at the very end to avoid duplicate messages caused my
  // multiple schematics/boards.
  if ((context.boardCount > 0) && (!context.hasGerberOutputJob)) {
    messages.append(buildMessage(
        Message::Severity::Warning,
        tr("The dedicated Gerber/Excellon generator dialog has been removed "
           "in favor of the more powerful output jobs, and the corresponding "
           "output settings will be removed from boards in an upcoming "
           "release. It is recommended to add a Gerber/Excellon output job "
           "now, as this allows to migrate the old export settings (choose "
           "\"Import Old Settings\")."),
        1));
  }
}

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

  // Remove legacy files.
  const QStringList filesToRemove = {
      "cache_v3",  //
      "cache_v4",  //
      "cache_v5",  //
      "cache_v6",  //
      "cache_v7",  //
  };
  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("api_endpoints")) {
      int index = 0;
      foreach (SExpression* child, node->getChildren("url")) {
        child->setName("endpoint");
        child->appendChild("libraries", SExpression::createToken("true"));
        child->appendChild(
            "parts", SExpression::createToken((index == 0) ? "true" : "false"));
        child->appendChild(
            "order", SExpression::createToken((index == 0) ? "true" : "false"));
        ++index;
      }
    }
    dir.write(settingsFp, root->toByteArray());
  }
}

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

void FileFormatMigrationV1::upgradeMetadata(SExpression& root,
                                            QList<Message>& messages) {
  // FileProofName does no longer allow string consisting of only dots
  // (e.g. "..") so we rename them.
  SExpression& versionNode = root.getChild("version/@0");
  if (auto newVersion = upgradeFileProofName(versionNode.getValue())) {
    versionNode.setValue(*newVersion);
    // Not translated because it's unlikely someone will ever see this message.
    messages.append(buildMessage(
        Message::Severity::Note,
        "Project version has been adjusted due to more restrictive naming "
        "requirements. Please review the new version number.",
        1));
  }
}

void FileFormatMigrationV1::upgradeSettings(SExpression& root,
                                            QList<Message>& messages) {
  // The manual BOM export has been removed. If the user has configured custom
  // BOM attributes, just remind him to use output jobs now.
  QStringList customBomAttributes;
  for (const SExpression* node :
       root.getChild("custom_bom_attributes").getChildren("attribute")) {
    customBomAttributes.append(node->getChild("@0").getValue());
  }
  if (!customBomAttributes.isEmpty()) {
    messages.append(buildMessage(
        Message::Severity::Note,
        tr("The project has set custom attributes for the BOM export (%1). But "
           "in LibrePCB 2.0, the manual BOM export has been removed in favor "
           "of the more powerful output jobs feature. Please use output jobs "
           "now to generate the BOM. When you add a new BOM output job, those "
           "custom attributes will automatically be imported.")
            .arg(customBomAttributes.join(", ")),
        1));
  }
}

void FileFormatMigrationV1::upgradeOutputJobs(SExpression& root,
                                              ProjectContext& context) {
  for (SExpression* jobNode : root.getChildren("job")) {
    if (jobNode->getChild("type/@0").getValue() == "graphics") {
      for (SExpression* contentNode : jobNode->getChildren("content")) {
        SExpression& contentTypeNode = contentNode->getChild("type/@0");
        if (contentTypeNode.getValue() == "schematic") {
          // Add the new layer for image borders.
          SExpression& imgBordersLayerNode = contentNode->appendList("layer");
          imgBordersLayerNode.appendChild(
              SExpression::createToken("schematic_image_borders"));
          imgBordersLayerNode.appendChild(
              "color", SExpression::createString("#ff808080"));
          // Add the new layer for buses.
          SExpression& busesLayerNode = contentNode->appendList("layer");
          busesLayerNode.appendChild(
              SExpression::createToken("schematic_buses"));
          busesLayerNode.appendChild("color",
                                     SExpression::createString("#ff008eff"));
          // Add the new layer for bus labels.
          SExpression& busLabelsLayerNode = contentNode->appendList("layer");
          busLabelsLayerNode.appendChild(
              SExpression::createToken("schematic_bus_labels"));
          busLabelsLayerNode.appendChild(
              "color", SExpression::createString("#ff008eff"));
        } else if (contentTypeNode.getValue() == "board") {
          // We don't need to check the option value since "realistic" was
          // the only supported option in v1.
          const auto optionNodes = contentNode->getChildren("option");
          for (SExpression* optionNode : optionNodes) {
            contentNode->removeChild(*optionNode);
          }
          if (!optionNodes.isEmpty()) {
            contentTypeNode.setValue("board_rendering");
            for (SExpression* layerNode : contentNode->getChildren("layer")) {
              contentNode->removeChild(*layerNode);
            }
            auto addLayer = [&contentNode](const QString& layer,
                                           const QString& color) {
              SExpression& node = contentNode->appendList("layer");
              node.appendChild(SExpression::createToken(layer));
              node.appendChild("color", color);
            };
            if (contentNode->getChild("mirror/@0").getValue() == "true") {
              addLayer("board_copper_bottom", "#ffbc9c69");
              addLayer("board_legend_bottom", "#00000000");
              addLayer("board_outlines", "#ff465046");
              addLayer("board_stop_mask_bottom", "#00000000");
            } else {
              addLayer("board_copper_top", "#ffbc9c69");
              addLayer("board_legend_top", "#00000000");
              addLayer("board_outlines", "#ff465046");
              addLayer("board_stop_mask_top", "#00000000");
            }
          }
        }
      }
    } else if (jobNode->getChild("type/@0").getValue() == "gerber_excellon") {
      context.hasGerberOutputJob = true;
    } else if (jobNode->getChild("type/@0").getValue() == "gerber_x3") {
      jobNode->getChild("top").setName("components_top");
      jobNode->getChild("bottom").setName("components_bot");
      SExpression& glueTop = jobNode->appendList("glue_top");
      glueTop.appendChild("create", SExpression::createToken("false"));
      glueTop.appendChild(
          "output",
          SExpression::createString(
              "assembly/{{PROJECT}}_{{VERSION}}_GLUE_{{VARIANT}}_TOP.gbr"));
      root.ensureLineBreak();
      SExpression& glueBot = jobNode->appendList("glue_bot");
      glueBot.appendChild("create", SExpression::createToken("false"));
      glueBot.appendChild(
          "output",
          SExpression::createString(
              "assembly/{{PROJECT}}_{{VERSION}}_GLUE_{{VARIANT}}_BOT.gbr"));
      root.ensureLineBreak();
    }
  }
}

void FileFormatMigrationV1::upgradeCircuit(SExpression& root,
                                           QList<Message>& messages) {
  // Assembly variants.
  int renamedAssemblyVariants = 0;
  for (SExpression* variantNode : root.getChildren("variant")) {
    // FileProofName does no longer allow string consisting of only dots
    // (e.g. "..") so we rename them. We don't do conflict resolution here
    // as it is very unlikely to ever happen.
    SExpression& nameNode = variantNode->getChild("name/@0");
    if (auto newName = upgradeFileProofName(nameNode.getValue())) {
      nameNode.setValue(*newName);
      ++renamedAssemblyVariants;
    }
  }
  if (renamedAssemblyVariants > 0) {
    // Not translated because it's unlikely someone will ever see this message.
    messages.append(buildMessage(
        Message::Severity::Note,
        "Assembly variants have been renamed due to more restrictive naming "
        "requirements. Please review the new names.",
        renamedAssemblyVariants));
  }

  // Net classes.
  for (SExpression* classNode : root.getChildren("netclass")) {
    classNode->appendChild("default_trace_width",
                           SExpression::createToken("inherit"));
    classNode->appendChild("default_via_drill_diameter",
                           SExpression::createToken("inherit"));
    classNode->appendChild("min_copper_copper_clearance",
                           SExpression::createToken("0"));
    classNode->appendChild("min_copper_width", SExpression::createToken("0"));
    classNode->appendChild("min_via_drill_diameter",
                           SExpression::createToken("0"));
  }
}

void FileFormatMigrationV1::upgradeSchematic(SExpression& root) {
  for (SExpression* symbolNode : root.getChildren("symbol")) {
    upgradeTexts(*symbolNode, true);  // Lock texts depending on layer.
  }
  upgradeTexts(root, false);  // Do not lock any schematic text.
}

void FileFormatMigrationV1::upgradeBoard(SExpression& root) {
  // Design rules
  {
    SExpression& rulesNode = root.getChild("design_rules");
    rulesNode.appendChild("default_trace_width",
                          SExpression::createToken("0.5"));
    rulesNode.appendChild("default_via_drill_diameter",
                          SExpression::createToken("0.3"));
  }

  // DRC settings.
  {
    SExpression& drcNode = root.getChild("design_rule_check");
    drcNode.ensureLineBreak();
    {
      SExpression& child = drcNode.appendList("min_pcb_size");
      child.appendChild(SExpression::createToken("0.0"));
      child.appendChild(SExpression::createToken("0.0"));
    }
    drcNode.ensureLineBreak();
    {
      SExpression& child = drcNode.appendList("max_pcb_size");
      SExpression& doubleSided = child.appendList("double_sided");
      doubleSided.appendChild(SExpression::createToken("0.0"));
      doubleSided.appendChild(SExpression::createToken("0.0"));
      SExpression& multilayer = child.appendList("multilayer");
      multilayer.appendChild(SExpression::createToken("0.0"));
      multilayer.appendChild(SExpression::createToken("0.0"));
    }
    drcNode.ensureLineBreak();
    drcNode.appendList("pcb_thickness");
    drcNode.ensureLineBreak();
    drcNode.appendChild("max_layers", SExpression::createToken("0"));
    drcNode.ensureLineBreak();
    drcNode.appendList("solder_resist");
    drcNode.ensureLineBreak();
    drcNode.appendList("silkscreen");
    drcNode.ensureLineBreak();
    drcNode.appendChild("max_tented_via_drill_diameter",
                        SExpression::createToken("0.5"));
    drcNode.ensureLineBreak();
  }

  // DRC approvals.
  {
    SExpression& drcNode = root.getChild("design_rule_check");
    const QString approvalsVersion =
        drcNode.getChild("approvals_version/@0").getValue();
    for (SExpression* approvalNode : drcNode.getChildren("approved")) {
      SExpression& approvalTypeNode = approvalNode->getChild("@0");
      if ((approvalTypeNode.getValue() == "useless_via") &&
          (approvalsVersion != "2")) {
        approvalTypeNode.setValue("invalid_via");
      } else if (approvalTypeNode.getValue() == "antennae_via") {
        approvalTypeNode.setValue("useless_via");
      }
    }
  }

  // Preferred footprint tags
  {
    SExpression& child = root.appendList("preferred_footprint_tags");
    child.ensureLineBreak();
    child.appendList("tht_top");
    child.ensureLineBreak();
    child.appendList("tht_bot");
    child.ensureLineBreak();
    child.appendList("smt_top");
    child.ensureLineBreak();
    child.appendList("smt_bot");
    child.ensureLineBreak();
    child.appendList("common");
    child.ensureLineBreak();
  }

  // Devices
  for (SExpression* devNode : root.getChildren("device")) {
    devNode->appendChild("glue", SExpression::createToken("true"));
  }

  // Net segments
  for (SExpression* devNode : root.getChildren("netsegment")) {
    // Vias
    for (SExpression* viaNode : devNode->getChildren("via")) {
      SExpression& drillNode = viaNode->getChild("drill/@0");
      SExpression& sizeNode = viaNode->getChild("size/@0");
      const PositiveLength drill = deserialize<PositiveLength>(drillNode);
      const PositiveLength size = deserialize<PositiveLength>(sizeNode);
      if (size < drill) {
        // No longer valid in LibrePCB 2.0!
        sizeNode.setValue(drillNode.getValue());
      }
    }
  }

  // Planes
  for (SExpression* planeNode : root.getChildren("plane")) {
    SExpression& clrNode = planeNode->getChild("min_clearance");
    clrNode.setName("min_copper_clearance");
    planeNode->appendChild(
        "min_board_clearance",
        std::make_unique<SExpression>(clrNode.getChild("@0")));
    planeNode->appendChild(
        "min_npth_clearance",
        std::make_unique<SExpression>(clrNode.getChild("@0")));
  }
}

void FileFormatMigrationV1::upgradeTexts(SExpression& node, bool allowLock) {
  for (SExpression* child : node.getChildren("text")) {
    const QString layer = child->getChild("layer/@0").getValue();
    const bool lock =
        allowLock && (layer != "sym_names") && (layer != "sym_values");
    child->appendChild("lock",
                       SExpression::createToken(lock ? "true" : "false"));
  }
}

std::optional<QString> FileFormatMigrationV1::upgradeFileProofName(
    QString name) {
  if (QRegularExpression("\\A\\.+\\z")
          .match(name, 0, QRegularExpression::PartialPreferCompleteMatch)
          .hasMatch()) {
    return name.replace(".", "_");
  }
  return std::nullopt;
}

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

}  // namespace librepcb
