/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "ecflow/node/Limit.hpp"

#include <stdexcept>

#include "ecflow/core/Converter.hpp"
#include "ecflow/core/Ecf.hpp"
#include "ecflow/core/Indentor.hpp"
#include "ecflow/core/PrintStyle.hpp"
#include "ecflow/core/Serialization.hpp"
#include "ecflow/core/Str.hpp"
#include "ecflow/node/Suite.hpp"

using namespace std;
using namespace ecf;

/////////////////////////////////////////////////////////////////////////////////////////////

Limit::Limit(const std::string& name, int limit) : n_(name), lim_(limit) {
    if (!Str::valid_name(name)) {
        throw std::runtime_error("Limit::Limit: Invalid Limit name: " + name);
    }
}

Limit::Limit(const std::string& name, int limit, int value, const std::set<std::string>& paths, bool check)
    : n_(name),
      lim_(limit),
      value_(value),
      paths_(paths) {
    if (check && !Str::valid_name(name)) {
        throw std::runtime_error("Limit::Limit: Invalid Limit name: " + name);
    }
}

Limit::Limit(const Limit& rhs) : n_(rhs.n_), lim_(rhs.lim_), value_(rhs.value_), paths_(rhs.paths_) {
}

bool Limit::operator==(const Limit& rhs) const {
    if (value_ != rhs.value_) {
#ifdef DEBUG
        if (Ecf::debug_equality()) {
            std::cout << "Limit::operator==value_(" << value_ << ") != rhs.value_(" << rhs.value_ << ") " << toString()
                      << "   rhs(" << rhs.toString() << ")\n";
        }
#endif
        return false;
    }
    if (lim_ != rhs.lim_) {
#ifdef DEBUG
        if (Ecf::debug_equality()) {
            std::cout << "Limit::operator==( lim_ != rhs.lim_) " << toString() << "   rhs(" << rhs.toString() << ")\n";
        }
#endif
        return false;
    }
    if (n_ != rhs.n_) {
#ifdef DEBUG
        if (Ecf::debug_equality()) {
            std::cout << "Limit::operator==( n_ != rhs.n_ ) " << toString() << "   rhs(" << rhs.toString() << ")\n";
        }
#endif
        return false;
    }
    if (paths_ != rhs.paths_) {
#ifdef DEBUG
        if (Ecf::debug_equality()) {
            std::cout << "Limit::operator==( paths_ != rhs.paths_ ) " << toString() << "   rhs(" << rhs.toString()
                      << ")\n";
        }
#endif
        return false;
    }
    return true;
}

void Limit::print(std::string& os) const {
    Indentor in;
    Indentor::indent(os);
    write(os);
    if (!PrintStyle::defsStyle()) {
        if (value_ != 0) {
            os += " # ";
            os += ecf::convert_to<std::string>(value_);
            for (const auto& path : paths_) {
                os += " ";
                os += path;
            }
        }
    }
    os += "\n";
}

std::string Limit::toString() const {
    std::string ret;
    write(ret);
    return ret;
}

void Limit::write(std::string& ret) const {
    ret += "limit ";
    ret += n_;
    ret += " ";
    ret += ecf::convert_to<std::string>(lim_);
}

void Limit::decrement(int tokens, const std::string& abs_node_path) {

    // cout << "Limit::decrement name = " << n_ << " current value_ = " << value_ << " limit = " <<  lim_ << " consume
    // tokens = " << tokens << " path = " << abs_node_path << "\n"; Note: we previously had 'if (value_ > 0) {
    //       However if the user had manually changed the value_, then we could be left with paths_,  that would never
    //       have been cleared
    if (delete_path(abs_node_path)) {
        // delete_path() will increment state_change_no
        value_ -= tokens;
        if (value_ < 0) {
            value_ = 0;
            paths_.clear();
        }
    }

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "Limit::decrement\n";
#endif
    // cout << "Limit::decrement name = " << n_ << " current value_ = " << value_ << "\n";
}

void Limit::increment(int tokens, const std::string& abs_node_path) {
    // cout << "Limit::increment name = " << n_ << " current value_ = " << value_ << " limit = " <<  lim_ << " consume
    // tokens = " << tokens << " path = " << abs_node_path << "\n";

    // increment should keep increasing limit value, *EVEN* if over the limit. See ECFLOW-324
    // Note: previously we had:
    //     if ( value_ < lim_ ) {

    auto result = paths_.insert(abs_node_path);
    if (result.second) {
        value_ += tokens;
        update_change_no();
    }

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "Limit::increment\n";
#endif
    // cout << "Limit::increment name = " << n_ << " current value_ = " << value_ << "\n";
}

void Limit::setValue(int v) {
    value_ = v;
    if (value_ == 0)
        paths_.clear();
    update_change_no();
#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "   Limit::setValue() value_ = " << value_ << "\n";
#endif
}

void Limit::setLimit(int v) {
    lim_ = v;
    update_change_no();

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "   Limit::setLimit() lim_ = " << value_ << "\n";
#endif
}

void Limit::set_paths(const std::set<std::string>& paths) {
    paths_ = paths;
    update_change_no();
}

void Limit::set_state(int limit, int value, const std::set<std::string>& paths) {
    value_ = value;
    lim_   = limit;
    paths_ = paths;
    update_change_no();
}

bool Limit::delete_path(const std::string& abs_node_path) {
    auto i = paths_.find(abs_node_path);
    if (i != paths_.end()) {
        paths_.erase(i);
        update_change_no();
        return true;
    }

#ifdef DEBUG_STATE_CHANGE_NO
    std::cout << "Limit::delete_path() \n";
#endif
    return false;
}

void Limit::reset() {
    paths_.clear();
    setValue(0); // will increment state_change_no_
}

void Limit::update_change_no() {
    state_change_no_ = Ecf::incr_state_change_no();
    if (node_) {
        Suite* suite = node_->suite();
        if (suite)
            suite->set_state_change_no(state_change_no_);
    }
}

template <class Archive>
void Limit::serialize(Archive& ar) {
    ar(CEREAL_NVP(n_), CEREAL_NVP(lim_));
    CEREAL_OPTIONAL_NVP(ar, value_, [this]() { return value_ != 0; });     // conditionally save
    CEREAL_OPTIONAL_NVP(ar, paths_, [this]() { return !paths_.empty(); }); // conditionally save
}
CEREAL_TEMPLATE_SPECIALIZE(Limit);
