/*
 * Copyright 2023 VMware, Inc.
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package org.dbsp.sqlCompiler.compiler.visitors.inner;

import org.apache.calcite.util.DateString;
import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import org.apache.commons.lang3.StringUtils;
import org.dbsp.sqlCompiler.compiler.DBSPCompiler;
import org.dbsp.sqlCompiler.compiler.errors.InternalCompilerError;
import org.dbsp.sqlCompiler.compiler.visitors.VisitDecision;
import org.dbsp.sqlCompiler.compiler.visitors.outer.intern.InternInner;
import org.dbsp.sqlCompiler.ir.IDBSPInnerNode;
import org.dbsp.sqlCompiler.ir.IsIntervalLiteral;
import org.dbsp.sqlCompiler.ir.IsNumericLiteral;
import org.dbsp.sqlCompiler.ir.expression.DBSPApplyExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPApplyMethodExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPBaseTupleExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPBinaryExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPBlockExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPBorrowExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPCastExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPCloneExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPDerefExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPFailExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPFieldExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPIfExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPIsNullExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPOpcode;
import org.dbsp.sqlCompiler.ir.expression.DBSPSomeExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPUnaryExpression;
import org.dbsp.sqlCompiler.ir.expression.DBSPUnwrapExpression;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPBoolLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPDateLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPDecimalLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPI128Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPI16Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPI32Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPI64Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPI8Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPISizeLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPIntLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPStringLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPTimeLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPTimestampLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPU128Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPU16Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPU32Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPU64Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPU8Literal;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPUSizeLiteral;
import org.dbsp.sqlCompiler.ir.expression.literal.DBSPVoidLiteral;
import org.dbsp.sqlCompiler.ir.type.DBSPType;
import org.dbsp.sqlCompiler.ir.type.IHasZero;
import org.dbsp.sqlCompiler.ir.type.IsNumericType;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeAny;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeBool;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeDate;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeDecimal;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeISize;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeInteger;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeNull;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeString;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeTime;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeTimestamp;
import org.dbsp.sqlCompiler.ir.type.primitive.DBSPTypeUSize;
import org.dbsp.sqlCompiler.ir.type.user.DBSPTypeArray;
import org.dbsp.util.Logger;
import org.dbsp.util.Utilities;

import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;
import java.util.Objects;

/** Visitor which does some Rust-level expression simplifications. */
public class Simplify extends ExpressionTranslator {
    public Simplify(DBSPCompiler compiler) {
        super(compiler);
    }

    public static DBSPExpression simplify(DBSPCompiler compiler, DBSPExpression expression) {
        Simplify simplify = new Simplify(compiler);
        return simplify.apply(expression).to(DBSPExpression.class);
    }

    @Override
    public void startVisit(IDBSPInnerNode node) {
        super.startVisit(node);
        Logger.INSTANCE.belowLevel(this, 3)
                .append("Starting Simplify on ")
                .appendSupplier(node::toString)
                .newline();
    }

    @Override
    public VisitDecision preorder(DBSPExpression expression) {
        // Effectively memoize results
        if (this.translationMap.containsKey(expression))
            return VisitDecision.STOP;
        return VisitDecision.CONTINUE;
    }

    @Override
    public void postorder(DBSPIsNullExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.is_null();
        if (!source.getType().mayBeNull || source.is(DBSPSomeExpression.class))
            result = new DBSPBoolLiteral(false);
        else if (source.is(DBSPCloneExpression.class))
            result = source.to(DBSPCloneExpression.class).expression.is_null();
        else if (source.isNone())
            result = new DBSPBoolLiteral(true);
        else if (source.is(DBSPIfExpression.class)) {
            DBSPIfExpression ifExp = source.to(DBSPIfExpression.class);
            Utilities.enforce(ifExp.negative != null);
            result = new DBSPIfExpression(
                    ifExp.getNode(),
                    ifExp.condition,
                    ifExp.positive.is_null(),
                    ifExp.negative.is_null());
        }
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPUnwrapExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.unwrap(expression.message);
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPApplyExpression expression) {
        String function = expression.getFunctionName();
        if (function != null &&
                function.equals(InternInner.uninternFunction) &&
                expression.arguments.length == 1) {
            DBSPExpression arg = this.getE(expression.arguments[0]);
            if (arg.is(DBSPApplyExpression.class)) {
                DBSPApplyExpression callee = arg.to(DBSPApplyExpression.class);
                String calleeFunction = callee.getFunctionName();
                if (calleeFunction != null &&
                        calleeFunction.equals(InternInner.internFunction) &&
                        callee.arguments.length == 1) {
                    DBSPExpression calleeArg = callee.arguments[0];
                    // unintern(intern(x)) = x
                    this.map(expression, calleeArg);
                    return;
                }
            }
        }
        super.postorder(expression);
    }

    @Override
    public void postorder(DBSPCloneExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.applyCloneIfNeeded();
        if (source.is(DBSPCloneExpression.class) ||
            source.is(DBSPApplyMethodExpression.class) ||
            source.is(DBSPApplyExpression.class) ||
            source.is(DBSPIfExpression.class) ||
            source.is(DBSPCastExpression.class)
        ) {
            result = source;
        }
        if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(
                    expression.getNode(), expression.getType(), source.to(DBSPFailExpression.class).message);
        }
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPCastExpression expression) {
        DBSPExpression source = this.getE(expression.source);
        DBSPType type = expression.getType();
        DBSPExpression result = source.cast(expression.getNode(), type, expression.safe);
        // (array<T>)(array<unknown>)a -> array<T>.
        // This should not be necessary, but there is a bug in Calcite somewhere
        if (source.is(DBSPCastExpression.class)) {
            if (source.getType().is(DBSPTypeArray.class) && type.is(DBSPTypeArray.class)) {
                DBSPTypeArray arr = source.getType().to(DBSPTypeArray.class);
                if (arr.getElementType().is(DBSPTypeAny.class)) {
                    result = source.to(DBSPCastExpression.class).source
                            .cast(expression.getNode(), type, expression.safe);
                }
            }
        } else if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(expression.getNode(), expression.getType(),
                    source.to(DBSPFailExpression.class).message);
        }
        DBSPLiteral lit = source.as(DBSPLiteral.class);
        if (lit != null) {
            DBSPType litType = lit.getType();
            if (type.withMayBeNull(false).sameType(litType) &&
                    !type.is(DBSPTypeString.class)) {
                // Casting to VARCHAR may change a string even if the source is the same type
                // Cast from type to Option<type>
                result = lit.getWithNullable(type.mayBeNull);
            } else if (lit.isNull()) {
                if (type.mayBeNull) {
                    result = DBSPLiteral.none(type);
                }
                // Otherwise this expression will certainly generate
                // a runtime error if evaluated.  But it may never be evaluated.
            } else if (litType.is(DBSPTypeNull.class)) {
                // This is a literal with type "NULL".
                // Convert it to a literal of the resulting type
                result = DBSPLiteral.none(type);
            } else if (lit.is(DBSPStringLiteral.class)) {
                // Constant folding string values
                // This is mostly useful for testing code, to fold various literals.
                DBSPStringLiteral str = lit.to(DBSPStringLiteral.class);
                Objects.requireNonNull(str.value);
                if (type.is(DBSPTypeDate.class)) {
                    // We have to validate the date, it may be invalid, and in this case
                    // the result is 'null'.  This pattern is hardwired in Calcite.
                    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.US)
                            .withResolverStyle(ResolverStyle.STRICT);
                    try {
                        LocalDate.parse(str.value, dateFormatter); // executed for exception
                        result = new DBSPDateLiteral(lit.getNode(), type, new DateString(str.value));
                    } catch (DateTimeParseException ex) {
                        this.compiler.reportWarning(expression.getSourcePosition(), "Suspicious argument",
                                "String " + Utilities.singleQuote(str.value) +
                                        " cannot be interpreted as a DATE");
                    }
                } else if (type.is(DBSPTypeTime.class)) {
                    try {
                        TimeString ts = new TimeString(str.value);
                        result = new DBSPTimeLiteral(lit.getNode(), type, ts);
                    } catch (DateTimeParseException | IllegalArgumentException ex) {
                        this.compiler.reportWarning(expression.getSourcePosition(), "Not a TIME",
                                " String " + Utilities.singleQuote(str.value) +
                                        " cannot be interpreted as a time");
                    }
                } else if (type.is(DBSPTypeTimestamp.class)) {
                    try {
                        TimestampString ts = new TimestampString(str.value);
                        result = new DBSPTimestampLiteral(lit.getNode(), type, ts);
                    } catch (DateTimeParseException | IllegalArgumentException ex) {
                        this.compiler.reportWarning(expression.getSourcePosition(), "Not a TIMESTAMP",
                                " String " + Utilities.singleQuote(str.value) +
                                        " cannot be interpreted as a timestamp");
                    }
                } else if (type.is(DBSPTypeDecimal.class)) {
                    try {
                        DBSPTypeDecimal decType = type.to(DBSPTypeDecimal.class);
                        BigDecimal value = new BigDecimal(str.value).setScale(decType.scale, RoundingMode.HALF_EVEN);
                        result = new DBSPDecimalLiteral(type, value);
                    } catch (NumberFormatException ex) {
                        // on parse error return 0.
                        this.compiler.reportWarning(expression.getSourcePosition(), "Not a number",
                                " String " + Utilities.singleQuote(str.value) +
                                        " cannot be interpreted as a DECIMAL");
                    }
                } else if (type.is(DBSPTypeString.class)) {
                    DBSPTypeString typeString = type.to(DBSPTypeString.class);
                    if (typeString.precision == DBSPTypeString.UNLIMITED_PRECISION) {
                        result = new DBSPStringLiteral(str.value, str.charset, type);
                    } else {
                        String value = str.value;
                        if (typeString.fixed) {
                            if (str.value.length() < typeString.precision) {
                                value = StringUtils.rightPad(str.value, typeString.precision);
                            } else {
                                value = str.value.substring(0, typeString.precision);
                            }
                        } else {
                            // Not fixed, but limited precision; truncate if necessary
                            if (str.value.length() > typeString.precision) {
                                value = str.value.substring(0, typeString.precision);
                            }
                        }
                        result = new DBSPStringLiteral(expression.getNode(), type, value, str.charset);
                    }
                } else if (type.is(DBSPTypeInteger.class)) {
                    DBSPTypeInteger ti = type.to(DBSPTypeInteger.class);
                    switch (ti.getWidth()) {
                        case 8: {
                            byte value;
                            try {
                                value = Byte.parseByte(str.value);
                                result = new DBSPI8Literal(lit.getNode(), type, value);
                            } catch (NumberFormatException ex) {
                                // SQL semantics: parsing failures return 0
                                this.compiler.reportWarning(expression.getSourcePosition(), "Not a number",
                                        " String " + Utilities.singleQuote(str.value) +
                                                " cannot be interpreted as a number");
                            }
                            break;
                        }
                        case 16: {
                            short value;
                            try {
                                value = Short.parseShort(str.value);
                                result = new DBSPI16Literal(lit.getNode(), type, value);
                            } catch (NumberFormatException ex) {
                                this.compiler.reportWarning(expression.getSourcePosition(), "Not a number",
                                        " String " + Utilities.singleQuote(str.value) +
                                                " cannot be interpreted as a number");
                            }
                            break;
                        }
                        case 32: {
                            int value;
                            try {
                                value = Integer.parseInt(str.value);
                                result = new DBSPI32Literal(lit.getNode(), type, value);
                            } catch (NumberFormatException ex) {
                                this.compiler.reportWarning(expression.getSourcePosition(), "Not a number",
                                        " String " + Utilities.singleQuote(str.value) +
                                                " cannot be interpreted as a number");
                            }
                            break;
                        }
                        case 64: {
                            long value;
                            try {
                                value = Long.parseLong(str.value);
                                result = new DBSPI64Literal(lit.getNode(), type, value);
                            } catch (Exception ex) {
                                this.compiler.reportWarning(expression.getSourcePosition(), "Not a number",
                                        " String " + Utilities.singleQuote(str.value) +
                                                " cannot be interpreted as a number");
                            }
                            break;
                        }
                    }
                }
            } else if (lit.is(DBSPI32Literal.class)) {
                DBSPI32Literal i = lit.to(DBSPI32Literal.class);
                Objects.requireNonNull(i.value);
                if (type.is(DBSPTypeDecimal.class)) {
                    result = new DBSPDecimalLiteral(source.getNode(), type, new BigDecimal(i.value));
                } else if (type.is(DBSPTypeInteger.class)) {
                    if (i.isNull()) {
                        result = DBSPLiteral.none(type);
                    } else {
                        switch (type.code) {
                            case INT8:
                                if (i.value >= Byte.MIN_VALUE && i.value <= Byte.MAX_VALUE) {
                                    result = new DBSPI8Literal(source.getNode(), type, i.value.byteValue());
                                }
                                break;
                            case INT16:
                                if (i.value >= Short.MIN_VALUE && i.value <= Short.MAX_VALUE) {
                                    result = new DBSPI16Literal(source.getNode(), type, i.value.shortValue());
                                }
                                break;
                            case INT64:
                                result = new DBSPI64Literal(source.getNode(), type, i.value.longValue());
                                break;
                            case INT128:
                                result = new DBSPI128Literal(source.getNode(), type, BigInteger.valueOf(i.value));
                                break;
                            case UINT8:
                                if (i.value >= 0 && i.value <= 255) {
                                    result = new DBSPU8Literal(source.getNode(), type, i.value);
                                }
                                break;
                            case UINT16:
                                if (i.value >= 0 && i.value <= 65535) {
                                    result = new DBSPU16Literal(source.getNode(), type, i.value);
                                }
                                break;
                            case UINT32:
                                if (i.value >= 0) {
                                    result = new DBSPU32Literal(source.getNode(), type, i.value.longValue());
                                }
                                break;
                            case UINT64:
                                if (i.value >= 0) {
                                    result = new DBSPU64Literal(source.getNode(), type, BigInteger.valueOf(i.value));
                                }
                                break;
                            case UINT128:
                                if (i.value >= 0) {
                                    result = new DBSPU128Literal(source.getNode(), type, BigInteger.valueOf(i.value));
                                }
                                break;
                        }
                    }
                } else if (type.is(DBSPTypeISize.class)) {
                    if (!i.isNull()) {
                        result = new DBSPISizeLiteral(source.getNode(), type, i.value.longValue());
                    }
                } else if (type.is(DBSPTypeUSize.class)) {
                    if (!i.isNull() && i.value >= 0) {
                        result = new DBSPUSizeLiteral(source.getNode(), type, i.value.longValue());
                    }
                }
            } else if (lit.is(DBSPDecimalLiteral.class)) {
                DBSPDecimalLiteral dec = lit.to(DBSPDecimalLiteral.class);
                BigDecimal value = Objects.requireNonNull(dec.value);
                if (type.is(DBSPTypeDecimal.class)) {
                    // must adjust precision and scale
                    DBSPTypeDecimal decType = type.to(DBSPTypeDecimal.class);
                    value = value.setScale(decType.scale, RoundingMode.DOWN);
                    if (value.precision() > decType.precision) {
                        this.compiler.reportWarning(expression.getSourcePosition(),
                                "Invalid DECIMAL",
                                "cannot represent " + lit + " as DECIMAL(" + decType.precision + ", " + decType.scale +
                                        "): precision of DECIMAL type too small to represent value"
                        );
                    }
                    result = new DBSPDecimalLiteral(source.getNode(), type, value);
                }
            }
        }
        Utilities.enforce(expression.getType().sameType(result.getType()));
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPFieldExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.field(expression.fieldNo);
        if (source.is(DBSPBaseTupleExpression.class)) {
            result = source.to(DBSPBaseTupleExpression.class).get(expression.fieldNo);
            if (source.getType().mayBeNull && !result.getType().mayBeNull)
                result = result.some();
        } if (source.is(DBSPBlockExpression.class)) {
            DBSPBlockExpression block = source.to(DBSPBlockExpression.class);
            Utilities.enforce(block.lastExpression != null);
            result = new DBSPBlockExpression(block.contents,
                    block.lastExpression.field(expression.fieldNo));
        } else if (source.is(DBSPIfExpression.class)) {
            DBSPIfExpression conditional = source.to(DBSPIfExpression.class);
            DBSPExpression negative = conditional.negative != null ? conditional.negative.field(expression.fieldNo) : null;
            result = new DBSPIfExpression(source.getNode(), conditional.condition,
                    conditional.positive.field(expression.fieldNo), negative);
        } else if (source.is(DBSPCloneExpression.class)) {
            result = new DBSPFieldExpression(expression.getNode(),
                    source.to(DBSPCloneExpression.class).expression,
                    expression.fieldNo);
        } else if (source.is(DBSPSomeExpression.class)) {
            // Some(Tup(x, y)).1 -> y or Some(y)
            DBSPSomeExpression some = source.to(DBSPSomeExpression.class);
            if (some.expression.is(DBSPBaseTupleExpression.class)) {
                result = some.expression.to(DBSPBaseTupleExpression.class).get(expression.fieldNo);
                if (!result.getType().mayBeNull)
                    result = result.some();
            }
        } else if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(expression.getNode(), expression.getType(),
                    source.to(DBSPFailExpression.class).message);
        }
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPDerefExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.deref();
        if (source.is(DBSPBorrowExpression.class))
            result = source.to(DBSPBorrowExpression.class).expression;
        else if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(expression.getNode(), expression.getType(),
                    source.to(DBSPFailExpression.class).message);
        }
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPBorrowExpression expression) {
        DBSPExpression source = this.getE(expression.expression);
        DBSPExpression result = source.borrow(expression.mut);
        if (source.is(DBSPDerefExpression.class)) {
            result = source.to(DBSPDerefExpression.class).expression;
        } else if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(expression.getNode(), expression.getType(),
                    source.to(DBSPFailExpression.class).message);
        }
        this.map(expression, result);
    }

    @Override
    public void postorder(DBSPIfExpression expression) {
        DBSPExpression condition = this.getE(expression.condition);
        @Nullable DBSPExpression negative = this.getEN(expression.negative);
        DBSPExpression positive = this.getE(expression.positive);
        DBSPExpression result = new DBSPIfExpression(expression.getNode(), condition, positive, negative);
        if (condition.is(DBSPBoolLiteral.class)) {
            DBSPBoolLiteral cond = condition.to(DBSPBoolLiteral.class);
            if (!cond.isNull()) {
                if (Objects.requireNonNull(cond.value)) {
                    result = positive;
                } else {
                    result = negative;
                    if (result == null)
                        result = new DBSPVoidLiteral();
                }
            }
        } else if (negative != null &&
                positive.isCompileTimeConstant() &&
                negative.isCompileTimeConstant() &&
                positive.equivalent(negative)) {
            result = positive;
        } else if (negative != null &&
                // if (condition) then { true } else { false } == condition
                positive.getType().sameType(condition.getType()) &&
                positive.is(DBSPBoolLiteral.class) &&
                positive.to(DBSPBoolLiteral.class).hasValue(true) &&
                negative.is(DBSPBoolLiteral.class) &&
                negative.to(DBSPBoolLiteral.class).hasValue(false)) {
            result = condition;
        } else if (negative != null &&
                // if (condition) then { false } else { true } == !condition
                positive.getType().sameType(condition.getType()) &&
                positive.is(DBSPBoolLiteral.class) &&
                positive.to(DBSPBoolLiteral.class).hasValue(false) &&
                negative.is(DBSPBoolLiteral.class) &&
                negative.to(DBSPBoolLiteral.class).hasValue(true)) {
            result = condition.not();
        } else if (condition != expression.condition ||
                positive != expression.positive ||
                negative != expression.negative) {
            result = new DBSPIfExpression(expression.getNode(), condition, positive, negative);
        }
        this.map(expression, result);
    }

    // Push a unary expression in both branches of a conditional
    private DBSPExpression pushIntoConditional(
            DBSPExpression newSource, DBSPUnaryExpression originalExpression, DBSPExpression currentResult) {
        if (newSource.is(DBSPIfExpression.class)) {
            DBSPIfExpression ifExp = newSource.to(DBSPIfExpression.class);
            Utilities.enforce(ifExp.negative != null);
            return new DBSPIfExpression(
                    ifExp.getNode(),
                    ifExp.condition,
                    originalExpression.replaceSource(ifExp.positive),
                    originalExpression.replaceSource(ifExp.negative));
        }
        return currentResult;
    }

    @SuppressWarnings("ReplaceNullCheck")
    @Override
    public void postorder(DBSPUnaryExpression expression) {
        DBSPExpression source = this.getE(expression.source);
        DBSPExpression result = new DBSPUnaryExpression(
                expression.getNode(), expression.type, expression.opcode, source);
        if (source.is(DBSPFailExpression.class)) {
            result = new DBSPFailExpression(expression.getNode(), expression.getType(),
                    source.to(DBSPFailExpression.class).message);
        } else if (expression.opcode == DBSPOpcode.NEG) {
            if (source.is(DBSPLiteral.class)) {
                DBSPLiteral lit = source.to(DBSPLiteral.class);
                if (lit.is(IsNumericLiteral.class)) {
                    try {
                        result = lit.to(IsNumericLiteral.class).negate().to(DBSPExpression.class);
                    } catch (ArithmeticException ex) {
                        // leave unchanged
                    }
                }
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.WRAP_BOOL) {
            // wrap_bool(cast_to_bn_b(expression)) => expression
            if (source.is(DBSPCastExpression.class)) {
                DBSPCastExpression cast = source.to(DBSPCastExpression.class);
                if (cast.source.getType().is(DBSPTypeBool.class) &&
                    !cast.source.getType().mayBeNull) {
                    result = cast.source;
                }
            } else if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                boolean r = b.value != null && b.value;
                result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), r);
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.NOT) {
            if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                if (b.value == null)
                    result = b;
                else
                    result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), !b.value);
            } else if (source.is(DBSPUnaryExpression.class)) {
                // !!e = e, true in ternary logic too
                DBSPUnaryExpression unarySource = source.to(DBSPUnaryExpression.class);
                if (unarySource.opcode == DBSPOpcode.NOT) {
                    result = unarySource.source;
                }
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.IS_FALSE) {
            if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                boolean r;
                if (b.value == null)
                    r = false;
                else
                    r = !b.value;
                result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), r);
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.IS_TRUE) {
            if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                boolean r;
                if (b.value == null)
                    r = false;
                else
                    r = b.value;
                result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), r);
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.IS_NOT_FALSE) {
            if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                boolean r;
                if (b.value == null)
                    r = true;
                else
                    r = !b.value;
                result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), r);
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        } else if (expression.opcode == DBSPOpcode.IS_NOT_TRUE) {
            if (source.is(DBSPBoolLiteral.class)) {
                DBSPBoolLiteral b = source.to(DBSPBoolLiteral.class);
                boolean r;
                if (b.value == null)
                    r = true;
                else
                    r = b.value;
                result = new DBSPBoolLiteral(expression.getNode(), expression.getType(), r);
            } else {
                result = this.pushIntoConditional(source, expression, result);
            }
        }
        this.map(expression, result.cast(expression.getNode(), expression.getType(), DBSPCastExpression.CastType.SqlUnsafe));
    }

    @Override
    public void postorder(DBSPBinaryExpression expression) {
        DBSPExpression left = this.getE(expression.left);
        DBSPExpression right = this.getE(expression.right);
        DBSPType leftType = left.getType();
        DBSPType rightType = right.getType();
        boolean leftMayBeNull = leftType.mayBeNull;
        boolean rightMayBeNull = rightType.mayBeNull;
        DBSPExpression result = new DBSPBinaryExpression(
                expression.getNode(), expression.type, expression.opcode, left, right);
        DBSPOpcode opcode = expression.opcode;

        if (opcode == DBSPOpcode.MAP_INDEX ||
                opcode == DBSPOpcode.SQL_INDEX ||
                opcode == DBSPOpcode.RUST_INDEX) {
            if (left.is(DBSPCloneExpression.class)) {
                result = new DBSPBinaryExpression(
                        expression.getNode(), expression.type, expression.opcode,
                        left.to(DBSPCloneExpression.class).expression, right);
            }
        }
        try {
            if (opcode == DBSPOpcode.AND) {
                if (left.is(DBSPBoolLiteral.class)) {
                    DBSPBoolLiteral bLeft = left.to(DBSPBoolLiteral.class);
                    if (!bLeft.isNull()) {
                        if (Objects.requireNonNull(bLeft.value)) {
                            result = right;
                        } else {
                            result = left;
                        }
                    }
                } else if (right.is(DBSPBoolLiteral.class)) {
                    DBSPBoolLiteral bRight = right.to(DBSPBoolLiteral.class);
                    if (!bRight.isNull()) {
                        if (Objects.requireNonNull(bRight.value)) {
                            result = left;
                        } else {
                            result = right;
                        }
                    }
                } else if (left instanceof DBSPUnaryExpression unaryLeft &&
                        right instanceof DBSPUnaryExpression unaryRight &&
                        unaryLeft.opcode == DBSPOpcode.WRAP_BOOL &&
                        unaryRight.opcode == DBSPOpcode.WRAP_BOOL) {
                    // (bool) a && (bool) b => (bool)(a && b)
                    result = new DBSPBinaryExpression(expression.getNode(), unaryLeft.source.getType(),
                            DBSPOpcode.AND, unaryLeft.source, unaryRight.source).wrapBoolIfNeeded();
                }
            } else if (opcode == DBSPOpcode.OR) {
                if (left.is(DBSPBoolLiteral.class)) {
                    DBSPBoolLiteral bLeft = left.to(DBSPBoolLiteral.class);
                    if (!bLeft.isNull()) {
                        if (Objects.requireNonNull(bLeft.value)) {
                            result = left;
                        } else {
                            result = right;
                        }
                    }
                } else if (right.is(DBSPBoolLiteral.class)) {
                    DBSPBoolLiteral bRight = right.to(DBSPBoolLiteral.class);
                    if (!bRight.isNull()) {
                        if (Objects.requireNonNull(bRight.value)) {
                            result = right;
                        } else {
                            result = left;
                        }
                    }
                }
            } else if (opcode == DBSPOpcode.ADD) {
                if (left.is(DBSPLiteral.class)) {
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    IHasZero iLeftType = leftType.as(IHasZero.class);
                    if (iLeftType != null) {
                        if (iLeftType.isZero(leftLit)) {
                            result = right;
                        } else if (leftLit.isNull()) {
                            // null + anything is null
                            result = left;
                        }
                    }
                    if (left.getType().is(IsNumericType.class) &&
                            right.is(DBSPLiteral.class) &&
                            result.is(DBSPBinaryExpression.class)) {
                        result = left.getType().to(IsNumericType.class).fold(result.to(DBSPBinaryExpression.class));
                    }
                } else if (right.is(DBSPLiteral.class)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    IHasZero iRightType = rightType.as(IHasZero.class);
                    if (iRightType != null) {
                        if (iRightType.isZero(rightLit)) {
                            result = left;
                        } else if (rightLit.isNull()) {
                            result = right;
                        }
                    }
                }
            } else if (opcode == DBSPOpcode.SUB) {
                if (left.is(DBSPLiteral.class)) {
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    if (leftLit.isNull()) {
                        // null - anything is null
                        result = left;
                    }
                    if (left.getType().is(IsNumericType.class) &&
                            right.is(DBSPLiteral.class) &&
                            result.is(DBSPBinaryExpression.class)) {
                        result = left.getType().to(IsNumericType.class).fold(result.to(DBSPBinaryExpression.class));
                    }
                } else if (right.is(DBSPLiteral.class)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    IHasZero iRightType = rightType.as(IHasZero.class);
                    if (iRightType != null) {
                        if (iRightType.isZero(rightLit)) {
                            result = left;
                        } else if (rightLit.isNull()) {
                            result = right;
                        }
                    }
                }
            }  else if (opcode == DBSPOpcode.MUL || opcode == DBSPOpcode.INTERVAL_MUL) {
                if (left.is(DBSPLiteral.class) && leftType.is(IsNumericType.class)) {
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    IsNumericType iLeftType = leftType.to(IsNumericType.class);
                    if (iLeftType.isOne(leftLit)) {
                        // This works even for null
                        result = right.cast(expression.getNode(), expression.getType(), DBSPCastExpression.CastType.SqlUnsafe);
                    } else if (iLeftType.isZero(leftLit) && !rightMayBeNull) {
                        // This is not true for null values
                        result = left;
                    } else if (leftLit.isNull()) {
                        result = left;
                    } else if (leftLit.is(DBSPIntLiteral.class) && right.is(IsIntervalLiteral.class)) {
                        result = right.to(IsIntervalLiteral.class)
                                .multiply(leftLit.to(DBSPIntLiteral.class).getValue())
                                .to(DBSPExpression.class);
                    }
                    if (left.getType().is(IsNumericType.class) &&
                            right.is(DBSPLiteral.class) &&
                            result.is(DBSPBinaryExpression.class)) {
                        result = left.getType().to(IsNumericType.class).fold(result.to(DBSPBinaryExpression.class));
                    }
                } else if (right.is(DBSPLiteral.class) && rightType.is(IsNumericType.class)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    IsNumericType iRightType = rightType.to(IsNumericType.class);
                    if (iRightType.isOne(rightLit)) {
                        result = left;
                    } else if (iRightType.isZero(rightLit) && !leftMayBeNull) {
                        // This is not true for null values
                        result = right;
                    } else if (rightLit.isNull()) {
                        result = right;
                    } else if (rightLit.is(DBSPIntLiteral.class) && left.is(IsIntervalLiteral.class)) {
                        result = left.to(IsIntervalLiteral.class)
                                .multiply(rightLit.to(DBSPIntLiteral.class).getValue())
                                .to(DBSPExpression.class);
                    }
                }
            } else if (opcode == DBSPOpcode.DIV || opcode == DBSPOpcode.INTERVAL_DIV) {
                if (right.is(DBSPLiteral.class)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    IsNumericType iRightType = rightType.to(IsNumericType.class);
                    if (iRightType.isOne(rightLit)) {
                        result = left;
                    } else if (iRightType.isZero(rightLit)) {
                        this.compiler.reportWarning(expression.getSourcePosition(), "Division by zero",
                                " Division by constant zero value.");
                    }
                    if (left.getType().is(IsNumericType.class) &&
                            right.is(DBSPLiteral.class) &&
                            result.is(DBSPBinaryExpression.class)) {
                        result = left.getType().to(IsNumericType.class).fold(result.to(DBSPBinaryExpression.class));
                    }
                }
            } else if (opcode == DBSPOpcode.MOD) {
                if (right.is(DBSPLiteral.class)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    IsNumericType iRightType = rightType.to(IsNumericType.class);
                    if (iRightType.isOne(rightLit)) {
                        result = iRightType.getZero();
                    } else if (iRightType.isZero(rightLit)) {
                        this.compiler.reportWarning(expression.getSourcePosition(),
                                "Division by zero",
                                " Modulus by constant zero value as divisor."
                        );
                    }
                }
            } else if (opcode == DBSPOpcode.EQ || opcode == DBSPOpcode.NEQ) {
                if (left.is(DBSPLiteral.class) && right.is(DBSPLiteral.class) && leftType.sameType(rightType)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    if (leftLit.isNull() || rightLit.isNull()) {
                        Utilities.enforce(expression.type.mayBeNull);
                        result = expression.type.none();
                    } else {
                        boolean same = leftLit.sameValue(rightLit);
                        //noinspection SimplifiableConditionalExpression
                        result = new DBSPBoolLiteral(expression.getNode(), expression.type,
                                opcode == DBSPOpcode.NEQ ? !same : same);
                    }
                }
            } else if (opcode == DBSPOpcode.GT || opcode == DBSPOpcode.GTE ||
                    opcode == DBSPOpcode.LTE || opcode == DBSPOpcode.LT) {
                if (left.is(DBSPLiteral.class) && right.is(DBSPLiteral.class) && leftType.sameType(rightType)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    if (leftLit.isNull() || rightLit.isNull()) {
                        Utilities.enforce(expression.type.mayBeNull);
                        result = expression.type.none();
                    } else {
                        if (leftLit.is(IsNumericLiteral.class)) {
                            int compare = leftLit.to(IsNumericLiteral.class).compare(rightLit.to(IsNumericLiteral.class));
                            boolean r = switch (opcode) {
                                case GT -> compare > 0;
                                case LT -> compare < 0;
                                case GTE -> compare >= 0;
                                case LTE -> compare <= 0;
                                default -> throw new InternalCompilerError("Unexpected comparison " + opcode);
                            };
                            result = new DBSPBoolLiteral(expression.getNode(), expression.type, r);
                        }
                    }
                }
            } else if (opcode == DBSPOpcode.IS_DISTINCT) {
                if (left.is(DBSPLiteral.class) && right.is(DBSPLiteral.class) && leftType.sameType(rightType)) {
                    DBSPLiteral rightLit = right.to(DBSPLiteral.class);
                    DBSPLiteral leftLit = left.to(DBSPLiteral.class);
                    boolean same = leftLit.sameValue(rightLit);
                    result = new DBSPBoolLiteral(expression.getNode(), expression.type, !same);
                }
            }
        } catch (ArithmeticException unused) {
            // ignore, defer to runtime
        }
        this.map(expression, result.cast(expression.getNode(), expression.getType(), DBSPCastExpression.CastType.SqlUnsafe));
    }

    @Override
    protected void map(DBSPExpression expression, DBSPExpression result) {
        if (!expression.sameFields(result)) {
            Logger.INSTANCE.belowLevel(this, 1)
                    .append("Simplified ")
                    .appendSupplier(expression::toString)
                    .append(" to ")
                    .appendSupplier(result::toString)
                    .newline();
        }
        Utilities.enforce(expression.getType().sameType(result.getType()),
                () -> "Expression with type " + expression.getType() + " has type " + result.getType() +
                        " after simplification");
        super.map(expression, result);
    }
}
