/*
 * 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/InLimitMgr.hpp"

#include <stdexcept>

#include "ecflow/core/Extract.hpp"
#include "ecflow/core/Serialization.hpp"
#include "ecflow/core/Str.hpp"
#include "ecflow/node/Limit.hpp"
#include "ecflow/node/Memento.hpp"
#include "ecflow/node/Node.hpp"

#ifdef DEBUG
    #include <ostream>

    #include "ecflow/core/Ecf.hpp"
#endif

using namespace ecf;
using namespace std;

void InLimitMgr::reset() {
    for (auto& i : vec_) {
        i.set_incremented(false);
    }
}

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

InLimitMgr& InLimitMgr::operator=(const InLimitMgr& rhs) {
    if (this != &rhs) {
        vec_  = rhs.vec_;
        node_ = nullptr;
    }
    return *this;
}

void InLimitMgr::print(std::string& os) const {
    for (const InLimit& i : vec_) {
        i.print(os);
    }
}

bool InLimitMgr::operator==(const InLimitMgr& rhs) const {
    if (vec_.size() != rhs.vec_.size()) {
#ifdef DEBUG
        if (Ecf::debug_equality()) {
            std::cout << "InLimitMgr::operator==  vec_.size() != rhs.vec_.size() " << node_->debugNodePath() << "\n";
        }
#endif
        return false;
    }
    for (size_t i = 0; i < vec_.size(); ++i) {
        if (!(vec_[i] == rhs.vec_[i])) {
#ifdef DEBUG
            if (Ecf::debug_equality()) {
                std::cout << "InLimitMgr::operator==  (!(vec_[i] == rhs.vec_[i] )) " << node_->debugNodePath() << "\n";
            }
#endif
            return false;
        }
    }
    return true;
}

void InLimitMgr::addInLimit(const InLimit& l, bool check) {
    if (check && findInLimitByNameAndPath(l)) {
        throw std::runtime_error("Add InLimit failed: Duplicate InLimit see node " + node_->debugNodePath());
    }
    vec_.push_back(l);
}

bool InLimitMgr::deleteInlimit(const std::string& name) {
    // cout << "InLimitMgr::deleteInlimit: " << name << "\n";
    if (name.empty()) {
        vec_.clear();
        return true;
    }

    string path_to_limit; // This can be empty
    string limit_name;
    (void)Extract::pathAndName(name, path_to_limit, limit_name); // already checked for empty name
    // cout << "   path_to_limit:" << path_to_limit << "\n";
    // cout << "   limit_name:" <<  limit_name << "\n";

    for (size_t i = 0; i < vec_.size(); i++) {
        // cout << "   " << i << ": " << vec_[i].pathToNode() << "  :  " << vec_[i].name() << "\n";
        if (path_to_limit.empty()) {
            if (vec_[i].name() == limit_name) {
                vec_.erase(vec_.begin() + i);
                return true;
            }
        }
        else {
            if (vec_[i].name() == limit_name && vec_[i].pathToNode() == path_to_limit) {
                vec_.erase(vec_.begin() + i);
                return true;
            }
        }
    }
    throw std::runtime_error("InLimitMgr::deleteInlimit: Cannot find inlimit: " + name);
}

Limit* InLimitMgr::findLimitViaInLimit(const InLimit& theInLimit) const {
    // Use in *test* only
    size_t theSize = vec_.size();
    for (size_t i = 0; i < theSize; i++) {
        if (vec_[i].name() == theInLimit.name() && vec_[i].pathToNode() == theInLimit.pathToNode()) {
            resolveInLimit(vec_[i]);
            return vec_[i].limit();
        }
    }
    return nullptr;
}

bool InLimitMgr::findInLimitByNameAndPath(const InLimit& theInLimit) const {
    size_t theSize = vec_.size();
    for (size_t i = 0; i < theSize; i++) {
        if (vec_[i].name() == theInLimit.name() && vec_[i].pathToNode() == theInLimit.pathToNode()) {
            return true;
        }
    }
    return false;
}

void InLimitMgr::get_memento(compound_memento_ptr& comp) const {
#ifdef DEBUG_MEMENTO
    std::cout << "InLimitMgr::get_memento " << node_->debugNodePath() << "\n";
#endif

    for (const InLimit& l : vec_) {
        comp->add(std::make_shared<NodeInLimitMemento>(l));
    }
}

bool InLimitMgr::inLimit() const {
    // Check in we are in limit.
    // ** WE need to do a lookahead. hence we pass down inlimit tokens **
    // In the case we have multiple inlimits then we are only in limit if _ALL_ are in limit.
    // This is like a logical AND.

    if (vec_.empty())
        return true;

    resolveInLimitReferences();

    int inlimitsWithLimits = 0;
    int inlimitCount       = 0;
    size_t theSize         = vec_.size();
    for (size_t i = 0; i < theSize; i++) {
        if (vec_[i].limit_this_node_only()) {
            if (vec_[i].incremented()) {
                continue; // Effectively, this inlimit no longer constrains any tasks, allowing them to run.
            }
        }
        Limit* limit = vec_[i].limit();
        if (limit) {
            inlimitsWithLimits++;
            if (limit->inLimit(vec_[i].tokens())) {
                inlimitCount++;
            }
        }
    }

    return (inlimitsWithLimits == inlimitCount);
}

void InLimitMgr::incrementInLimit(std::set<Limit*>& limitSet, const std::string& task_path) {
    // cout << "InLimitMgr::incrementInLimit " << node_->absNodePath() << endl;

    // *NOTE* each limit is incremented if within LIMIT, and that
    //        has not previously been updated.
    //  we could have the same in limit at the task and family level.
    //  in this case the task takes priority.
    //  suite suite
    //    family family
    //       inlimit limitname 12
    //       task t1
    //          inlimit limitname 4
    //    endfamily
    //  endsuite
    //
    // In this case the limit <limitname> is incremented by 4 _only_
    //
    // Note: It is illegal for a node to have the same inlimit but with
    //       different tokens:
    //
    //       task t1
    //          inlimit limitname 4
    //          inlimit limitname 2    // illegal and trapped by parser

    if (vec_.empty())
        return;

    resolveInLimitReferences();

    for (InLimit& inlimit : vec_) {
        Limit* limit = inlimit.limit();
        if (limit && limitSet.find(limit) == limitSet.end()) {
            limitSet.insert(limit);

            // cout << "InLimitMgr::incrementInLimit " << node_->absNodePath() << " LIMIT incremented " << endl;
            if (inlimit.limit_this_node_only()) {
                if (!inlimit.incremented()) {
                    // Can only increment this once, Notice we pass down this node path, i.e since this node is being
                    // limited
                    limit->increment(inlimit.tokens(), node_->absNodePath()); // node could suite || family || task
                    inlimit.set_incremented(true);
                }
            }
            else {
                limit->increment(inlimit.tokens(), task_path);
            }
        }
    }
}

void InLimitMgr::decrementInLimit(std::set<Limit*>& limitSet, const std::string& task_path) {
    // *NOTE* each limit is incremented if within LIMIT, and that has not previously been updated.
    //  we could have the same in limit at the task and family level.
    //  in this case the task takes priority.
    //  suite suite
    //    family family
    //       inlimit limitname 12
    //       task t1
    //          inlimit limitname 4
    //    endfamily
    //  endsuite
    //
    // In this case the limit <limitname> is incremented by 4 _only_
    //
    // Note: It is illegal for a node to have the same inlimit but with different tokens:
    //       task t1
    //          inlimit limitname 4
    //          inlimit limitname 2    // illegal and trapped by parser

    if (vec_.empty())
        return;

    resolveInLimitReferences();

    std::vector<task_ptr> task_vec;
    for (InLimit& inlimit : vec_) {
        Limit* limit = inlimit.limit();
        if (limit && limitSet.find(limit) == limitSet.end()) {
            limitSet.insert(limit);
            // cout << "InLimitMgr::incrementInLimit " << node_->absNodePath() << " LIMIT decremented " << endl;

            if (inlimit.limit_this_node_only()) {
                if (inlimit.incremented()) {

                    // Can only decrement this once, i.e when all child tasks are completed or aborted or queued or
                    // unknown
                    bool at_least_one_active = false;
                    if (task_vec.empty())
                        node_->get_all_tasks(task_vec); // Get tasks once, inside for loop
                    for (task_ptr task : task_vec) {
                        if (task->state() == NState::ACTIVE || task->state() == NState::SUBMITTED) {
                            at_least_one_active = true;
                            break;
                        }
                    }
                    if (at_least_one_active)
                        continue;

                    limit->decrement(inlimit.tokens(), node_->absNodePath());
                    inlimit.set_incremented(false);
                }
            }
            else {
                limit->decrement(inlimit.tokens(), task_path);
            }
        }
    }
}

void InLimitMgr::decrementInLimitForSubmission(std::set<Limit*>& limitSet, const std::string& task_path) {
    if (vec_.empty())
        return;

    resolveInLimitReferences();

    for (InLimit& inlimit : vec_) {
        Limit* limit = inlimit.limit();
        if (limit && limitSet.find(limit) == limitSet.end()) {
            limitSet.insert(limit);
            // cout << "InLimitMgr::incrementInLimit " << node_->absNodePath() << " LIMIT decremented " << endl;

            if (inlimit.limit_submission()) {
                limit->decrement(inlimit.tokens(), task_path);
            }
        }
    }
}

// #define DEBUG_WHY 1

static void add_consumed_paths(Limit* limit, std::stringstream& ss) {
    ss << "(";
    const std::set<std::string>& consumed_paths = limit->paths();
    int count                                   = 0;
    for (const auto& consumed_path : consumed_paths) {
        if (4 == count) {
            ss << "...";
            break;
        }
        ss << consumed_path << ",";
        count++;
    }
    ss << ")";
}

bool InLimitMgr::why(std::vector<std::string>& vec, bool html) const {
    // Note: if this correspond to a leaf node, like a task. Then it may not be
    // sufficient to just check in limits at this level  *BUT*
    // Node::top_down_why() and Node::bottom_up_why() will traverse the hierarchy
    // so we don't need to replicate it here.

#ifdef DEBUG_WHY
    std::cout << "InLimitMgr::why " << node_->debugNodePath() << " top_down:" << top_down << "\n";
#endif
    bool why_found = false;
    if (!inLimit()) {
#ifdef DEBUG_WHY
        std::cout << "   InLimitMgr::why " << node_->debugNodePath() << " NOT in limit\n";
#endif
        for (auto& i : vec_) {
            Limit* limit = i.limit();
            if (limit && !limit->inLimit(i.tokens())) {
                std::stringstream ss;
                if (i.pathToNode().empty()) {
                    ss << "limit " << limit->name() << " is full";
                }
                else {
                    if (html) {
                        std::stringstream s;
                        s << "[limit]" << i.pathToNode() << Str::COLON() << limit->name();
                        ss << Node::path_href_attribute(s.str()) << " is full";
                    }
                    else
                        ss << "limit " << i.pathToNode() << Str::COLON() << limit->name() << " is full";
                }

                // show node paths that have consumed a limit, Only show first 5, Otherwise string may be too long
                add_consumed_paths(limit, ss);

                vec.push_back(ss.str());
                why_found = true;
            }
        }
    }
    return why_found;
}

void InLimitMgr::check(std::string& errorMsg, std::string& warningMsg, bool reportErrors, bool reportWarnings) const {
    size_t theSize = vec_.size();
    for (size_t i = 0; i < theSize; i++) {
        (void)find_limit(vec_[i], errorMsg, warningMsg, reportErrors, reportWarnings);
    }
}

void InLimitMgr::resolveInLimit(InLimit& inLimit,
                                std::string& errorMsg,
                                std::string& warningMsg,
                                bool reportErrors,
                                bool reportWarnings) const {
    //		cout << "Inlimit " << inLimit.toString() << "\n";

    /// if limit pointer already setup use them. These are shared ptr backed, Hence if deleted beneath use should return
    /// 0;
    if (inLimit.limit()) {
        //		cout << "InLimitMgr::resolveInLimit " << inLimit.toString() << " Reusing limit ptr \n";
        return;
    }

    /// Find the limit referenced by the InLimit. i.e. Link inLimit to its LIMIT
    /// The return value can be NULL
    limit_ptr referencedLimit = find_limit(inLimit, errorMsg, warningMsg, reportErrors, reportWarnings);
    inLimit.limit(referencedLimit);
}

void InLimitMgr::auto_add_inlimit_externs(Defs* defs) const {
    std::string errorMsg;
    std::string warningMsg;
    size_t theSize = vec_.size();
    for (size_t i = 0; i < theSize; i++) {
        limit_ptr referencedLimit = find_limit(vec_[i], errorMsg, warningMsg, false, false);
        if (!referencedLimit.get()) {
            if (vec_[i].pathToNode().empty())
                defs->add_extern(vec_[i].name());
            else
                defs->add_extern(vec_[i].pathToNode() + ":" + vec_[i].name());
        }
    }
}

limit_ptr InLimitMgr::find_limit(const InLimit& inLimit,
                                 std::string& errorMsg,
                                 std::string& warningMsg,
                                 bool reportErrors,
                                 bool reportWarnings) const {
    if (inLimit.pathToNode().empty()) {

        // cout << "inLimit.pathToNode().empty() search " << debugType() << " " << node_->absNodePath() << "\n";
        limit_ptr referencedLimit = node_->findLimitUpNodeTree(inLimit.name());
        if (referencedLimit.get())
            return referencedLimit;

        if (reportWarnings) {

            // See if the name is defined, as an extern, in which case *DONT* warn:
            // This is client side specific, since server does not have externs.
            if (node_->defs()->find_extern(inLimit.name(), Str::EMPTY())) {
                return referencedLimit; // this is empty/NULL
            }

            std::stringstream ss;
            ss << "Warning: ";
            ss << node_->debugType() << " " << node_->absNodePath() << " has a " << inLimit.toString()
               << ", which cannot be found on the parent nodes\n";
            warningMsg += ss.str();
        }
        return referencedLimit; // this is empty/NULL
    }

    // *FIND* the node referenced by the In-Limit, this should hold the Limit.
    // cout << "Inlimit path not empty \n";
    string warning_message;
    node_ptr referenceNode = node_->findReferencedNode(inLimit.pathToNode(), inLimit.name(), warning_message);
    if (!referenceNode.get()) {
        /// Could not find the node which *HOLDS* the limit
        if (reportWarnings) {

            // OK a little bit of duplication, since findReferencedNode, will also look for externs
            // See if the Path:name is defined as an extern, in which case *DONT* warn:
            // This is client side specific, since server does not have externs.
            if (node_->defs()->find_extern(inLimit.pathToNode(), inLimit.name())) {
                return limit_ptr();
            }

            std::stringstream ss;
            ss << "Warning: " << node_->debugType() << " " << node_->absNodePath() << " has a " << inLimit.toString()
               << ", which cannot be found\n";
            warningMsg += ss.str();
        }
        return limit_ptr();
    }

    // *FOUND* the node which should hold the Limit.
    limit_ptr referencedLimit = referenceNode->find_limit(inLimit.name());
    if (!referencedLimit.get()) {

        // See if the name is defined, as an extern, in which case *DONT* warn:
        // This is client side specific, since server does not have externs.
        if (node_->defs()->find_extern(inLimit.pathToNode(), inLimit.name())) {
            return limit_ptr();
        }

        if (reportWarnings) {
            std::stringstream ss;
            ss << node_->debugType() << " " << node_->absNodePath() << " has a " << inLimit.toString() << " :";
            ss << "The referenced " << referenceNode->debugType() << " '" << referenceNode->absNodePath()
               << "' does not define the limit " << inLimit.name() << "\n";
            warning_message += ss.str();
            warningMsg += "Warning: ";
            warningMsg += warning_message;
            warningMsg += "\n";
        }
        return referencedLimit; // this is empty/NULL
    }

    // *FOUND* the referenced LIMIT. inlimit tokens must be less than limit.
    // ECFLOW-713 make this a warning and not error, it allows job control over multiple suites, at load/begin time
    if (inLimit.tokens() > referencedLimit->theLimit()) {
        if (reportWarnings) {
            // in limit exceeds the LIMIT value
            std::stringstream ss;
            ss << "Warning: ";
            ss << node_->debugType() << " " << node_->absNodePath() << " has a " << inLimit.toString()
               << " reference\n";
            ss << " with value '" << inLimit.tokens() << "' which exceeds '" << referencedLimit->theLimit()
               << "' defined on the Limit\n";
            warningMsg += ss.str();
        }
    }
    return referencedLimit;
}

void InLimitMgr::resolveInLimit(InLimit& inLimit) const {
    // Used in *test* only
    std::string errorMsg;
    std::string warningMsg;
    resolveInLimit(inLimit, errorMsg, warningMsg, false, false);
}

void InLimitMgr::resolveInLimitReferences() const {
    size_t theSize = vec_.size();
    if (theSize > 0) {
        std::string warningMsg;
        std::string errorMsg;
        for (size_t i = 0; i < theSize; i++) {
            resolveInLimit(vec_[i], errorMsg, warningMsg, false, false);
        }
    }
}

template <class Archive>
void InLimitMgr::serialize(Archive& ar) {
    ar(CEREAL_NVP(vec_));
}
CEREAL_TEMPLATE_SPECIALIZE(InLimitMgr);
