/** @file
 * @brief Operations on NFAs.
 */

#include <algorithm>
#include <list>
#include <unordered_set>

// MATA headers
#include <ranges>

#include "mata/nfa/delta.hh"
#include "mata/utils/sparse-set.hh"
#include "mata/nfa/nfa.hh"
#include "mata/nfa/algorithms.hh"
#include <mata/simlib/explicit_lts.hh>

using std::tie;

using namespace mata::utils;
using namespace mata::nfa;
using mata::Symbol;

using StateBoolArray = std::vector<bool>; ///< Bool array for states in the automaton.

namespace {
    Simlib::Util::BinaryRelation compute_fw_direct_simulation(const Nfa& aut) {
        OrdVector<mata::Symbol> used_symbols = aut.delta.get_used_symbols();
        mata::Symbol unused_symbol = 0;
        if (!used_symbols.empty() && *used_symbols.begin() == 0) {
            auto it = used_symbols.begin();
            unused_symbol = *it + 1;
            ++it;
            const auto used_symbols_end = used_symbols.end();
            while (it != used_symbols_end && unused_symbol == *it) {
                unused_symbol = *it + 1;
                ++it;
            }
            if (unused_symbol == 0) { // sanity check to see if we did not use the full range of mata::Symbol
                throw std::runtime_error("all symbols are used, we cannot compute simulation reduction");
            }
        }

        const size_t state_num{ aut.num_of_states() };
        Simlib::ExplicitLTS lts_for_simulation(state_num);

        for (const Transition& transition : aut.delta.transitions()) {
            lts_for_simulation.add_transition(transition.source, transition.symbol, transition.target);
        }

        // final states cannot be simulated by nonfinal -> we add new self-loops over final states with new symbol in LTS
        for (const State final_state : aut.final) {
            lts_for_simulation.add_transition(final_state, unused_symbol, final_state);
        }

        lts_for_simulation.init();
        return lts_for_simulation.compute_simulation();
    }

    void remove_covered_state(const StateSet& covering_set, const State remove, Nfa& nfa) {
        // help set to store elements to remove
        const auto delta_begin = nfa.delta[remove].begin();
        const auto remove_size = nfa.delta[remove].size();
        for (size_t i = 0; i < remove_size; i++) { // remove trans from covered state
            for (StateSet tmp_targets{ delta_begin->targets }; const State target : tmp_targets) {
                nfa.delta.remove(remove, delta_begin->symbol, target);
            }
        }

        for (const auto remove_transitions = nfa.delta.get_transitions_to(remove);
             const auto& move : remove_transitions) {
            // transfer transitions from covered state to covering set
            for (const State switch_target: covering_set) {
                nfa.delta.add(move.source, move.symbol, switch_target);
            }
            nfa.delta.remove(move);
        }

        // check final  and initial states
        nfa.final.erase(remove);
        if (nfa.initial.contains(remove)) {
            nfa.initial.erase(remove);
            for (const State new_init: covering_set) {
                nfa.initial.insert(new_init);
            }
        }
    }

    void check_covered_and_covering(std::vector<StateSet>& covering_states,                 // covering sets for each state
                                    std::vector<StateSet>& covering_indexes,                // indexes of covering states
                                    std::unordered_map<StateSet, State>& covered,           // map of covered states
                                    std::unordered_map<StateSet, State>& subset_map,        // map of non-covered states
                                    const State target_res, const StateSet& targets_orig,                      // current state to check
                                    Nfa& result) {

        auto it = subset_map.begin();

        // initiate with empty StateSets
        covering_states.emplace_back();
        covering_indexes.emplace_back();

        while (it != subset_map.end()) {               // goes through all found states
            if (it->first.is_subset_of(targets_orig)) {
                // check if targets_orig is covered
                // if so add covering state to its covering StateSet

                covering_states[target_res].insert(it->first);
                covering_indexes[target_res].insert(it->second);
            }
            else if (targets_orig.is_subset_of(it->first)) {
                // check if state in map is covered
                // if so add covering state to its covering StateSet

                covering_states[it->second].insert(targets_orig);
                covering_indexes[it->second].insert(target_res);

                // check is some already existing state that had a new covering state added turned fully covered
                if (it->first == covering_states[it->second]) {
                    // if any covered state is in the covering set of newly turned covered state,
                    // then it has to be replaced by its covering set
                    //
                    // same applies for any covered state, if it contains newly turned state in theirs
                    // covering set, then it has to be updated
                    State erase_state = it->second; // covered state to remove
                    for (const auto& state : covered | std::views::values) {
                        if (covering_indexes[state].contains(erase_state)) {
                            covering_indexes[state].erase(erase_state);
                            covering_indexes[state].insert(covering_indexes[erase_state]);
                        }
                        if (covering_indexes[erase_state].contains(state)) {
                            covering_indexes[erase_state].erase(state);
                            covering_indexes[erase_state].insert(covering_indexes[state]);
                        }
                    }

                    // remove covered state from the automaton, replace with covering set
                    remove_covered_state(covering_indexes[erase_state], erase_state, result);

                    auto temp = it++;
                    // move state from subset_map to covered
                    auto transfer = subset_map.extract(temp);
                    covered.insert(std::move(transfer));
                    continue;           // skip increasing map pointer
                }
            }
            ++it;
        }
    }

    void residual_recurse_coverable(
        const std::vector<StateSet>& macrostate_vec, // vector of nfa macrostates
        const std::vector<State>& covering_indexes, // sub-vector of macrostates indexes
        std::vector<bool>& covered, // flags of covered states
        std::vector<bool>& visited, // flags for visited states
        const size_t start_index, // starting index for covering_indexes vec
        std::unordered_map<StateSet, State>* subset_map, // mapping of indexes to macrostates
        Nfa& nfa) {
        const StateSet& check_state = macrostate_vec[covering_indexes[start_index]];
        StateSet covering_set; // doesn't contain duplicates
        std::vector<State> sub_covering_indexes; // // indexes of covering states

        for (auto i = covering_indexes.begin() + static_cast<long int>(start_index + 1), e = covering_indexes.end();
             i != e; ++i) {
            if (covered[*i]) // was already processed
                continue;

            if (macrostate_vec[*i].is_subset_of(check_state)) {
                covering_set.insert(macrostate_vec[*i]);                // is never covered
                sub_covering_indexes.push_back(*i);
            }
        }

        if (covering_set == check_state) {       // can recurse even without covered :thinking:

            const size_t covering_size = sub_covering_indexes.size()-1;
            for (size_t k = 0; k < covering_size; k++) {
                if (macrostate_vec[sub_covering_indexes[k]].size() == 1)            // end on single-sized states
                    break;

                if (visited[sub_covering_indexes[k]])                               // already processed
                    continue;

                visited[sub_covering_indexes[k]] = true;

                residual_recurse_coverable(macrostate_vec, sub_covering_indexes, covered, visited, k, subset_map, nfa);
            }

            covering_set.clear();                 // clear variable to store only needed macrostates
            for (const auto index : sub_covering_indexes) {
                if (covered[index] == 0) {
                    auto macrostate_ptr = subset_map->find(macrostate_vec[index]);
                        if (macrostate_ptr == subset_map->end())        // should never happen
                             throw std::runtime_error(std::to_string(__func__) + " couldn't find expected element in a map.");

                    covering_set.insert(macrostate_ptr->second);
                }
            }

            remove_covered_state(covering_set, subset_map->find(check_state)->second, nfa);
            covered[covering_indexes[start_index]] = true;
        }


    }
}

std::ostream &std::operator<<(std::ostream &os, const mata::nfa::Transition &trans) { // {{{
    const std::string result = "(" + std::to_string(trans.source) + ", " +
                         std::to_string(trans.symbol) + ", " + std::to_string(trans.target) + ")";
    return os << result;
}

bool mata::nfa::Nfa::make_complete(const Alphabet* const alphabet, const std::optional<State> sink_state) {
    return make_complete(get_symbols_to_work_with(*this, alphabet), sink_state);
}

bool mata::nfa::Nfa::make_complete(const OrdVector<Symbol>& symbols, const std::optional<State> sink_state) {
    bool transition_added{ false };
    const size_t num_of_states{ this->num_of_states() };
    const State sink_state_val{ sink_state.value_or(num_of_states) };

    OrdVector<Symbol> used_symbols{};
    for (State state{ 0 }; state < num_of_states; ++state) {
        for (const SymbolPost& symbol_post: delta[state]) {
            used_symbols.insert(symbol_post.symbol);
        }
        for (const OrdVector<Symbol> unused_symbols{ symbols.difference(used_symbols) };
             const Symbol symbol : unused_symbols) {
            delta.add(state, symbol, sink_state_val);
            transition_added = true;
        }
        used_symbols.clear();
    }

    if (transition_added && num_of_states <= sink_state_val) {
        for (const Symbol symbol: symbols) {
            delta.add(sink_state_val, symbol, sink_state_val);
        }
    }

    return transition_added;
}

//TODO: based on the comments inside, this function needs to be rewritten in a more optimal way.
Nfa mata::nfa::remove_epsilon(const Nfa& aut, const Symbol epsilon) {
    // cannot use multimap, because it can contain multiple occurrences of (a -> a), (a -> a)
    std::unordered_map<State, StateSet> epsilon_closure;

    // TODO: grossly inefficient
    // first we compute the epsilon closure
    const size_t num_of_states{aut.num_of_states() };
    for (size_t i{ 0 }; i < num_of_states; ++i) {
        for (const auto& trans : aut.delta[i]) {
            const auto [it, inserted] = epsilon_closure.insert({ i, { i } });
            if (trans.symbol == epsilon) {
                StateSet& closure = it->second;
                // TODO: Fix possibly insert to OrdVector. Create list already ordered, then merge (do not need to resize each time);
                closure.insert(trans.targets);
            }
        }
    }

    bool changed = true;
    while (changed) { // Compute the fixpoint.
        changed = false;
        for (size_t i = 0; i < num_of_states; ++i) {
            const StatePost& post{ aut.delta[i] };
            //TODO: make faster if default epsilon
            if (const auto eps_move_it{ post.find(epsilon) }; eps_move_it != post.end()) {
                StateSet& src_eps_cl = epsilon_closure[i];
                for (const State tgt : eps_move_it->targets) {
                    const StateSet& tgt_eps_cl = epsilon_closure[tgt];
                    for (const State st: tgt_eps_cl) {
                        if (src_eps_cl.count(st) == 0) {
                            changed = true;
                            break;
                        }
                    }
                    src_eps_cl.insert(tgt_eps_cl);
                }
            }
        }
    }

    // Construct the automaton without epsilon transitions.
    Nfa result{ Delta{}, aut.initial, aut.final, aut.alphabet };
    for (const auto& [state, closure_states] : epsilon_closure) {
        for (const State eps_cl_state : closure_states) {
            if (aut.final[eps_cl_state]) result.final.insert(state);
            for (const SymbolPost& move : aut.delta[eps_cl_state]) {
                if (move.symbol == epsilon) continue;
                // TODO: this could be done more efficiently if we had a better add method
                for (const State tgt_state : move.targets) {
                    result.delta.add(state, move.symbol, tgt_state);
                }
            }
        }
    }
    return result;
}

Nfa mata::nfa::fragile_revert(const Nfa& aut) {
    const size_t num_of_states{ aut.num_of_states() };

    Nfa result(num_of_states);

    result.initial = aut.final;
    result.final = aut.initial;

    // Compute non-epsilon symbols.
    OrdVector<Symbol> symbols = aut.delta.get_used_symbols();
    if (symbols.empty()) { return result; }
    if (symbols.back() == EPSILON) { symbols.pop_back(); }
    // size of the "used alphabet", i.e. max symbol+1 or 0
    const Symbol alphabet_size =  (symbols.empty()) ? 0 : (symbols.back()+1);

#ifdef _STATIC_STRUCTURES_
    //STATIC DATA STRUCTURES:
    // Not sure that it works ideally, whether the space for the inner vectors stays there.
    static std::vector<std::vector<State>> sources;
    static std::vector<std::vector<State>> targets;
    static std::vector<State> e_sources;
    static std::vector<State> e_targets;
    if (alphabet_size>sources.size()) {
        sources.resize(alphabet_size);
        targets.resize(alphabet_size);
    }

    e_sources.clear();
    e_targets.clear();

    //WHEN ONLY MAX SYMBOL IS COMPUTED
    // for (int i = 0;i<alphasize;i++) {
    //     for (int i = 0;i<alphasize;i++) {
    //         if (!sources[i].empty())
    //         {
    //             sources[i].resize(0);
    //             targets[i].resize(0);
    //         }
    //     }
    // }

    //WHEN ALL SYMBOLS ARE COMPUTED
    for (Symbol symbol: symbols) {
        if(!sources[symbol].empty()) {
            sources[symbol].clear();
            targets[symbol].clear();
        }
    }
#else
    // NORMAL, NON-STATIC DATA STRUCTURES:
    // All transition of delta are to be copied here, into two arrays of transition sources and targets indexed by the
    //  transition symbol.
    // There is a special treatment for epsilon, since we want the arrays to be only as long as the largest symbol in
    // the automaton, and epsilon is the maximum (so we don't want to have the maximum array length whenever epsilon is
    // present).
    std::vector<std::vector<State>> sources (alphabet_size);
    std::vector<std::vector<State>> targets (alphabet_size);
    std::vector<State> e_sources;
    std::vector<State> e_targets;
#endif

    //Copy all transition with non-e symbols to the arrays of sources and targets indexed by symbols.
    //Targets and sources of e-transitions go to the special place.
    //Important: since we are going through delta in order of sources, the sources arrays are all ordered.
    for (State source_state{ 0 }; source_state < num_of_states; ++source_state) {
        for (const SymbolPost &move: aut.delta[source_state]) {
            if (move.symbol == EPSILON) {
                for (const State target_state: move.targets) {
                    //reserve_on_insert(e_sources);
                    e_sources.push_back(source_state);
                    //reserve_on_insert(e_targets);
                    e_targets.push_back(target_state);
                }
            }
            else {
                for (const State target_state: move.targets) {
                    //reserve_on_insert(sources[move.symbol]);
                    sources[move.symbol].push_back(source_state);
                    //reserve_on_insert(targets[move.symbol]);
                    targets[move.symbol].push_back(target_state);
                }
            }
        }
    }

    //Now make the delta of the reversed automaton.
    //Important: since sources are ordered, when adding them as targets, we can just push them back.
    result.delta.reserve(num_of_states);

    // adding non-e transitions
    for (const Symbol symbol: symbols) {
        for (size_t i{ 0 }; i < sources[symbol].size(); ++i) {
            const State tgt_state = sources[symbol][i];
            const State src_state = targets[symbol][i];
            StatePost& src_post = result.delta.mutable_state_post(src_state);
            if (src_post.empty() || src_post.back().symbol != symbol) { src_post.push_back(SymbolPost(symbol)); }
            src_post.back().push_back(tgt_state);
        }
    }

    // adding e-transitions
    for (size_t i{ 0 }; i < e_sources.size(); ++i) {
        const State tgt_state = e_sources[i];
        const State src_state = e_targets[i];
        StatePost & src_post = result.delta.mutable_state_post(src_state);
        if (src_post.empty() || src_post.back().symbol != EPSILON) {
            src_post.push_back(SymbolPost(EPSILON));
        }
        src_post.back().push_back(tgt_state);
    }

    //sorting the targets
    //Hm I don't know why I put this here, but it should not be needed ...
    //for (State q = 0, states_num = result.delta.post_size(); q<states_num;++q) {
    //    for (auto m = result.delta.get_mutable_post(q).begin(); m != result.delta.get_mutable_post(q).end(); ++m) {
    //        sort_and_rmdupl(m->targets);
    //    }
    //}

    return result;
}

Nfa mata::nfa::simple_revert(const Nfa& aut) {
    Nfa result;
    result.clear();

    const size_t num_of_states{ aut.num_of_states() };
    result.delta.allocate(num_of_states);

    for (State source_state{ 0 }; source_state < num_of_states; ++source_state) {
        for (const SymbolPost &transition: aut.delta[source_state]) {
            for (const State target_state: transition.targets) {
                result.delta.add(target_state, transition.symbol, source_state);
            }
        }
    }

    result.initial = aut.final;
    result.final = aut.initial;

    return result;
}

//not so great, can be removed
Nfa mata::nfa::somewhat_simple_revert(const Nfa& aut) {
    const size_t num_of_states{ aut.num_of_states() };

    Nfa result(num_of_states);

    result.initial = aut.final;
    result.final = aut.initial;

    for (State source_state{ 0 }; source_state < num_of_states; ++source_state) {
        for (const SymbolPost &transition: aut.delta[source_state]) {
            for (const State target_state: transition.targets) {
                StatePost & post = result.delta.mutable_state_post(target_state);
                //auto move = std::find(post.begin(),post.end(),Move(transition.symbol));
                if (auto move = post.find(SymbolPost(transition.symbol)); move == post.end()) {
                    //post.push_back(Move(transition.symbol,sourceState));
                    post.insert(SymbolPost(transition.symbol, source_state));
                } else
                    move->push_back(source_state);
                    //move->insert(sourceState);
            }
        }
    }

    //sorting the targets
    for (State q = 0, states_num = result.delta.num_of_states(); q < states_num; ++q) {
        //Post & post = result.delta.get_mutable_post(q);
        //utils::sort_and_rmdupl(post);
        for (SymbolPost& m: result.delta.mutable_state_post(q)) { sort_and_rmdupl(m.targets); }
    }

    return result;
}

Nfa mata::nfa::revert(const Nfa& aut) {
    return simple_revert(aut);
    //return fragile_revert(aut);
    //return somewhat_simple_revert(aut);
}

bool mata::nfa::Nfa::is_deterministic() const {
    if (initial.size() != 1) { return false; }
    if (delta.empty()) { return true; }

    const size_t aut_size{ num_of_states() };
    for (size_t i = 0; i < aut_size; ++i) {
        for (const auto& symbol_post : delta[i]) { if (symbol_post.num_of_targets() != 1) { return false; } }
    }
    return true;
}

bool Nfa::is_complete(const Alphabet* const alphabet) const {
    return is_complete(get_symbols_to_work_with(*this, alphabet));
}

bool mata::nfa::Nfa::is_complete(const OrdVector<Symbol>& symbols) const {
    // TODO: make a general function for traversal over reachable states that can be shared by other functions?
    std::list<State> worklist(initial.begin(), initial.end());
    std::unordered_set<State> processed(initial.begin(), initial.end());

    while (!worklist.empty()) {
        const State state = *worklist.begin();
        worklist.pop_front();

        size_t n = 0;      // counter of symbols
        if (!delta.empty()) {
            for (const auto &symb_stateset: delta[state]) {
                ++n;
                if (!haskey(symbols, symb_stateset.symbol)) {
                    throw std::runtime_error(std::to_string(__func__) +
                                             ": encountered a symbol that is not in the provided alphabet");
                }

                for (const auto &tgt_state: symb_stateset.targets) {
                    bool inserted;
                    tie(std::ignore, inserted) = processed.insert(tgt_state);
                    if (inserted) { worklist.push_back(tgt_state); }
                }
            }
        }

        if (symbols.size() != n) { return false; }
    }

    return true;
}

std::pair<Run, bool> mata::nfa::Nfa::get_word_for_path(const Run& run) const {
    if (run.path.empty()) { return {{}, true}; }

    Run word;
    State cur = run.path[0];
    for (size_t i = 1; i < run.path.size(); ++i) {
        const State new_st = run.path[i];
        bool found = false;
        if (!this->delta.empty()) {
            for (const auto &symbol_map: this->delta[cur]) {
                for (const State st: symbol_map.targets) {
                    if (st == new_st) {
                        word.word.push_back(symbol_map.symbol);
                        found = true;
                        break;
                    }
                }
                if (found) { break; }
            }
        }
        if (!found) { return {{}, false}; }
        cur = new_st;    // update current state
    }
    return {word, true};
}

//TODO: this is not efficient
StateSet mata::nfa::Nfa::read_word(const Run& run, const bool use_epsilon) const {
    StateSet current_post(this->initial);

    if (use_epsilon) {
        // Compute epsilon closure from the initial states.
        current_post = this->post(current_post, EPSILON, EpsilonClosureOpt::Before);
    }

    const EpsilonClosureOpt epsilon_closure_opt = use_epsilon ? EpsilonClosureOpt::After : EpsilonClosureOpt::None;
    for (const Symbol sym : run.word) {
        current_post = this->post(current_post, sym, epsilon_closure_opt);
        if (current_post.empty()) { return current_post; }
    }

    return current_post;
}

std::optional<State> mata::nfa::Nfa::read_word_det(const Run& run) const {
    if (initial.empty()) return std::nullopt;
    State current_post = *initial.begin();

    for (const Symbol sym : run.word) {
        const StateSet& successors = delta.get_successors(current_post, sym);
        if (successors.empty()) return std::nullopt;
        current_post = *successors.begin();
    }

    return current_post;
}

//TODO: this is not efficient
bool Nfa::is_in_lang(const Run& run, const bool use_epsilon, const bool match_prefix) const {
    if (match_prefix) {
        StateSet current_post(this->initial);

        if (use_epsilon) {
            // Compute epsilon closure from the initial states.
            current_post = this->post(current_post, EPSILON, EpsilonClosureOpt::Before);
        }

        const EpsilonClosureOpt epsilon_closure_opt = use_epsilon ? EpsilonClosureOpt::After : EpsilonClosureOpt::None;
        for (const Symbol sym : run.word) {
            if (this->final.intersects_with(current_post)) { return true; }
            current_post = this->post(current_post, sym, epsilon_closure_opt);
            if (current_post.empty()) { return false; }
        }

        return this->final.intersects_with(current_post);
    }

    return this->final.intersects_with(read_word(run, use_epsilon));
}

bool mata::nfa::Nfa::is_lang_empty(Run* cex) const {
    //TODO: hot fix for performance reasons for TACAS.
    // Perhaps make the get_useful_states return a witness on demand somehow.
    if (!cex) {
        return is_lang_empty_scc();
    }

    std::list<State> worklist(initial.begin(), initial.end());
    std::unordered_set<State> processed(initial.begin(), initial.end());

    // 'paths[s] == t' denotes that state 's' was accessed from state 't',
    // 'paths[s] == s' means that 's' is an initial state
    std::map<State, State> paths;
    // Initialize paths.
    for (const State s: worklist) { paths[s] = s; }

    while (!worklist.empty()) {
        State state{ worklist.front() };
        worklist.pop_front();

        if (final[state]) {
            if (nullptr != cex) {
                cex->path.clear();
                cex->path.push_back(state);
                while (paths[state] != state) {
                    state = paths[state];
                    cex->path.push_back(state);
                }
                std::ranges::reverse(cex->path);
                cex->word = this->get_word_for_path(*cex).first.word;
            }
            return false;
        }

        if (delta.empty()) { continue; }

        for (const SymbolPost& symbol_post: delta[state]) {
            for (const State& target: symbol_post.targets) {
                bool inserted;
                tie(std::ignore, inserted) = processed.insert(target);
                if (inserted) {
                    worklist.push_back(target);
                    // Also set that tgt_state was accessed from state.
                    paths[target] = state;
                } else { assert(haskey(paths, target)); /* Invariant. */ }
            }
        }
    } // while (!worklist.empty()).
    return true;
} // is_lang_empty().


Nfa mata::nfa::algorithms::minimize_brzozowski(const Nfa& aut) {
    //compute the minimal deterministic automaton, Brzozovski algorithm
    return determinize(revert(determinize(revert(aut))));
}

Nfa mata::nfa::minimize(
                const Nfa& aut,
                const ParameterMap& params)
{
    Nfa result;
    // setting the default algorithm
    decltype(algorithms::minimize_brzozowski)* algo = algorithms::minimize_brzozowski;
    if (!haskey(params, "algorithm")) {
        throw std::runtime_error(std::to_string(__func__) +
            " requires setting the \"algorithm\" key in the \"params\" argument; "
            "received: " + std::to_string(params));
    }

    if (const std::string& str_algo = params.at("algorithm"); "brzozowski" == str_algo) { /* default */ } else if (
        "hopcroft" == str_algo) { algo = algorithms::minimize_hopcroft; } else {
        throw std::runtime_error(std::to_string(__func__) +
                                 " received an unknown value of the \"algorithm\" key: " + str_algo);
    }

    return algo(aut);
}

// Anonymous namespace for the Hopcroft minimization algorithm.
namespace {
template <typename T>
/**
 * A class for partitionable sets of elements. All elements are stored in a contiguous vector.
 * Elements from the same set are contiguous in the vector. Auxiliary data (indices, positions, etc.) are
 * stored in separate vectors.
 */
class RefinablePartition {
public:
    static_assert(std::is_unsigned_v<T>, "T must be an unsigned type.");
    static const T no_more_elements = std::numeric_limits<T>::max();
    static constexpr size_t no_split = std::numeric_limits<size_t>::max();
    size_t num_of_sets;            ///< The number of sets in the partition.
    std::vector<size_t> set_idx;   ///< For each element, tells the index of the set it belongs to.

private:
    std::vector<T> elems_;       ///< Contains all elements in an order such that the elements of the same set are contiguous.
    std::vector<T> location_;    ///< Contains the location of each element in the elements vector.
    std::vector<size_t> first_;  ///< Contains the index of the first element of each set in the elements vector.
    std::vector<size_t> end_;    ///< Contains the index after the last element of each set in the elements vector.
    std::vector<size_t> mid_;    ///< Contains the index after the last element of the first half of the set in the elements vector.

public:
    /**
     * @brief Construct a new Refinable Partition for equivalence classes of states.
     *
     * @param num_of_states The number of states.
     */
    explicit RefinablePartition(const size_t num_of_states)
        : num_of_sets(1), set_idx(num_of_states), elems_(num_of_states), location_(num_of_states),
          first_(num_of_states), end_(num_of_states), mid_(num_of_states) {
        // Initially, all states are in the same equivalence class.
        first_[0] = mid_[0] = 0;
        end_[0] = num_of_states;
        for (State e = 0; e < num_of_states; ++e) {
            elems_[e] = location_[e] = e;
            set_idx[e] = 0;
        }
    }

    /**
     * @brief Construct a new Refinable Partition for sets of splitters (transitions incoming to
     *  equivalence classes under a common symbol). Initially, the partition has n sets, where n is the alphabet size.
     *
     * @param delta The transition function.
     */
    explicit RefinablePartition(const Delta &delta)
        : num_of_sets(0), set_idx(), elems_(), location_(), first_(), end_(), mid_() {
        size_t num_of_transitions = 0;
        std::vector<size_t> counts;
        std::unordered_map<Symbol, size_t> symbol_map;

        // Transitions are grouped by symbols using counting sort in time O(m).
        // Count the number of elements and the number of sets.
        for (const auto &trans : delta.transitions()) {
            if (const Symbol a{ trans.symbol }; !symbol_map.contains(a)) {
                symbol_map[a] = num_of_sets++;
                counts.push_back(1);
            } else {
                ++counts[symbol_map[a]];
            }
            ++num_of_transitions;
        }

        // Initialize data structures.
        elems_.resize(num_of_transitions);
        location_.resize(num_of_transitions);
        set_idx.resize(num_of_transitions);
        first_.resize(num_of_transitions);
        end_.resize(num_of_transitions);
        mid_.resize(num_of_transitions);

        // Compute set indices.
        // Use (mid - 1) as an index for the insertion.
        first_[0] = 0;
        end_[0] = counts[0];
        mid_[0] = end_[0];
        for (size_t i = 1; i < num_of_sets; ++i) {
            first_[i] = end_[i - 1];
            end_[i] = first_[i] + counts[i];
            mid_[i] = end_[i];
        }

        // Fill the sets from the back.
        // Mid, decremented before use, is used as an index for the next element.
        size_t trans_idx = 0;   // Index of the transition in the (flattened) delta.
        for (const auto &trans : delta.transitions()) {
            const Symbol a = trans.symbol;
            const size_t a_idx = symbol_map[a];
            const size_t trans_loc = mid_[a_idx] - 1;
            mid_[a_idx] = trans_loc;
            elems_[trans_loc] = trans_idx;
            location_[trans_idx] = trans_loc;
            set_idx[trans_idx] = a_idx;
            ++trans_idx;
        }
    }

    RefinablePartition(const RefinablePartition &other)
        : num_of_sets(other.num_of_sets), set_idx(other.set_idx), elems_(other.elems_),
          location_(other.location_), first_(other.first_), end_(other.end_), mid_(other.mid_) {}

    RefinablePartition(RefinablePartition &&other) noexcept
        : num_of_sets(other.num_of_sets), set_idx(std::move(other.set_idx)), elems_(std::move(other.elems_)),
          location_(std::move(other.location_)), first_(std::move(other.first_)), end_(std::move(other.end_)), mid_(std::move(other.mid_)) {}

    RefinablePartition& operator=(const RefinablePartition &other) {
        if (this != &other) {
            elems_ = other.elems_;
            set_idx = other.set_idx;
            location_ = other.location_;
            first_ = other.first_;
            end_ = other.end_;
            mid_ = other.mid_;
            num_of_sets = other.num_of_sets;
        }
        return *this;
    }

    RefinablePartition& operator=(RefinablePartition &&other) noexcept {
        if (this != &other) {
            elems_ = std::move(other.elems_);
            set_idx = std::move(other.set_idx);
            location_ = std::move(other.location_);
            first_ = std::move(other.first_);
            end_ = std::move(other.end_);
            mid_ = std::move(other.mid_);
            num_of_sets = other.num_of_sets;
        }
        return *this;
    }

    /**
     * @brief Get the number of elements across all sets.
     *
     * @return The number of elements.
     */
    inline size_t size() const { return elems_.size(); }

    /**
     * @brief Get the size of the set.
     *
     * @param s The set index.
     * @return The size of the set.
     */
    inline size_t size_of_set(const size_t s) const { return end_[s] - first_[s]; }

    /**
     * @brief Get the first element of the set.
     *
     * @param s The set index.
     * @return The first element of the set.
     */
    inline T get_first(const size_t s) const { return elems_[first_[s]]; }

    /**
     * @brief Get the next element of the set.
     *
     * @param e The element.
     * @return The next element of the set or NO_MORE_ELEMENTS if there is no next element.
     */
    inline T get_next(const T e) const {
        if (location_[e] + 1 >= end_[set_idx[e]]) { return no_more_elements; }
        return elems_[location_[e] + 1];
    }

    /**
     * @brief Mark the element and move it to the first half of the set.
     *
     * @param e The element.
     */
    void mark(const T e) {
        const size_t e_set = set_idx[e];
        const size_t e_loc = location_[e];
        const size_t e_set_mid = mid_[e_set];
        if (e_loc >= e_set_mid) {
            elems_[e_loc] = elems_[e_set_mid];
            location_[elems_[e_loc]] = e_loc;
            elems_[e_set_mid] = e;
            location_[e] = e_set_mid;
            mid_[e_set] = e_set_mid + 1;
        }
    }

    /**
     * @brief Test if the set has no marked elements.
     *
     * @param s The set index.
     * @return True if the set has no marked elements, false otherwise.
     */
    inline bool has_no_marks(const size_t s) const { return mid_[s] == first_[s]; }

    /**
     * @brief Split the set into two sets according to the marked elements (the mid).
     * The first set name remains the same.
     *
     * @param s The set index.
     * @return The new set index.
     */
    size_t split(const size_t s) {
        if (mid_[s] == end_[s]) {
            // If no elements were marked, move the mid to the end (no split needed).
            mid_[s] = first_[s];
        }
        if (mid_[s] == first_[s]) {
            // If all or no elements were marked, there is no need to split the set.
            return no_split;
        } else {
            // Split the set according to the mid.
            first_[num_of_sets] = first_[s];
            mid_[num_of_sets] = first_[s];
            end_[num_of_sets] = mid_[s];
            first_[s] = mid_[s];
            for (size_t l = first_[num_of_sets]; l < end_[num_of_sets]; ++l) {
                set_idx[elems_[l]] = num_of_sets;
            }
            return num_of_sets++;
        }
    }
};
} // namespace


Nfa mata::nfa::algorithms::minimize_hopcroft(const Nfa& dfa_trimmed) {
    if (dfa_trimmed.delta.num_of_transitions() == 0) {
        // The automaton is trivially minimal.
        return Nfa{ dfa_trimmed };
    }
    assert(dfa_trimmed.is_deterministic());
    assert(dfa_trimmed.get_useful_states().count() == dfa_trimmed.num_of_states());

    // Initialize equivalence classes. The initial partition is {Q}.
    RefinablePartition<State> brp(dfa_trimmed.num_of_states());
    // Initialize splitters. A splitter is a set of transitions
    // over a common symbol incoming to an equivalence class. Initially,
    // the partition has m splitters, where m is the alphabet size.
    RefinablePartition<size_t> trp(dfa_trimmed.delta);

    // Initialize vector of incoming transitions for each state. Transitions
    // are represented only by their indices in the (flattened) delta.
    // Initialize mapping from transition index to its source state.
    std::vector<State> trans_source_map(trp.size());
    std::vector<std::vector<size_t>> incoming_trans_idxs(brp.size(), std::vector<size_t>());
    size_t trans_idx = 0;
    for (const auto &trans : dfa_trimmed.delta.transitions()) {
        trans_source_map[trans_idx] = trans.source;
        incoming_trans_idxs[trans.target].push_back(trans_idx);
        ++trans_idx;
    }

    // Worklists for the Hopcroft algorithm.
    std::stack<size_t> unready_splitters;    // Splitters that will be used in the backpropagation.
    std::stack<size_t> touched_blocks;  // Blocks (equivalence classes) touched during backpropagation.
    std::stack<size_t> touched_splitters;    // Splitters touched (in the split_block function) as a result of backpropagation.

    /**
     * @brief Split the block (equivalence class) according to the marked states.
     *
     * @param b The block index.
     */
    auto split_block = [&](const size_t b) {
        // touched_splitters has been moved to a higher scope to avoid multiple constructions/destructions.
        assert(touched_splitters.empty());
        size_t b_prime = brp.split(b);  // One block will keep the old name 'b'.
        if (b_prime == RefinablePartition<size_t>::no_split) {
            // All or no states in the block were marked (touched) during the backpropagation.
            return;
        }
        // According to the Hopcroft's algorithm, it is sufficient to work only with the smaller block.
        if (brp.size_of_set(b) < brp.size_of_set(b_prime)) {
            b_prime = b;
        }
        // Split the transitions of the splitters according to the new partitioning.
        // Transitions in one splitter must have the same symbol and go to the same block.
        for (State q = brp.get_first(b_prime); q != RefinablePartition<State>::no_more_elements; q = brp.get_next(q)) {
            for (const size_t trans_index : incoming_trans_idxs[q]) {
                if (const size_t splitter_idx = trp.set_idx[trans_index]; trp.has_no_marks(splitter_idx)) {
                    touched_splitters.push(splitter_idx);
                }
                // Mark the transition in the splitter and move it to the first half of the set.
                trp.mark(trans_index);
            }
        }
        // Refine all splitters where some transitions were marked.
        while (!touched_splitters.empty()) {
            const size_t splitter_idx = touched_splitters.top();
            touched_splitters.pop();
            if (const size_t splitter_pime = trp.split(splitter_idx);
                splitter_pime != RefinablePartition<size_t>::no_split) {
                // Use the new splitter for further refinement of the equivalence classes.
                unready_splitters.push(splitter_pime);
            }
        }
    };

    // Use all splitters for the initial refinement.
    for (size_t splitter_idx = 0; splitter_idx < trp.num_of_sets; ++splitter_idx) {
        unready_splitters.push(splitter_idx);
    }

    // In the first refinement, we split the equivalence classes according to the final states.
    for (const State q : dfa_trimmed.final) {
        brp.mark(q);
    }
    split_block(0);

    // Main loop of the Hopcroft's algorithm.
    while (!unready_splitters.empty()) {
        const size_t splitter_idx = unready_splitters.top();
        unready_splitters.pop();
        // Backpropagation.
        // Fire back all transitions of the splitter. (Transitions over the same
        // symbol that go to the same block.) Mark the source states of these transitions.
        for (size_t trans_index = trp.get_first(splitter_idx);
             trans_index != RefinablePartition<size_t>::no_more_elements; trans_index = trp.get_next(trans_index)) {
            const State q = trans_source_map[trans_index];
            if (const size_t b_prime = brp.set_idx[q]; brp.has_no_marks(b_prime)) { touched_blocks.push(b_prime); }
            brp.mark(q);
        }
        // Try to split the blocks touched during the backpropagation.
        // The block will be split only if some states (not all) were touched (marked).
        while (!touched_blocks.empty()) {
            const size_t b = touched_blocks.top();
            touched_blocks.pop();
            split_block(b);
        }
    }

    // Construct the minimized automaton using equivalence classes (BRP).
    assert(dfa_trimmed.initial.size() == 1);
    Nfa result(brp.num_of_sets, { brp.set_idx[*dfa_trimmed.initial.begin()] }, {});
    for (size_t block_idx = 0; block_idx < brp.num_of_sets; ++block_idx) {
        const State q = brp.get_first(block_idx);
        if (dfa_trimmed.final.contains(q)) {
            result.final.insert(block_idx);
        }
        StatePost &mut_state_post = result.delta.mutable_state_post(block_idx);
        for (const SymbolPost &symbol_post : dfa_trimmed.delta[q]) {
            assert(symbol_post.targets.size() == 1);
            const State target = brp.set_idx[*symbol_post.targets.begin()];
            mut_state_post.push_back(SymbolPost{ symbol_post.symbol, StateSet{ target } });
        }
    }

    return result;
}

Nfa mata::nfa::product(const Nfa &lhs, const Nfa &rhs, const ProductFinalStateCondition final_condition,
                       const Symbol first_epsilon, std::unordered_map<std::pair<State,State>,State> *prod_map) {

    std::function<bool(const State, const State)> is_product_state_final_func;
    if (final_condition == ProductFinalStateCondition::Or) {
        if (lhs.initial.empty() || lhs.final.empty()) { return rhs; }
        if (rhs.initial.empty() || rhs.final.empty()) { return lhs; }
        is_product_state_final_func = [&](const State lhs_state, const State rhs_state) {
            return lhs.final.contains(lhs_state) || rhs.final.contains(rhs_state);
        };
    } else if (final_condition == ProductFinalStateCondition::And) {
        if (lhs.initial.empty() || lhs.final.empty()) { return Nfa{}; }
        if (rhs.initial.empty() || rhs.final.empty()) { return Nfa{}; }
        is_product_state_final_func = [&](const State lhs_state, const State rhs_state) {
            return lhs.final.contains(lhs_state) && rhs.final.contains(rhs_state);
        };
    } else {
        throw std::runtime_error(std::to_string(__func__) + " received an unknown value of the \"final_condition\"");
    }

    return algorithms::product(lhs, rhs, std::move(is_product_state_final_func), first_epsilon, prod_map);
}

Nfa mata::nfa::intersection(const Nfa& lhs, const Nfa& rhs, const Symbol first_epsilon, std::unordered_map<std::pair<State, State>, State>  *prod_map) {
    return product(lhs, rhs, ProductFinalStateCondition::And, first_epsilon, prod_map);
}

Nfa mata::nfa::union_nondet(const Nfa &lhs, const Nfa &rhs) { return Nfa{ lhs }.unite_nondet_with(rhs); }

Nfa mata::nfa::union_det_complete(const Nfa &lhs, const Nfa &rhs) {
    assert(lhs.is_deterministic());
    assert(rhs.is_deterministic());
    assert(lhs.is_complete());
    assert(rhs.is_complete());
    return product(lhs, rhs, ProductFinalStateCondition::Or, EPSILON);
}

Simlib::Util::BinaryRelation mata::nfa::algorithms::compute_relation(const Nfa& aut, const ParameterMap& params) {
    if (!haskey(params, "relation")) {
        throw std::runtime_error(std::to_string(__func__) +
                                 " requires setting the \"relation\" key in the \"params\" argument; "
                                 "received: " + std::to_string(params));
    }
    if (!haskey(params, "direction")) {
        throw std::runtime_error(std::to_string(__func__) +
                                 " requires setting the \"direction\" key in the \"params\" argument; "
                                 "received: " + std::to_string(params));
    }

    const std::string& relation = params.at("relation");
    if (const std::string& direction = params.at("direction"); "simulation" == relation && direction == "forward") {
        return compute_fw_direct_simulation(aut);
    } else {
        throw std::runtime_error(
            std::to_string(__func__) +
            " received an unknown value of the \"relation\" key: " + relation
        );
    }
}

Nfa mata::nfa::reduce(const Nfa &aut, StateRenaming *state_renaming, const ParameterMap& params) {
    if (!haskey(params, "algorithm")) {
        throw std::runtime_error(std::to_string(__func__) +
                                 " requires setting the \"algorithm\" key in the \"params\" argument; "
                                 "received: " + std::to_string(params));
    }

    Nfa result;
    std::unordered_map<State,State> reduced_state_map;
    if (const std::string& algorithm = params.at("algorithm"); "simulation" == algorithm) {
        result = algorithms::reduce_simulation(aut, reduced_state_map);
    } else if ("residual" == algorithm) {
        // reduce type either 'after' or 'with' creation of residual automaton
        if (!haskey(params, "type")) {
            throw std::runtime_error(std::to_string(__func__) +
                                    " requires setting the \"type\" key in the \"params\" argument; "
                                    "received: " + std::to_string(params));
        }
        // forward or backward canonical residual automaton
        if (!haskey(params, "direction")) {
            throw std::runtime_error(std::to_string(__func__) +
                                    " requires setting the \"direction\" key in the \"params\" argument; "
                                    "received: " + std::to_string(params));
        }

        const std::string& residual_type = params.at("type");
        const std::string& residual_direction = params.at("direction");

        result = algorithms::reduce_residual(aut, reduced_state_map, residual_type, residual_direction);
    } else {
        throw std::runtime_error(std::to_string(__func__) +
                                 " received an unknown value of the \"algorithm\" key: " + algorithm);
    }

    if (state_renaming) {
        state_renaming->clear();
        *state_renaming = reduced_state_map;
    }
    return result;
}

Nfa mata::nfa::determinize(
    const Nfa&  aut, std::unordered_map<StateSet, State>* subset_map,
    std::optional<std::function<bool(const Nfa&, const State, const StateSet&)>> macrostate_discover
) {
    Nfa result{};
    //assuming all sets targets are non-empty
    std::vector<std::pair<State, StateSet>> worklist{};
    std::unordered_map<StateSet, State> subset_map_local{};
    if (subset_map == nullptr) { subset_map = &subset_map_local; }

    const StateSet initial_states_orig{ aut.initial };
    const State initial_state_res{ result.add_state() };
    result.initial.insert(initial_state_res);

    if (aut.final.intersects_with(initial_states_orig)) {
        result.final.insert(initial_state_res);
    }
    worklist.emplace_back(initial_state_res, initial_states_orig);
    (*subset_map)[mata::utils::OrdVector<State>(initial_states_orig)] = initial_state_res;
    if (aut.delta.empty()) { return result; }
    if (macrostate_discover.has_value() && !(*macrostate_discover)(result, initial_state_res, initial_states_orig)) { return result; }

    using Iterator = mata::utils::OrdVector<SymbolPost>::const_iterator;
    SynchronizedExistentialSymbolPostIterator synchronized_iterator;

    while (!worklist.empty()) {
        const auto [state_res, states_orig]{ std::move(worklist.back()) };
        worklist.pop_back();
        if (states_orig.empty()) {
            // This should not happen assuming all sets targets are non-empty.
            break;
        }

        // add moves of S to the sync ex iterator
        synchronized_iterator.reset();
        for (State q: states_orig) {
            push_back(synchronized_iterator, aut.delta[q]);
        }

        while (synchronized_iterator.advance()) {
            // extract post from the synchronized_iterator iterator
            const std::vector<Iterator>& symbol_posts = synchronized_iterator.get_current();
            Symbol current_symbol = (*symbol_posts.begin())->symbol;
            StateSet targets_orig = synchronized_iterator.unify_targets();

            const auto existing_target_it = subset_map->find(targets_orig);
            State target_res;
            if (existing_target_it != subset_map->end()) {
                target_res = existing_target_it->second;
            } else {
                target_res = result.add_state();
                (*subset_map)[mata::utils::OrdVector<State>(targets_orig)] = target_res;
                if (aut.final.intersects_with(targets_orig)) {
                    result.final.insert(target_res);
                }
                worklist.emplace_back(target_res, targets_orig);
            }
            result.delta.mutable_state_post(state_res).insert(SymbolPost(current_symbol, target_res));
            if (macrostate_discover.has_value() && existing_target_it == subset_map->end()
                && !(*macrostate_discover)(result, target_res, targets_orig)) { return result; }
        }
    }
    return result;
}

std::ostream& std::operator<<(std::ostream& os, const Nfa& nfa) {
    nfa.print_to_mata(os);
    return os;
}

void mata::nfa::Nfa::fill_alphabet(OnTheFlyAlphabet& alphabet_to_fill) const {
    for (const StatePost& state_post: this->delta) {
        for (const SymbolPost& symbol_post: state_post) {
            alphabet_to_fill.update_next_symbol_value(symbol_post.symbol);
            alphabet_to_fill.try_add_new_symbol(std::to_string(symbol_post.symbol), symbol_post.symbol);
        }
    }
}

mata::OnTheFlyAlphabet mata::nfa::create_alphabet(const std::vector<std::reference_wrapper<const Nfa>>& nfas) {
    mata::OnTheFlyAlphabet alphabet{};
    for (const auto& nfa: nfas) {
        nfa.get().fill_alphabet(alphabet);
    }
    return alphabet;
}

mata::OnTheFlyAlphabet mata::nfa::create_alphabet(const std::vector<std::reference_wrapper<Nfa>>& nfas) {
    mata::OnTheFlyAlphabet alphabet{};
    for (const auto& nfa: nfas) {
        nfa.get().fill_alphabet(alphabet);
    }
    return alphabet;
}

mata::OnTheFlyAlphabet mata::nfa::create_alphabet(const std::vector<const Nfa *>& nfas) {
    mata::OnTheFlyAlphabet alphabet{};
    for (const Nfa* const nfa: nfas) {
        nfa->fill_alphabet(alphabet);
    }
    return alphabet;
}

mata::OnTheFlyAlphabet mata::nfa::create_alphabet(const std::vector<Nfa*>& nfas) {
    mata::OnTheFlyAlphabet alphabet{};
    for (const Nfa* const nfa: nfas) {
        nfa->fill_alphabet(alphabet);
    }
    return alphabet;
}

Run mata::nfa::encode_word(const Alphabet* alphabet, const std::vector<std::string>& input) {
    return { .word = alphabet->translate_word(input) };
}

std::set<mata::Word> mata::nfa::Nfa::get_words(const size_t max_length) const {
    std::set<mata::Word> result;

    // contains a pair: a state s and the word with which we got to the state s
    std::vector<std::pair<State, mata::Word>> worklist;
    // initializing worklist
    for (State init_state : initial) {
        worklist.push_back({init_state, {}});
        if (final.contains(init_state)) {
            result.insert(mata::Word());
        }
    }

    // will be used during the loop
    std::vector<std::pair<State, mata::Word>> new_worklist;

    unsigned cur_length = 0;
    while (!worklist.empty() && cur_length < max_length) {
        new_worklist.clear();
        for (const auto& [state, word] : worklist) {
            for (const SymbolPost& sp : delta[state]) {
                mata::Word new_word = word;
                new_word.push_back(sp.symbol);
                for (State s_to : sp.targets) {
                    new_worklist.emplace_back(s_to, new_word);
                    if (final.contains(s_to)) {
                        result.insert(new_word);
                    }
                }
            }
        }
        worklist.swap(new_worklist);
        ++cur_length;
    }

    return result;
}

OrdVector<Symbol> mata::nfa::get_symbols_to_work_with(const Nfa& nfa, const mata::Alphabet *const shared_alphabet) {
    if (shared_alphabet != nullptr) { return shared_alphabet->get_alphabet_symbols(); }
    else if (nfa.alphabet != nullptr) { return nfa.alphabet->get_alphabet_symbols(); }
    else { return nfa.delta.get_used_symbols(); }
}

std::optional<mata::Word> Nfa::get_word(const std::optional<Symbol> first_epsilon) const {
    if (initial.empty() || final.empty()) { return std::nullopt; }
    if (initial.intersects_with(final)) { return Word{}; }

    /// Current state state post iterator, its end iterator, and iterator in the current symbol post to target states.
    std::vector<std::tuple<StatePost::const_iterator, StatePost::const_iterator, StateSet::const_iterator>> worklist{};
    std::vector<bool> searched(num_of_states(), false);
    bool final_found{};
    for (const State initial_state: initial) {
        if (searched[initial_state]) { continue; }
        searched[initial_state] = true;

        const StatePost& initial_state_post{ delta[initial_state] };
        auto initial_symbol_post_it{ initial_state_post.cbegin() };
        auto initial_symbol_post_end{ initial_state_post.cend() };
        // Initialize the worklist for the given @p initial_state with the first valid transition.
        while (worklist.empty() && initial_symbol_post_it != initial_state_post.cend()) {
            auto initial_target_it{ initial_symbol_post_it->targets.cbegin() };
            do {
                if (searched[*initial_target_it]) {
                    ++initial_target_it;
                } else {
                    worklist.emplace_back(initial_symbol_post_it, initial_symbol_post_end, initial_target_it);
                    break;
                }
            } while (initial_target_it != initial_symbol_post_it->targets.cend());
            if (initial_target_it == initial_symbol_post_it->targets.cend()) {
                ++initial_symbol_post_it;
            }
        }

        // Try to recursively walk through the NFA transitions from the current @p initial_state until a final state is
        //  encountered or all states reachable from @p initial_state are searched without finding any final state.
        while (!worklist.empty()) {
            // Using references to iterators to be able to increment the top-most element in the worklist in place.
            auto& [state_post_it, state_post_end, targets_it]{ worklist.back() };
            if (state_post_it != state_post_end) {
                if (targets_it != state_post_it->targets.cend()) {
                    if (searched[*targets_it]) { ++targets_it; continue; }
                    if (final.contains(*targets_it)) { final_found = true; break; }
                    searched[*targets_it] = true;
                    const StatePost& state_post{ delta[*targets_it] };
                    if (!state_post.empty()) {
                        auto new_state_post_it{ state_post.cbegin() };
                        auto new_targets_it{ new_state_post_it->cbegin() };
                        worklist.emplace_back(new_state_post_it, state_post.cend(), new_targets_it);
                    } else { ++targets_it; }
                } else { // targets_it == state_post_it->targets.cend().
                    ++state_post_it;
                    if (state_post_it != state_post_end) { targets_it = state_post_it->cbegin(); }
                }
            } else { // state_post_it == state_post_end.
                worklist.pop_back();
                if (!worklist.empty()) {
                    auto& [_prev_state_post_it, _prev_state_post_end, prev_targets_it]{ worklist.back() };
                    ++prev_targets_it;
                }
            }
        }
        if (final_found) { break; }
    }

    if (!final_found) { return std::nullopt; }
    Word word;
    word.reserve(worklist.size());
    for (const auto& [symbol_post_it, symbol_post_end, _targets_it]: worklist) {
        if (!first_epsilon.has_value() || symbol_post_it->symbol < first_epsilon) { word.push_back(symbol_post_it->symbol); }
    }
    return word;
}

std::optional<mata::Word> Nfa::get_word_from_complement(const Alphabet* alphabet) const {
    if (are_disjoint(initial, final)) { return Word{}; }

    std::vector<std::unordered_map<StateSet, State>::const_pointer> worklist{};
    std::unordered_map<StateSet, State> subset_map{};

    Nfa nfa_complete{};
    const State sink_state{ nfa_complete.add_state() };
    nfa_complete.final.insert(sink_state);
    const State new_initial{ nfa_complete.add_state() };
    nfa_complete.initial.insert(new_initial);
    auto subset_map_it{ subset_map.emplace(initial, new_initial).first };
    worklist.emplace_back(subset_map_it.operator->());

    using Iterator = mata::utils::OrdVector<SymbolPost>::const_iterator;
    SynchronizedExistentialSymbolPostIterator synchronized_iterator{};

    const utils::OrdVector<Symbol> symbols{ get_symbols_to_work_with(*this, alphabet) };
    const auto symbols_end{ symbols.end() };
    bool continue_complementation{ true };
    while (continue_complementation && !worklist.empty()) {
        const auto curr_state_set_it{ worklist.back() };
        const State macrostate{ curr_state_set_it->second };
        const StateSet& curr_state_set{ curr_state_set_it->first };
        worklist.pop_back();

        synchronized_iterator.reset();
        for (const State orig_state: curr_state_set) { mata::utils::push_back(synchronized_iterator, delta[orig_state]); }
        bool sync_it_advanced{ synchronized_iterator.advance() };
        auto symbols_it{ symbols.begin() };
        while (sync_it_advanced || symbols_it != symbols_end) {
            if (!sync_it_advanced) {
                assert(symbols_it != symbols_end);
                // There are no more transitions from the 'orig_states' but there is a symbol from the 'symbols'. Make
                //  the complemented NFA complete by adding a transition to a sink state. We can now return the access
                //  word for the sink state.
                nfa_complete.delta.add(macrostate, *symbols_it, sink_state);
                continue_complementation = false;
                break;
            }
            assert(sync_it_advanced);
            const std::vector<Iterator>& orig_symbol_posts{ synchronized_iterator.get_current() };
            const Symbol symbol_advanced_to{ (*orig_symbol_posts.begin())->symbol };
            StateSet orig_targets{ synchronized_iterator.unify_targets() };

            if (symbols_it == symbols_end || symbol_advanced_to <= *symbols_it) {
                State target_macrostate;
                // Continue with the determinization of the NFA.
                if (const auto target_macrostate_it = subset_map.find(orig_targets);
                    target_macrostate_it != subset_map.end()) {
                    target_macrostate = target_macrostate_it->second;
                } else {
                    target_macrostate = nfa_complete.add_state();
                    if (!final.intersects_with(orig_targets)) {
                        nfa_complete.final.insert(target_macrostate);
                        continue_complementation = false;
                    }
                    subset_map_it = subset_map.emplace(std::move(orig_targets), target_macrostate).first;
                    worklist.emplace_back(subset_map_it.operator->());
                }
                nfa_complete.delta.add(macrostate, symbol_advanced_to, target_macrostate);
            } else {
                assert(symbol_advanced_to > *symbols_it);
                // There are more transitions from the 'orig_states', but there is a missing transition over
                //  '*symbols_it'. Make the complemented NFA complete by adding a transition to a sink state. We can now
                //  return the access word for the sink state.
                nfa_complete.delta.add(macrostate, *symbols_it, sink_state);
                continue_complementation = false;
                break;
            }

            if (!continue_complementation) { break; }
            if(symbol_advanced_to >= *symbols_it) { ++symbols_it; }
            sync_it_advanced = synchronized_iterator.advance();
        }
    }
    return nfa_complete.get_word();
}

Nfa mata::nfa::lang_difference(
    const Nfa& nfa_included, const Nfa& nfa_excluded,
    std::optional<
        std::function<bool(const Nfa&, const Nfa&, const StateSet&, const StateSet&, const State, const Nfa&)>
    > macrostate_discover
) {
    std::unordered_set<StateSet> subset_set_included{};
    std::unordered_set<StateSet> subset_set_excluded{};
    using SubsetMacrostateMap = std::unordered_map<std::pair<
        std::unordered_set<StateSet>::const_pointer,
        std::unordered_set<StateSet>::const_pointer
    >, State>;
    SubsetMacrostateMap subset_macrostate_map{};
    std::vector<SubsetMacrostateMap::const_pointer> worklist{};

    // '{}' represents a sink state that is always final in the complement.
    const auto subset_set_excluded_sink_ptr{ subset_set_excluded.insert(StateSet{}).first.operator->() };

    Nfa nfa_lang_difference{};
    const State new_initial{ nfa_lang_difference.add_state() };
    nfa_lang_difference.initial.insert(new_initial);
    if (nfa_included.final.intersects_with(nfa_included.initial) &&
        !nfa_excluded.final.intersects_with(nfa_excluded.initial)) {
        nfa_lang_difference.final.insert(new_initial);
    }
    auto subset_set_included_ptr{
        subset_set_included.emplace(nfa_included.initial).first.operator->() };
    auto subset_set_excluded_ptr{
        subset_set_excluded.emplace(nfa_excluded.initial).first.operator->() };
    auto subset_macrostate_map_ptr{
        subset_macrostate_map.emplace(
            std::make_pair(subset_set_included_ptr, subset_set_excluded_ptr), new_initial).first.operator->() };
    worklist.emplace_back(subset_macrostate_map_ptr);
    if (macrostate_discover.has_value()
        && !(*macrostate_discover)(
            nfa_included, nfa_excluded,
            *subset_set_included_ptr, *subset_set_excluded_ptr,
            new_initial, nfa_lang_difference)
    ) { return nfa_lang_difference; }

    using Iterator = mata::utils::OrdVector<SymbolPost>::const_iterator;
    SynchronizedExistentialSymbolPostIterator synchronized_iterator_included{};
    SynchronizedExistentialSymbolPostIterator synchronized_iterator_excluded{};

    while (!worklist.empty()) {
        const auto curr_macrostate_ptr{ worklist.back() };
        worklist.pop_back();
        const auto curr_state_set_included_ptr{ curr_macrostate_ptr->first.first };
        const auto curr_state_set_excluded_ptr{ curr_macrostate_ptr->first.second };
        const StateSet& curr_state_set_included{ *curr_state_set_included_ptr };
        const StateSet& curr_state_set_excluded{ *curr_state_set_excluded_ptr };
        const State macrostate{ curr_macrostate_ptr->second };

        synchronized_iterator_included.reset();
        synchronized_iterator_excluded.reset();
        for (const State orig_state: curr_state_set_included) {
            mata::utils::push_back(synchronized_iterator_included, nfa_included.delta[orig_state]);
        }
        for (const State orig_state: curr_state_set_excluded) {
            mata::utils::push_back(synchronized_iterator_excluded, nfa_excluded.delta[orig_state]);
        }
        bool sync_it_included_advanced{ synchronized_iterator_included.advance() };
        bool sync_it_excluded_advanced{ false };
        while (sync_it_included_advanced) {
            const std::vector<Iterator>& orig_symbol_posts{ synchronized_iterator_included.get_current() };
            const Symbol symbol_advanced_to{ (*orig_symbol_posts.begin())->symbol };
            StateSet orig_targets_included{ synchronized_iterator_included.unify_targets() };
            sync_it_excluded_advanced = synchronized_iterator_excluded.synchronize_with(symbol_advanced_to);
            StateSet orig_targets_excluded{
                sync_it_excluded_advanced ? synchronized_iterator_excluded.unify_targets() : StateSet{}
            };
            const bool final_included_intersects_targets{ nfa_included.final.intersects_with(orig_targets_included) };
            const bool final_excluded_intersects_targets{ nfa_excluded.final.intersects_with(orig_targets_excluded) };
            subset_set_included_ptr = subset_set_included.insert(std::move(orig_targets_included)).first.operator->();
            subset_set_excluded_ptr = subset_set_excluded.insert(std::move(orig_targets_excluded)).first.operator->();
            const auto [target_macrostate_it, macrostate_inserted]{ subset_macrostate_map.emplace(
                std::make_pair(subset_set_included_ptr, subset_set_excluded_ptr), nfa_lang_difference.num_of_states()
            ) };
            subset_macrostate_map_ptr = target_macrostate_it.operator->();
            const State target_macrostate{ target_macrostate_it->second };
            nfa_lang_difference.delta.add(macrostate, symbol_advanced_to, target_macrostate);
            if (macrostate_inserted) {
                // 'sync_it_excluded_advanced' is true iff there is a transition in the excluded NFA over the symbol
                //  'symbol_advanced_to'. If sync_it_excluded_advanced == false, the complement of the excluded NFA will
                //  have a transition over 'symbol_advanced_to' to a "sink state" which is a final state in the
                //  complement, and therefore must always be final in the language difference.
                if (final_included_intersects_targets) {
                    if (subset_set_excluded_ptr == subset_set_excluded_sink_ptr
                        || (sync_it_excluded_advanced && !final_excluded_intersects_targets)) {
                        nfa_lang_difference.final.insert(target_macrostate);
                    }
                }
                if (macrostate_discover.has_value()
                    && !(*macrostate_discover)(
                        nfa_included, nfa_excluded,
                        *subset_set_included_ptr, *subset_set_excluded_ptr,
                        target_macrostate, nfa_lang_difference)) { return nfa_lang_difference; }

                worklist.emplace_back(subset_macrostate_map_ptr);
            }
            sync_it_included_advanced = synchronized_iterator_included.advance();
        }
    }
    return nfa_lang_difference;
}

std::optional<mata::Word> mata::nfa::get_word_from_lang_difference(const Nfa & nfa_included, const Nfa & nfa_excluded) {
    return lang_difference(nfa_included, nfa_excluded,
        [](const Nfa& nfa_included, const Nfa& nfa_excluded,
            const StateSet& macrostate_included_state_set, const StateSet& macrostate_excluded_state_set,
            const State macrostate, const Nfa& nfa_lang_difference) {
            (void)nfa_included, (void) nfa_excluded;
            (void) macrostate_included_state_set, (void) macrostate_excluded_state_set;
            (void) macrostate;
            return nfa_lang_difference.final.empty();
        }
    ).get_word();
}

Nfa mata::nfa::algorithms::reduce_simulation(const Nfa& aut, StateRenaming &state_renaming) {
    Nfa result;
    const auto sim_relation = algorithms::compute_relation(
            aut, ParameterMap{{ "relation", "simulation"}, { "direction", "forward"}});

    auto sim_relation_symmetric = sim_relation;
    sim_relation_symmetric.restrict_to_symmetric();

    // for State q, quot_proj[q] should be the representative state representing the symmetric class of states in simulation
    std::vector<size_t> quot_proj;
    sim_relation_symmetric.get_quotient_projection(quot_proj);

    const size_t num_of_states = aut.num_of_states();

    // map each state q of aut to the state of the reduced automaton representing the simulation class of q
    for (State q = 0; q < num_of_states; ++q) {
        if (const State q_repr_state = quot_proj[q]; !state_renaming.contains(q_repr_state)) {
            // we need to map q's class to a new state in reducedAut
            const State q_class = result.add_state();
            state_renaming[q_repr_state] = q_class;
            state_renaming[q] = q_class;
        } else {
            state_renaming[q] = state_renaming[q_repr_state];
        }
    }

    for (State q = 0; q < num_of_states; ++q) {
        const State q_class_state = state_renaming.at(q);

        if (aut.initial[q]) { // if a symmetric class contains initial state, then the whole class should be initial
            result.initial.insert(q_class_state);
        }

        if (quot_proj[q] == q) { // we process only transitions starting from the representative state, this is enough for simulation
            for (const auto &q_trans : aut.delta.state_post(q)) {
                const StateSet representatives_of_states_to = [&]{
                    StateSet state_set;
                    for (const auto s : q_trans.targets) {
                        state_set.insert(quot_proj[s]);
                    }
                    return state_set;
                }();

                // get the class states of those representatives that are not simulated by another representative in representatives_of_states_to
                StateSet representatives_class_states;
                for (const State s : representatives_of_states_to) {
                    bool is_state_important = true; // if true, we need to keep the transition from q to s
                    for (const State p : representatives_of_states_to) {
                        if (s != p && sim_relation.get(s, p)) { // if p (different from s) simulates s
                            is_state_important = false; // as p simulates s, the transition from q to s is not important to keep, as it is subsumed in transition from q to p
                            break;
                        }
                    }
                    if (is_state_important) {
                        representatives_class_states.insert(state_renaming.at(s));
                    }
                }

                // add the transition 'q_class_state-q_trans.symbol->representatives_class_states' at the end of transition list of transitions starting from q_class_state
                // as the q_trans.symbol should be the largest symbol we saw (as we iterate trough getTransitionsFromState(q) which is ordered)
                result.delta.mutable_state_post(q_class_state).insert(SymbolPost(q_trans.symbol, representatives_class_states));
            }

            if (aut.final[q]) { // if q is final, then all states in its class are final => we make q_class_state final
                result.final.insert(q_class_state);
            }
        }
    }

    return result;
}

Nfa mata::nfa::algorithms::reduce_residual(const Nfa& nfa, StateRenaming &state_renaming, const std::string& type, const std::string& direction) {
    Nfa back_determinized = nfa;
    Nfa result;

    if (direction != "forward" && direction != "backward"){
        throw std::runtime_error(std::to_string(__func__) +
                             " received an unknown value of the \"direction\" key: " + direction);
    }

    // forward canonical residual automaton is firstly backward determinized and
    // then the residual construction is done forward, for backward residual automaton
    // is it the opposite, so the automaton is reverted once more before and after
    // construction, however the first two reversion negate each other out
    if (direction == "forward")
        back_determinized = revert(back_determinized);
    back_determinized = revert(determinize(back_determinized)); // Backward determinization.

    // TODO: Not really sure how to handle state_renaming.
    (void) state_renaming;

    // two different implementations of the same algorithm, for type "after" the
    // residual automaton and removal of covering states is done after the final
    // determinization if finished, for type "with" this residual construction is
    // done during the last determinization, both types had similar results in
    // effectivity, their output is almost the same expect the transitions, those
    // may slightly differ, but number of states is the same for both types
    if (type == "with") {
        result = reduce_residual_with(back_determinized);
    }
    else if (type == "after") {
        result = reduce_residual_after(back_determinized);
    } else {
        throw std::runtime_error(std::to_string(__func__) +
                             " received an unknown value of the \"type\" key: " + type);
    }

    if (direction == "backward")
        result = revert(result);

    return result.trim();
}

Nfa mata::nfa::algorithms::reduce_residual_with(const Nfa& nfa) {

    Nfa result;

    //assuming all sets targets are non-empty
    std::vector<std::pair<State, StateSet>> worklist;
    std::unordered_map<StateSet, State> subset_map;

    std::vector<StateSet> covering_states;          // check covering set
    std::vector<StateSet> covering_indexes;         // indexes of covering macrostates
    std::unordered_map<StateSet, State> covered;    // map of covered states for transferring new transitions

    result.clear();
    const StateSet initials_orig =  StateSet(nfa.initial);
    const State initial_res = result.add_state();
    result.initial.insert(initial_res);

    if (nfa.final.intersects_with(initials_orig)) {
        result.final.insert(initial_res);
    }
    worklist.emplace_back(initial_res, initials_orig);

    (subset_map)[mata::utils::OrdVector<State>(initials_orig)] = initial_res;
    covering_states.emplace_back();
    covering_indexes.emplace_back();

    if (nfa.delta.empty()){
        return result;
    }

    using Iterator = OrdVector<SymbolPost>::const_iterator;
    SynchronizedExistentialSymbolPostIterator synchronized_iterator;

    while (!worklist.empty()) {
        const auto [state_res, states_orig] = worklist.back();
        worklist.pop_back();
        if (states_orig.empty()) {
            // This should not happen assuming all sets targets are non-empty.
            break;
        }

        // add moves of S to the sync ex iterator
        synchronized_iterator.reset();
        for (State q: states_orig) {
            mata::utils::push_back(synchronized_iterator, nfa.delta[q]);
        }

        while (synchronized_iterator.advance()) {
            bool add = false;               // check whether to add transitions

            // extract post from the synchronized_iterator iterator
            const std::vector<Iterator>& moves = synchronized_iterator.get_current();
            Symbol current_symbol = (*moves.begin())->symbol;
            StateSet targets = synchronized_iterator.unify_targets(); // new state unify

            auto existing_target_res = subset_map.find(targets); // check if state was already discovered
            State target_res;
            if (existing_target_res != subset_map.end()) { // already visited state
                target_res = existing_target_res->second;
                add = true;
            } else if ((existing_target_res = covered.find(targets)) != covered.end()) {
                target_res = existing_target_res->second;
            } else {                                        // add new state
                target_res = result.add_state();
                check_covered_and_covering(covering_states, covering_indexes, covered, subset_map, target_res, targets, result);

                if (targets != covering_states[target_res]){     // new state is not covered, replace transitions
                    subset_map[mata::utils::OrdVector<State>(targets)] = target_res;      // add to map

                    if (nfa.final.intersects_with(targets))                      // add to final
                        result.final.insert(target_res);

                    worklist.emplace_back(target_res, targets);
                    add  = true;

                } else {            // new state is covered
                    covered[mata::utils::OrdVector<State>(targets)] = target_res;
                }
            }

            if (covered.contains(states_orig)) {
                continue; // skip generating any transitions as the source state was covered right now
            }

            if (add) {
                result.delta.mutable_state_post(state_res).insert(SymbolPost(current_symbol, target_res));
            } else {
                for (State switch_target: covering_indexes[target_res]){
                        result.delta.add(state_res, current_symbol, switch_target);
                }
            }
        }
    }

    return result;
}

Nfa mata::nfa::algorithms::reduce_residual_after(const Nfa& nfa) {
    std::unordered_map<StateSet, State> subset_map{};
    Nfa result;
    result = determinize(nfa, &subset_map);

    std::vector <StateSet> macrostate_vec;              // ordered vector of macrostates
    macrostate_vec.reserve(subset_map.size());
    for (const auto& state_set : subset_map | std::views::keys) {
        // Ordered by size from largest to smallest.
        macrostate_vec.insert(
            std::ranges::upper_bound(
                macrostate_vec, state_set,
                [](const StateSet& a, const StateSet& b) { return a.size() > b.size(); }
            ), state_set
        );
    }

    std::vector <bool> covered(subset_map.size(), false);          // flag of covered states, removed from nfa
    std::vector <bool> visited(subset_map.size(), false);          // flag of processed state

    StateSet covering_set;                // doesn't contain duplicates
    std::vector<State> covering_indexes;        // indexes of covering states
    size_t macrostate_size = macrostate_vec.size();
    for (size_t i = 0; i < macrostate_size-1; i++) {
        if (macrostate_vec[i].size() == 1)      // end searching on single-sized macrostates
            break;

        if (visited[i])                         // was already processed
            continue;

        covering_set.clear();
        covering_indexes.clear();
        visited[i] = true;

        for (size_t j = i+1; j < macrostate_size; j++) {        // find covering macrostates
            if (covered[j])     // if covered there are smaller macrostates, skip
                continue;

            if (macrostate_vec[j].is_subset_of(macrostate_vec[i])) {           // found covering state
                covering_set.insert(macrostate_vec[j]);               // is not covered
                covering_indexes.push_back(j);
            }
        }

        if (covering_set == macrostate_vec[i]) {
            size_t covering_size = covering_indexes.size()-1;
            for (size_t k = 0; k < covering_size; k++) {
                // check recurse coverability
                if (macrostate_vec[covering_indexes[k]].size() == 1) {
                    // end on single-sized
                    break;
                }

                if (visited[covering_indexes[k]]) { // already processed
                    continue;
                }

                visited[covering_indexes[k]] = true;

                residual_recurse_coverable(macrostate_vec, covering_indexes, covered, visited, k, &subset_map, result);
            }

            covering_set.clear();                 // clear variable to store only needed macrostates
            for (auto index : covering_indexes) {
                if (covered[index] == 0) {
                    auto macrostate_ptr = subset_map.find(macrostate_vec[index]);
                    if (macrostate_ptr == subset_map.end())        // should never happen
                         throw std::runtime_error(std::to_string(__func__) + " couldn't find expected element in a map.");

                    covering_set.insert(macrostate_ptr->second);
                }
            }

            remove_covered_state(covering_set, subset_map.find(macrostate_vec[i])->second, result);
            covered[i] = true;
        }
    }

    return result;
}
