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

#include "../../exceptions.h"
#include "../../library/cmp/component.h"
#include "../../serialization/sexpression.h"
#include "../project.h"
#include "assemblyvariant.h"
#include "bus.h"
#include "componentinstance.h"
#include "netclass.h"
#include "netsignal.h"

#include <QtCore>

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

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

Circuit::Circuit(Project& project) : QObject(&project), mProject(project) {
}

Circuit::~Circuit() noexcept {
  // delete all component instances (and catch all thrown exceptions)
  foreach (ComponentInstance* compInstance, mComponentInstances) try {
      removeComponentInstance(*compInstance);
      delete compInstance;
    } catch (...) {
    }

  // delete all buses (and catch all thrown exceptions)
  foreach (Bus* bus, mBuses) try {
      removeBus(*bus);
      delete bus;
    } catch (...) {
    }

  // delete all netsignals (and catch all thrown exceptions)
  foreach (NetSignal* netsignal, mNetSignals) try {
      removeNetSignal(*netsignal);
      delete netsignal;
    } catch (...) {
    }

  // delete all netclasses (and catch all thrown exceptions)
  foreach (NetClass* netclass, mNetClasses) try {
      removeNetClass(*netclass);
      delete netclass;
    } catch (...) {
    }

  // Delete all assembly variants
  mAssemblyVariants.clear();
}

/*******************************************************************************
 *  AssemblyVariant Methods
 ******************************************************************************/

int Circuit::addAssemblyVariant(std::shared_ptr<AssemblyVariant> av,
                                int index) {
  if ((!av) || (mAssemblyVariants.contains(av.get()))) {
    throw LogicError(__FILE__, __LINE__);
  }
  if (mAssemblyVariants.contains(av->getUuid())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("There is already an assembly variant with the UUID \"%1\"!")
            .arg(av->getUuid().toStr()));
  }
  if (mAssemblyVariants.contains(*av->getName())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already an assembly variant with the name \"%1\"!")
            .arg(*av->getName()));
  }
  if ((index < 0) || (index > mAssemblyVariants.count())) {
    index = mAssemblyVariants.count();
  }
  mAssemblyVariants.insert(index, av);
  emit assemblyVariantAdded(av);
  return index;
}

void Circuit::removeAssemblyVariant(std::shared_ptr<AssemblyVariant> av) {
  if (!mAssemblyVariants.contains(av.get())) {
    throw LogicError(__FILE__, __LINE__);
  }
  if (mAssemblyVariants.count() == 1) {
    throw LogicError(__FILE__, __LINE__,
                     "The last assembly variant cannot be removed!");
  }
  mAssemblyVariants.remove(av.get());
  assemblyVariantRemoved(av);
}

void Circuit::setAssemblyVariantName(std::shared_ptr<AssemblyVariant> av,
                                     const FileProofName& newName) {
  // Check if there is no assembly variant with the same name.
  if ((av->getName() != newName) && mAssemblyVariants.contains(*newName)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already an assembly variant with the name \"%1\"!")
            .arg(*newName));
  }
  // Apply the new name.
  av->setName(newName);
}

/*******************************************************************************
 *  NetClass Methods
 ******************************************************************************/

NetClass* Circuit::getNetClassByName(const ElementName& name) const noexcept {
  foreach (NetClass* netclass, mNetClasses) {
    if (netclass->getName() == name) {
      return netclass;
    }
  }
  return nullptr;
}

void Circuit::addNetClass(NetClass& netclass) {
  if ((mNetClasses.values().contains(&netclass)) ||
      (&netclass.getCircuit() != this)) {
    throw LogicError(__FILE__, __LINE__);
  }
  if (mNetClasses.contains(netclass.getUuid())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("There is already a net class with the UUID \"%1\"!")
            .arg(netclass.getUuid().toStr()));
  }
  if (getNetClassByName(netclass.getName())) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("There is already a net class with the name \"%1\"!")
                           .arg(*netclass.getName()));
  }
  netclass.addToCircuit();  // can throw
  mNetClasses.insert(netclass.getUuid(), &netclass);
  connect(&netclass, &NetClass::designRulesModified, this,
          &Circuit::netClassDesignRulesModified);
  emit netClassAdded(netclass);
}

void Circuit::removeNetClass(NetClass& netclass) {
  if (mNetClasses.value(netclass.getUuid()) != &netclass) {
    throw LogicError(__FILE__, __LINE__);
  }
  disconnect(&netclass, &NetClass::designRulesModified, this,
             &Circuit::netClassDesignRulesModified);
  netclass.removeFromCircuit();  // can throw
  mNetClasses.remove(netclass.getUuid());
  emit netClassRemoved(netclass);
}

void Circuit::setNetClassName(NetClass& netclass, const ElementName& newName) {
  if (netclass.getName() == newName) {
    return;
  }
  // check if the netclass was added to the circuit
  if (mNetClasses.value(netclass.getUuid()) != &netclass) {
    throw LogicError(__FILE__, __LINE__);
  }
  // check if there is no netclass with the same name in the list
  if (getNetClassByName(newName)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already a net class with the name \"%1\"!").arg(*newName));
  }
  // apply the new name
  netclass.setName(newName);  // can throw
}

/*******************************************************************************
 *  NetSignal Methods
 ******************************************************************************/

QString Circuit::generateAutoNetSignalName() const noexcept {
  QString name;
  int i = 1;
  do {
    name = QString("N%1").arg(i++);
  } while (getNetSignalByName(name));
  return name;
}

NetSignal* Circuit::getNetSignalByName(const QString& name) const noexcept {
  foreach (NetSignal* netsignal, mNetSignals) {
    if (netsignal->getName() == name) {
      return netsignal;
    }
  }
  return nullptr;
}

NetSignal* Circuit::getNetSignalWithMostElements() const noexcept {
  NetSignal* netsignal = nullptr;
  foreach (NetSignal* ns, mNetSignals) {
    if ((!netsignal) ||
        (ns->getRegisteredElementsCount() >
         netsignal->getRegisteredElementsCount())) {
      netsignal = ns;
    }
  }
  return netsignal;
}

void Circuit::addNetSignal(NetSignal& netsignal) {
  if ((mNetSignals.values().contains(&netsignal)) ||
      (&netsignal.getCircuit() != this)) {
    throw LogicError(__FILE__, __LINE__);
  }
  if (mNetSignals.contains(netsignal.getUuid())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("There is already a net signal with the UUID \"%1\"!")
            .arg(netsignal.getUuid().toStr()));
  }
  if (getNetSignalByName(*netsignal.getName())) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("There is already a net signal with the name \"%1\"!")
                           .arg(*netsignal.getName()));
  }
  netsignal.addToCircuit();  // can throw
  mNetSignals.insert(netsignal.getUuid(), &netsignal);
  emit netSignalAdded(netsignal);
}

void Circuit::removeNetSignal(NetSignal& netsignal) {
  if (mNetSignals.value(netsignal.getUuid()) != &netsignal) {
    throw LogicError(__FILE__, __LINE__);
  }
  netsignal.removeFromCircuit();  // can throw
  mNetSignals.remove(netsignal.getUuid());
  emit netSignalRemoved(netsignal);
}

void Circuit::setNetSignalName(NetSignal& netsignal,
                               const CircuitIdentifier& newName,
                               bool isAutoName) {
  // check if the netsignal was added to the circuit
  if (mNetSignals.value(netsignal.getUuid()) != &netsignal) {
    throw LogicError(__FILE__, __LINE__);
  }
  // check if there is no netsignal with the same name in the list
  if (getNetSignalByName(*newName)) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("There is already a net signal with the name \"%1\"!")
                           .arg(*newName));
  }
  // apply the new name
  netsignal.setName(newName, isAutoName);  // can throw
}

/*******************************************************************************
 *  Bus Methods
 ******************************************************************************/

QString Circuit::generateAutoBusName() const noexcept {
  QString name;
  int i = 1;
  do {
    name = QString("B%1").arg(i++);
  } while (getBusByName(name));
  return name;
}

Bus* Circuit::getBusByName(const QString& name) const noexcept {
  foreach (Bus* bus, mBuses) {
    if (bus->getName() == name) {
      return bus;
    }
  }
  return nullptr;
}

void Circuit::addBus(Bus& bus) {
  if ((mBuses.values().contains(&bus)) || (&bus.getCircuit() != this)) {
    throw LogicError(__FILE__, __LINE__);
  }
  if (mBuses.contains(bus.getUuid())) {
    throw RuntimeError(__FILE__, __LINE__,
                       QString("There is already a bus with the UUID \"%1\"!")
                           .arg(bus.getUuid().toStr()));
  }
  if (getBusByName(*bus.getName())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already a bus with the name \"%1\"!").arg(*bus.getName()));
  }
  bus.addToCircuit();  // can throw
  mBuses.insert(bus.getUuid(), &bus);
  emit busAdded(bus);
}

void Circuit::removeBus(Bus& bus) {
  if (mBuses.value(bus.getUuid()) != &bus) {
    throw LogicError(__FILE__, __LINE__);
  }
  bus.removeFromCircuit();  // can throw
  mBuses.remove(bus.getUuid());
  emit busRemoved(bus);
}

void Circuit::setBusName(Bus& bus, const BusName& newName, bool isAutoName) {
  // check if the bus was added to the circuit
  if (mBuses.value(bus.getUuid()) != &bus) {
    throw LogicError(__FILE__, __LINE__);
  }
  // check if there is no bus with the same name in the list
  if (getBusByName(*newName)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already a bus with the name \"%1\"!").arg(*newName));
  }
  // apply the new name
  bus.setName(newName, isAutoName);  // can throw
  emit busRenamed(bus);
}

/*******************************************************************************
 *  ComponentInstance Methods
 ******************************************************************************/

QString Circuit::generateAutoComponentInstanceName(
    const ComponentPrefix& cmpPrefix) const noexcept {
  QString name;
  int i = 1;
  do {
    name =
        QString("%1%2").arg(cmpPrefix->isEmpty() ? "?" : *cmpPrefix).arg(i++);
  } while (getComponentInstanceByName(name));
  return name;
}

ComponentInstance* Circuit::getComponentInstanceByUuid(
    const Uuid& uuid) const noexcept {
  return mComponentInstances.value(uuid, nullptr);
}

ComponentInstance* Circuit::getComponentInstanceByName(
    const QString& name) const noexcept {
  foreach (ComponentInstance* compInstance, mComponentInstances) {
    if (compInstance->getName() == name) return compInstance;
  }
  return nullptr;
}

void Circuit::addComponentInstance(ComponentInstance& cmp) {
  if (&cmp.getCircuit() != this) {
    throw LogicError(__FILE__, __LINE__);
  }
  // check if there is no component with the same uuid in the list
  if (getComponentInstanceByUuid(cmp.getUuid())) {
    throw RuntimeError(
        __FILE__, __LINE__,
        QString("There is already a component with the UUID \"%1\"!")
            .arg(cmp.getUuid().toStr()));
  }
  // check if there is no component with the same name in the list
  if (getComponentInstanceByName(*cmp.getName())) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("There is already a component with the name \"%1\"!")
                           .arg(*cmp.getName()));
  }
  // add to circuit
  cmp.addToCircuit();  // can throw
  mComponentInstances.insert(cmp.getUuid(), &cmp);
  emit componentAdded(cmp);
}

void Circuit::removeComponentInstance(ComponentInstance& cmp) {
  // check if the component instance was added to the circuit
  if (mComponentInstances.value(cmp.getUuid()) != &cmp) {
    throw LogicError(__FILE__, __LINE__);
  }
  // remove from circuit
  cmp.removeFromCircuit();  // can throw
  mComponentInstances.remove(cmp.getUuid());
  emit componentRemoved(cmp);
}

void Circuit::setComponentInstanceName(ComponentInstance& cmp,
                                       const CircuitIdentifier& newName) {
  // check if the component instance was added to the circuit
  if (mComponentInstances.value(cmp.getUuid()) != &cmp) {
    throw LogicError(__FILE__, __LINE__);
  }
  // check if there is no component with the same name in the list
  if ((newName != cmp.getName()) && getComponentInstanceByName(*newName)) {
    throw RuntimeError(
        __FILE__, __LINE__,
        tr("There is already a component with the name \"%1\"!").arg(*newName));
  }
  // apply the new name
  cmp.setName(newName);  // can throw
}

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

void Circuit::serialize(SExpression& root) const {
  root.ensureLineBreak();
  mAssemblyVariants.serialize(root);
  root.ensureLineBreak();
  for (const NetClass* obj : mNetClasses) {
    root.ensureLineBreak();
    obj->serialize(root.appendList("netclass"));
  }
  for (const NetSignal* obj : mNetSignals) {
    root.ensureLineBreak();
    obj->serialize(root.appendList("net"));
  }
  for (const Bus* obj : mBuses) {
    root.ensureLineBreak();
    obj->serialize(root.appendList("bus"));
  }
  for (const ComponentInstance* obj : mComponentInstances) {
    root.ensureLineBreak();
    obj->serialize(root.appendList("component"));
  }
  root.ensureLineBreak();
}

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

}  // namespace librepcb
