/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.js.translate.operation;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.descriptors.CallableDescriptor;
import org.jetbrains.kotlin.descriptors.FunctionDescriptor;
import org.jetbrains.kotlin.js.backend.ast.*;
import org.jetbrains.kotlin.js.backend.ast.metadata.MetadataProperties;
import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator;
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
import org.jetbrains.kotlin.js.translate.general.AbstractTranslator;
import org.jetbrains.kotlin.js.translate.general.Translation;
import org.jetbrains.kotlin.js.translate.intrinsic.functions.factories.TopLevelFIF;
import org.jetbrains.kotlin.js.translate.intrinsic.operation.BinaryOperationIntrinsic;
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils;
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils;
import org.jetbrains.kotlin.lexer.KtToken;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.KtBinaryExpression;
import org.jetbrains.kotlin.psi.KtExpression;
import org.jetbrains.kotlin.psi.KtPsiUtil;
import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilsKt;
import org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilKt;
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
import org.jetbrains.kotlin.types.KotlinType;
import org.jetbrains.kotlin.types.TypeUtils;
import org.jetbrains.kotlin.types.expressions.OperatorConventions;
import org.jetbrains.kotlin.types.typeUtil.TypeUtilsKt;

import java.util.Collections;

import static org.jetbrains.kotlin.js.translate.operation.AssignmentTranslator.isAssignmentOperator;
import static org.jetbrains.kotlin.js.translate.operation.CompareToTranslator.isCompareToCall;
import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.getCallableDescriptorForOperationExpression;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.not;
import static org.jetbrains.kotlin.js.translate.utils.PsiUtils.getOperationToken;
import static org.jetbrains.kotlin.js.translate.utils.PsiUtils.isNegatedOperation;

public final class BinaryOperationTranslator extends AbstractTranslator {

    @NotNull
    public static JsExpression translate(@NotNull KtBinaryExpression expression,
            @NotNull TranslationContext context) {
        JsExpression jsExpression = new BinaryOperationTranslator(expression, context).translate();
        return jsExpression.source(expression);
    }

    @NotNull
    /*package*/ static JsExpression translateAsOverloadedCall(@NotNull KtBinaryExpression expression,
            @NotNull TranslationContext context) {
        JsExpression jsExpression = (new BinaryOperationTranslator(expression, context)).translateAsOverloadedBinaryOperation();
        return jsExpression.source(expression);
    }

    @NotNull
    private final KtBinaryExpression expression;

    @NotNull
    private final KtExpression leftKtExpression;

    @NotNull
    private final KtExpression rightKtExpression;

    @NotNull
    private final KtToken operationToken;

    @Nullable
    private final CallableDescriptor operationDescriptor;

    private BinaryOperationTranslator(@NotNull KtBinaryExpression expression,
            @NotNull TranslationContext context) {
        super(context);
        this.expression = expression;

        assert expression.getLeft() != null : "Binary expression should have a left expression: " + expression.getText();
        leftKtExpression = expression.getLeft();

        assert expression.getRight() != null : "Binary expression should have a right expression: " + expression.getText();
        rightKtExpression = expression.getRight();

        this.operationToken = getOperationToken(expression);
        this.operationDescriptor = getCallableDescriptorForOperationExpression(bindingContext(), expression);
    }

    @NotNull
    private JsExpression translate() {
        JsExpression e = tryApplyIntrinsic();
        if (e != null) {
            return e;
        }
        if (operationToken == KtTokens.ELVIS) {
            return translateElvis();
        }
        if (isAssignmentOperator(operationToken)) {
            return AssignmentTranslator.translate(this.expression, context());
        }
        if (isNotOverloadable()) {
            return translateAsUnOverloadableBinaryOperation();
        }
        if (isCompareToCall(operationToken, operationDescriptor)) {
            return CompareToTranslator.translate(this.expression, context());
        }
        if (isEquals()) {
            return translateEquals();
        }
        assert operationDescriptor != null :
                "Overloadable operations must have not null descriptor";
        return translateAsOverloadedBinaryOperation();
    }

    @NotNull
    private JsExpression translateElvis() {
        KotlinType expressionType = context().bindingContext().getType(expression);
        assert expressionType != null;

        JsExpression leftExpression = TranslationUtils.coerce(
                context(), Translation.translateAsExpression(leftKtExpression, context()), TypeUtilsKt.makeNullable(expressionType));

        JsBlock rightBlock = new JsBlock();
        JsExpression rightExpression = TranslationUtils.coerce(
                context(), Translation.translateAsExpression(rightKtExpression, context(), rightBlock), expressionType);

        if (rightBlock.isEmpty()) {
            return TranslationUtils.notNullConditional(leftExpression, rightExpression, context());
        }

        JsExpression result;
        JsIf ifStatement;
        if (BindingContextUtilsKt.isUsedAsExpression(expression, context().bindingContext())) {
            result = context().cacheExpressionIfNeeded(leftExpression);
            JsExpression testExpression = TranslationUtils.isNullCheck(result);
            rightBlock.getStatements().add(JsAstUtils.assignment(result, rightExpression).makeStmt());
            ifStatement = JsAstUtils.newJsIf(testExpression, rightBlock);
        }
        else {
            result = new JsNullLiteral();
            JsExpression testExpression = TranslationUtils.isNullCheck(leftExpression);
            ifStatement = JsAstUtils.newJsIf(testExpression, rightBlock);
        }
        ifStatement.setSource(expression);
        context().addStatementToCurrentBlock(ifStatement);
        return result;
    }

    @Nullable
    private JsExpression tryApplyIntrinsic() {
        BinaryOperationIntrinsic intrinsic =
                context().intrinsics().getBinaryOperationIntrinsic(expression, context());

        if (intrinsic == null) return null;

        JsExpression leftExpression = Translation.translateAsExpression(leftKtExpression, context());

        JsBlock rightBlock = new JsBlock();
        JsExpression rightExpression = Translation.translateAsExpression(rightKtExpression, context(), rightBlock);

        if (rightBlock.isEmpty()) {
            return intrinsic.invoke(expression, leftExpression, rightExpression, context());
        }

        leftExpression = context().cacheExpressionIfNeeded(leftExpression);
        context().addStatementsToCurrentBlockFrom(rightBlock);

        return intrinsic.invoke(expression, leftExpression, rightExpression, context());
    }

    private boolean isNotOverloadable() {
        return OperatorConventions.NOT_OVERLOADABLE.contains(operationToken);
    }

    @NotNull
    private JsExpression translateAsUnOverloadableBinaryOperation() {
        assert OperatorConventions.NOT_OVERLOADABLE.contains(operationToken);
        JsBinaryOperator operator = OperatorTable.getBinaryOperator(operationToken);
        JsExpression leftExpression = Translation.translateAsExpression(leftKtExpression, context());

        JsBlock rightBlock = new JsBlock();
        JsExpression rightExpression = Translation.translateAsExpression(rightKtExpression, context(), rightBlock);

        if (rightBlock.isEmpty()) {
            return new JsBinaryOperation(operator, leftExpression, rightExpression);
        }
        else if (OperatorConventions.IDENTITY_EQUALS_OPERATIONS.contains(operationToken)) {
            context().addStatementsToCurrentBlockFrom(rightBlock);
            return new JsBinaryOperation(operator, leftExpression, rightExpression);
        }

        assert operationToken.equals(KtTokens.ANDAND) || operationToken.equals(KtTokens.OROR) : "Unsupported binary operation: " + expression.getText();
        boolean isOror = operationToken.equals(KtTokens.OROR);
        JsExpression literalResult = new JsBooleanLiteral(isOror).source(rightKtExpression);
        leftExpression = isOror ? not(leftExpression) : leftExpression;

        JsIf ifStatement;
        JsExpression result;
        if (BindingContextUtilsKt.isUsedAsExpression(expression, context().bindingContext())) {
            if (rightExpression instanceof JsNameRef) {
                result = rightExpression; // Reuse tmp variable
            } else {
                result = context().declareTemporary(null, rightKtExpression).reference();
                JsExpression rightAssignment = JsAstUtils.assignment(result.deepCopy(), rightExpression).source(rightKtExpression);
                rightBlock.getStatements().add(JsAstUtils.asSyntheticStatement(rightAssignment));
            }
            JsStatement assignmentStatement = JsAstUtils.asSyntheticStatement(
                    JsAstUtils.assignment(result.deepCopy(), literalResult).source(rightKtExpression));
            ifStatement = JsAstUtils.newJsIf(leftExpression, rightBlock, assignmentStatement);
            MetadataProperties.setSynthetic(ifStatement, true);
        }
        else {
            ifStatement = JsAstUtils.newJsIf(leftExpression, rightBlock);
            result = new JsNullLiteral();
        }
        ifStatement.source(expression);
        context().addStatementToCurrentBlock(ifStatement);
        return result;
    }

    private boolean isEquals() {
        return operationToken == KtTokens.EQEQ || operationToken == KtTokens.EXCLEQ;
    }

    private JsExpression translateEquals() {
        JsExpression left = Translation.translateAsExpression(leftKtExpression, context());
        JsExpression right = Translation.translateAsExpression(rightKtExpression, context());

        if (left instanceof JsNullLiteral || right instanceof JsNullLiteral) {
            JsBinaryOperator operator = operationToken == KtTokens.EXCLEQ ? JsBinaryOperator.NEQ : JsBinaryOperator.EQ;
            return new JsBinaryOperation(operator, left, right);
        }

        KotlinType leftType = context().bindingContext().getType(leftKtExpression);
        KotlinType rightType = context().bindingContext().getType(rightKtExpression);

        if (leftType != null && TypeUtils.isNullableType(leftType) || rightType != null && TypeUtils.isNullableType(rightType)) {
            return mayBeWrapWithNegation(TopLevelFIF.KOTLIN_EQUALS.apply(left, Collections.singletonList(right), context()));
        }

        return translateAsOverloadedBinaryOperation();
    }

    @NotNull
    private JsExpression translateAsOverloadedBinaryOperation() {
        ResolvedCall<? extends FunctionDescriptor> resolvedCall = CallUtilKt.getFunctionResolvedCallWithAssert(expression, bindingContext());
        JsExpression result = CallTranslator.translate(context(), resolvedCall, getReceiver());
        return mayBeWrapWithNegation(result);
    }

    @NotNull
    private JsExpression getReceiver() {
        if (KtPsiUtil.isInOrNotInOperation(expression)) {
            return Translation.translateAsExpression(rightKtExpression, context());
        } else {
            return Translation.translateAsExpression(leftKtExpression, context());
        }
    }

    @NotNull
    private JsExpression mayBeWrapWithNegation(@NotNull JsExpression result) {
        if (isNegatedOperation(expression)) {
            return not(result);
        }
        else {
            return result;
        }
    }
}
