/*
 *  Permission is hereby granted, free of charge, to any person obtaining
 *  a copy of this software and associated documentation files (the
 *  "Software"), to deal in the Software without restriction, including
 *  without limitation the rights to use, copy, modify, merge, publish,
 *  distribute, sublicense, and/or sell copies of the Software, and to
 *  permit persons to whom the Software is furnished to do so, subject to
 *  the following conditions:
 *
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

#include <chuffed/support/vec.h>
#include <chuffed/flatzinc/flatzinc.h>
#include <chuffed/core/engine.h>
#include <chuffed/branching/branching.h>


using namespace std;

void output_var(Branching *v);

namespace FlatZinc {

    FlatZincSpace *s;

    VarBranch ann2ivarsel(AST::Node* ann) {
        if (AST::Atom* s = dynamic_cast<AST::Atom*>(ann)) {
            if (s->id == "input_order") return VAR_INORDER;
            if (s->id == "first_fail") return VAR_SIZE_MIN;
            if (s->id == "anti_first_fail") return VAR_SIZE_MAX;
            if (s->id == "smallest") return VAR_MIN_MIN;
            if (s->id == "smallest_largest") return VAR_MAX_MIN;
            if (s->id == "largest") return VAR_MAX_MAX;
            if (s->id == "largest_smallest") return VAR_MIN_MAX;
            if (s->id == "occurrence") return VAR_DEGREE_MAX;
            if (s->id == "most_constrained") return VAR_SIZE_MIN;
            if (s->id == "max_regret") return VAR_REGRET_MIN_MAX;
            if (s->id == "random_order") return VAR_RANDOM;
        }
        cerr << "% Warning: Unknown or not support variable selection annotation '";
        ann->print(cerr);
        cerr << "'! Ignore variable selection annotation and replace it by 'input_order'." << endl;
        return VAR_INORDER;
    }

    ValBranch ann2ivalsel(AST::Node* ann) {
        if (AST::Atom* s = dynamic_cast<AST::Atom*>(ann)) {
            if (s->id == "default") return VAL_DEFAULT;
            if (s->id == "indomain") return VAL_MIN;
            if (s->id == "indomain_min") return VAL_MIN;
            if (s->id == "indomain_max") return VAL_MAX;
            //if (s->id == "indomain_middle") return VAL_MIDDLE;
            if (s->id == "indomain_middle") CHUFFED_ERROR("The value choice annotation 'indomain_middle' is not supported yet!\n");
            if (s->id == "indomain_median") return VAL_MEDIAN;
            if (s->id == "indomain_split") return VAL_SPLIT_MIN;
            if (s->id == "indomain_reverse_split") return VAL_SPLIT_MAX;
            //if (s->id == "indomain_random") return VAL_RANDOM;
            if (s->id == "indomain_random") CHUFFED_ERROR("The value choice annotation 'indomain_random' is not supported yet!\n");
        }
        std::cerr << "% Warning, ignored search annotation: ";
        ann->print(std::cerr);
        std::cerr << std::endl;
        return VAL_DEFAULT;
    }

    FlatZincSpace::FlatZincSpace(int intVars, int boolVars, int setVars) 
    :   intVarCount(0), boolVarCount(0), iv(intVars), iv_introduced(intVars),
        bv(boolVars), bv_introduced(boolVars), output(NULL) { 
        s = this; 
    }

    void FlatZincSpace::newIntVar(IntVarSpec* vs) {
        // Resizing of the vectors if required
        if (intVarCount == iv.size()) {
            const int newSize = intVarCount > 0 ? 2 * intVarCount : 1;
            iv.growTo(newSize);
            iv_introduced.resize(newSize);
        }
        bool considerIntroduced = false;
        if (so.use_var_is_introduced) considerIntroduced = vs->introduced;
        else                          considerIntroduced = !vs->output;
        if (so.introduced_heuristic && vs->looks_introduced) considerIntroduced = true;
        if (vs->alias) {
            iv[intVarCount++] = iv[vs->i];
        } else {
            IntVar* v = NULL;
            if (vs->assigned) v = getConstant(vs->i);
            else if (vs->domain()) {
                AST::SetLit* sl = vs->domain.some();
                if (sl->interval) v = ::newIntVar(sl->min, sl->max);
                else {
                    vec<int> d;
                    for (unsigned int i = 0; i < sl->s.size(); i++) d.push(sl->s[i]);
                    sort((int*) d, (int*) d + d.size());
                    v = ::newIntVar(d[0], d.last());
                    if ((d.last()-d[0] >= d.size() * mylog2(d.size())) ||
                        (d.size() <= so.eager_limit && (d.last() - d[0] + 1) > so.eager_limit)) {
                        new (v) IntVarSL(*v, d);
                    } else {
                        if (!v->allowSet(d)) TL_FAIL();
                    }
                }
            } 
            else {
                v = ::newIntVar();
            }
            /* std::cerr << "int var: " << intVarCount << " " << v << "\n"; */
                        
            if (so.exclude_introduced && considerIntroduced)
                v->should_be_learnable = false;
            if (!so.decide_introduced && considerIntroduced)
                v->should_be_decidable = false;
            iv[intVarCount++] = v;
        }
        iv_introduced[intVarCount-1] = considerIntroduced;
    }

    void FlatZincSpace::newBoolVar(BoolVarSpec* vs) {
        // Resizing of the vectors if required
        if (boolVarCount == bv.size()) {
            const int newSize = boolVarCount > 0 ? 2 * boolVarCount : 1;
            bv.growTo(newSize);
            bv_introduced.resize(newSize);
        }
        bool considerIntroduced = false;
        if (so.use_var_is_introduced) considerIntroduced = vs->introduced;
        else                          considerIntroduced = !vs->output;
        if (so.introduced_heuristic && vs->looks_introduced) considerIntroduced = true;
        if (vs->alias) {
            bv[boolVarCount++] = bv[vs->i];
        } 
        else {
            BoolView v;
#if EXPOSE_INT_LITS
            if (vs->alias_var >= 0) {
                iv[vs->alias_var]->specialiseToEL();
                switch (vs->alias_irt) {
                case IRT_EQ:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val, 1));
                    break;
                case IRT_NE:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val, 0));
                    break;
                case IRT_GE:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val, 2));
                    break;
                case IRT_GT:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val + 1, 2));
                    break;
                case IRT_LE:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val, 3));
                    break;
                case IRT_LT:
                    v = BoolView(iv[vs->alias_var]->getLit(vs->alias_val - 1, 3));
                    break;
                default:
                    NEVER;
                }
            }
            else
#endif
                v = ::newBoolVar();

            if (vs->assigned) v.setVal(vs->i);
            else if (vs->domain()) {
                AST::SetLit* sl = vs->domain.some();
                assert(sl->interval);
                assert(sl->min <= 1);
                assert(sl->max >= 0);
                assert(sl->min <= sl->max);
                if (sl->min == 1) v.setVal(true);
                if (sl->max == 0) v.setVal(false);
            }
            if (so.exclude_introduced && considerIntroduced) {
                v.setLearnable(false);
                v.setDecidable(false);
                v.setUIPable(false);
            }
            if (!so.decide_introduced && considerIntroduced) {
                v.setDecidable(false);
            }
            bv[boolVarCount++] = v;
        }
        bv_introduced[boolVarCount-1] = considerIntroduced;
    }

    void
    FlatZincSpace::newSetVar(SetVarSpec*) {
        throw FlatZinc::Error("LazyGeoff", "set variables not supported");
    }

    void FlatZincSpace::postConstraint(const ConExpr& ce, AST::Node* ann) {
        try {
            registry().post(ce, ann);
        } catch (AST::TypeError& e) {
            throw FlatZinc::Error("Type error", e.what());
        } catch (exception& e) {
            throw FlatZinc::Error("LazyGeoff", e.what());
        }
    }

    
    // Parsing the 'int_search' annotation and setting up the branching for it
    void FlatZincSpace::parseSolveAnnIntSearch(AST::Node* elemAnn, BranchGroup* branching, int& nbNonEmptySearchAnnotations) {
        assert(elemAnn->isCall("int_search"));
        try {
            // Retrieval of the data
            AST::Call *call = elemAnn->getCall("int_search");
            AST::Array *args = call->getArgs(4);
            AST::Array *vars = args->a[0]->getArray();
            vec<Branching*> va;
            for (unsigned int i = 0; i < vars->a.size(); i++) {
                // Removal of constants
                if (vars->a[i]->isInt()) continue;
                IntVar* v = iv[vars->a[i]->getIntVar()];
                // Removal of fixed variables
                if (v->isFixed()) continue;
                va.push(v);
            }
            branching->add(createBranch(va, ann2ivarsel(args->a[1]), ann2ivalsel(args->a[2])));
            if (AST::String* s = dynamic_cast<AST::String*>(args->a[3])) {
                if (s->s == "all") so.nof_solutions = 0;
            }
            nbNonEmptySearchAnnotations++;
        }
        catch (AST::TypeError& e) {
            throw FlatZinc::Error("Type error in int_search annotation", e.what());
        }
    }

    // Parsing the 'bool_search' annotation and setting up the branching for it
    void FlatZincSpace::parseSolveAnnBoolSearch(AST::Node* elemAnn, BranchGroup* branching, int& nbNonEmptySearchAnnotations) {
        assert(elemAnn->isCall("bool_search"));
        try {
            // Retrieval of the data
            AST::Call *call = elemAnn->getCall("bool_search");
            AST::Array *args = call->getArgs(4);
            AST::Array *vars = args->a[0]->getArray();
            vec<Branching*> va(vars->a.size());
            for (int i=vars->a.size(); i--; )
                va[i] = new BoolView(bv[vars->a[i]->getBoolVar()]);
            branching->add(createBranch(va, ann2ivarsel(args->a[1]), ann2ivalsel(args->a[2]))); 
            if (AST::String* s = dynamic_cast<AST::String*>(args->a[3])) {
                if (s->s == "all") so.nof_solutions = 0;
            }
            nbNonEmptySearchAnnotations++;
        }
        catch (AST::TypeError& e) {
            throw FlatZinc::Error("Type error in bool_search annotation", e.what());
        }
    }

    // Parsing the 'priority_search' annotation and setting up the branching for it
    void FlatZincSpace::parseSolveAnnPrioritySearch(AST::Node* elemAnn, BranchGroup* branching, int& nbNonEmptySearchAnnotations) {
        assert(elemAnn->isCall("priority_search"));
        try {
            // Retrieval of the data
            AST::Call *call = elemAnn->getCall("priority_search");
            AST::Array *args = call->getArgs(4);
            AST::Array *vars = args->a[0]->getArray();
            AST::Array *annotations = args->a[1]->getArray();
            // Retrieval of all variables
            // NOTE that constants or fixed variables cannot be removed, because they act as 
            // delegates for choosing the branch group to search on next
            vec<Branching*> va;
            for (unsigned int i = 0; i < vars->a.size(); i++) {
                // Removal of constants
                IntVar* v = NULL;
                if (vars->a[i]->isInt()) {
                    int value = vars->a[i]->getInt();
                    v = getConstant(value);
                }
                else {
                    v = iv[vars->a[i]->getIntVar()];
                }
                va.push(v);
            }
            // Create a new priority branch group and add to branching
            PriorityBranchGroup * priorityBranching = new PriorityBranchGroup(va, ann2ivarsel(args->a[2]));
            // Parse search annotations
            int nbChildSearchAnnotations = 0;
            parseSolveAnn(annotations, priorityBranching, nbChildSearchAnnotations);
            if (vars->a.size() != nbChildSearchAnnotations)
                throw FlatZinc::Error("Type error in priority_search annotation", "Variable and annotation array must have the same size");
            if (AST::String* s = dynamic_cast<AST::String*>(args->a[3])) {
                if (s->s == "all") so.nof_solutions = 0;
            }
            branching->add(priorityBranching);
            nbNonEmptySearchAnnotations++;
        } 
        catch (AST::TypeError& e) {
            throw FlatZinc::Error("Type error in priority_search annotation", e.what());
        }
    }

    void FlatZincSpace::parseSolveAnnAux(AST::Node* elemAnn, BranchGroup* branching, int& nbNonEmptySearchAnnotations) {
        if (elemAnn->isCall("int_search")) {
            parseSolveAnnIntSearch(elemAnn, branching, nbNonEmptySearchAnnotations);
        }
        else if (elemAnn->isCall("bool_search")) {
            parseSolveAnnBoolSearch(elemAnn, branching, nbNonEmptySearchAnnotations);
        }
        else if (elemAnn->isCall("priority_search")) {
            parseSolveAnnPrioritySearch(elemAnn, branching, nbNonEmptySearchAnnotations);
        }
        else {
            throw FlatZinc::Error("Error in search annotation", "Unknown search annotation");
        }
    }
    
    // Users should add search annotation with (core vars, default, default) even if they know nothing

    // Entry function for parsing the solve annotation
    void FlatZincSpace::parseSolveAnn(AST::Array* ann) {
        int nbNonEmptySearchAnnotations = 0;
        try {
            // Parse the search annotation
            parseSolveAnn(ann, engine.branching, nbNonEmptySearchAnnotations);
        } 
        catch (FlatZinc::Error& e) {
            cerr << "% " << e.toString() << ". Ignore search annotation!" << endl;
            // Removal of successful parsed parts of the search annotation
            engine.branching = new BranchGroup();
            // Reset counter
            nbNonEmptySearchAnnotations = 0;
        }
        // Check whether a search was specified
        if (nbNonEmptySearchAnnotations == 0) {
            if (!so.vsids) {
                so.vsids = true;
                engine.branching->add(&sat);
            }
        }
    }

    void FlatZincSpace::parseSolveAnn(AST::Array* ann, BranchGroup* branching, int & nbNonEmptySearchAnnotations) {
        if (ann) {
            for (unsigned int i = 0; i < ann->a.size(); i++) {
                if (ann->a[i]->isCall("restart_none") && so.restart_type_override) {
                    so.restart_type = NONE;
                } else if (ann->a[i]->isCall("restart_constant")) {
                    AST::Call* call = ann->a[i]->getCall("restart_constant");
                    if (so.restart_type_override) { so.restart_type = CONSTANT; }
                    if (so.restart_scale_override) { so.restart_scale = static_cast<unsigned int>(call->args->getInt()); }
                } else if (ann->a[i]->isCall("restart_linear")) {
                    AST::Call* call = ann->a[i]->getCall("restart_linear");
                    if (so.restart_type_override) { so.restart_type = LINEAR; }
                    if (so.restart_scale_override) { so.restart_scale = static_cast<unsigned int>(call->args->getInt()); }
                } else if (ann->a[i]->isCall("restart_luby")) {
                    AST::Call* call = ann->a[i]->getCall("restart_luby");
                    if (so.restart_type_override) { so.restart_type = LUBY; }
                    if (so.restart_scale_override) { so.restart_scale = static_cast<unsigned int>(call->args->getInt()); }
                } else if (ann->a[i]->isCall("restart_geometric")) {
                    AST::Call* call = ann->a[i]->getCall("restart_geometric");
                    if (so.restart_type_override) { so.restart_type = GEOMETRIC; }
                    AST::Array* args = call->getArgs(2);
                    if (so.restart_base_override) { so.restart_base = args->a[0]->getFloat(); }
                    if (so.restart_base < 1.0) {
                        CHUFFED_ERROR("Illegal restart base. Restart count will converge to zero.");
                    }
                    if (so.restart_scale_override) { so.restart_scale = static_cast<unsigned int>(args->a[1]->getInt()); }
                } else if (ann->a[i]->isCall("seq_search")) {
                    // Get the call
                    AST::Call* c = ann->a[i]->getCall();
                    // Create a new branch group and add to the branching
                    BranchGroup* newBranching = new BranchGroup();
                    branching->add(newBranching);
                    if (c->args->isArray()) {
                        // Parse annotation and add to the newly created branching
                        int nbChildSearchAnnotations = 0;
                        parseSolveAnn(c->args->getArray(), newBranching, nbChildSearchAnnotations);
                        if (nbChildSearchAnnotations > 0)
                            nbNonEmptySearchAnnotations++;
                    }
                    else {
                        // Parse annotation and add to the newly created branching
                        parseSolveAnnAux(c->args, newBranching, nbNonEmptySearchAnnotations);
                    }
                }
                else {
                    // Parse annotation and add to the branching
                    parseSolveAnnAux(ann->a[i], branching, nbNonEmptySearchAnnotations);
                }
            }
        }
    }


    void FlatZincSpace::fixAllSearch() {
        vec<Branching*> va;
        for (int i = 0; i < intVarCount; i++) {
            if (iv_introduced[i]) continue;
            IntVar* v = iv[i];
            if (v->isFixed()) continue;
            va.push(v);
        }
        for (int i = 0; i < boolVarCount; i++) {
            if (bv_introduced[i]) continue;
            va.push(new BoolView(bv[i]));
        }
        for (int i = intVarCount; i--; ) {
            if (!iv_introduced[i]) continue;
            IntVar* v = iv[i];
            if (v->isFixed()) continue;
            va.push(v);
        }
        for (int i = 0; i < boolVarCount; i++) {
            if (!bv_introduced[i]) continue;
            va.push(new BoolView(bv[i]));
        }
        if (va.size()) branch(va, VAR_INORDER, VAL_DEFAULT);
    }

    void FlatZincSpace::solve(AST::Array* ann) {
        parseSolveAnn(ann);
        fixAllSearch();
    }

    void FlatZincSpace::minimize(int var, AST::Array* ann) {
        parseSolveAnn(ann);
        optimize(iv[var], OPT_MIN);
        fixAllSearch();     
    }

    void FlatZincSpace::maximize(int var, AST::Array* ann) {
        parseSolveAnn(ann);
        optimize(iv[var], OPT_MAX);
        fixAllSearch();
    }

    void FlatZincSpace::setOutputElem(AST::Node* ai) const {
        if (ai->isIntVar()) {
            output_var(iv[ai->getIntVar()]);
        } else if (ai->isBoolVar()) {
            output_var(new BoolView(bv[ai->getBoolVar()]));
        }
    }

    void FlatZincSpace::setOutput() {
        if (output == NULL) return;
        for (unsigned int i=0; i< output->a.size(); i++) {
            AST::Node* ai = output->a[i];
            if (ai->isArray()) {
                AST::Array* aia = ai->getArray();
                int size = aia->a.size();
                for (int j=0; j<size; j++) {
                    setOutputElem(aia->a[j]);
                }
            } else if (ai->isCall("ifthenelse")) {
                AST::Array* aia = ai->getCall("ifthenelse")->getArgs(3);
                setOutputElem(aia->a[1]);
                setOutputElem(aia->a[2]);
            } else {
                setOutputElem(ai);
            }
        }
    }

    void FlatZincSpace::printElem(AST::Node* ai, ostream& out) const {
        int k;
        if (ai->isInt(k)) {
            out << k;
        } 
        else if (ai->isIntVar()) {
            out << iv[ai->getIntVar()]->getVal();
        } 
        else if (ai->isBoolVar()) {
            if (bv[ai->getBoolVar()].isTrue()) {
                out << "true";
            } 
            else if (bv[ai->getBoolVar()].isFalse()) {
                out << "false";
            } 
            else {
                out << "false..true";
            }
        } 
        else if (ai->isBool()) {
            out << (ai->getBool() ? "true" : "false");
        } 
        else if (ai->isSet()) {
            AST::SetLit* s = ai->getSet();
            if (s->interval) {
                out << s->min << ".." << s->max;
            } 
            else {
                out << "{";
                for (unsigned int i=0; i<s->s.size(); i++) {
                    out << s->s[i] << (i < s->s.size()-1 ? ", " : "}");
                }
            }
        } 
        else if (ai->isString()) {
            std::string s = ai->getString();
            for (unsigned int i=0; i<s.size(); i++) {
                if (s[i] == '\\' && i<s.size()-1) {
                    switch (s[i+1]) {
                    case 'n': out << "\n"; break;
                    case '\\': out << "\\"; break;
                    case 't': out << "\t"; break;
                    default: out << "\\" << s[i+1];
                    }
                    i++;
                } 
                else {
                    out << s[i];
                }
            }
        }
    }

}
