package at.oefai.aaa.agent.jam;

import java.io.Serializable;
import java.util.Iterator;

import at.oefai.aaa.agent.jam.types.Binding;
import at.oefai.aaa.agent.jam.types.ExpList;
import at.oefai.aaa.agent.jam.types.Expression;
import at.oefai.aaa.agent.jam.types.SymbolMap;
import at.oefai.aaa.agent.jam.types.Value;
import at.oefai.aaa.agent.jam.types.Variable;

/**
 * Represents an 'attribute' 'value_list' pair.
 *
 * @author Marc Huber
 * @author Jaeho Lee
 */
public class Relation implements Serializable {
    public static final String SUCCESS_FACT = "SUCCESS";
    public static final String REQUEST_FACT = "REQUEST";

    public static final String DO_BEHAVIOUR_FACT = "do";
    public static final String HELP_BEHAVIOUR_FACT = "help";
    public static final String HINDER_BEHAVIOUR_FACT = "hinder";

    public static final String IWANT_FACT = "IWantTo";
    public static final String IDID_FACT = "IDid";
    public static final String IFAILED_FACT = "IFailedTo";
    public static final String AGENTWANTS_FACT = "AgentWantsTo";
    public static final String AGENTDID_FACT = "AgentDid";
    public static final String AGENTFAILED_FACT = "AgentFailedTo";

    public static final String PREFER_FACT = "PrefFor";
    public static final String STANDARD_FACT = "Standard";

    public static final String FAILED_INTENTION_FACT = "FailedIntention";
    private static final Value QUESTIONMARK_VARIABLE_VALUE = Value.newValue("?");

    private String name;
    private ExpList args;

    /** Name and parameter list constructor. */
    public Relation(final String s, final ExpList expList) {
        assert (s != null) : "unnamed relation";
        this.name = s;
        this.args = (expList == null) ? new ExpList() : expList;
    }

    /** Name and binding constructor. */
    public Relation(final Relation r, final Binding binding) {
        assert (r != null) : "null relation in relation constructor";
        this.name = r.getName();
        this.args = r.getArgs().explistEval(binding);
    }

    public static Relation newSuccessRelation(final ExpList expList) {
        return new Relation(SUCCESS_FACT, expList);
    }

    public static Relation newRequestRelation(final ExpList expList) {
        return new Relation(REQUEST_FACT, expList);
    }

    public static Relation newPreferenceRelation(final ExpList expList) {
        return new Relation(PREFER_FACT, expList);
    }

    public static Relation newStandardRelation(final ExpList expList) {
        return new Relation(STANDARD_FACT, expList);
    }

    // special type checks: (clean up into inheritance hierarchy, i.e. special subclasses)

    public final boolean isRequest() {
        return this.name.equals(Relation.REQUEST_FACT);
    }

    public final boolean isSuccess() {
        return this.name.equals(Relation.SUCCESS_FACT);
    }

    public final boolean isBehaviour() {
        return (this.isDoBehaviour() || this.isHelpBehaviour() || this.isHinderBehaviour());
    }

    public final boolean isDoBehaviour() {
        return this.name.equals(Relation.DO_BEHAVIOUR_FACT);
    }

    public final boolean isHelpBehaviour() {
        return this.name.equals(Relation.HELP_BEHAVIOUR_FACT);
    }

    public final boolean isHinderBehaviour() {
        return this.name.equals(Relation.HINDER_BEHAVIOUR_FACT);
    }

    public final boolean isIWant() {
        return this.name.equals(Relation.IWANT_FACT);
    }

    public final boolean isIDid() {
        return this.name.equals(Relation.IDID_FACT);
    }

    public final boolean isIFailed() {
        return this.name.equals(Relation.IFAILED_FACT);
    }

    public final boolean isAgentWants() {
        return this.name.equals(Relation.AGENTWANTS_FACT);
    }

    public final boolean isAgentDid() {
        return this.name.equals(Relation.AGENTDID_FACT);
    }

    public final boolean isAgentFailed() {
        return this.name.equals(Relation.AGENTFAILED_FACT);
    }

    public final boolean isProspect() {
        return this.isAgentWants() || this.isIWant();
    }

    public final boolean isBehaviourInterpretation() {
        return (this.isAgentWants() || this.isAgentDid() || this.isAgentFailed());
    }

    public final boolean isMyBehaviour() {
        return (this.isIWant() || this.isIDid() || this.isIFailed());
    }

    public final boolean isStandard() {
        return this.name.equals(Relation.STANDARD_FACT);
    }

    public final boolean isPreference() {
        return this.name.equals(Relation.PREFER_FACT);
    }

    /**
     * If this is an *FailedTo or *Did fact, return the corresponding *WantsTo.
     * @return a *WantsTo fact or null
     */
    public final Relation getCorrespondingProspect() {
        if (this.isAgentDid() || this.isAgentFailed()) {
            return new Relation(Relation.AGENTWANTS_FACT, this.args);
        }
        if (this.isIDid() || this.isIFailed()) {
            return new Relation(Relation.IWANT_FACT, this.args);
        }
        return null;
    }

    public final String getResponsible() {
        if (isSuccess() || isRequest()) {
            return this.args.getNth(2).toString();
        } else if (isBehaviour() || isMyBehaviour() || isBehaviourInterpretation()) {
            return this.args.getNth(1).toString();
        } else {
            assert false : "getResponsible not applicable to relation: " + simpleString(null);
            return "";
        }
    }

    /**
     * returns success value of a success fact or a behaviour interpretation.
     * @param b Binding under which the Sucess Variable will be evaluated
     * @return 1 if success true, -1 if false and 0 if no success fact at all
     */
    public final int getSuccessValue(final Binding b) {
        if (isAgentFailed() || isIFailed() || isHinderBehaviour()) {
            return -1;
        } else if (isAgentDid() || isIDid() || isDoBehaviour() || isHelpBehaviour()) {
            return 1;
        } else if (isAgentWants() || isIWant()) {
            return 0;
        } else if (isSuccess()) {
            if (this.args.getLast().eval(b).getString().equals("True")) {
                return 1;
            }
            return -1;
        } else {
            return 0;
        }
    }

    public final float getStandardValue(final Binding b) {
        if (isStandard()) {
            return (float) this.args.getLast().eval(b).getReal();
        }
        return 0f;
    }

    public final float getPreferenceValue(final Binding b) {
        if (isPreference()) {
            return (float) this.args.getLast().eval(b).getReal();
        }
        return 0f;
    }

    public final String getPreferenceName(final Binding b) {
        if (isPreference()) {
            return this.args.getFirst().eval(b).getString();
        }
        return null;
    }

    final Relation getSubRelation(final Binding b) {
        if (isBehaviour() || isMyBehaviour() || isBehaviourInterpretation()) {
            ExpList el = new ExpList(this.args);
            Expression exp = el.removeNth(2);
            String subname = exp.eval(b).getString();
            return new Relation(subname, el.explistEval(b));
        } else if (isSuccess() || isRequest()) {
            ExpList el = new ExpList(this.args);
            Expression exp = el.removeFirst();
            String subname = exp.eval(b).getString();
            if (isSuccess()) {
                el.removeLast();
            }
            return new Relation(subname, el.explistEval(b));
        } else if (isStandard()) {
            ExpList el = new ExpList(this.args);
            Expression exp = el.removeNth(2);
            String subname = exp.eval(b).getString();
            el.removeLast(); // the standards positiveness value
            return new Relation(subname, el.explistEval(b));
        } else {
            return this;
        }
    }

    final Relation variableQuestionMarks(final SymbolMap sm) {
        int i = 1;
        ExpList el = new ExpList();
        for (Expression exp : this.args) {
            if (exp.equals(QUESTIONMARK_VARIABLE_VALUE, null)) {
                el.addLast(sm.getVariable(QUESTIONMARK_VARIABLE_VALUE.getString() + i++));
            } else {
                el.addLast(exp);
            }
        }
        return new Relation(this.name, el);
    }

    public final String getName() {
        return this.name;
    }

    public final ExpList getArgs() {
        return this.args;
    }

    /** Convert all variables elements of the relation into constants. */
    public final Relation evalArgs(final Binding b) {
        this.args = this.args.explistEval(b);
        return this;
    }

    /** Format the output and don't worry about being printed out in-line with other information. */
    public final String verboseString(final Binding b) {
        String s = getName() + " " + this.args.verboseString(b);
        if (b != null) {
            s += b.verboseString();
        }
        return s;
    }

    /** Format the output so that it's conducive to being printed out in-line with other information. */
    public final String formattedString(final Binding b) {
        String s = getName() + " " + this.args.formattedString(b);
        if (b != null) {
            s += b.formattedString();
        }
        return s;
    }

    /** Format the output for in-line printing, no binding. */
    public final String simpleString(final Binding b) {
        return getName() + " " + this.args.formattedString(b);
    }

    /**
     * If the source & the destination relations do not match, return false. Otherwise change the
     * destination binding with linked variables to the source relation binding and return true.
     */
    public static final boolean unify(final Relation dstRelation, final Binding dstBinding,
                                      final Relation srcRelation, final Binding srcBinding) {
        if ((dstRelation == null) || (srcRelation == null)) {
            return false;
        }
        if (!srcRelation.getName().equals(dstRelation.getName())) {
            return false;
        }
        if (srcRelation.getArgs().size() <= 0 && dstRelation.getArgs().size() <= 0) {
            return true;
        }
        Binding dstBindingCopy = new Binding(dstBinding);
        // use iterators instead of for each, as we have to iterate in parallel
        Iterator<Expression> srcArgsEnum = srcRelation.getArgs().iterator();
        Iterator<Expression> dstArgsEnum = dstRelation.getArgs().iterator();
        while (srcArgsEnum.hasNext() && dstArgsEnum.hasNext()) {
            Expression srcArg = srcArgsEnum.next();
            Expression dstArg = dstArgsEnum.next();
            Value srcVal = srcArg.eval(srcBinding);
            Value dstVal = dstArg.eval(dstBinding);
            if (dstVal.isDefined()) {
                if (srcVal.isDefined()) {
                    if (srcVal.eq(dstVal)) {
                        continue;
                    }
                    // log.info("\nUnify failed because both defined but srcVal != dstVal (" +
                    // srcVal.toString() + ", " + dstVal.toString() + ")\n");
                    return false;
                }
                // dst is defined but src isnt -> no matching possible as only destination can be
                // changed
                return false;
            } else if (dstArg instanceof Variable) {
                if (srcArg instanceof Variable) {
                    // log.info("Linking src and dst variables.");
                    dstBindingCopy.linkVariables((Variable) dstArg, (Variable) srcArg, srcBinding);
                } else {
                    dstBindingCopy.setValue(dstArg, srcVal);
                    // log.info("srcVal type is: " + srcVal.getType() +", dstArg type is: " +
                    // dstArg.getType());
                    // log.info("Setting dst value to src value (" +srcVal.toString() + ").");
                }
            } else { // strange because we shouldnt reach this stage
                // this means dstArg is not a variable, but is also not defined
                // lets accept this if srcVal is also not defined otherwise false
                if (srcVal.isDefined()) {
                    // log.info("\nUnify failed because dst is undefined and not a variable but src
                    // is defined\n");
                    return false;
                }
            }
        }
        if (dstBinding != null) {
            dstBinding.copy(dstBindingCopy);
        }
        return true;
    }

}
