/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.cypher.operations;

import static java.lang.Double.parseDouble;
import static java.lang.Long.parseLong;
import static java.lang.String.format;
import static org.neo4j.cypher.operations.CursorUtils.propertyKeys;
import static org.neo4j.internal.kernel.api.Read.NO_ID;
import static org.neo4j.values.storable.Values.EMPTY_STRING;
import static org.neo4j.values.storable.Values.FALSE;
import static org.neo4j.values.storable.Values.NO_VALUE;
import static org.neo4j.values.storable.Values.TRUE;
import static org.neo4j.values.storable.Values.booleanValue;
import static org.neo4j.values.storable.Values.doubleValue;
import static org.neo4j.values.storable.Values.longValue;
import static org.neo4j.values.storable.Values.stringValue;
import static org.neo4j.values.virtual.VirtualValues.EMPTY_LIST;
import static org.neo4j.values.virtual.VirtualValues.asList;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;
import org.neo4j.cypher.internal.expressions.BooleanTypeName;
import org.neo4j.cypher.internal.expressions.CypherTypeName;
import org.neo4j.cypher.internal.expressions.DateTypeName;
import org.neo4j.cypher.internal.expressions.DurationTypeName;
import org.neo4j.cypher.internal.expressions.FloatTypeName;
import org.neo4j.cypher.internal.expressions.IntegerTypeName;
import org.neo4j.cypher.internal.expressions.LocalDateTimeTypeName;
import org.neo4j.cypher.internal.expressions.LocalTimeTypeName;
import org.neo4j.cypher.internal.expressions.PointTypeName;
import org.neo4j.cypher.internal.expressions.StringTypeName;
import org.neo4j.cypher.internal.expressions.ZonedDateTimeTypeName;
import org.neo4j.cypher.internal.expressions.ZonedTimeTypeName;
import org.neo4j.cypher.internal.runtime.DbAccess;
import org.neo4j.cypher.internal.runtime.ExpressionCursors;
import org.neo4j.exceptions.CypherTypeException;
import org.neo4j.exceptions.InvalidArgumentException;
import org.neo4j.internal.kernel.api.NodeCursor;
import org.neo4j.internal.kernel.api.PropertyCursor;
import org.neo4j.internal.kernel.api.Read;
import org.neo4j.internal.kernel.api.RelationshipScanCursor;
import org.neo4j.kernel.api.StatementConstants;
import org.neo4j.token.api.TokenConstants;
import org.neo4j.util.CalledFromGeneratedCode;
import org.neo4j.values.AnyValue;
import org.neo4j.values.ElementIdMapper;
import org.neo4j.values.SequenceValue;
import org.neo4j.values.storable.ArrayValue;
import org.neo4j.values.storable.BooleanValue;
import org.neo4j.values.storable.CoordinateReferenceSystem;
import org.neo4j.values.storable.DateTimeValue;
import org.neo4j.values.storable.DateValue;
import org.neo4j.values.storable.DoubleValue;
import org.neo4j.values.storable.DurationValue;
import org.neo4j.values.storable.FloatingPointArray;
import org.neo4j.values.storable.FloatingPointValue;
import org.neo4j.values.storable.IntegralArray;
import org.neo4j.values.storable.IntegralValue;
import org.neo4j.values.storable.LocalDateTimeValue;
import org.neo4j.values.storable.LocalTimeValue;
import org.neo4j.values.storable.LongValue;
import org.neo4j.values.storable.NoValue;
import org.neo4j.values.storable.NumberValue;
import org.neo4j.values.storable.PointValue;
import org.neo4j.values.storable.StringValue;
import org.neo4j.values.storable.TemporalValue;
import org.neo4j.values.storable.TextValue;
import org.neo4j.values.storable.TimeValue;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.Values;
import org.neo4j.values.virtual.ListValue;
import org.neo4j.values.virtual.ListValueBuilder;
import org.neo4j.values.virtual.MapValue;
import org.neo4j.values.virtual.MapValueBuilder;
import org.neo4j.values.virtual.NodeValue;
import org.neo4j.values.virtual.PathValue;
import org.neo4j.values.virtual.RelationshipValue;
import org.neo4j.values.virtual.VirtualNodeValue;
import org.neo4j.values.virtual.VirtualPathValue;
import org.neo4j.values.virtual.VirtualRelationshipValue;
import org.neo4j.values.virtual.VirtualValues;

/**
 * This class contains static helper methods for the set of Cypher functions
 */
@SuppressWarnings({"ReferenceEquality"})
public final class CypherFunctions {
    private static final BigDecimal MAX_LONG = BigDecimal.valueOf(Long.MAX_VALUE);
    private static final BigDecimal MIN_LONG = BigDecimal.valueOf(Long.MIN_VALUE);
    private static final String[] POINT_KEYS =
            new String[] {"crs", "x", "y", "z", "longitude", "latitude", "height", "srid"};

    private CypherFunctions() {
        throw new UnsupportedOperationException("Do not instantiate");
    }

    public static DoubleValue sin(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.sin(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("sin()");
        }
    }

    public static DoubleValue asin(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.asin(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("asin()");
        }
    }

    public static DoubleValue haversin(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue((1.0 - Math.cos(((NumberValue) in).doubleValue())) / 2);
        } else {
            throw needsNumbers("haversin()");
        }
    }

    public static DoubleValue cos(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.cos(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("cos()");
        }
    }

    public static DoubleValue cot(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(1.0 / Math.tan(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("cot()");
        }
    }

    public static DoubleValue acos(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.acos(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("acos()");
        }
    }

    public static DoubleValue tan(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.tan(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("tan()");
        }
    }

    public static DoubleValue atan(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.atan(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("atan()");
        }
    }

    public static DoubleValue atan2(AnyValue y, AnyValue x) {
        assert y != NO_VALUE && x != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (y instanceof NumberValue && x instanceof NumberValue) {
            return doubleValue(Math.atan2(((NumberValue) y).doubleValue(), ((NumberValue) x).doubleValue()));
        } else {
            throw needsNumbers("atan2()");
        }
    }

    public static DoubleValue ceil(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.ceil(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("ceil()");
        }
    }

    public static DoubleValue floor(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.floor(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("floor()");
        }
    }

    @CalledFromGeneratedCode
    public static DoubleValue round(AnyValue in) {
        return round(in, Values.ZERO_INT, Values.stringValue("HALF_UP"), Values.booleanValue(false));
    }

    @CalledFromGeneratedCode
    public static DoubleValue round(AnyValue in, AnyValue precision) {
        return round(in, precision, Values.stringValue("HALF_UP"), Values.booleanValue(false));
    }

    public static DoubleValue round(AnyValue in, AnyValue precisionValue, AnyValue modeValue) {
        return round(in, precisionValue, modeValue, Values.booleanValue(true));
    }

    public static DoubleValue round(
            AnyValue in, AnyValue precisionValue, AnyValue modeValue, AnyValue explicitModeValue) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        assert precisionValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";

        if (!(modeValue instanceof StringValue)) {
            throw notAModeString("round", modeValue);
        }

        RoundingMode mode;
        try {
            mode = RoundingMode.valueOf(((StringValue) modeValue).stringValue());
        } catch (IllegalArgumentException e) {
            throw new InvalidArgumentException(
                    "Unknown rounding mode. Valid values are: CEILING, FLOOR, UP, DOWN, HALF_EVEN, HALF_UP, HALF_DOWN, UNNECESSARY.");
        }

        if (in instanceof NumberValue && precisionValue instanceof NumberValue) {
            int precision = asIntExact(precisionValue, () -> "Invalid input for precision value in function 'round()'");
            boolean explicitMode = ((BooleanValue) explicitModeValue).booleanValue();

            if (precision < 0) {
                throw new InvalidArgumentException("Precision argument to 'round()' cannot be negative");
            } else {
                double value = ((NumberValue) in).doubleValue();
                if (Double.isInfinite(value) || Double.isNaN(value)) {
                    return doubleValue(value);
                }
                /*
                 * For precision zero and no explicit rounding mode, we want to fall back to Java Math.round().
                 * This rounds towards the nearest integer and if there is a tie, towards positive infinity,
                 * which doesn't correspond to any of the rounding modes.
                 */
                else if (precision == 0 && !explicitMode) {
                    return doubleValue(Math.round(value));
                } else {
                    BigDecimal bigDecimal = BigDecimal.valueOf(value);
                    int newScale = Math.min(bigDecimal.scale(), precision);
                    return doubleValue(bigDecimal.setScale(newScale, mode).doubleValue());
                }
            }
        } else {
            throw needsNumbers("round()");
        }
    }

    public static NumberValue abs(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            if (in instanceof IntegralValue) {
                return longValue(Math.abs(((NumberValue) in).longValue()));
            } else {
                return doubleValue(Math.abs(((NumberValue) in).doubleValue()));
            }
        } else {
            throw needsNumbers("abs()");
        }
    }

    public static BooleanValue isNaN(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof FloatingPointValue f) {
            return booleanValue(f.isNaN());
        } else if (in instanceof NumberValue) {
            return BooleanValue.FALSE;
        } else {
            throw needsNumbers("isNaN()");
        }
    }

    public static DoubleValue toDegrees(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.toDegrees(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("toDegrees()");
        }
    }

    public static DoubleValue exp(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.exp(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("exp()");
        }
    }

    public static DoubleValue log(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.log(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("log()");
        }
    }

    public static DoubleValue log10(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.log10(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("log10()");
        }
    }

    public static DoubleValue toRadians(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.toRadians(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("toRadians()");
        }
    }

    @CalledFromGeneratedCode
    public static ListValue range(AnyValue startValue, AnyValue endValue) {
        return VirtualValues.range(
                asLong(startValue, () -> "Invalid input for start value in function 'range()'"),
                asLong(endValue, () -> "Invalid input for end value in function 'range()'"),
                1L);
    }

    public static ListValue range(AnyValue startValue, AnyValue endValue, AnyValue stepValue) {
        long step = asLong(stepValue, () -> "Invalid input for step value in function 'range()'");
        if (step == 0L) {
            throw new InvalidArgumentException("Step argument to 'range()' cannot be zero");
        }

        return VirtualValues.range(
                asLong(startValue, () -> "Invalid input for start value in function 'range()'"),
                asLong(endValue, () -> "Invalid input for end value in function 'range()'"),
                step);
    }

    @CalledFromGeneratedCode
    public static LongValue signum(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return longValue((long) Math.signum(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("signum()");
        }
    }

    public static DoubleValue sqrt(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof NumberValue) {
            return doubleValue(Math.sqrt(((NumberValue) in).doubleValue()));
        } else {
            throw needsNumbers("sqrt()");
        }
    }

    public static DoubleValue rand() {
        return doubleValue(ThreadLocalRandom.current().nextDouble());
    }

    public static TextValue randomUuid() {
        return stringValue(UUID.randomUUID().toString());
    }

    // TODO: Support better calculations, like https://en.wikipedia.org/wiki/Vincenty%27s_formulae
    // TODO: Support more coordinate systems
    public static Value distance(AnyValue lhs, AnyValue rhs) {
        if (lhs instanceof PointValue && rhs instanceof PointValue) {
            return calculateDistance((PointValue) lhs, (PointValue) rhs);
        } else {
            return NO_VALUE;
        }
    }

    public static Value withinBBox(AnyValue point, AnyValue lowerLeft, AnyValue upperRight) {
        if (point instanceof PointValue && lowerLeft instanceof PointValue && upperRight instanceof PointValue) {
            return withinBBox((PointValue) point, (PointValue) lowerLeft, (PointValue) upperRight);
        } else {
            return NO_VALUE;
        }
    }

    public static Value withinBBox(PointValue point, PointValue lowerLeft, PointValue upperRight) {
        CoordinateReferenceSystem crs = point.getCoordinateReferenceSystem();
        if (crs.equals(lowerLeft.getCoordinateReferenceSystem())
                && crs.equals(upperRight.getCoordinateReferenceSystem())) {
            return Values.booleanValue(crs.getCalculator().withinBBox(point, lowerLeft, upperRight));
        } else {
            return NO_VALUE;
        }
    }

    public static VirtualNodeValue startNode(AnyValue anyValue, DbAccess access, RelationshipScanCursor cursor) {
        assert anyValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (anyValue instanceof RelationshipValue) {
            return ((RelationshipValue) anyValue).startNode();
        } else if (anyValue instanceof VirtualRelationshipValue) {
            return startNode((VirtualRelationshipValue) anyValue, access, cursor);
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'startNode()': Expected %s to be a RelationshipValue", anyValue));
        }
    }

    public static VirtualNodeValue startNode(
            VirtualRelationshipValue relationship, DbAccess access, RelationshipScanCursor cursor) {
        return VirtualValues.node(relationship.startNodeId(relationshipVisitor -> {
            access.singleRelationship(relationshipVisitor.id(), cursor);
            if (cursor.next()) {
                relationshipVisitor.visit(cursor.sourceNodeReference(), cursor.targetNodeReference(), cursor.type());
            }
        }));
    }

    public static VirtualNodeValue endNode(AnyValue anyValue, DbAccess access, RelationshipScanCursor cursor) {
        assert anyValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (anyValue instanceof RelationshipValue) {
            return ((RelationshipValue) anyValue).endNode();
        } else if (anyValue instanceof VirtualRelationshipValue) {
            return endNode((VirtualRelationshipValue) anyValue, access, cursor);
        } else {
            throw new CypherTypeException(
                    format("Invalid input for function 'endNode()': Expected %s to be a RelationshipValue", anyValue));
        }
    }

    public static VirtualNodeValue endNode(
            VirtualRelationshipValue relationship, DbAccess access, RelationshipScanCursor cursor) {
        return VirtualValues.node(relationship.endNodeId(relationshipVisitor -> {
            access.singleRelationship(relationshipVisitor.id(), cursor);
            if (cursor.next()) {
                relationshipVisitor.visit(cursor.sourceNodeReference(), cursor.targetNodeReference(), cursor.type());
            }
        }));
    }

    @CalledFromGeneratedCode
    public static VirtualNodeValue otherNode(
            AnyValue anyValue, DbAccess access, VirtualNodeValue node, RelationshipScanCursor cursor) {
        // This is not a function exposed to the user
        assert anyValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (anyValue instanceof VirtualRelationshipValue) {
            return otherNode((VirtualRelationshipValue) anyValue, access, node, cursor);
        } else {
            throw new CypherTypeException(format("Expected %s to be a RelationshipValue", anyValue));
        }
    }

    public static VirtualNodeValue otherNode(
            VirtualRelationshipValue relationship,
            DbAccess access,
            VirtualNodeValue node,
            RelationshipScanCursor cursor) {
        return VirtualValues.node(relationship.otherNodeId(node.id(), relationshipVisitor -> {
            access.singleRelationship(relationshipVisitor.id(), cursor);
            if (cursor.next()) {
                relationshipVisitor.visit(cursor.sourceNodeReference(), cursor.targetNodeReference(), cursor.type());
            }
        }));
    }

    @CalledFromGeneratedCode
    public static AnyValue propertyGet(
            String key,
            AnyValue container,
            DbAccess dbAccess,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        if (container == NO_VALUE) {
            return NO_VALUE;
        } else if (container instanceof VirtualNodeValue node) {
            return dbAccess.nodeProperty(node.id(), dbAccess.propertyKey(key), nodeCursor, propertyCursor, true);
        } else if (container instanceof VirtualRelationshipValue rel) {
            return dbAccess.relationshipProperty(
                    rel.id(), dbAccess.propertyKey(key), relationshipScanCursor, propertyCursor, true);
        } else if (container instanceof MapValue map) {
            return map.get(key);
        } else if (container instanceof TemporalValue<?, ?> temporal) {
            return temporal.get(key);
        } else if (container instanceof DurationValue duration) {
            return duration.get(key);
        } else if (container instanceof PointValue point) {
            return point.get(key);
        } else {
            throw new CypherTypeException(format("Type mismatch: expected a map but was %s", container));
        }
    }

    @CalledFromGeneratedCode
    public static AnyValue[] propertiesGet(
            String[] keys,
            AnyValue container,
            DbAccess dbAccess,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        if (container instanceof VirtualNodeValue node) {
            return dbAccess.nodeProperties(node.id(), propertyKeys(keys, dbAccess), nodeCursor, propertyCursor);
        } else if (container instanceof VirtualRelationshipValue rel) {
            return dbAccess.relationshipProperties(
                    rel.id(), propertyKeys(keys, dbAccess), relationshipScanCursor, propertyCursor);
        } else {
            return CursorUtils.propertiesGet(keys, container);
        }
    }

    @CalledFromGeneratedCode
    public static AnyValue containerIndex(
            AnyValue container,
            AnyValue index,
            DbAccess dbAccess,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        if (container == NO_VALUE || index == NO_VALUE) {
            return NO_VALUE;
        } else if (container instanceof VirtualNodeValue node) {
            return dbAccess.nodeProperty(node.id(), propertyKeyId(dbAccess, index), nodeCursor, propertyCursor, true);
        } else if (container instanceof VirtualRelationshipValue rel) {
            return dbAccess.relationshipProperty(
                    rel.id(), propertyKeyId(dbAccess, index), relationshipScanCursor, propertyCursor, true);
        }
        if (container instanceof MapValue map) {
            return mapAccess(map, index);
        } else if (container instanceof SequenceValue seq) {
            return listAccess(seq, index);
        } else {
            throw new CypherTypeException(format(
                    "`%s` is not a collection or a map. Element access is only possible by performing a collection "
                            + "lookup using an integer index, or by performing a map lookup using a string key (found: %s[%s])",
                    container, container, index));
        }
    }

    @CalledFromGeneratedCode
    public static Value containerIndexExists(
            AnyValue container,
            AnyValue index,
            DbAccess dbAccess,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        if (container == NO_VALUE || index == NO_VALUE) {
            return NO_VALUE;
        } else if (container instanceof VirtualNodeValue node) {
            return booleanValue(
                    dbAccess.nodeHasProperty(node.id(), propertyKeyId(dbAccess, index), nodeCursor, propertyCursor));
        } else if (container instanceof VirtualRelationshipValue rel) {
            return booleanValue(dbAccess.relationshipHasProperty(
                    rel.id(), propertyKeyId(dbAccess, index), relationshipScanCursor, propertyCursor));
        }
        if (container instanceof MapValue map) {
            return booleanValue(map.containsKey(asString(
                    index,
                    () ->
                            // this string assumes that the asString method fails and gives context which
                            // operation went wrong
                            "Cannot use non string value as or in map keys. It was " + index.toString())));
        } else {
            throw new CypherTypeException(format(
                    "`%s` is not a map. Element access is only possible by performing a collection "
                            + "lookup by performing a map lookup using a string key (found: %s[%s])",
                    container, container, index));
        }
    }

    @CalledFromGeneratedCode
    public static AnyValue head(AnyValue container) {
        assert container != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (container instanceof SequenceValue sequence) {
            if (sequence.length() == 0) {
                return NO_VALUE;
            }

            return sequence.value(0);
        } else {
            throw new CypherTypeException(
                    format("Invalid input for function 'head()': Expected %s to be a list", container));
        }
    }

    @CalledFromGeneratedCode
    public static ListValue tail(AnyValue container) {
        assert container != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (container instanceof ListValue) {
            return ((ListValue) container).tail();
        } else if (container instanceof ArrayValue) {
            return VirtualValues.fromArray((ArrayValue) container).tail();
        } else {
            return EMPTY_LIST;
        }
    }

    @CalledFromGeneratedCode
    public static AnyValue last(AnyValue container) {
        assert container != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (container instanceof SequenceValue sequence) {
            int length = sequence.length();
            if (length == 0) {
                return NO_VALUE;
            }

            return sequence.value(length - 1);
        } else {
            throw new CypherTypeException(
                    format("Invalid input for function 'last()': Expected %s to be a list", container));
        }
    }

    public static TextValue left(AnyValue in, AnyValue endPos) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue text) {
            final long len = asLong(endPos, () -> "Invalid input for length value in function 'left()'");
            return text.substring(0, (int) Math.min(len, Integer.MAX_VALUE));
        } else {
            throw notAString("left", in);
        }
    }

    public static TextValue ltrim(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue) {
            return ((TextValue) in).ltrim();
        } else {
            throw notAString("ltrim", in);
        }
    }

    public static TextValue rtrim(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue) {
            return ((TextValue) in).rtrim();
        } else {
            throw notAString("rtrim", in);
        }
    }

    public static TextValue trim(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue) {
            return ((TextValue) in).trim();
        } else {
            throw notAString("trim", in);
        }
    }

    public static TextValue replace(AnyValue original, AnyValue search, AnyValue replaceWith) {
        assert original != NO_VALUE && search != NO_VALUE && replaceWith != NO_VALUE
                : "NO_VALUE checks need to happen outside this call";

        if (original instanceof TextValue) {
            return ((TextValue) original).replace(asString(search), asString(replaceWith));
        } else {
            throw notAString("replace", original);
        }
    }

    public static AnyValue reverse(AnyValue original) {
        assert original != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (original instanceof TextValue) {
            return ((TextValue) original).reverse();
        } else if (original instanceof ListValue) {
            return ((ListValue) original).reverse();
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'reverse()': "
                            + "Expected a string or a list; consider converting the value to a string with toString() or creating a list.");
        }
    }

    public static TextValue right(AnyValue original, AnyValue length) {
        assert original != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (original instanceof TextValue asText) {
            final long len = asLong(length, () -> "Invalid input for length value in function 'right()'");
            if (len < 0) {
                throw new IndexOutOfBoundsException("negative length");
            }
            final long startVal = asText.length() - len;
            return asText.substring((int) Math.max(0, startVal));
        } else {
            throw notAString("right", original);
        }
    }

    public static ListValue split(AnyValue original, AnyValue separator) {
        assert original != NO_VALUE && separator != NO_VALUE : "NO_VALUE checks need to happen outside this call";

        if (original instanceof TextValue asText) {
            if (asText.length() == 0) {
                return VirtualValues.list(EMPTY_STRING);
            }
            if (separator instanceof ListValue) {
                var separators = new ArrayList<String>();
                for (var s : (ListValue) separator) {
                    separators.add(asString(s));
                }
                return asText.split(separators);
            } else {
                return asText.split(asString(separator));
            }
        } else {
            throw notAString("split", original);
        }
    }

    public static TextValue substring(AnyValue original, AnyValue start) {
        assert original != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (original instanceof TextValue asText) {

            return asText.substring(asIntExact(start, () -> "Invalid input for start value in function 'substring()'"));
        } else {
            throw notAString("substring", original);
        }
    }

    public static TextValue substring(AnyValue original, AnyValue start, AnyValue length) {
        assert original != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (original instanceof TextValue asText) {

            return asText.substring(
                    asIntExact(start, () -> "Invalid input for start value in function 'substring()'"),
                    asIntExact(length, () -> "Invalid input for length value in function 'substring()'"));
        } else {
            throw notAString("substring", original);
        }
    }

    public static TextValue toLower(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue) {
            return ((TextValue) in).toLower();
        } else {
            throw notAString("toLower", in);
        }
    }

    public static TextValue toUpper(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof TextValue) {
            return ((TextValue) in).toUpper();
        } else {
            throw notAString("toUpper", in);
        }
    }

    public static Value id(AnyValue item) {
        if (item == NO_VALUE) {
            return NO_VALUE;
        } else if (item instanceof VirtualNodeValue) {
            return longValue(((VirtualNodeValue) item).id());
        } else if (item instanceof VirtualRelationshipValue) {
            return longValue(((VirtualRelationshipValue) item).id());
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'id()': Expected %s to be a node or relationship, but it was `%s`",
                    item, item.getTypeName()));
        }
    }

    public static TextValue elementId(AnyValue entity, ElementIdMapper idMapper) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";

        if (entity instanceof NodeValue node) {
            // Needed to get correct ids in certain fabric queries.
            return stringValue(node.elementId());
        } else if (entity instanceof VirtualNodeValue node) {
            return stringValue(idMapper.nodeElementId(node.id()));
        } else if (entity instanceof RelationshipValue relationship) {
            // Needed to get correct ids in certain fabric queries.
            return stringValue(relationship.elementId());
        } else if (entity instanceof VirtualRelationshipValue relationship) {
            return stringValue(idMapper.relationshipElementId(relationship.id()));
        }

        throw new CypherTypeException(format(
                "Invalid input for function 'elementId()': Expected %s to be a node or relationship, but it was `%s`",
                entity, entity.getTypeName()));
    }

    public static AnyValue elementIdToNodeId(AnyValue elementId, ElementIdMapper idMapper) {
        assert elementId != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (elementId instanceof TextValue str) {
            try {
                return longValue(idMapper.nodeId(str.stringValue()));
            } catch (IllegalArgumentException e) {
                return NO_VALUE;
            }
        }
        return NO_VALUE;
    }

    public static AnyValue elementIdToRelationshipId(AnyValue elementId, ElementIdMapper idMapper) {
        assert elementId != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (elementId instanceof TextValue str) {
            try {
                return longValue(idMapper.relationshipId(str.stringValue()));
            } catch (IllegalArgumentException e) {
                return NO_VALUE;
            }
        }
        return NO_VALUE;
    }

    public static AnyValue elementIdListToNodeIdList(AnyValue collection, ElementIdMapper idMapper) {
        assert collection != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (collection instanceof SequenceValue elementIds) {
            var builder = ListValueBuilder.newListBuilder(elementIds.length());
            for (var elementId : elementIds) {
                AnyValue value = (elementId == NO_VALUE) ? NO_VALUE : elementIdToNodeId(elementId, idMapper);
                builder.add(value);
            }
            return builder.build();
        }
        return NO_VALUE;
    }

    public static AnyValue elementIdListToRelationshipIdList(AnyValue collection, ElementIdMapper idMapper) {
        assert collection != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (collection instanceof SequenceValue elementIds) {
            var builder = ListValueBuilder.newListBuilder(elementIds.length());
            for (var elementId : elementIds) {
                AnyValue value = (elementId == NO_VALUE) ? NO_VALUE : elementIdToRelationshipId(elementId, idMapper);
                builder.add(value);
            }
            return builder.build();
        }
        return NO_VALUE;
    }

    public static TextValue nodeElementId(long id, ElementIdMapper idMapper) {
        assert id > NO_ID;
        return Values.stringValue(idMapper.nodeElementId(id));
    }

    public static TextValue relationshipElementId(long id, ElementIdMapper idMapper) {
        assert id > NO_ID;
        return Values.stringValue(idMapper.relationshipElementId(id));
    }

    public static AnyValue labels(AnyValue item, DbAccess access, NodeCursor nodeCursor) {
        if (item == NO_VALUE) {
            return NO_VALUE;
        } else if (item instanceof VirtualNodeValue node) {
            return access.getLabelsForNode(node.id(), nodeCursor);
        } else {
            throw new CypherTypeException("Invalid input for function 'labels()': Expected a Node, got: " + item);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasLabel(AnyValue entity, int labelToken, DbAccess access, NodeCursor nodeCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue node) {
            return access.isLabelSetOnNode(labelToken, node.id(), nodeCursor);
        } else {
            throw new CypherTypeException("Expected a Node, got: " + entity);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasLabels(AnyValue entity, int[] labelTokens, DbAccess access, NodeCursor nodeCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue node) {
            return access.areLabelsSetOnNode(labelTokens, node.id(), nodeCursor);
        } else {
            throw new CypherTypeException("Expected a Node, got: " + entity);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasALabel(AnyValue entity, DbAccess access, NodeCursor nodeCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue virtualNodeValue) {
            return access.isALabelSetOnNode(virtualNodeValue.id(), nodeCursor);
        } else {
            throw new CypherTypeException("Expected a Node, got: " + entity);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasALabelOrType(AnyValue entity, DbAccess access, NodeCursor nodeCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue node) {
            return access.isALabelSetOnNode(node.id(), nodeCursor);
        } else if (entity instanceof VirtualRelationshipValue) {
            return true;
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'hasALabelOrType()': Expected %s to be a node or relationship, but it was `%s`",
                    entity, entity.getTypeName()));
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasLabelsOrTypes(
            AnyValue entity,
            DbAccess access,
            int[] labels,
            NodeCursor nodeCursor,
            int[] types,
            RelationshipScanCursor relationshipScanCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue node) {
            return access.areLabelsSetOnNode(labels, node.id(), nodeCursor);
        } else if (entity instanceof VirtualRelationshipValue relationship) {
            return access.areTypesSetOnRelationship(types, relationship.id(), relationshipScanCursor);
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'hasALabelOrType()': Expected %s to be a node or relationship, but it was `%s`",
                    entity, entity.getTypeName()));
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasAnyLabel(AnyValue entity, int[] labels, DbAccess access, NodeCursor nodeCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualNodeValue node) {
            return access.isAnyLabelSetOnNode(labels, node.id(), nodeCursor);
        } else {
            throw new CypherTypeException("Expected a Node, got: " + entity);
        }
    }

    public static AnyValue type(AnyValue item, DbAccess access, RelationshipScanCursor relCursor, Read read) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (item instanceof RelationshipValue relationship) {
            return relationship.type();
        } else if (item instanceof VirtualRelationshipValue relationship) {

            int typeToken = relationship.relationshipTypeId(relationshipVisitor -> {
                long relationshipId = relationshipVisitor.id();
                access.singleRelationship(relationshipId, relCursor);

                if (relCursor.next() || read.relationshipDeletedInTransaction(relationshipId)) {
                    relationshipVisitor.visit(
                            relCursor.sourceNodeReference(), relCursor.targetNodeReference(), relCursor.type());
                }
            });

            if (typeToken == TokenConstants.NO_TOKEN) {
                return NO_VALUE;
            } else {
                return Values.stringValue(access.relationshipTypeName(typeToken));
            }
        } else {
            throw new CypherTypeException("Invalid input for function 'type()': Expected a Relationship, got: " + item);
        }
    }

    public static AnyValue threadSafeType(AnyValue item, DbAccess access, RelationshipScanCursor relCursor, Read read) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (item instanceof VirtualRelationshipValue relationship) {
            long relationshipId = relationship.id();
            access.singleRelationship(relationshipId, relCursor);
            if (!relCursor.next() && !read.relationshipDeletedInTransaction(relationshipId)) {
                return NO_VALUE;
            }
            return Values.stringValue(access.relationshipTypeName(relCursor.type()));
        } else {
            throw new CypherTypeException("Invalid input for function 'type()': Expected a Relationship, got: " + item);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasType(AnyValue entity, int typeToken, DbAccess access, RelationshipScanCursor relCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualRelationshipValue relationship) {
            if (typeToken == StatementConstants.NO_SUCH_RELATIONSHIP_TYPE) {
                return false;
            } else {
                int actualType = relationship.relationshipTypeId(relationshipVisitor -> {
                    access.singleRelationship(relationshipVisitor.id(), relCursor);
                    if (relCursor.next()) {
                        relationshipVisitor.visit(
                                relCursor.sourceNodeReference(), relCursor.targetNodeReference(), relCursor.type());
                    }
                });
                return typeToken == actualType;
            }
        } else {
            throw new CypherTypeException("Expected a Relationship, got: " + entity);
        }
    }

    @CalledFromGeneratedCode
    public static boolean hasTypes(
            AnyValue entity, int[] typeTokens, DbAccess access, RelationshipScanCursor relCursor) {
        assert entity != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (entity instanceof VirtualRelationshipValue relationship) {
            return access.areTypesSetOnRelationship(typeTokens, relationship.id(), relCursor);
        } else {
            throw new CypherTypeException("Expected a Relationship, got: " + entity);
        }
    }

    public static ListValue nodes(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof PathValue) {
            return VirtualValues.list(((PathValue) in).nodes());
        } else if (in instanceof VirtualPathValue) {
            long[] ids = ((VirtualPathValue) in).nodeIds();
            ListValueBuilder builder = ListValueBuilder.newListBuilder(ids.length);
            for (long id : ids) {
                builder.add(VirtualValues.node(id));
            }
            return builder.build();
        } else {
            throw new CypherTypeException(format("Invalid input for function 'nodes()': Expected %s to be a path", in));
        }
    }

    public static ListValue relationships(AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof PathValue) {
            return VirtualValues.list(((PathValue) in).relationships());
        } else if (in instanceof VirtualPathValue) {
            long[] ids = ((VirtualPathValue) in).relationshipIds();
            ListValueBuilder builder = ListValueBuilder.newListBuilder(ids.length);
            for (long id : ids) {
                builder.add(VirtualValues.relationship(id));
            }
            return builder.build();
        } else {
            throw new CypherTypeException(
                    format("Invalid input for function 'relationships()': Expected %s to be a path", in));
        }
    }

    public static Value point(AnyValue in, DbAccess access, ExpressionCursors cursors) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof VirtualNodeValue) {
            return asPoint(access, (VirtualNodeValue) in, cursors.nodeCursor(), cursors.propertyCursor());
        } else if (in instanceof VirtualRelationshipValue) {
            return asPoint(
                    access, (VirtualRelationshipValue) in, cursors.relationshipScanCursor(), cursors.propertyCursor());
        } else if (in instanceof MapValue map) {
            if (containsNull(map)) {
                return NO_VALUE;
            }
            return PointValue.fromMap(map);
        } else {
            throw new CypherTypeException(
                    format("Invalid input for function 'point()': Expected a map but got %s", in));
        }
    }

    public static ListValue keys(
            AnyValue in,
            DbAccess access,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof VirtualNodeValue) {
            return extractKeys(
                    access, access.nodePropertyIds(((VirtualNodeValue) in).id(), nodeCursor, propertyCursor));
        } else if (in instanceof VirtualRelationshipValue) {
            return extractKeys(
                    access,
                    access.relationshipPropertyIds(
                            ((VirtualRelationshipValue) in).id(), relationshipScanCursor, propertyCursor));
        } else if (in instanceof MapValue) {
            return ((MapValue) in).keys();
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'keys()': Expected a node, a relationship or a literal map but got %s",
                    in));
        }
    }

    public static MapValue properties(
            AnyValue in,
            DbAccess access,
            NodeCursor nodeCursor,
            RelationshipScanCursor relationshipCursor,
            PropertyCursor propertyCursor) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (in instanceof VirtualNodeValue) {
            return access.nodeAsMap(((VirtualNodeValue) in).id(), nodeCursor, propertyCursor);
        } else if (in instanceof VirtualRelationshipValue) {
            return access.relationshipAsMap(((VirtualRelationshipValue) in).id(), relationshipCursor, propertyCursor);
        } else if (in instanceof MapValue) {
            return (MapValue) in;
        } else {
            throw new CypherTypeException(format(
                    "Invalid input for function 'properties()': Expected a node, a relationship or a literal map but got %s",
                    in));
        }
    }

    public static IntegralValue size(AnyValue item) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";

        if (item instanceof TextValue) {
            return longValue(((TextValue) item).length());
        } else if (item instanceof SequenceValue) {
            return longValue(((SequenceValue) item).length());
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'size()': Expected a String or List, got: " + item);
        }
    }

    public static BooleanValue isEmpty(AnyValue item) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (item instanceof SequenceValue) {
            return Values.booleanValue(((SequenceValue) item).isEmpty());
        } else if (item instanceof MapValue) {
            return Values.booleanValue(((MapValue) item).isEmpty());
        } else if (item instanceof TextValue) {
            return Values.booleanValue(((TextValue) item).isEmpty());
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'isEmpty()': Expected a List, Map, or String, got: " + item);
        }
    }

    public static IntegralValue length(AnyValue item) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (item instanceof VirtualPathValue) {
            return longValue(((VirtualPathValue) item).size());
        } else {
            throw new CypherTypeException("Invalid input for function 'length()': Expected a Path, got: " + item);
        }
    }

    public static Value toBoolean(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof BooleanValue) {
            return (BooleanValue) in;
        } else if (in instanceof TextValue) {
            return switch (((TextValue) in).trim().stringValue().toLowerCase(Locale.ROOT)) {
                case "true" -> TRUE;
                case "false" -> FALSE;
                default -> NO_VALUE;
            };
        } else if (in instanceof IntegralValue integer) {
            return integer.longValue() == 0L ? FALSE : TRUE;
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'toBoolean()': Expected a Boolean, Integer or String, got: " + in);
        }
    }

    public static Value toBooleanOrNull(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof BooleanValue || in instanceof TextValue || in instanceof IntegralValue) {
            return toBoolean(in);
        } else {
            return NO_VALUE;
        }
    }

    public static AnyValue toBooleanList(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof ListValue lv) {
            return Arrays.stream(lv.asArray())
                    .map(entry -> entry == NO_VALUE ? NO_VALUE : toBooleanOrNull(entry))
                    .collect(ListValueBuilder.collector());
        } else {
            throw new CypherTypeException(
                    String.format("Invalid input for function 'toBooleanList()': Expected a List, got: %s", in));
        }
    }

    public static Value toFloat(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof DoubleValue) {
            return (DoubleValue) in;
        } else if (in instanceof NumberValue) {
            return doubleValue(((NumberValue) in).doubleValue());
        } else if (in instanceof TextValue) {
            try {
                return doubleValue(parseDouble(((TextValue) in).stringValue()));
            } catch (NumberFormatException ignore) {
                return NO_VALUE;
            }
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'toFloat()': Expected a String or Number, got: " + in);
        }
    }

    public static Value toFloatOrNull(AnyValue in) {
        if (in instanceof NumberValue || in instanceof TextValue) {
            return toFloat(in);
        } else {
            return NO_VALUE;
        }
    }

    public static AnyValue toFloatList(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof ListValue lv) {
            return Arrays.stream(lv.asArray())
                    .map(entry -> entry == NO_VALUE ? NO_VALUE : toFloatOrNull(entry))
                    .collect(ListValueBuilder.collector());
        } else {
            throw new CypherTypeException(
                    String.format("Invalid input for function 'toFloatList()': Expected a List, got: %s", in));
        }
    }

    public static Value toInteger(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof IntegralValue) {
            return (IntegralValue) in;
        } else if (in instanceof NumberValue) {
            return longValue(((NumberValue) in).longValue());
        } else if (in instanceof TextValue) {
            return stringToLongValue((TextValue) in);
        } else if (in instanceof BooleanValue) {
            if (((BooleanValue) in).booleanValue()) {
                return longValue(1L);
            } else {
                return longValue(0L);
            }
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'toInteger()': Expected a String, Number or Boolean, got: " + in);
        }
    }

    public static Value toIntegerOrNull(AnyValue in) {
        if (in instanceof NumberValue || in instanceof BooleanValue) {
            return toInteger(in);
        } else if (in instanceof TextValue) {
            try {
                return stringToLongValue((TextValue) in);
            } catch (CypherTypeException e) {
                return NO_VALUE;
            }
        } else {
            return NO_VALUE;
        }
    }

    public static AnyValue toIntegerList(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof IntegralArray array) {
            return VirtualValues.fromArray(array);
        } else if (in instanceof FloatingPointArray array) {
            return toIntegerList(array);
        } else if (in instanceof SequenceValue sequenc) {
            return toIntegerList(sequenc);
        } else {
            throw new CypherTypeException(
                    String.format("Invalid input for function 'toIntegerList()': Expected a List, got: %s", in));
        }
    }

    public static Value toString(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof TextValue) {
            return (TextValue) in;
        } else if (in instanceof NumberValue) {
            return stringValue(((NumberValue) in).prettyPrint());
        } else if (in instanceof BooleanValue) {
            return stringValue(((BooleanValue) in).prettyPrint());
        } else if (in instanceof TemporalValue || in instanceof DurationValue || in instanceof PointValue) {
            return stringValue(in.toString());
        } else {
            throw new CypherTypeException(
                    "Invalid input for function 'toString()': Expected a String, Number, Boolean, Temporal or Duration, got: "
                            + in);
        }
    }

    public static AnyValue toStringOrNull(AnyValue in) {
        if (in instanceof TextValue
                || in instanceof NumberValue
                || in instanceof BooleanValue
                || in instanceof TemporalValue
                || in instanceof DurationValue
                || in instanceof PointValue) {
            return toString(in);
        } else {
            return NO_VALUE;
        }
    }

    public static AnyValue toStringList(AnyValue in) {
        if (in == NO_VALUE) {
            return NO_VALUE;
        } else if (in instanceof ListValue lv) {
            return Arrays.stream(lv.asArray())
                    .map(entry -> entry == NO_VALUE ? NO_VALUE : toStringOrNull(entry))
                    .collect(ListValueBuilder.collector());
        } else {
            throw new CypherTypeException(
                    String.format("Invalid input for function 'toStringList()': Expected a List, got: %s", in));
        }
    }

    public static ListValue fromSlice(AnyValue collection, AnyValue fromValue) {
        assert collection != NO_VALUE && fromValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";

        int from = asIntExact(fromValue);
        ListValue list = asList(collection);
        if (from >= 0) {
            return list.drop(from);
        } else {
            return list.drop(list.size() + from);
        }
    }

    public static ListValue toSlice(AnyValue collection, AnyValue toValue) {
        assert collection != NO_VALUE && toValue != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        int from = asIntExact(toValue);
        ListValue list = asList(collection);
        if (from >= 0) {
            return list.take(from);
        } else {
            return list.take(list.size() + from);
        }
    }

    public static ListValue fullSlice(AnyValue collection, AnyValue fromValue, AnyValue toValue) {
        assert collection != NO_VALUE && fromValue != NO_VALUE && toValue != NO_VALUE
                : "NO_VALUE checks need to happen outside this call";

        int from = asIntExact(fromValue);
        int to = asIntExact(toValue);
        ListValue list = asList(collection);
        int size = list.size();
        if (from >= 0 && to >= 0) {
            return list.slice(from, to);
        } else if (from >= 0) {
            return list.slice(from, size + to);
        } else if (to >= 0) {
            return list.slice(size + from, to);
        } else {
            return list.slice(size + from, size + to);
        }
    }

    @CalledFromGeneratedCode
    public static TextValue asTextValue(AnyValue value) {
        return asTextValue(value, null);
    }

    public static TextValue asTextValue(AnyValue value, Supplier<String> contextForErrorMessage) {
        if (!(value instanceof TextValue)) {
            String errorMessage;
            if (contextForErrorMessage == null) {
                errorMessage = format(
                        "Expected %s to be a %s, but it was a %s",
                        value, TextValue.class.getName(), value.getClass().getName());
            } else {
                errorMessage = format(
                        "%s: Expected %s to be a %s, but it was a %s",
                        contextForErrorMessage.get(),
                        value,
                        TextValue.class.getName(),
                        value.getClass().getName());
            }

            throw new CypherTypeException(errorMessage);
        }
        return (TextValue) value;
    }

    private static Value stringToLongValue(TextValue in) {
        try {
            return longValue(parseLong(in.stringValue()));
        } catch (Exception e) {
            try {
                BigDecimal bigDecimal = new BigDecimal(in.stringValue());
                if (bigDecimal.compareTo(MAX_LONG) <= 0 && bigDecimal.compareTo(MIN_LONG) >= 0) {
                    return longValue(bigDecimal.longValue());
                } else {
                    throw new CypherTypeException(format("integer, %s, is too large", in.stringValue()));
                }
            } catch (NumberFormatException ignore) {
                return NO_VALUE;
            }
        }
    }

    private static ListValue extractKeys(DbAccess access, int[] keyIds) {
        String[] keysNames = new String[keyIds.length];
        for (int i = 0; i < keyIds.length; i++) {
            keysNames[i] = access.getPropertyKeyName(keyIds[i]);
        }
        return VirtualValues.fromArray(Values.stringArray(keysNames));
    }

    private static Value asPoint(
            DbAccess access, VirtualNodeValue nodeValue, NodeCursor nodeCursor, PropertyCursor propertyCursor) {
        MapValueBuilder builder = new MapValueBuilder();
        for (String key : POINT_KEYS) {
            Value value =
                    access.nodeProperty(nodeValue.id(), access.propertyKey(key), nodeCursor, propertyCursor, true);
            if (value == NO_VALUE) {
                continue;
            }
            builder.add(key, value);
        }

        return PointValue.fromMap(builder.build());
    }

    private static Value asPoint(
            DbAccess access,
            VirtualRelationshipValue relationshipValue,
            RelationshipScanCursor relationshipScanCursor,
            PropertyCursor propertyCursor) {
        MapValueBuilder builder = new MapValueBuilder();
        for (String key : POINT_KEYS) {
            Value value = access.relationshipProperty(
                    relationshipValue.id(), access.propertyKey(key), relationshipScanCursor, propertyCursor, true);
            if (value == NO_VALUE) {
                continue;
            }
            builder.add(key, value);
        }

        return PointValue.fromMap(builder.build());
    }

    private static boolean containsNull(MapValue map) {
        boolean[] hasNull = {false};
        map.foreach((s, value) -> {
            if (value == NO_VALUE) {
                hasNull[0] = true;
            }
        });
        return hasNull[0];
    }

    private static AnyValue listAccess(SequenceValue container, AnyValue index) {
        NumberValue number = asNumberValue(
                index,
                () -> "Cannot access a list '" + container.toString() + "' using a non-number index, got "
                        + index.toString());
        if (!(number instanceof IntegralValue)) {
            throw new CypherTypeException(
                    format("Cannot access a list using an non-integer number index, got %s", number), null);
        }
        long idx = number.longValue();
        if (idx > Integer.MAX_VALUE || idx < Integer.MIN_VALUE) {
            throw new InvalidArgumentException(format(
                    "Cannot index a list using a value greater than %d or lesser than %d, got %d",
                    Integer.MAX_VALUE, Integer.MIN_VALUE, idx));
        }

        if (idx < 0) {
            idx = container.length() + idx;
        }
        if (idx >= container.length() || idx < 0) {
            return NO_VALUE;
        }
        return container.value((int) idx);
    }

    private static int propertyKeyId(DbAccess dbAccess, AnyValue index) {
        return dbAccess.propertyKey(asString(
                index,
                () ->
                        // this string assumes that the asString method fails and gives context which operation went
                        // wrong
                        "Cannot use a property key with non string name. It was " + index.toString()));
    }

    private static AnyValue mapAccess(MapValue container, AnyValue index) {
        return container.get(asString(
                index,
                () ->
                        // this string assumes that the asString method fails and gives context which operation went
                        // wrong
                        "Cannot access a map '" + container.toString() + "' by key '" + index.toString() + "'"));
    }

    private static String asString(AnyValue value) {
        return asTextValue(value).stringValue();
    }

    private static String asString(AnyValue value, Supplier<String> contextForErrorMessage) {
        return asTextValue(value, contextForErrorMessage).stringValue();
    }

    private static NumberValue asNumberValue(AnyValue value, Supplier<String> contextForErrorMessage) {
        if (!(value instanceof NumberValue)) {
            throw new CypherTypeException(format(
                    "%s: Expected %s to be a %s, but it was a %s",
                    contextForErrorMessage.get(),
                    value,
                    NumberValue.class.getName(),
                    value.getClass().getName()));
        }
        return (NumberValue) value;
    }

    private static Value calculateDistance(PointValue p1, PointValue p2) {
        if (p1.getCoordinateReferenceSystem().equals(p2.getCoordinateReferenceSystem())) {
            return doubleValue(p1.getCoordinateReferenceSystem().getCalculator().distance(p1, p2));
        } else {
            return NO_VALUE;
        }
    }

    private static long asLong(AnyValue value, Supplier<String> contextForErrorMessage) {
        if (value instanceof NumberValue) {
            return ((NumberValue) value).longValue();
        } else {
            String errorMsg;
            if (contextForErrorMessage == null) {
                errorMsg = "Expected a numeric value but got: " + value;
            } else {
                errorMsg = contextForErrorMessage.get() + ": Expected a numeric value but got: " + value;
            }
            throw new CypherTypeException(errorMsg);
        }
    }

    public static int asIntExact(AnyValue value) {
        return asIntExact(value, null);
    }

    public static int asIntExact(AnyValue value, Supplier<String> contextForErrorMessage) {
        final long longValue = asLong(value, contextForErrorMessage);
        final int intValue = (int) longValue;
        if (intValue != longValue) {
            String errorMsg = format(
                    "Expected an integer between %d and %d, but got: %d",
                    Integer.MIN_VALUE, Integer.MAX_VALUE, longValue);
            if (contextForErrorMessage != null) {
                errorMsg = contextForErrorMessage.get() + ": " + errorMsg;
            }
            throw new IllegalArgumentException(errorMsg);
        }
        return intValue;
    }

    public static long nodeId(AnyValue value) {
        assert value != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (value instanceof VirtualNodeValue) {
            return ((VirtualNodeValue) value).id();
        } else {
            throw new CypherTypeException(
                    "Expected VirtualNodeValue got " + value.getClass().getName());
        }
    }

    public static BooleanValue isTyped(AnyValue item, CypherTypeName typeName) {
        boolean result;
        if (item instanceof NoValue) {
            result = true;
        } else if (typeName instanceof BooleanTypeName) {
            result = Values.isBooleanValue(item);
        } else if (typeName instanceof StringTypeName) {
            result = Values.isTextValue(item);
        } else if (typeName instanceof IntegerTypeName) {
            result = item instanceof IntegralValue;
        } else if (typeName instanceof FloatTypeName) {
            result = item instanceof FloatingPointValue;
        } else if (typeName instanceof DateTypeName) {
            result = item instanceof DateValue;
        } else if (typeName instanceof LocalTimeTypeName) {
            result = item instanceof LocalTimeValue;
        } else if (typeName instanceof ZonedTimeTypeName) {
            result = item instanceof TimeValue;
        } else if (typeName instanceof LocalDateTimeTypeName) {
            result = item instanceof LocalDateTimeValue;
        } else if (typeName instanceof ZonedDateTimeTypeName) {
            result = item instanceof DateTimeValue;
        } else if (typeName instanceof DurationTypeName) {
            result = item instanceof DurationValue;
        } else if (typeName instanceof PointTypeName) {
            result = item instanceof PointValue;
        } else {
            throw new IllegalArgumentException(String.format("Unexpected type: %s", typeName.typeName()));
        }

        return Values.booleanValue(result);
    }

    public static BooleanValue assertIsNode(AnyValue item) {
        assert item != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        if (item instanceof VirtualNodeValue) {
            return TRUE;
        } else {
            throw new CypherTypeException("Expected a Node, got: " + item);
        }
    }

    private static CypherTypeException needsNumbers(String method) {
        return new CypherTypeException(format("%s requires numbers", method));
    }

    private static CypherTypeException notAString(String method, AnyValue in) {
        assert in != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        return new CypherTypeException(format(
                "Expected a string value for `%s`, but got: %s; consider converting it to a string with "
                        + "toString().",
                method, in));
    }

    private static CypherTypeException notAModeString(String method, AnyValue mode) {
        assert mode != NO_VALUE : "NO_VALUE checks need to happen outside this call";
        return new CypherTypeException(format("Expected a string value for `%s`, but got: %s.", method, mode));
    }

    private static ListValue toIntegerList(FloatingPointArray array) {
        var converted = ListValueBuilder.newListBuilder(array.length());
        for (int i = 0; i < array.length(); i++) {
            converted.add(longValue((long) array.doubleValue(i)));
        }
        return converted.build();
    }

    private static ListValue toIntegerList(SequenceValue sequenceValue) {
        var converted = ListValueBuilder.newListBuilder();
        for (AnyValue value : sequenceValue) {
            converted.add(value != NO_VALUE ? toIntegerOrNull(value) : NO_VALUE);
        }
        return converted.build();
    }
}
