#include <Models/ModelBox.hpp>

#include <utility>

#include <QtCore/QString>

#include <Execution/ParallelExecutor.hpp>

std::vector<ModelBox*> ModelBox::allModelBoxes;

ModelBox::ModelBox(ModelType p_type, size_t p_maxInputCount)
    : type(p_type)
    , maxInputCount(p_maxInputCount)
    , inputs(p_maxInputCount)
{
    // Input box cannot have inputs, other boxes must have inputs
    Q_ASSERT((this->type == ModelType::Input) == (this->maxInputCount == 0));
    ModelBox::allModelBoxes.push_back(this);
}

ModelBox::~ModelBox() {
    auto& bs = ModelBox::allModelBoxes;
    bs.erase(std::remove(bs.begin(), bs.end(), this), bs.end());
}

void ModelBox::setInput(size_t slot, ModelBox* model) {
    if (slot >= this->maxInputCount) {
        throw std::runtime_error { "Cannot connect to an nonexistent slot." };
    }

    if (model != nullptr && this->inputs[slot]) {
        throw std::runtime_error { "Cannot connect to an occupied input." };
    }

    this->inputs[slot] = model;
}

void ModelBox::addOutput(ModelBox* target, size_t targetSlot) {
    Q_ASSERT(this->canHaveOutput());
    if (this->outputs.find({target, targetSlot}) != this->outputs.end()) {
        throw std::runtime_error { "This target is already connected to this output." };
    }
    this->outputs.emplace(target, targetSlot);
}

void ModelBox::removeOutput(ModelBox* target, size_t targetSlot) {
    Q_ASSERT(this->canHaveOutput());
    auto it = this->outputs.find({target, targetSlot});
    if (it == this->outputs.end()) {
        throw std::runtime_error { "Cannot remove an output that is not connected.." };
    }
    this->outputs.erase(it);
}

std::shared_ptr<abstraction::Value> ModelBox::getCachedResultOrEvaluate() {
    if (this->result)
        return this->result;
    return this->evaluate();
}

void ModelBox::clearCachedResults() {
    for (auto* box: ModelBox::allModelBoxes) {
        box->clearCachedResult();
    }
}

void ModelBox::connect(ModelBox* origin, ModelBox* target, size_t targetSlot) {
    origin->addOutput(target, targetSlot);
    target->setInput(targetSlot, origin);
}

void ModelBox::disconnect(ModelBox* origin, ModelBox* target, size_t targetSlot) {
    origin->removeOutput(target, targetSlot);
    target->setInput(targetSlot, nullptr);
}
