/*
 * Copyright (C) 2013-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.symbolsolver.resolution.promotion;

import com.github.javaparser.resolution.promotion.ConditionalExprHandler;
import com.github.javaparser.resolution.types.ResolvedPrimitiveType;
import com.github.javaparser.resolution.types.ResolvedType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * Numeric conditional expressions are standalone expressions (§15.2).
 * The type of a numeric conditional expression is determined as follows:
 * If the second and third operands have the same type, then that is the type of the conditional expression.
 * If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.
 * If one of the operands is of type byte or Byte and the other is of type short or Short, then the type of the conditional expression is short.
 * If one of the operands is of type T where T is byte, short, or char, and the other operand is a constant expression (§15.28) of type int whose value is representable in type T, then the type of the conditional expression is T.
 * If one of the operands is of type T, where T is Byte, Short, or Character, and the other operand is a constant expression of type int whose value is representable in the type U which is the result of applying unboxing conversion to T, then the type of the conditional expression is U.
 * Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types, and the type of the conditional expression is the promoted type of the second and third operands.
 */
public class NumericConditionalExprHandler implements ConditionalExprHandler {

    private static Map<ResolvedType, List<ResolvedType>> NumericConverter = new HashMap();

    static {
        // first type is the type of the resolution
        // the list indicates the types that can be resolved in the type specified as
        // key
        NumericConverter.put(
                ResolvedPrimitiveType.SHORT, Arrays.asList(ResolvedPrimitiveType.SHORT, ResolvedPrimitiveType.BYTE));
    }

    private static ResolvedPrimitiveType[] resolvedPrimitiveTypeSubList = new ResolvedPrimitiveType[] {
        ResolvedPrimitiveType.BYTE, ResolvedPrimitiveType.SHORT, ResolvedPrimitiveType.CHAR
    };

    ResolvedType thenExpr;
    ResolvedType elseExpr;

    public NumericConditionalExprHandler(ResolvedType thenExpr, ResolvedType elseExpr) {
        this.thenExpr = thenExpr;
        this.elseExpr = elseExpr;
    }

    @Override
    public ResolvedType resolveType() {
        String qnameTypeThenExpr = thenExpr.isPrimitive()
                ? thenExpr.asPrimitive().describe()
                : thenExpr.asReferenceType().describe();
        String qnameTypeElseExpr = elseExpr.isPrimitive()
                ? elseExpr.asPrimitive().describe()
                : elseExpr.asReferenceType().describe();
        if (qnameTypeThenExpr.equals(qnameTypeElseExpr)) {
            return thenExpr;
        }
        /*
         * If one of the second and third operands is of primitive type T, and the type of the other is the result of
         * applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.
         */
        if (thenExpr.isPrimitive()
                && elseExpr.isReferenceType()
                && elseExpr.asReferenceType()
                        .isUnboxableTo((ResolvedPrimitiveType) ResolvedPrimitiveType.byName(qnameTypeThenExpr))) {
            return thenExpr;
        }
        if (elseExpr.isPrimitive()
                && thenExpr.isReferenceType()
                && thenExpr.asReferenceType()
                        .isUnboxableTo((ResolvedPrimitiveType) ResolvedPrimitiveType.byName(qnameTypeElseExpr))) {
            return elseExpr;
        }
        /*
         * If one of the operands is of type byte or Byte and the other is of type short or Short, then the type of the
         * conditional expression is short.
         */

        if ((isResolvableTo(ResolvedPrimitiveType.SHORT, thenExpr) && relaxMatch(elseExpr, ResolvedPrimitiveType.BYTE))
                || (isResolvableTo(ResolvedPrimitiveType.SHORT, elseExpr)
                        && relaxMatch(thenExpr, ResolvedPrimitiveType.BYTE))) {
            return ResolvedPrimitiveType.SHORT;
        }

        //        if ((in(thenExpr, ResolvedPrimitiveType.SHORT) && in(elseExpr, ResolvedPrimitiveType.BYTE))
        //                || (in(elseExpr, ResolvedPrimitiveType.SHORT) && in(thenExpr, ResolvedPrimitiveType.BYTE))) {
        //            return ResolvedPrimitiveType.SHORT;
        //        }

        // If one of the operands is of type T where T is byte, short, or char, and the
        // other operand is a constant expression (§15.28) of type int whose value is
        // representable in type T, then the type of the conditional expression is T
        // How can we know if the constant expression of type int is representable in
        // type T ?

        if (thenExpr.isPrimitive() && elseExpr.isPrimitive()) {
            if (((ResolvedPrimitiveType) thenExpr).in(resolvedPrimitiveTypeSubList)
                    && ((ResolvedPrimitiveType) elseExpr).equals(ResolvedPrimitiveType.INT)) {
                return thenExpr;
            }
            if (((ResolvedPrimitiveType) elseExpr).in(resolvedPrimitiveTypeSubList)
                    && ((ResolvedPrimitiveType) thenExpr).equals(ResolvedPrimitiveType.INT)) {
                return elseExpr;
            }
        }

        // If one of the operands is of type T, where T is Byte, Short, or Character,
        // and the other operand is a constant expression of type int whose value is
        // representable in the type U which is the result of applying unboxing
        // conversion to T, then the type of the conditional expression is U.
        // How can we know it?

        if (thenExpr.isReference()
                && elseExpr.isPrimitive()
                && thenExpr.asReferenceType().isUnboxable()
                && thenExpr.asReferenceType().toUnboxedType().get().in(resolvedPrimitiveTypeSubList)
                && ((ResolvedPrimitiveType) elseExpr).equals(ResolvedPrimitiveType.INT)) {
            return thenExpr.asReferenceType().toUnboxedType().get();
        }
        if (elseExpr.isReference()
                && thenExpr.isPrimitive()
                && elseExpr.asReferenceType().isUnboxable()
                && elseExpr.asReferenceType().toUnboxedType().get().in(resolvedPrimitiveTypeSubList)
                && ((ResolvedPrimitiveType) thenExpr).equals(ResolvedPrimitiveType.INT)) {
            return elseExpr.asReferenceType().toUnboxedType().get();
        }

        // Otherwise, binary numeric promotion (§5.6.2) is applied to the operand types,
        // and the type of the conditional expression is the promoted type of the second
        // and third operands.
        ResolvedPrimitiveType PrimitiveThenExpr = thenExpr.isPrimitive()
                ? thenExpr.asPrimitive()
                : thenExpr.asReferenceType().toUnboxedType().get();
        ResolvedPrimitiveType PrimitiveElseExpr = elseExpr.isPrimitive()
                ? elseExpr.asPrimitive()
                : elseExpr.asReferenceType().toUnboxedType().get();
        return PrimitiveThenExpr.bnp(PrimitiveElseExpr);
    }

    /*
     * Verify if the {code ResolvedType} is equals to one of the specified primitive types
     */
    protected boolean exactMatch(ResolvedType type, ResolvedPrimitiveType... types) {
        return type.isPrimitive() && type.asPrimitive().in(types);
    }

    protected boolean relaxMatch(ResolvedType type, ResolvedPrimitiveType... types) {
        return exactMatch(type, types)
                || (type.isReferenceType()
                        && Arrays.stream(types).anyMatch(t -> type.asReferenceType()
                                .getQualifiedName()
                                .equals(t.getBoxTypeQName())));
    }

    /*
     * Verify if the {code ResolvedType} can be resolve to the specified primitive type
     */
    protected boolean isResolvableTo(ResolvedPrimitiveType toType, ResolvedType resolvedType) {
        // force to verify boxed type
        return isResolvableTo(toType, resolvedType, true);
    }

    protected boolean isResolvableTo(ResolvedPrimitiveType toType, ResolvedType resolvedType, boolean verifyBoxedType) {
        return NumericConverter.entrySet().stream()
                .filter(e -> e.getKey() == toType)
                .flatMap(entry -> entry.getValue().stream())
                .anyMatch(rt -> rt == resolvedType
                        || (verifyBoxedType
                                && resolvedType.isReferenceType()
                                && resolvedType
                                                .asReferenceType()
                                                .toUnboxedType()
                                                .get()
                                        == toType));
    }
}
