import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;

//deals with binary operations (e.g. a + b) -- tries to work out in which class the operation is defined
//(could be either first or second element, e.g. 1 + "2" could be defined either in Int or String)
//also has some hardcoded functionality for the conditional operator ?: and casting operator "as"
//handles some logic around assignments, e.g. optional assignment dictionary?["key"] = "val", but the bulk of that is done by Prefix
public class BinaryExpression implements PrefixOrExpression {

    static public int minOperatorPriority = 4;
    static public int maxOperatorPriority = 10;

    Instance type;
    String code;
    ParserRuleContext originalCtx;
    public String code(ParseTree ctx, Visitor visitor) {return code;}
    public Instance type() {return type;}
    public ParserRuleContext originalCtx() {return originalCtx;}
    private Object L;
    private Object R;
    private ParserRuleContext operator;

    public BinaryExpression(Object L, Object R, ParserRuleContext operator) {
        this.L = L; this.R = R; this.operator = operator;
    }

    public void compute(Instance type, ParseTree ctx, Visitor visitor) {
        String alias = BinaryExpression.operatorAlias(operator);
        String definitionCode = null;

        PrefixOrExpression L, R;
        if(this.L instanceof SwiftParser.Prefix_expressionContext) {
            this.L = new Prefix((SwiftParser.Prefix_expressionContext) this.L, type, visitor);
            if(type == null && isAssignment(alias)) type = ((Prefix)this.L).type();
        }
        else ((BinaryExpression)this.L).compute(type, ctx, visitor);
        if(this.R instanceof SwiftParser.Prefix_expressionContext) {
            this.R = new Prefix((SwiftParser.Prefix_expressionContext) this.R, type, visitor);
        }
        else if(this.R != null) ((BinaryExpression)this.R).compute(type, ctx, visitor);
        L = (PrefixOrExpression)this.L;
        R = (PrefixOrExpression)this.R;

        if(operator instanceof SwiftParser.Conditional_operatorContext) {
            //TODO should be grouping conditionals from right to left, e.g. true ? 1 : true ? 2 : 3 to true ? 1 : (true ? 2 : 3), currently that would be evaluated as 'true ? 1 : true'
            SwiftParser.Conditional_operatorContext conditionalOperator = (SwiftParser.Conditional_operatorContext)operator;
            Instance passType = TypeUtil.infer(conditionalOperator.expression(), visitor);
            Expression passExpression = new Expression(conditionalOperator.expression(), passType, visitor);
            this.type = TypeUtil.alternative(passExpression, R);
            this.code = L.code(ctx, visitor) + " ? " + passExpression.code + " : " + R.code(ctx, visitor);
        }
        else if(operator instanceof SwiftParser.Type_casting_operatorContext) {
            Instance castType = TypeUtil.fromDefinition(((SwiftParser.Type_casting_operatorContext) operator).type(), visitor);
            if(operator.getChild(0).getText().equals("as")) {
                this.type = castType;
                this.code = L.code(ctx, visitor);// + " as " + this.type.jsType();
            }
            else {
                this.type = new Instance("Bool", ctx, visitor.cache);
                this.code = L.code(ctx, visitor) + " instanceof " + this.type.targetType(visitor.targetLanguage);
            }
        }
        else {
            String assignment = isAssignment(alias) ? R.type().uniqueId().equals("Void") ? "N" : R.type().isOptional ? "TN" : "T" : null,
                   lCode = isAssignment(alias) ? ((Prefix)L).code(assignment, ctx, visitor) : L.code(ctx, visitor), rCode = R.code(ctx, visitor);

            if(assignment != null) {
                if(lCode.equals("this")) {
                    definitionCode = "Object.assign(#A0, #A1)";
                }

                rCode = AssignmentUtil.augment(rCode, type, R.originalCtx(), visitor);
            }

            List<Instance> parameterTypes = new ArrayList<Instance>();
            List<String> parameterExternalNames = new ArrayList<String>();
            parameterTypes.add(L.type());
            parameterExternalNames.add("");
            if(R != null) {
                parameterTypes.add(R.type());
                parameterExternalNames.add("");
            }
            Instance functionOwner = null;
            String augment = FunctionUtil.augmentFromCall(alias, parameterTypes, parameterExternalNames, L.type(), false, ((ClassDefinition)L.type().definition).getAllProperties());
            if(augment != null) functionOwner = L.type();
            else {
                augment = FunctionUtil.augmentFromCall(alias, parameterTypes, parameterExternalNames, R.type(), false, ((ClassDefinition)L.type().definition).getAllProperties());
                if(augment != null) functionOwner = R.type();
            }

            Operator operator = (Operator)visitor.cache.find(alias, ctx).object;
            
            this.type = augment != null ? functionOwner.getProperty(alias + augment).result() : operator.result != null ? new Instance(operator.result) : TypeUtil.alternative(L, R);

            if(definitionCode == null) {
                definitionCode =
                    augment != null && functionOwner.getProperty(alias + augment).codeReplacement != null && functionOwner.getProperty(alias + augment).codeReplacement.get(visitor.targetLanguage) != null ? functionOwner.getProperty(alias + augment).codeReplacement.get(visitor.targetLanguage) :
                    //TODO perhaps add that later; currently screws with native operations, e.g. "" + "": augment != null ? functionOwner.targetType(visitor.targetLanguage, true, true) + "." + alias + "(#A0, #A1)" :
                    operator.codeReplacement != null ? operator.codeReplacement.get(visitor.targetLanguage) :
                    "#A0 " + alias + " #A1";
            }
            boolean assignmentIsReplaced = assignment != null && (
                ((Prefix.replacements(((Prefix) L).elems, ((Prefix) L).elems.size() - 1, true, assignment, visitor).containsKey("T") || Prefix.replacements(((Prefix) L).elems, ((Prefix) L).elems.size() - 1, true, assignment, visitor).containsKey("N"))) ||
                ((Prefix) L).elems.get(((Prefix) L).elems.size() - 1).type.isGetterSetter ||
                ((Prefix) L).elems.get(((Prefix) L).elems.size() - 1).type.isInout
            );
            this.code =
                    assignmentIsReplaced ? lCode.replaceAll("#ASS", Matcher.quoteReplacement(rCode)) :
                    definitionCode.replaceAll("#A0", Matcher.quoteReplacement(lCode)).replaceAll("#A1", Matcher.quoteReplacement(rCode));
            if(assignment != null && ((Prefix) L).hasOptionals()) {
                this.code = "if(" + optionalsGuardingIf(((Prefix) L), assignment, ctx, visitor) + "){" + this.code + ";}";
            }
        }
    }

    static public int priorityForOperator(ParserRuleContext operator, ParseTree ctx, Visitor visitor) {
        String operatorAlias = BinaryExpression.operatorAlias(operator);
        return ((Operator)visitor.cache.find(operatorAlias, ctx).object).priority;
    }
    static public String operatorAlias(ParserRuleContext operator) {
        if(operator instanceof SwiftParser.Conditional_operatorContext) return "?:";
        if(operator instanceof SwiftParser.Type_casting_operatorContext) return operator.getChild(0).getText();
        return operator.getText();
    }

    static private boolean isAssignment(String alias) {
        return alias.equals("=") || alias.equals("+=") || alias.equals("-=") || alias.equals("*=") || alias.equals("/=") || alias.equals("%=");
    }

    static private String optionalsGuardingIf(Prefix L, String assignment, ParseTree ctx, Visitor visitor) {
        String ifCode = "";
        for(int i = 0; i < L.elems.size(); i++) {
            if(L.elems.get(i).isOptional) ifCode += (ifCode.length() > 0 ? " && " : "") + "(" + L.code(assignment, i, ctx, visitor) + ") != null";
        }
        return ifCode;
    }
}