/*
 * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the license/LICENSE.txt file.
 */

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

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.KtNodeTypes;
import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
import org.jetbrains.kotlin.builtins.PrimitiveType;
import org.jetbrains.kotlin.builtins.ReflectionTypes;
import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.js.backend.ast.*;
import org.jetbrains.kotlin.js.patterns.NamePredicate;
import org.jetbrains.kotlin.js.patterns.typePredicates.TypePredicatesKt;
import org.jetbrains.kotlin.js.translate.context.Namer;
import org.jetbrains.kotlin.js.translate.context.TemporaryVariable;
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.ArrayFIF;
import org.jetbrains.kotlin.js.translate.intrinsic.functions.factories.TopLevelFIF;
import org.jetbrains.kotlin.js.translate.reference.ReferenceTranslator;
import org.jetbrains.kotlin.js.translate.utils.*;
import org.jetbrains.kotlin.name.Name;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.checkers.PrimitiveNumericComparisonInfo;
import org.jetbrains.kotlin.types.KotlinType;

import java.util.Collections;

import static org.jetbrains.kotlin.builtins.FunctionTypesKt.isFunctionTypeOrSubtype;
import static org.jetbrains.kotlin.builtins.KotlinBuiltIns.isArray;
import static org.jetbrains.kotlin.js.descriptorUtils.DescriptorUtilsKt.getNameIfStandardType;
import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.getTypeByReference;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.equality;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.not;
import static org.jetbrains.kotlin.psi.KtPsiUtil.*;
import static org.jetbrains.kotlin.types.TypeUtils.*;

public final class PatternTranslator extends AbstractTranslator {

    @NotNull
    public static PatternTranslator newInstance(@NotNull TranslationContext context) {
        return new PatternTranslator(context);
    }

    private PatternTranslator(@NotNull TranslationContext context) {
        super(context);
    }

    public static boolean isCastExpression(@NotNull KtBinaryExpressionWithTypeRHS expression) {
        return isSafeCast(expression) || isUnsafeCast(expression);
    }

    @NotNull
    public JsExpression translateCastExpression(@NotNull KtBinaryExpressionWithTypeRHS expression) {
        assert isCastExpression(expression): "Expected cast expression, got " + expression;
        KtExpression left = expression.getLeft();
        JsExpression expressionToCast = Translation.translateAsExpression(left, context());

        KtTypeReference typeReference = expression.getRight();
        assert typeReference != null: "Cast expression must have type reference";

        KotlinType anyType = context().getCurrentModule().getBuiltIns().getAnyType();
        expressionToCast = TranslationUtils.coerce(context(), expressionToCast, anyType);

        TemporaryVariable temporary = context().declareTemporary(expressionToCast, expression);
        JsExpression isCheck = translateIsCheck(temporary.assignmentExpression(), typeReference);
        if (isCheck == null) return expressionToCast;

        JsExpression onFail;

        if (isSafeCast(expression)) {
            onFail = new JsNullLiteral();
        }
        else {
            JsExpression throwCCEFunRef = context().getReferenceToIntrinsic(Namer.THROW_CLASS_CAST_EXCEPTION_FUN_NAME);
            onFail = new JsInvocation(throwCCEFunRef);
        }

        JsExpression result = new JsConditional(isCheck, temporary.reference(), onFail);

        KotlinType targetType = getTypeByReference(bindingContext(), typeReference);
        if (isSafeCast(expression)) {
            targetType = targetType.unwrap().makeNullableAsSpecified(true);
        }
        return TranslationUtils.coerce(context(), result, targetType);
    }

    @NotNull
    public JsExpression translateIsExpression(@NotNull KtIsExpression expression) {
        KtExpression left = expression.getLeftHandSide();
        JsExpression expressionToCheck = Translation.translateAsExpression(left, context());

        KotlinType anyType = context().getCurrentModule().getBuiltIns().getAnyType();
        expressionToCheck = TranslationUtils.coerce(context(), expressionToCheck, anyType);

        KtTypeReference typeReference = expression.getTypeReference();
        assert typeReference != null;
        JsExpression result = translateIsCheck(expressionToCheck, typeReference);
        if (result == null) return new JsBooleanLiteral(!expression.isNegated());

        if (expression.isNegated()) {
            return not(result);
        }
        return result;
    }

    @Nullable
    public JsExpression translateIsCheck(@NotNull JsExpression subject, @NotNull KtTypeReference targetTypeReference) {
        KotlinType targetType = getTypeByReference(bindingContext(), targetTypeReference);

        JsExpression checkFunReference = doGetIsTypeCheckCallable(targetType);
        if (checkFunReference == null) {
            return null;
        }

        boolean isReifiedType = isReifiedTypeParameter(targetType);
        if (!isReifiedType && isNullableType(targetType) ||
            isReifiedType && findChildByType(targetTypeReference, KtNodeTypes.NULLABLE_TYPE) != null
        ) {
            checkFunReference = namer().orNull(checkFunReference);
        }

        return new JsInvocation(checkFunReference, subject);
    }

    @NotNull
    public JsExpression getIsTypeCheckCallable(@NotNull KotlinType type) {
        JsExpression callable = doGetIsTypeCheckCallable(type);
        assert callable != null : "This method should be called only to translate reified type parameters. " +
                                  "`callable` should never be null for reified type parameters. " +
                                  "Actual type: " + type;

        // If the type is reified, rely on the corresponding is type callable.
        // Otherwise make sure that passing null yields true
        if (!isReifiedTypeParameter(type) && isNullableType(type)) {
            return namer().orNull(callable);
        }

        return callable;
    }

    @Nullable
    private JsExpression doGetIsTypeCheckCallable(@NotNull KotlinType type) {
        ClassifierDescriptor targetDescriptor = type.getConstructor().getDeclarationDescriptor();
        if (targetDescriptor != null && AnnotationsUtils.isNativeInterface(targetDescriptor)) {
            return type.isMarkedNullable() ? null : namer().isInstanceOf(JsAstUtils.pureFqn("Object", null));
        }

        JsExpression builtinCheck = getIsTypeCheckCallableForBuiltin(type);
        if (builtinCheck != null) return builtinCheck;

        builtinCheck = getIsTypeCheckCallableForPrimitiveBuiltin(type);
        if (builtinCheck != null) return builtinCheck;

        TypeParameterDescriptor typeParameterDescriptor = getTypeParameterDescriptorOrNull(type);
        if (typeParameterDescriptor != null) {
            if (typeParameterDescriptor.isReified()) {
                return getIsTypeCheckCallableForReifiedType(typeParameterDescriptor);
            }

            JsExpression result = null;
            for (KotlinType upperBound : typeParameterDescriptor.getUpperBounds()) {
                JsExpression next = doGetIsTypeCheckCallable(upperBound);
                if (next != null) {
                    result = result != null ? namer().andPredicate(result, next) : next;
                }
            }
            return result;
        }

        ClassDescriptor referencedClass = DescriptorUtils.getClassDescriptorForType(type);
        JsExpression typeName = ReferenceTranslator.translateAsTypeReference(referencedClass, context());
        return namer().isInstanceOf(typeName);
    }

    @Nullable
    private JsExpression getIsTypeCheckCallableForBuiltin(@NotNull KotlinType type) {
        if (isFunctionTypeOrSubtype(type) && !ReflectionTypes.isNumberedKPropertyOrKMutablePropertyType(type)) {
            return namer().isTypeOf(new JsStringLiteral("function"));
        }

        if (isArray(type)) {
            if (ArrayFIF.typedArraysEnabled(context().getConfig())) {
                return namer().isArray();
            }
            else {
                return Namer.IS_ARRAY_FUN_REF;
            }

        }

        if (TypePredicatesKt.getCHAR_SEQUENCE().test(type)) return namer().isCharSequence();

        if (TypePredicatesKt.getCOMPARABLE().test(type)) return namer().isComparable();

        return null;
    }

    @Nullable
    private JsExpression getIsTypeCheckCallableForPrimitiveBuiltin(@NotNull KotlinType type) {
        Name typeName = getNameIfStandardType(type);

        if (NamePredicate.STRING.test(typeName)) {
            return namer().isTypeOf(new JsStringLiteral("string"));
        }

        if (NamePredicate.BOOLEAN.test(typeName)) {
            return namer().isTypeOf(new JsStringLiteral("boolean"));
        }

        if (NamePredicate.LONG.test(typeName)) {
            return namer().isInstanceOf(Namer.kotlinLong());
        }

        if (NamePredicate.NUMBER.test(typeName)) {
            return namer().kotlin(Namer.IS_NUMBER);
        }

        if (NamePredicate.CHAR.test(typeName)) {
            return namer().kotlin(Namer.IS_CHAR);
        }

        if (NamePredicate.PRIMITIVE_NUMBERS_MAPPED_TO_PRIMITIVE_JS.test(typeName)) {
            return namer().isTypeOf(new JsStringLiteral("number"));
        }

        if (ArrayFIF.typedArraysEnabled(context().getConfig())) {
            if (KotlinBuiltIns.isPrimitiveArray(type)) {
                PrimitiveType arrayType = KotlinBuiltIns.getPrimitiveArrayElementType(type);
                assert arrayType != null;
                return namer().isPrimitiveArray(arrayType);
            }
        }

        return null;
    }

    @NotNull
    private JsExpression getIsTypeCheckCallableForReifiedType(@NotNull TypeParameterDescriptor typeParameter) {
        assert typeParameter.isReified(): "Expected reified type, actual: " + typeParameter;
        DeclarationDescriptor containingDeclaration = typeParameter.getContainingDeclaration();
        assert containingDeclaration instanceof CallableDescriptor:
                "Expected type parameter " + typeParameter +
                " to be contained in CallableDescriptor, actual: " + containingDeclaration.getClass();

        JsExpression alias = context().getAliasForDescriptor(typeParameter);
        assert alias != null: "No alias found for reified type parameter: " + typeParameter;
        return alias;
    }

    @NotNull
    public JsExpression translateExpressionPattern(
            @NotNull KotlinType subjectType,
            @NotNull JsExpression expressionToMatch,
            @NotNull KtExpression patternExpression
    ) {
        PrimitiveNumericComparisonInfo ieeeInfo = UtilsKt.getPrimitiveNumericComparisonInfo(context(), patternExpression);

        KotlinType actualSubjectType;
        KotlinType patternType;
        if (ieeeInfo != null) {
            actualSubjectType = ieeeInfo.getLeftType();
            patternType = ieeeInfo.getRightType();
        } else {
            actualSubjectType = UtilsKt.refineType(subjectType);
            patternType = UtilsKt.getPrecisePrimitiveTypeNotNull(context(), patternExpression);
        }

        EqualityType matchEquality = equalityType(actualSubjectType);
        EqualityType patternEquality = equalityType(patternType);

        JsExpression expressionToMatchAgainst = TranslationUtils.coerce(context(), translateExpressionForExpressionPattern(patternExpression), actualSubjectType);

        if (matchEquality == EqualityType.PRIMITIVE && patternEquality == EqualityType.PRIMITIVE) {
            return equality(expressionToMatch, expressionToMatchAgainst);
        }
        else if (expressionToMatchAgainst instanceof JsNullLiteral) {
            return TranslationUtils.nullCheck(actualSubjectType, expressionToMatch, context(), false);
        }
        else {
            return TopLevelFIF.KOTLIN_EQUALS.apply(expressionToMatch, Collections.singletonList(expressionToMatchAgainst), context());
        }
    }

    @NotNull
    private static EqualityType equalityType(@NotNull KotlinType type) {
        DeclarationDescriptor descriptor = type.getConstructor().getDeclarationDescriptor();
        if (!(descriptor instanceof ClassDescriptor)) return EqualityType.GENERAL;

        PrimitiveType primitive = KotlinBuiltIns.getPrimitiveType(descriptor);
        if (primitive == null) return EqualityType.GENERAL;

        return primitive == PrimitiveType.LONG ? EqualityType.LONG : EqualityType.PRIMITIVE;
    }

    private enum EqualityType {
        PRIMITIVE,
        LONG,
        GENERAL
    }

    @NotNull
    public JsExpression translateExpressionForExpressionPattern(@NotNull KtExpression patternExpression) {
        return Translation.translateAsExpression(patternExpression, context());
    }
}
