/*++
Copyright (c) 2015 Microsoft Corporation

Module Name:

    symbolic_automata_def.h

Abstract:

    Symbolic Automata over Boolean Algebras, a la Margus Veanes Automata library.

Author:

    Nikolaj Bjorner (nbjorner) 2016-02-27.

Revision History:


--*/

#pragma once


#include "math/automata/symbolic_automata.h"
#include "util/hashtable.h"
#include "util/vector.h"




template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_total(automaton_t& a) {
    unsigned dead_state = a.num_states();
    moves_t mvs, new_mvs;
    for (unsigned i = 0; i < dead_state; ++i) {
        mvs.reset();
        a.get_moves_from(i, mvs, true);
        refs_t vs(m);
        
        for (unsigned j = 0; j < mvs.size(); ++j) {
            vs.push_back(mvs[j].t());
        }
        ref_t cond(m_ba.mk_not(m_ba.mk_or(vs.size(), vs.data())), m);
        lbool is_sat = m_ba.is_sat(cond);
        if (is_sat == l_undef) {
            return nullptr;
        }
        if (is_sat == l_true) {
            new_mvs.push_back(move_t(m, i, dead_state, cond));
        }
    }
    if (new_mvs.empty()) {
        return a.clone();
    }
    new_mvs.push_back(move_t(m, dead_state, dead_state, m_ba.mk_true()));

    // TBD private: automaton_t::append_moves(0, a, new_mvs);
    
    return alloc(automaton_t, m, a.init(), a.final_states(), new_mvs);        
}

template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_minimize(automaton_t& a) {
    if (a.is_empty()) {
        return a.clone();
    }

    if (a.is_epsilon()) {
        return a.clone();
    }
    // SASSERT(a.is_deterministic());
    
    scoped_ptr<automaton_t> fa = mk_total(a);
    if (!fa) {
        return 0;
    }
    return mk_minimize_total(*fa.get());
}


template<class T, class M>
void symbolic_automata<T, M>::add_block(block const& p1, unsigned p0_index, unsigned_vector& blocks, vector<block>& pblocks, unsigned_vector& W) {
    block& p0 = pblocks[p0_index];
    if (p1.size() < p0.size()) {
        unsigned p1_index = pblocks.size();
        pblocks.push_back(p1);
        for (uint_set::iterator it = p1.begin(), end = p1.end(); it != end; ++it) {
            p0.remove(*it);
            blocks[*it] = p1_index;
        }
        if (W.contains(p0_index)) {
            W.push_back(p1_index);
        }
        else if (p0.size() <= p1.size()) {
            W.push_back(p0_index);
        }
        else {
            W.push_back(p1_index);
        }
    }                
}

template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_minimize_total(automaton_t& a) {    
    vector<block> pblocks;
    unsigned_vector blocks;
    unsigned_vector non_final;
    for (unsigned i = 0; i < a.num_states(); ++i) {
        if (!a.is_final_state(i)) {
            non_final.push_back(i);
            blocks.push_back(1);
        }
        else {
            blocks.push_back(0);
        }
    }
    pblocks.push_back(block(a.final_states()));      // 0 |-> final states
    pblocks.push_back(block(non_final));             // 1 |-> non-final states

    unsigned_vector W;
    W.push_back(pblocks[0].size() > pblocks[1].size() ? 1 : 0);
        
    refs_t trail(m);
    u_map<T*> gamma;
    moves_t mvs;
    while (!W.empty()) {
        block R(pblocks[W.back()]);
        W.pop_back();
        gamma.reset();
        uint_set::iterator it = R.begin(), end = R.end();
        for (; it != end; ++it) {
            unsigned dst = *it;
            mvs.reset();
            a.get_moves_to(dst, mvs);
            for (unsigned i = 0; i < mvs.size(); ++i) {
                unsigned src = mvs[i].src();
                if (pblocks[src].size() > 1) {
                    T* t = mvs[i].t();
                    T* t1;
                    if (gamma.find(src, t1)) {
                        t = m_ba.mk_or(t, t1);
                        trail.push_back(t);
                    }
                    gamma.insert(src, t);
                }
            }
        }
        uint_set relevant1;
        typedef typename u_map<T*>::iterator gamma_iterator;
        gamma_iterator gend = gamma.end();
        for (gamma_iterator git = gamma.begin(); git != gend; ++git) {
            unsigned p0A_index = blocks[git->m_key];
            if (relevant1.contains(p0A_index)) {
                continue;
            }
            relevant1.insert(p0A_index);
            block& p0A = pblocks[p0A_index];
            block p1;
            for (gamma_iterator it = gamma.begin(); it != gend; ++it) {
                if (p0A.contains(it->m_key)) p1.insert(it->m_key);
            }
            
            add_block(p1, p0A_index, blocks, pblocks, W);

            bool iterate = true;
            while (iterate) {
                iterate = false;
                uint_set relevant2;
                for (gamma_iterator it = gamma.begin(); it != gend; ++it) {
                    unsigned p0B_index = blocks[it->m_key];
                    if (pblocks[p0B_index].size() <= 1 || relevant2.contains(p0B_index)) {
                        continue;
                    }
                    relevant2.insert(p0B_index);
                    block const& p0B = pblocks[p0B_index];
                    uint_set::iterator bi = p0B.begin(), be = p0B.end();

                    block p1;
                    p1.insert(*bi);
                    bool split_found = false;
                    ref_t psi(gamma[*bi], m); 
                    ++bi;
                    for (; bi != be; ++bi) {
                        unsigned q = *bi;
                        ref_t phi(gamma[q], m);
                        if (split_found) {
                            ref_t phi_and_psi(m_ba.mk_and(phi, psi), m);
                            switch (m_ba.is_sat(phi_and_psi)) {
                            case l_true:
                                p1.insert(q);
                                break;
                            case l_undef:
                                return nullptr;
                            default:
                                break;
                            }                            
                        }
                        else {
                            ref_t psi_min_phi(m_ba.mk_and(psi, m_ba.mk_not(phi)), m);
                            lbool is_sat = m_ba.is_sat(psi_min_phi);
                            if (is_sat == l_undef) {
                                return nullptr;
                            }
                            if (is_sat == l_true) {
                                psi = psi_min_phi;
                                split_found = true;
                                continue;
                            }
                            // psi is a subset of phi
                            ref_t phi_min_psi(m_ba.mk_and(phi, m_ba.mk_not(psi)), m);
                            is_sat = m_ba.is_sat(phi_min_psi);
                            if (is_sat == l_undef) {
                                return nullptr;
                            }
                            else if (is_sat == l_false) {
                                p1.insert(q); // psi and phi are equivalent
                            }
                            else {
                                p1.clear();
                                p1.insert(q);
                                psi = phi_min_psi;
                                split_found = true;
                            }
                        }
                    }
                    if (p1.size() < p0B.size() && p0B.size() > 2) iterate = true;
                    add_block(p1, p0B_index, blocks, pblocks, W);
                }
            }
        }
    }

    unsigned new_init = pblocks[blocks[a.init()]].get_representative();

    // set moves
    u2_map<T*> conds;
    svector<unsigned_pair> keys;
    moves_t new_moves;

    for (unsigned i = 0; i < a.num_states(); ++i) {
        unsigned src = pblocks[blocks[i]].get_representative();
        typename automaton_t::moves const& mvs = a.get_moves_from(i);
        for (unsigned j = 0; j < mvs.size(); ++j) {
            unsigned dst = pblocks[blocks[mvs[j].dst()]].get_representative();
            unsigned_pair st(src, dst);
            T* t = 0;
            if (conds.find(st, t)) {
                t = m_ba.mk_or(t, mvs[j].t());
                trail.push_back(t);
                conds.insert(st, t);
            }
            else {
                conds.insert(st, mvs[j].t());
                keys.push_back(st);
            }
        }
    }    
    for (unsigned i = 0; i < keys.size(); ++i) {
        unsigned_pair st = keys[i];
        new_moves.push_back(move_t(m, st.first, st.second, conds[st]));
    }
    // set final states.
    unsigned_vector new_final;
    uint_set new_final_set;
    for (unsigned i = 0; i < a.final_states().size(); ++i) {
        unsigned f = pblocks[blocks[a.final_states()[i]]].get_representative();
        if (!new_final_set.contains(f)) {
            new_final_set.insert(f);
            new_final.push_back(f);
        }
    }

    return alloc(automaton_t, m, new_init, new_final, new_moves);
}

template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_determinstic(automaton_t& a) {
    return mk_determinstic_param(a);
}

template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_complement(automaton_t& a) {
    return mk_determinstic_param(a, true);
}

template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* 
symbolic_automata<T, M>::mk_determinstic_param(automaton_t& a, bool flip_acceptance) {
    vector<std::pair<vector<bool>, ref_t> > min_terms;
    vector<ref_t> predicates;

    map<uint_set, unsigned, uint_set::hash, uint_set::eq> s2id; // set of states to unique id
    vector<uint_set> id2s;                           // unique id to set of b-states
    uint_set set;
    unsigned_vector vector;
    moves_t new_mvs;                                 // moves in the resulting automaton
    unsigned_vector new_final_states;                // new final states
    unsigned p_state_id = 0;                         // next state identifier

    TRACE(seq, tout << "mk-deterministic " << flip_acceptance << " " << set << " " << a.is_final_configuration(set) << "\n";);    
    // adds non-final states of a to final if flipping and final otherwise
    unsigned_vector init_states;
    a.get_epsilon_closure(a.init(), init_states);    
    for (unsigned s : init_states) {
        set.insert(s);
    }
    if (a.is_final_configuration(set) != flip_acceptance) {
        new_final_states.push_back(p_state_id);
    }
    
    s2id.insert(set, p_state_id++);               // the index to the initial state is 0
    id2s.push_back(set);
    
    ::vector<uint_set> todo; //States to visit
    todo.push_back(set);
    
    uint_set state;
    moves_t mvsA;
    
    new_mvs.reset();
    
    // or just make todo a vector whose indices coincide with state_id.
    while (!todo.empty()) {
        uint_set state = todo.back();
        
        unsigned state_id = s2id[state];
        todo.pop_back();
        mvsA.reset();
        
        min_terms.reset();
        predicates.reset();
        
        a.get_moves_from_states(state, mvsA);
        
        for (unsigned j = 0; j < mvsA.size(); ++j) {
            ref_t mv_guard(mvsA[j].t(),m);
            predicates.push_back(mv_guard);
        }
        
        min_terms = generate_min_terms(predicates);
        for (unsigned j = 0; j < min_terms.size(); ++j) {
            set = uint_set();
            for (unsigned i = 0; i < mvsA.size(); ++i) {
                if (min_terms[j].first[i])
                    set.insert(mvsA[i].dst());
            }
            
            bool is_new = !s2id.contains(set);
            if (is_new) {
                TRACE(seq, tout << "mk-deterministic " << flip_acceptance << " " << set << " " << a.is_final_configuration(set) << "\n";);    
                if (a.is_final_configuration(set) != flip_acceptance) {
                    new_final_states.push_back(p_state_id);
                }
                
                s2id.insert(set, p_state_id++);
                id2s.push_back(set);
                todo.push_back(set);
            }
            new_mvs.push_back(move_t(m, state_id, s2id[set], min_terms[j].second));
        }
    }
    
    if (new_final_states.empty()) {
        return alloc(automaton_t, m);
    }
    
    return alloc(automaton_t, m, 0, new_final_states, new_mvs);
}



template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_product(automaton_t& a, automaton_t& b) {
    u2_map<unsigned> pair2id;
    unsigned_pair init_pair(a.init(), b.init());
    svector<unsigned_pair> todo;
    todo.push_back(init_pair);
    pair2id.insert(init_pair, 0);
    moves_t mvs;
    unsigned_vector final;
    unsigned_vector a_init, b_init;
    a.get_epsilon_closure(a.init(), a_init);
    bool a_init_is_final = false, b_init_is_final = false;
    for (unsigned ia : a_init) {
        if (a.is_final_state(ia)) {
            a_init_is_final = true;
            b.get_epsilon_closure(b.init(), b_init);
            for (unsigned ib : b_init) {
                if (b.is_final_state(ib)) {
                    b_init_is_final = true;
                    final.push_back(0);
                    break;
                }
            }
            break;
        }
    }
    
    unsigned n = 1;
    moves_t mvsA, mvsB;
    while (!todo.empty()) {
        unsigned_pair curr_pair = todo.back();
        todo.pop_back();
        unsigned src = pair2id[curr_pair];
        mvsA.reset(); mvsB.reset();
        a.get_moves_from(curr_pair.first,  mvsA, true);
        b.get_moves_from(curr_pair.second, mvsB, true);
        for (unsigned i = 0; i < mvsA.size(); ++i) {
            for (unsigned j = 0; j < mvsB.size(); ++j) {
                ref_t ab(m_ba.mk_and(mvsA[i].t(), mvsB[j].t()), m);   
                lbool is_sat = m_ba.is_sat(ab);
                if (is_sat == l_false) {
                    continue;
                }
                else if (is_sat == l_undef) {
                    return nullptr;
                }
                unsigned_pair tgt_pair(mvsA[i].dst(), mvsB[j].dst());
                unsigned tgt;
                if (!pair2id.find(tgt_pair, tgt)) {
                    tgt = n++;
                    pair2id.insert(tgt_pair, tgt);
                    todo.push_back(tgt_pair);
                    if (a.is_final_state(tgt_pair.first) && b.is_final_state(tgt_pair.second)) {
                        final.push_back(tgt);
                    }
                }
                mvs.push_back(move_t(m, src, tgt, ab));
            }
        }
    }
    
    if (final.empty()) {
        return alloc(automaton_t, m);
    }
    vector<moves_t> inv(n, moves_t());
    for (unsigned i = 0; i < mvs.size(); ++i) {
        move_t const& mv = mvs[i];
        inv[mv.dst()].push_back(move_t(m, mv.dst(), mv.src(), mv.t())); 
    }
    
    bool_vector back_reachable(n, false);
    for (unsigned f : final) {
        back_reachable[f] = true;
    }
    
    unsigned_vector stack(final);
    while (!stack.empty()) {
        unsigned state = stack.back();
        stack.pop_back();
        moves_t const& mv = inv[state];
        for (unsigned i = 0; i < mv.size(); ++i) {
            state = mv[i].dst();
            if (!back_reachable[state]) {
                back_reachable[state] = true;
                stack.push_back(state);
            }
        }
    }
    
    moves_t mvs1;
    for (unsigned i = 0; i < mvs.size(); ++i) {
        move_t const& mv = mvs[i];
        if (back_reachable[mv.dst()]) {
            mvs1.push_back(mv);
        }
    }
    if (mvs1.empty()) {
        if (a_init_is_final && b_init_is_final) {
            // special case: automaton has no moves, but the initial state is final on both sides
            // this results in the automaton which accepts the empty sequence and nothing else
            final.clear();
            final.push_back(0);
            return alloc(automaton_t, m, 0, final, mvs1);
        } else {
            return alloc(automaton_t, m);
        }
    }
    else {
        return alloc(automaton_t, m, 0, final, mvs1);
    }
} 



template<class T, class M>
typename symbolic_automata<T, M>::automaton_t* symbolic_automata<T, M>::mk_difference(automaton_t& a, automaton_t& b) {
    return mk_product(a,mk_complement(b));
}

