
/*++
Copyright (c) 2015 Microsoft Corporation

--*/


#include "qe/qe.h"
#include "ast/array_decl_plugin.h"
#include "ast/rewriter/expr_safe_replace.h"
#include "ast/ast_pp.h"
#include "ast/arith_decl_plugin.h"

namespace qe {
    // ---------------------
    // arrays

    class array_plugin : public qe_solver_plugin {

        expr_safe_replace m_replace;

    public:

        array_plugin(i_solver_context& ctx, ast_manager& m) : 
            qe_solver_plugin(m, m.mk_family_id("array"), ctx),
            m_replace(m)
        {
        }

        ~array_plugin() override {}

        
        void assign(contains_app& x, expr* fml, rational const& vl) override {
            UNREACHABLE();
        }
        
        bool get_num_branches( contains_app& x, expr* fml, rational& num_branches) override {
            return false;
        }
        
        void subst(contains_app& x, rational const& vl, expr_ref& fml, expr_ref* def) override {
            UNREACHABLE();
        }
        

        bool solve(conj_enum& conjs, expr* fml) override {

            conj_enum::iterator it = conjs.begin(), end = conjs.end();
            for (; it != end; ++it) {
                expr* e = *it;
                if (m.is_eq(e) && solve_eq(to_app(e), fml)) {
                    return true;
                }
            }
            expr_ref_vector eqs(m);
            conjs.extract_equalities(eqs);
            for (unsigned i = 0; i < eqs.size(); ++i) {
                TRACE("qe_verbose", 
                      tout << mk_pp(eqs[i].get(), m) << "\n";);
                expr* e = eqs[i].get();
                if (solve_eq_zero(e, fml)) {
                    return true;
                }
            }
            return false;
        }

        bool is_uninterpreted(app* f) override {
            return true;
        }

           
    private:
        
        bool solve_eq(app* eq, expr* fml) {
            SASSERT(m.is_eq(eq));
            expr* arg1 = eq->get_arg(0);
            expr* arg2 = eq->get_arg(1);
            return solve_eq(arg1, arg2, fml) || solve_eq(arg2, arg1, fml);
        }

        bool solve_eq_zero(expr* e, expr* fml) {
            arith_util arith(m); 
            if (arith.is_add(e)) {
                app* a = to_app(e);
                expr* e1, *e2;
                unsigned sz = a->get_num_args();
                expr_ref_vector args(m);
                expr_ref lhs(m), rhs(m);
                rational r;
                args.append(sz, a->get_args());
                for (unsigned i = 0; i < sz; ++i) {
                    expr_ref save(m);
                    save = lhs = args[i].get();                    
                    args[i] = arith.mk_numeral(rational(0), lhs->get_sort());
                    rhs = arith.mk_uminus(arith.mk_add(args.size(), args.data()));
                    if (arith.is_mul(lhs, e1, e2) && 
                        arith.is_numeral(e1, r) &&
                        r.is_minus_one()) {
                        lhs = to_app(e2);
                        rhs = arith.mk_uminus(rhs);
                    }
                    if (solve_eq(lhs, rhs, fml)) {
                        return true;
                    }
                    args[i] = save;
                }
            }
            return false;
        }


        
        bool solve_eq(expr* lhs, expr* rhs, expr* fml) {
            
            if (!is_app(lhs)) {
                return false;
            }
            
            TRACE("qe_verbose", 
                  tout << mk_pp(lhs, m) << 
                  " == " << mk_pp(rhs, m) << "\n";);
            expr_ref tmp(m);
            app* a = to_app(lhs);
            //
            // A = t, A not in t.
            //
            unsigned idx = 0;
            if (m_ctx.is_var(a, idx) && 
                !m_ctx.contains(idx)(rhs)) {
                expr_ref result(fml, m);
                m_replace.apply_substitution(a, rhs, result);
                m_ctx.elim_var(idx, result, rhs);
                return true;
            }                 
            if (solve_store(a, rhs, fml)) {
                return true;
            }
            if (solve_select(a, rhs, fml)) {
                return true;
            }
            return false;
        }

        bool solve_select2(app* lhs, expr* rhs, expr* fml) {
            //
            // (select (select A i) j) = t, A not in i, j,  t
            // A |-> (store B' j (store B i t)), where B, B' are fresh.
            // 
            // TBD
            return false;
        }

        bool solve_select(app* lhs, expr* rhs, expr* fml) {
            //
            // (select A i) = t, A not in i, v, t
            // A |-> (store B i t), where B is fresh.
            // 
            unsigned idx = 0;
            vector<ptr_vector<expr> > args;
            if (is_select(lhs, idx, rhs, args) && args.size() == 1) {
                contains_app& contains_A = m_ctx.contains(idx);
                app* A = contains_A.x();
                app_ref B(m);
                expr_ref store_B_i_t(m);

                unsigned num_args = args[0].size();
                B = m.mk_fresh_const("B", A->get_sort());
                ptr_buffer<expr> args2;
                args2.push_back(B);
                for (unsigned i = 0; i < num_args; ++i) {
                    args2.push_back(args[0][i]);
                }
                args2.push_back(rhs);
                
                store_B_i_t = m.mk_app(m_fid, OP_STORE, args2.size(), args2.data());
                
                TRACE("qe", 
                      tout << "fml: " << mk_pp(fml, m) << "\n";
                      tout << "solved form: " << mk_pp(store_B_i_t, m) << "\n";
                      tout << "eq: " << mk_pp(lhs, m) << " == " << mk_pp(rhs, m) << "\n";
                      );
                expr_ref result(fml, m);
                m_replace.apply_substitution(A, store_B_i_t, result);
                m_ctx.add_var(B);
                m_ctx.elim_var(idx, result, store_B_i_t);
                return true;

            }
            return false;
        }

        bool is_array_app_of(app* a, unsigned& idx, expr* t, decl_kind k) {
            if (!is_app_of(a, m_fid, k)) {
                return false;
            }
            expr* v = a->get_arg(0);
            if (!m_ctx.is_var(v, idx)) {
                return false;
            }
            contains_app& contains_v = m_ctx.contains(idx);
            
            for (unsigned i = 1; i < a->get_num_args(); ++i) {
                if (contains_v(a->get_arg(i))) {
                    return false;
                }
            }        
            if (contains_v(t)) {
                return false;
            }
            return true;
        }


        bool solve_store(app* lhs, expr* rhs, expr* fml) {
            //
            // store(store(A, j, u), i, v) = t, A not in i, j, u, v, t
            // -> 
            // A |-> store(store(t, i, w), j, w') where w, w' are fresh.
            // t[i] = v
            // store(t, i, v)[j] = u
            //
            unsigned idx = 0;
            vector<ptr_vector<expr> > args;
            if (is_store_update(lhs, idx, rhs, args)) {
                contains_app& contains_A = m_ctx.contains(idx);
                app* A = contains_A.x();
                app_ref w(m);

                expr_ref store_t(rhs, m), store_T(rhs, m), select_t(m);
                ptr_vector<expr> args2;
                for (unsigned i = args.size(); i > 0; ) {
                    --i;
                    args2.reset();
                    w = m.mk_fresh_const("w", args[i].back()->get_sort());
                    args2.push_back(store_T);
                    args2.append(args[i]);
                    
                    select_t = m.mk_app(m_fid, OP_SELECT, args2.size()-1, args2.data());
                    fml = m.mk_and(fml, m.mk_eq(select_t, args2.back()));
                    store_T = m.mk_app(m_fid, OP_STORE, args2.size(), args2.data());

                    args2[0] = store_t;
                    args2.back() = w;
                    store_t = m.mk_app(m_fid, OP_STORE, args2.size(), args2.data());

                    m_ctx.add_var(w);
                }
                               
                TRACE("qe", 
                      tout << "Variable: " << mk_pp(A, m) << "\n";
                      tout << "fml: " << mk_pp(fml, m) << "\n";
                      tout << "solved form: " << mk_pp(store_t, m) << "\n";
                      tout << "eq: " << mk_pp(lhs, m) << " == " << mk_pp(rhs, m) << "\n";
                      );
                expr_ref result(fml, m);
                m_replace.apply_substitution(A, store_t, result);
                m_ctx.elim_var(idx, result, store_t);
                return true;
            }        
            return false;
        }

        bool is_array_app_of(app* a, unsigned& idx, expr* t, decl_kind k, vector<ptr_vector<expr> >& args) {
            if (m_ctx.is_var(a, idx)) {
                contains_app& contains_v = m_ctx.contains(idx);
                if (args.empty() || contains_v(t)) 
                    return false;
                for (auto const& vs : args)
                    for (auto v : vs)
                        if (contains_v(v))
                            return false;
                return true;
            }            
            if (!is_app_of(a, m_fid, k)) 
                return false;
            args.push_back(ptr_vector<expr>());
            for (unsigned i = 1; i < a->get_num_args(); ++i) {
                args.back().push_back(a->get_arg(i));
            }
            if (!is_app(a->get_arg(0))) {
                return false;
            }
            return is_array_app_of(to_app(a->get_arg(0)), idx, t, k, args);
        }

        bool is_store_update(app* a, unsigned& idx, expr* t, vector<ptr_vector<expr> >& args) {
            return is_array_app_of(a, idx, t, OP_STORE, args);
        }
        
        bool is_select(app* a, unsigned& idx, expr* t, vector<ptr_vector<expr> >& args) {
            return is_array_app_of(a, idx, t, OP_SELECT, args);
        }
    };

    qe_solver_plugin* mk_array_plugin(i_solver_context& ctx) {
        return alloc(array_plugin, ctx, ctx.get_manager());
    }
    
}
