/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.flink.table.types.logical.utils;

import org.apache.flink.annotation.Internal;
import org.apache.flink.table.types.logical.DateType;
import org.apache.flink.table.types.logical.DayTimeIntervalType;
import org.apache.flink.table.types.logical.DistinctType;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.LogicalTypeFamily;
import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.flink.table.types.logical.RowType;
import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.table.types.logical.VarBinaryType;
import org.apache.flink.table.types.logical.VarCharType;
import org.apache.flink.table.types.logical.YearMonthIntervalType;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;

import static org.apache.flink.table.types.logical.LogicalTypeFamily.BINARY_STRING;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.CHARACTER_STRING;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.CONSTRUCTED;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.DATETIME;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.EXACT_NUMERIC;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.INTEGER_NUMERIC;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.INTERVAL;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.NUMERIC;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.PREDEFINED;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.TIME;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.TIMESTAMP;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BIGINT;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BINARY;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BITMAP;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BOOLEAN;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.CHAR;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.DATE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.DECIMAL;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.DISTINCT_TYPE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.DOUBLE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.FLOAT;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.INTEGER;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.INTERVAL_DAY_TIME;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.INTERVAL_YEAR_MONTH;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.NULL;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.RAW;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.ROW;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.SMALLINT;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.STRUCTURED_TYPE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.SYMBOL;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.TIMESTAMP_WITHOUT_TIME_ZONE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.TIMESTAMP_WITH_LOCAL_TIME_ZONE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.TIMESTAMP_WITH_TIME_ZONE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.TIME_WITHOUT_TIME_ZONE;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.TINYINT;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.VARBINARY;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.VARCHAR;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getDayPrecision;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getFractionalPrecision;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getLength;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getPrecision;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getScale;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.getYearPrecision;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.hasMaxLength;
import static org.apache.flink.table.types.logical.utils.LogicalTypeChecks.isSingleFieldInterval;

/**
 * Utilities for casting {@link LogicalType}.
 *
 * <p>This class aims to be compatible with the SQL standard. It is inspired by Apache Calcite's
 * {@code SqlTypeUtil#canCastFrom} method.
 *
 * <p>Casts can be performed in two ways: implicit or explicit.
 *
 * <p>Explicit casts correspond to the SQL cast specification and represent the logic behind a
 * {@code CAST(sourceType AS targetType)} operation. For example, it allows for converting most
 * types of the {@link LogicalTypeFamily#PREDEFINED} family to types of the {@link
 * LogicalTypeFamily#CHARACTER_STRING} family.
 *
 * <p>Implicit casts are used for safe type widening and type generalization (finding a common
 * supertype for a set of types) without loss of information. Implicit casts are similar to the Java
 * semantics (e.g. this is not possible: {@code int x = (String) z}).
 *
 * <p>Conversions that are defined by the {@link LogicalType} (e.g. interpreting a {@link DateType}
 * as integer value) are not considered here. They are an internal bridging feature that is not
 * standard compliant. If at all, {@code CONVERT} methods should make such conversions available.
 */
@Internal
public final class LogicalTypeCasts {

    private static final Map<LogicalTypeRoot, Set<LogicalTypeRoot>> implicitCastingRules;

    private static final Map<LogicalTypeRoot, Set<LogicalTypeRoot>> explicitCastingRules;

    private static final Map<LogicalTypeRoot, List<InjectiveRule>> injectiveRules;

    // Types with deterministic, unique string representations (for injective casts to STRING)
    private static final LogicalTypeRoot[] STRING_INJECTIVE_SOURCES = {
        TINYINT,
        SMALLINT,
        INTEGER,
        BIGINT,
        FLOAT,
        DOUBLE,
        BOOLEAN,
        DATE,
        TIME_WITHOUT_TIME_ZONE,
        TIMESTAMP_WITHOUT_TIME_ZONE,
        TIMESTAMP_WITH_LOCAL_TIME_ZONE
    };

    // ----- Injective cast conditions -----

    /** Injective when the target length can hold any value of the source length. */
    private static final BiPredicate<LogicalType, LogicalType> WHEN_LENGTH_FITS =
            (source, target) -> getLength(target) >= getLength(source);

    /** Injective when the target length can hold the source type's max string representation. */
    private static final BiPredicate<LogicalType, LogicalType> WHEN_MAX_CHAR_LENGTH_FITS =
            (source, target) -> getLength(target) >= maxStringRepresentationLength(source);

    /** Injective when source and target share identical precision. */
    private static final BiPredicate<LogicalType, LogicalType> WHEN_PRECISION_MATCHES =
            (source, target) -> getPrecision(source) == getPrecision(target);

    /** Injective when source and target share identical precision and scale (DECIMAL). */
    private static final BiPredicate<LogicalType, LogicalType> WHEN_PRECISION_AND_SCALE_MATCH =
            (source, target) ->
                    getPrecision(source) == getPrecision(target)
                            && getScale(source) == getScale(target);

    /**
     * Injective when the target binary length can hold worst-case UTF-8 encoding (4 bytes/char).
     */
    private static final BiPredicate<LogicalType, LogicalType> WHEN_BINARY_LENGTH_FITS_UTF8 =
            (source, target) -> {
                if (hasMaxLength(target)) {
                    return true;
                }
                if (hasMaxLength(source)) {
                    return false;
                }
                return (long) getLength(target) >= (long) getLength(source) * 4;
            };

    static {
        implicitCastingRules = new HashMap<>();
        explicitCastingRules = new HashMap<>();
        injectiveRules = new HashMap<>();

        // Identity casts: all types can be implicitly cast to themselves.
        // Injective identity for parameterized types is declared per-type with conditions.
        for (LogicalTypeRoot typeRoot : allTypes()) {
            castTo(typeRoot).implicitFrom(typeRoot).build();
        }

        // -----------------------------------------------------------------------------------------
        // Character string types
        // -----------------------------------------------------------------------------------------

        castTo(CHAR)
                .implicitFrom(CHAR)
                .explicitFromFamily(PREDEFINED, CONSTRUCTED)
                .explicitFrom(RAW, NULL, STRUCTURED_TYPE, BITMAP)
                .injectiveFrom(WHEN_LENGTH_FITS, CHAR)
                .injectiveFrom(WHEN_MAX_CHAR_LENGTH_FITS, STRING_INJECTIVE_SOURCES)
                .build();

        castTo(VARCHAR)
                .implicitFromFamily(CHARACTER_STRING)
                .explicitFromFamily(PREDEFINED, CONSTRUCTED)
                .explicitFrom(RAW, NULL, STRUCTURED_TYPE, BITMAP)
                .injectiveFrom(WHEN_LENGTH_FITS, CHAR, VARCHAR)
                .injectiveFrom(WHEN_MAX_CHAR_LENGTH_FITS, STRING_INJECTIVE_SOURCES)
                .build();

        // -----------------------------------------------------------------------------------------
        // Binary string types
        // -----------------------------------------------------------------------------------------

        castTo(BINARY)
                .implicitFrom(BINARY)
                .explicitFromFamily(CHARACTER_STRING)
                .explicitFrom(VARBINARY, RAW)
                .injectiveFrom(WHEN_LENGTH_FITS, BINARY)
                .injectiveFrom(WHEN_BINARY_LENGTH_FITS_UTF8, CHAR, VARCHAR)
                .build();

        castTo(VARBINARY)
                .implicitFromFamily(BINARY_STRING)
                .explicitFromFamily(CHARACTER_STRING)
                .explicitFrom(BINARY, RAW)
                .injectiveFrom(WHEN_LENGTH_FITS, BINARY, VARBINARY)
                .injectiveFrom(WHEN_BINARY_LENGTH_FITS_UTF8, CHAR, VARCHAR)
                .build();

        // -----------------------------------------------------------------------------------------
        // Exact numeric types
        // -----------------------------------------------------------------------------------------

        castTo(TINYINT)
                .implicitFrom(TINYINT)
                .explicitFromFamily(NUMERIC, CHARACTER_STRING, INTERVAL)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(TINYINT)
                .build();

        castTo(SMALLINT)
                .implicitFrom(TINYINT, SMALLINT)
                .explicitFromFamily(NUMERIC, CHARACTER_STRING, INTERVAL)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(TINYINT, SMALLINT)
                .build();

        castTo(INTEGER)
                .implicitFrom(TINYINT, SMALLINT, INTEGER)
                .explicitFromFamily(NUMERIC, CHARACTER_STRING, INTERVAL)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(TINYINT, SMALLINT, INTEGER)
                .build();

        castTo(BIGINT)
                .implicitFrom(TINYINT, SMALLINT, INTEGER, BIGINT)
                .explicitFromFamily(NUMERIC, CHARACTER_STRING, INTERVAL)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(TINYINT, SMALLINT, INTEGER, BIGINT)
                .build();

        castTo(DECIMAL)
                .implicitFromFamily(NUMERIC)
                .explicitFromFamily(CHARACTER_STRING, INTERVAL)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(WHEN_PRECISION_AND_SCALE_MATCH, DECIMAL)
                .build();

        // -----------------------------------------------------------------------------------------
        // Approximate numeric types
        // -----------------------------------------------------------------------------------------

        castTo(FLOAT)
                .implicitFrom(TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, DECIMAL)
                .explicitFromFamily(NUMERIC, CHARACTER_STRING)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(FLOAT)
                .build();

        castTo(DOUBLE)
                .implicitFromFamily(NUMERIC)
                .explicitFromFamily(CHARACTER_STRING)
                .explicitFrom(BOOLEAN, TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .injectiveFrom(DOUBLE)
                .build();

        // -----------------------------------------------------------------------------------------
        // Boolean type
        // -----------------------------------------------------------------------------------------

        castTo(BOOLEAN)
                .implicitFrom(BOOLEAN)
                .explicitFromFamily(CHARACTER_STRING, INTEGER_NUMERIC)
                .injectiveFrom(BOOLEAN)
                .build();

        // -----------------------------------------------------------------------------------------
        // Date and time types
        // -----------------------------------------------------------------------------------------

        castTo(DATE)
                .implicitFrom(DATE, TIMESTAMP_WITHOUT_TIME_ZONE)
                .explicitFromFamily(TIMESTAMP, CHARACTER_STRING)
                .injectiveFrom(DATE)
                .build();

        castTo(TIME_WITHOUT_TIME_ZONE)
                .implicitFrom(TIME_WITHOUT_TIME_ZONE, TIMESTAMP_WITHOUT_TIME_ZONE)
                .explicitFromFamily(TIME, TIMESTAMP, CHARACTER_STRING)
                .injectiveFrom(WHEN_PRECISION_MATCHES, TIME_WITHOUT_TIME_ZONE)
                .build();

        castTo(TIMESTAMP_WITHOUT_TIME_ZONE)
                .implicitFrom(TIMESTAMP_WITHOUT_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .explicitFromFamily(DATETIME, CHARACTER_STRING, NUMERIC)
                .injectiveFrom(
                        WHEN_PRECISION_MATCHES,
                        TIMESTAMP_WITHOUT_TIME_ZONE,
                        TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .build();

        castTo(TIMESTAMP_WITH_TIME_ZONE)
                .implicitFrom(TIMESTAMP_WITH_TIME_ZONE)
                .explicitFromFamily(DATETIME, CHARACTER_STRING)
                .injectiveFrom(WHEN_PRECISION_MATCHES, TIMESTAMP_WITH_TIME_ZONE)
                .build();

        castTo(TIMESTAMP_WITH_LOCAL_TIME_ZONE)
                .implicitFrom(TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_WITHOUT_TIME_ZONE)
                .explicitFromFamily(DATETIME, CHARACTER_STRING, NUMERIC)
                .injectiveFrom(
                        WHEN_PRECISION_MATCHES,
                        TIMESTAMP_WITH_LOCAL_TIME_ZONE,
                        TIMESTAMP_WITHOUT_TIME_ZONE)
                .build();

        // -----------------------------------------------------------------------------------------
        // Interval types
        // -----------------------------------------------------------------------------------------

        castTo(INTERVAL_YEAR_MONTH)
                .implicitFrom(INTERVAL_YEAR_MONTH)
                .explicitFromFamily(EXACT_NUMERIC, CHARACTER_STRING)
                .build();

        castTo(INTERVAL_DAY_TIME)
                .implicitFrom(INTERVAL_DAY_TIME)
                .explicitFromFamily(EXACT_NUMERIC, CHARACTER_STRING)
                .build();
    }

    /**
     * Returns whether the source type can be safely interpreted as the target type. This allows
     * avoiding casts by ignoring some logical properties. This is basically a relaxed {@link
     * LogicalType#equals(Object)}.
     *
     * <p>In particular this means:
     *
     * <p>Atomic, non-string types (INT, BOOLEAN, ...) and user-defined structured types must be
     * fully equal (i.e. {@link LogicalType#equals(Object)}). However, a NOT NULL type can be stored
     * in NULL type but not vice versa.
     *
     * <p>Atomic, string types must be contained in the target type (e.g. CHAR(2) is contained in
     * VARCHAR(3), but VARCHAR(2) is not contained in CHAR(3)). Same for binary strings.
     *
     * <p>Constructed types (ARRAY, ROW, MAP, etc.) and user-defined distinct type must be of same
     * kind but ignore field names and other logical attributes. Structured and row kinds are
     * compatible. However, all the children types ({@link LogicalType#getChildren()}) must be
     * compatible.
     */
    public static boolean supportsAvoidingCast(LogicalType sourceType, LogicalType targetType) {
        final CastAvoidanceChecker checker = new CastAvoidanceChecker(sourceType);
        return targetType.accept(checker);
    }

    /** See {@link #supportsAvoidingCast(LogicalType, LogicalType)}. */
    public static boolean supportsAvoidingCast(
            List<LogicalType> sourceTypes, List<LogicalType> targetTypes) {
        if (sourceTypes.size() != targetTypes.size()) {
            return false;
        }
        for (int i = 0; i < sourceTypes.size(); i++) {
            if (!supportsAvoidingCast(sourceTypes.get(i), targetTypes.get(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns whether the source type can be safely casted to the target type without loosing
     * information.
     *
     * <p>Implicit casts are used for type widening and type generalization (finding a common
     * supertype for a set of types). Implicit casts are similar to the Java semantics (e.g. this is
     * not possible: {@code int x = (String) z}).
     */
    public static boolean supportsImplicitCast(LogicalType sourceType, LogicalType targetType) {
        return supportsCasting(sourceType, targetType, false);
    }

    /**
     * Returns whether the source type can be casted to the target type.
     *
     * <p>Explicit casts correspond to the SQL cast specification and represent the logic behind a
     * {@code CAST(sourceType AS targetType)} operation. For example, it allows for converting most
     * types of the {@link LogicalTypeFamily#PREDEFINED} family to types of the {@link
     * LogicalTypeFamily#CHARACTER_STRING} family.
     */
    public static boolean supportsExplicitCast(LogicalType sourceType, LogicalType targetType) {
        return supportsCasting(sourceType, targetType, true);
    }

    /**
     * Returns whether the cast from source type to target type is injective
     * (uniqueness-preserving).
     *
     * <p>An injective cast guarantees that every distinct input value maps to a distinct output
     * value. This property is useful for upsert key tracking through projections: if a key column
     * is cast using an injective conversion, the uniqueness of the key is preserved.
     *
     * <p>Injective casts are explicitly defined in the casting rules, separate from implicit casts.
     * Not all implicit casts are injective (e.g., TIMESTAMP → DATE loses time information).
     *
     * <p>For constructed types (ROW), this method recursively checks if all field casts are
     * injective.
     *
     * @param sourceType the source type
     * @param targetType the target type
     * @return {@code true} if the cast preserves uniqueness
     */
    public static boolean supportsInjectiveCast(
            final LogicalType sourceType, final LogicalType targetType) {
        final LogicalTypeRoot sourceRoot = sourceType.getTypeRoot();
        final LogicalTypeRoot targetRoot = targetType.getTypeRoot();

        // Handle DISTINCT types by unwrapping
        if (sourceRoot == DISTINCT_TYPE) {
            return supportsInjectiveCast(((DistinctType) sourceType).getSourceType(), targetType);
        }
        if (targetRoot == DISTINCT_TYPE) {
            return supportsInjectiveCast(sourceType, ((DistinctType) targetType).getSourceType());
        }

        // Handle NULL type
        if (sourceRoot == NULL) {
            return true;
        }

        // Handle constructed types (ROW, STRUCTURED_TYPE) with recursive field checks
        final boolean isSourceConstructed = sourceRoot == ROW || sourceRoot == STRUCTURED_TYPE;
        final boolean isTargetConstructed = targetRoot == ROW || targetRoot == STRUCTURED_TYPE;
        if (isSourceConstructed && isTargetConstructed) {
            return supportsInjectiveConstructedCast(sourceType, targetType);
        }

        // Check declarative injective rules
        final List<InjectiveRule> rules = injectiveRules.get(targetRoot);
        if (rules == null) {
            return false;
        }
        for (final InjectiveRule rule : rules) {
            if (rule.test(sourceType, targetType)) {
                return true;
            }
        }
        return false;
    }

    private static boolean supportsInjectiveConstructedCast(
            final LogicalType sourceType, final LogicalType targetType) {
        final List<LogicalType> sourceChildren = sourceType.getChildren();
        final List<LogicalType> targetChildren = targetType.getChildren();

        if (sourceChildren.size() != targetChildren.size()) {
            return false;
        }

        for (int i = 0; i < sourceChildren.size(); i++) {
            if (!supportsInjectiveCast(sourceChildren.get(i), targetChildren.get(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the maximum number of characters needed to represent any value of the given type as a
     * string, or -1 if the type does not have a bounded deterministic string representation that
     * qualifies for injective casts.
     */
    private static int maxStringRepresentationLength(final LogicalType type) {
        switch (type.getTypeRoot()) {
            case BOOLEAN:
                return 5; // "false"
            case TINYINT:
                return 4; // "-128"
            case SMALLINT:
                return 6; // "-32768"
            case INTEGER:
                return 11; // "-2147483648"
            case BIGINT:
                return 20; // "-9223372036854775808"
            case FLOAT:
                return 15; // e.g. "-3.4028235E38"
            case DOUBLE:
                return 24; // e.g. "-2.2250738585072014E-308"
            case DATE:
                return 10; // "9999-12-31"
            case TIME_WITHOUT_TIME_ZONE:
                {
                    final int p = getPrecision(type);
                    return p > 0 ? 9 + p : 8; // "HH:MM:SS" + optional ".fff..."
                }
            case TIMESTAMP_WITHOUT_TIME_ZONE:
            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                {
                    final int p = getPrecision(type);
                    return p > 0 ? 20 + p : 19; // "YYYY-MM-DD HH:MM:SS" + optional ".fff..."
                }
            default:
                return -1;
        }
    }

    /**
     * Returns whether the source type can be reinterpreted as the target type.
     *
     * <p>Reinterpret casts correspond to the SQL reinterpret_cast and represent the logic behind a
     * {@code REINTERPRET_CAST(sourceType AS targetType)} operation.
     */
    public static boolean supportsReinterpretCast(LogicalType sourceType, LogicalType targetType) {
        if (sourceType.getTypeRoot() == targetType.getTypeRoot()) {
            return true;
        }

        switch (sourceType.getTypeRoot()) {
            case INTEGER:
                switch (targetType.getTypeRoot()) {
                    case DATE:
                    case TIME_WITHOUT_TIME_ZONE:
                    case INTERVAL_YEAR_MONTH:
                        return true;
                    default:
                        return false;
                }
            case BIGINT:
                switch (targetType.getTypeRoot()) {
                    case TIMESTAMP_WITHOUT_TIME_ZONE:
                    case INTERVAL_DAY_TIME:
                        return true;
                    default:
                        return false;
                }
            case DATE:
            case TIME_WITHOUT_TIME_ZONE:
            case INTERVAL_YEAR_MONTH:
                switch (targetType.getTypeRoot()) {
                    case INTEGER:
                    case BIGINT:
                        return true;
                    default:
                        return false;
                }

            case TIMESTAMP_WITHOUT_TIME_ZONE:
            case INTERVAL_DAY_TIME:
                return targetType.getTypeRoot() == BIGINT;
            default:
                return false;
        }
    }

    // --------------------------------------------------------------------------------------------

    private static boolean supportsCasting(
            LogicalType sourceType, LogicalType targetType, boolean allowExplicit) {
        // a NOT NULL type cannot store a NULL type
        // but it might be useful to cast explicitly with knowledge about the data
        if (sourceType.isNullable() && !targetType.isNullable() && !allowExplicit) {
            return false;
        }
        // ignore nullability during compare
        if (sourceType.copy(true).equals(targetType.copy(true))) {
            return true;
        }

        final LogicalTypeRoot sourceRoot = sourceType.getTypeRoot();
        final LogicalTypeRoot targetRoot = targetType.getTypeRoot();

        if (sourceRoot == NULL) {
            // null can be cast to an arbitrary type
            return true;
        } else if (sourceRoot == DISTINCT_TYPE && targetRoot == DISTINCT_TYPE) {
            // the two distinct types are not equal (from initial invariant), casting is not
            // possible
            return false;
        } else if (sourceRoot == DISTINCT_TYPE) {
            return supportsCasting(
                    ((DistinctType) sourceType).getSourceType(), targetType, allowExplicit);
        } else if (targetRoot == DISTINCT_TYPE) {
            return supportsCasting(
                    sourceType, ((DistinctType) targetType).getSourceType(), allowExplicit);
        } else if (sourceType.is(INTERVAL) && targetType.is(EXACT_NUMERIC)) {
            // cast between interval and exact numeric is only supported if interval has a single
            // field
            return isSingleFieldInterval(sourceType);
        } else if (sourceType.is(EXACT_NUMERIC) && targetType.is(INTERVAL)) {
            // cast between interval and exact numeric is only supported if interval has a single
            // field
            return isSingleFieldInterval(targetType);
        } else if ((sourceType.is(CONSTRUCTED) || sourceType.is(STRUCTURED_TYPE))
                && (targetType.is(CONSTRUCTED) || targetType.is(STRUCTURED_TYPE))) {
            if (sourceType.is(CONSTRUCTED) || targetType.is(CONSTRUCTED)) {
                return supportsConstructedCasting(sourceType, targetType, allowExplicit);
            }
            return supportsStructuredCasting(
                    sourceType, targetType, (s, t) -> supportsCasting(s, t, allowExplicit));

        } else if (sourceRoot == RAW
                        && !targetType.is(BINARY_STRING)
                        && !targetType.is(CHARACTER_STRING)
                || targetRoot == RAW) {
            // the two raw types are not equal (from initial invariant), casting is not possible
            return false;
        } else if (sourceRoot == SYMBOL || targetRoot == SYMBOL) {
            // the two symbol types are not equal (from initial invariant), casting is not possible
            return false;
        } else if (sourceRoot == BITMAP && targetRoot == VARBINARY) {
            // BITMAP can only be cast to BYTES (unbounded VARBINARY), because trimming or padding
            // would corrupt the serialized bitmap data.
            return allowExplicit && getLength(targetType) == VarBinaryType.MAX_LENGTH;
        }

        if (implicitCastingRules.get(targetRoot).contains(sourceRoot)) {
            return true;
        }
        if (allowExplicit) {
            return explicitCastingRules.get(targetRoot).contains(sourceRoot);
        }
        return false;
    }

    private static boolean supportsStructuredCasting(
            LogicalType sourceType,
            LogicalType targetType,
            BiFunction<LogicalType, LogicalType, Boolean> childPredicate) {
        final LogicalTypeRoot sourceRoot = sourceType.getTypeRoot();
        final LogicalTypeRoot targetRoot = targetType.getTypeRoot();
        if (sourceRoot != STRUCTURED_TYPE || targetRoot != STRUCTURED_TYPE) {
            return false;
        }
        final StructuredType sourceStructuredType = (StructuredType) sourceType;
        final StructuredType targetStructuredType = (StructuredType) targetType;
        // non-anonymous structured types must be fully equal
        if (sourceStructuredType.getObjectIdentifier().isPresent()
                || targetStructuredType.getObjectIdentifier().isPresent()) {
            return false;
        }
        // for anonymous structured types we are a bit more lenient, if they provide similar fields
        // e.g. this is necessary when structured types derived from type information and
        // structured types derived within Table API are slightly different
        final Class<?> sourceClass = sourceStructuredType.getImplementationClass().orElse(null);
        final Class<?> targetClass = targetStructuredType.getImplementationClass().orElse(null);
        if (sourceClass != targetClass) {
            return false;
        }
        final List<String> sourceNames =
                sourceStructuredType.getAttributes().stream()
                        .map(StructuredType.StructuredAttribute::getName)
                        .collect(Collectors.toList());
        final List<String> targetNames =
                sourceStructuredType.getAttributes().stream()
                        .map(StructuredType.StructuredAttribute::getName)
                        .collect(Collectors.toList());
        if (!sourceNames.equals(targetNames)) {
            return false;
        }
        final List<LogicalType> sourceChildren = sourceType.getChildren();
        final List<LogicalType> targetChildren = targetType.getChildren();
        for (int i = 0; i < sourceChildren.size(); i++) {
            if (!childPredicate.apply(sourceChildren.get(i), targetChildren.get(i))) {
                return false;
            }
        }
        return true;
    }

    private static boolean supportsConstructedCasting(
            LogicalType sourceType, LogicalType targetType, boolean allowExplicit) {
        final LogicalTypeRoot sourceRoot = sourceType.getTypeRoot();
        final LogicalTypeRoot targetRoot = targetType.getTypeRoot();
        // all constructed types can only be casted within the same type root,
        // however, rows can be converted to structured types and vice versa
        if (sourceRoot == targetRoot
                || (sourceRoot == ROW && targetRoot == STRUCTURED_TYPE)
                || (sourceRoot == STRUCTURED_TYPE && targetRoot == ROW)) {
            final List<LogicalType> sourceChildren = sourceType.getChildren();
            final List<LogicalType> targetChildren = targetType.getChildren();
            if (sourceChildren.size() != targetChildren.size()) {
                return false;
            }
            for (int i = 0; i < sourceChildren.size(); i++) {
                if (!supportsCasting(sourceChildren.get(i), targetChildren.get(i), allowExplicit)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    private static CastingRuleBuilder castTo(LogicalTypeRoot targetType) {
        return new CastingRuleBuilder(targetType);
    }

    private static LogicalTypeRoot[] allTypes() {
        return LogicalTypeRoot.values();
    }

    /** A declarative rule describing when a cast from certain source types is injective. */
    private static final class InjectiveRule {

        private final Set<LogicalTypeRoot> sourceRoots;
        private final BiPredicate<LogicalType, LogicalType> condition;

        InjectiveRule(
                Set<LogicalTypeRoot> sourceRoots, BiPredicate<LogicalType, LogicalType> condition) {
            this.sourceRoots = sourceRoots;
            this.condition = condition;
        }

        boolean test(LogicalType sourceType, LogicalType targetType) {
            return sourceRoots.contains(sourceType.getTypeRoot())
                    && condition.test(sourceType, targetType);
        }
    }

    private static class CastingRuleBuilder {

        private final LogicalTypeRoot targetType;
        private final Set<LogicalTypeRoot> implicitSourceTypes = new HashSet<>();
        private final Set<LogicalTypeRoot> explicitSourceTypes = new HashSet<>();
        private final List<InjectiveRule> injectiveRuleList = new ArrayList<>();

        CastingRuleBuilder(LogicalTypeRoot targetType) {
            this.targetType = targetType;
        }

        CastingRuleBuilder implicitFrom(LogicalTypeRoot... sourceTypes) {
            this.implicitSourceTypes.addAll(Arrays.asList(sourceTypes));
            return this;
        }

        CastingRuleBuilder implicitFromFamily(LogicalTypeFamily... sourceFamilies) {
            for (LogicalTypeFamily family : sourceFamilies) {
                for (LogicalTypeRoot root : LogicalTypeRoot.values()) {
                    if (root.getFamilies().contains(family)) {
                        this.implicitSourceTypes.add(root);
                    }
                }
            }
            return this;
        }

        CastingRuleBuilder explicitFrom(LogicalTypeRoot... sourceTypes) {
            this.explicitSourceTypes.addAll(Arrays.asList(sourceTypes));
            return this;
        }

        CastingRuleBuilder explicitFromFamily(LogicalTypeFamily... sourceFamilies) {
            for (LogicalTypeFamily family : sourceFamilies) {
                for (LogicalTypeRoot root : LogicalTypeRoot.values()) {
                    if (root.getFamilies().contains(family)) {
                        this.explicitSourceTypes.add(root);
                    }
                }
            }
            return this;
        }

        CastingRuleBuilder injectiveFrom(LogicalTypeRoot... sourceTypes) {
            return injectiveFrom((s, t) -> true, sourceTypes);
        }

        CastingRuleBuilder injectiveFrom(
                BiPredicate<LogicalType, LogicalType> condition, LogicalTypeRoot... sourceTypes) {
            injectiveRuleList.add(
                    new InjectiveRule(new HashSet<>(Arrays.asList(sourceTypes)), condition));
            return this;
        }

        void build() {
            implicitCastingRules.put(targetType, implicitSourceTypes);
            explicitCastingRules.put(targetType, explicitSourceTypes);
            injectiveRules.put(targetType, injectiveRuleList);
        }
    }

    // --------------------------------------------------------------------------------------------

    /** Checks if a source type can safely be interpreted as the target type. */
    private static class CastAvoidanceChecker extends LogicalTypeDefaultVisitor<Boolean> {

        private final LogicalType sourceType;

        private CastAvoidanceChecker(LogicalType sourceType) {
            this.sourceType = sourceType;
        }

        @Override
        public Boolean visit(YearMonthIntervalType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            if (sourceType.is(LogicalTypeRoot.INTERVAL_YEAR_MONTH)
                    && getYearPrecision(sourceType) <= targetType.getYearPrecision()) {
                return true;
            }
            return defaultMethod(targetType);
        }

        @Override
        public Boolean visit(DayTimeIntervalType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            if (sourceType.is(LogicalTypeRoot.INTERVAL_DAY_TIME)
                    && getDayPrecision(sourceType) <= targetType.getDayPrecision()
                    && getFractionalPrecision(sourceType) <= targetType.getFractionalPrecision()) {
                return true;
            }
            return defaultMethod(targetType);
        }

        @Override
        public Boolean visit(VarCharType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            // CHAR and VARCHAR are very compatible within bounds
            if (sourceType.isAnyOf(LogicalTypeRoot.CHAR, LogicalTypeRoot.VARCHAR)
                    && getLength(sourceType) <= targetType.getLength()) {
                return true;
            }
            return defaultMethod(targetType);
        }

        @Override
        public Boolean visit(VarBinaryType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            // BINARY and VARBINARY are very compatible within bounds
            if (sourceType.isAnyOf(LogicalTypeRoot.BINARY, LogicalTypeRoot.VARBINARY)
                    && getLength(sourceType) <= targetType.getLength()) {
                return true;
            }
            return defaultMethod(targetType);
        }

        @Override
        public Boolean visit(RowType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            // ROW and structured types are very compatible if field types match
            if (sourceType.is(STRUCTURED_TYPE)) {
                final List<LogicalType> sourceChildren = sourceType.getChildren();
                final List<LogicalType> targetChildren = targetType.getChildren();
                return supportsAvoidingCast(sourceChildren, targetChildren);
            }
            return defaultMethod(targetType);
        }

        @Override
        public Boolean visit(StructuredType targetType) {
            if (sourceType.isNullable() && !targetType.isNullable()) {
                return false;
            }
            // ROW and structured types are very compatible if field types match
            if (sourceType.is(ROW)) {
                final List<LogicalType> sourceChildren = sourceType.getChildren();
                final List<LogicalType> targetChildren = targetType.getChildren();
                return supportsAvoidingCast(sourceChildren, targetChildren);
            }
            if (sourceType.equals(targetType) || sourceType.copy(true).equals(targetType)) {
                return true;
            }
            return supportsStructuredCasting(
                    sourceType, targetType, LogicalTypeCasts::supportsAvoidingCast);
        }

        @Override
        protected Boolean defaultMethod(LogicalType targetType) {
            // quick path
            if (sourceType == targetType) {
                return true;
            }

            if (sourceType.isNullable() && !targetType.isNullable()
                    || sourceType.getClass() != targetType.getClass()
                    || // TODO drop this line once we remove legacy types
                    sourceType.getTypeRoot() != targetType.getTypeRoot()) {
                return false;
            }

            final List<LogicalType> sourceChildren = sourceType.getChildren();
            final List<LogicalType> targetChildren = targetType.getChildren();
            if (sourceChildren.isEmpty()) {
                // handles all types that are not of family CONSTRUCTED or USER DEFINED
                return sourceType.equals(targetType) || sourceType.copy(true).equals(targetType);
            } else {
                // handles all types of CONSTRUCTED family as well as distinct types
                return supportsAvoidingCast(sourceChildren, targetChildren);
            }
        }
    }

    private LogicalTypeCasts() {
        // no instantiation
    }
}
