package org.cyclops.integrateddynamics.core.evaluate.operator;

import com.google.common.collect.*;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import lombok.Lombok;
import net.minecraft.ResourceLocationException;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.component.TypedDataComponent;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.*;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.StringUtil;
import net.minecraft.world.entity.AgeableMob;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.projectile.ProjectileUtil;
import net.minecraft.world.entity.vehicle.AbstractMinecart;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.phys.*;
import net.neoforged.neoforge.capabilities.Capabilities;
import net.neoforged.neoforge.common.IShearable;
import net.neoforged.neoforge.common.SoundActions;
import net.neoforged.neoforge.energy.IEnergyStorage;
import net.neoforged.neoforge.event.EventHooks;
import net.neoforged.neoforge.fluids.FluidStack;
import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.server.ServerLifecycleHooks;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch;
import org.cyclops.commoncapabilities.api.capability.recipehandler.IPrototypedIngredientAlternatives;
import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition;
import org.cyclops.commoncapabilities.api.capability.recipehandler.PrototypedIngredientAlternativesList;
import org.cyclops.commoncapabilities.api.capability.recipehandler.RecipeDefinition;
import org.cyclops.commoncapabilities.api.ingredient.*;
import org.cyclops.cyclopscore.helper.BlockHelpers;
import org.cyclops.cyclopscore.helper.FluidHelpers;
import org.cyclops.cyclopscore.helper.MinecraftHelpers;
import org.cyclops.cyclopscore.nbt.path.INbtPathExpression;
import org.cyclops.cyclopscore.nbt.path.NbtParseException;
import org.cyclops.cyclopscore.nbt.path.NbtPath;
import org.cyclops.integrateddynamics.IntegratedDynamics;
import org.cyclops.integrateddynamics.api.evaluate.EvaluationException;
import org.cyclops.integrateddynamics.api.evaluate.operator.IOperator;
import org.cyclops.integrateddynamics.api.evaluate.operator.IOperatorRegistry;
import org.cyclops.integrateddynamics.api.evaluate.variable.*;
import org.cyclops.integrateddynamics.api.logicprogrammer.IConfigRenderPattern;
import org.cyclops.integrateddynamics.core.evaluate.IOperatorValuePropagator;
import org.cyclops.integrateddynamics.core.evaluate.OperatorBuilders;
import org.cyclops.integrateddynamics.core.evaluate.variable.*;
import org.cyclops.integrateddynamics.core.helper.Helpers;
import org.cyclops.integrateddynamics.core.helper.L10NValues;
import org.cyclops.integrateddynamics.core.helper.NbtHelpers;
import org.cyclops.integrateddynamics.core.ingredient.ExtendedIngredientsList;
import org.cyclops.integrateddynamics.core.ingredient.ExtendedIngredientsSingle;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Collection of available operators.
 *
 * @author rubensworks
 */
public final class Operators {

    public static final IOperatorRegistry REGISTRY = constructRegistry();

    private static IOperatorRegistry constructRegistry() {
        // This also allows this registry to be used outside of a minecraft environment.
        if(MinecraftHelpers.isModdedEnvironment()) {
            return IntegratedDynamics._instance.getRegistryManager().getRegistry(IOperatorRegistry.class);
        } else {
            return OperatorRegistry.getInstance();
        }
    }

    public static void load() {}

    /**
     * ----------------------------------- LOGICAL OPERATORS -----------------------------------
     */

    /**
     * Short-circuit logical AND operator with two input booleans and one output boolean.
     */
    public static final IOperator LOGICAL_AND = REGISTRY.register(OperatorBuilders.LOGICAL_2.symbol("&&").operatorInteract("and")
            .function(variables -> {
                ValueTypeBoolean.ValueBoolean a = variables.getValue(0, ValueTypes.BOOLEAN);
                if (!a.getRawValue()) {
                    return ValueTypeBoolean.ValueBoolean.of(false);
                } else {
                    return variables.getValue(1, ValueTypes.BOOLEAN);
                }
            }).build());

    /**
     * Short-circuit logical AND operator with two input booleans and one output boolean.
     */
    public static final IOperator LOGICAL_OR = REGISTRY.register(OperatorBuilders.LOGICAL_2.symbol("||").operatorInteract("or")
            .function(variables -> {
                ValueTypeBoolean.ValueBoolean a = variables.getValue(0, ValueTypes.BOOLEAN);
                if (a.getRawValue()) {
                    return ValueTypeBoolean.ValueBoolean.of(true);
                } else {
                    return variables.getValue(1, ValueTypes.BOOLEAN);
                }
            }).build());

    /**
     * Logical NOT operator with one input booleans and one output boolean.
     */
    public static final IOperator LOGICAL_NOT = REGISTRY.register(OperatorBuilders.LOGICAL_1_PREFIX.symbol("!").operatorInteract("not")
            .function(variables -> {
                        ValueTypeBoolean.ValueBoolean valueBoolean = variables.getValue(0, ValueTypes.BOOLEAN);
                        return ValueTypeBoolean.ValueBoolean.of(!valueBoolean.getRawValue());
                    }
            ).build());

    /**
     * Short-circuit logical NAND operator with two input booleans and one output boolean.
     */
    public static final IOperator LOGICAL_NAND = REGISTRY.register(
            new CompositionalOperator.AppliedOperatorBuilder(LOGICAL_NOT).apply(LOGICAL_AND).build(
                    "!&&", "nand", "nand", IConfigRenderPattern.INFIX, "logical"));

    /**
     * Short-circuit logical NAND operator with two input booleans and one output boolean.
     */
    public static final IOperator LOGICAL_NOR = REGISTRY.register(
            new CompositionalOperator.AppliedOperatorBuilder(LOGICAL_NOT).apply(LOGICAL_OR).build(
                    "!||", "nor", "nor", IConfigRenderPattern.INFIX, "logical"));

    /**
     * ----------------------------------- ARITHMETIC OPERATORS -----------------------------------
     */

    /**
     * Arithmetic ADD operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_ADDITION = REGISTRY.register(OperatorBuilders.ARITHMETIC_2.symbol("+").operatorName("addition").interactName("add")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.add(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic MINUS operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_SUBTRACTION = REGISTRY.register(OperatorBuilders.ARITHMETIC_2.symbol("-").operatorName("subtraction").interactName("subtract")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.subtract(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic MULTIPLY operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_MULTIPLICATION = REGISTRY.register(OperatorBuilders.ARITHMETIC_2.symbol("*").operatorName("multiplication").interactName("multiply")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.multiply(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic DIVIDE operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_DIVISION = REGISTRY.register(OperatorBuilders.ARITHMETIC_2.symbol("/").operatorName("division").interactName("divide")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.divide(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic MAX operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_MAXIMUM = REGISTRY.register(OperatorBuilders.ARITHMETIC_2_PREFIX.symbol("max").operatorName("maximum").interactName("max")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.max(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic MIN operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_MINIMUM = REGISTRY.register(OperatorBuilders.ARITHMETIC_2_PREFIX.symbol("min").operatorName("minimum").interactName("min")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.min(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * Arithmetic INCREMENT operator with one input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_INCREMENT = REGISTRY.register(OperatorBuilders.ARITHMETIC_1_SUFFIX.symbol("++").operatorInteract("increment")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.increment(variables.getVariables()[0])
            ).build());

    /**
     * Arithmetic DECREMENT operator with one input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_DECREMENT = REGISTRY.register(OperatorBuilders.ARITHMETIC_1_SUFFIX.symbol("--").operatorInteract("decrement")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.decrement(variables.getVariables()[0])
            ).build());

    /**
     * Arithmetic MODULO operator with two input numbers and one output number.
     */
    public static final IOperator ARITHMETIC_MODULUS = REGISTRY.register(OperatorBuilders.ARITHMETIC_2.symbol("%").operatorInteract("modulus")
            .function(
                variables -> ValueTypes.CATEGORY_NUMBER.modulus(variables.getVariables()[0], variables.getVariables()[1])
            ).build());

    /**
     * ----------------------------------- INTEGER OPERATORS -----------------------------------
     */

     private static final ValueTypeInteger.ValueInteger ZERO = ValueTypeInteger.ValueInteger.of(0);

    /**
     * ----------------------------------- DOUBLE OPERATORS -----------------------------------
     */

    /**
     * Integer SQRT operator with two input numbers and one output number.
     */
    public static final IOperator DOUBLE_SQRT = REGISTRY.register(OperatorBuilders.DOUBLE_1_PREFIX.symbolOperatorInteract("sqrt")
            .function(variables -> {
                ValueTypeDouble.ValueDouble a = variables.getValue(0, ValueTypes.DOUBLE);
                return ValueTypeDouble.ValueDouble.of(Math.sqrt(a.getRawValue()));
            }).build());

    /**
     * Integer POW operator with two input numbers and one output number.
     */
    public static final IOperator DOUBLE_POW = REGISTRY.register(OperatorBuilders.DOUBLE_2.symbolOperatorInteract("pow")
            .function(variables -> {
                ValueTypeDouble.ValueDouble a = variables.getValue(0, ValueTypes.DOUBLE);
                ValueTypeDouble.ValueDouble b = variables.getValue(1, ValueTypes.DOUBLE);
                return ValueTypeDouble.ValueDouble.of(Math.pow(a.getRawValue(), b.getRawValue()));
            }).build());

    /**
     * ----------------------------------- RELATIONAL OPERATORS -----------------------------------
     */

    /**
     * Relational == operator with two inputs of any type (but equal) and one output boolean.
     */
    public static final IOperator RELATIONAL_EQUALS = REGISTRY.register(OperatorBuilders.RELATIONAL
            .inputTypes(2, ValueTypes.CATEGORY_ANY).renderPattern(IConfigRenderPattern.INFIX)
            .symbol("==").operatorInteract("equals")
            .function(
                variables -> ValueTypeBoolean.ValueBoolean.of(variables.getValue(0).equals(variables.getValue(1)))
            )
            .typeValidator((operator, input) -> {
                // Input size checking
                int requiredInputLength = operator.getRequiredInputLength();
                if(input.length != requiredInputLength) {
                    return Component.translatable(L10NValues.OPERATOR_ERROR_WRONGINPUTLENGTH,
                            operator.getOperatorName(), input.length, requiredInputLength);
                }
                // Input types checking
                IValueType temporarySecondInputType = null;
                for(int i = 0; i < requiredInputLength; i++) {
                    IValueType inputType = input[i];
                    if (inputType instanceof IValueTypeNumber) {
                        inputType = ValueTypes.CATEGORY_NUMBER;
                    }
                    if(inputType == null) {
                        return Component.translatable(L10NValues.OPERATOR_ERROR_NULLTYPE, operator.getOperatorName(), Integer.toString(i));
                    }
                    if(i == 0) {
                        temporarySecondInputType = inputType;
                    } else if(i == 1) {
                        if(!ValueHelpers.correspondsTo(temporarySecondInputType, inputType)) {
                            return Component.translatable(L10NValues.OPERATOR_ERROR_WRONGTYPE,
                                    operator.getOperatorName(), Component.translatable(inputType.getTranslationKey()),
                                    Integer.toString(i), Component.translatable(temporarySecondInputType.getTranslationKey()));
                        }
                    }
                }
                return null;
            })
            .build());

    /**
     * Relational &gt; operator with two input integers and one output boolean.
     */
    public static final IOperator RELATIONAL_GT = REGISTRY.register(OperatorBuilders.RELATIONAL_2
            .inputTypes(2, ValueTypes.CATEGORY_NUMBER).symbol(">").operatorName("gt").interactName("greaterThan")
            .function(
                variables -> ValueTypeBoolean.ValueBoolean.of(ValueTypes.CATEGORY_NUMBER.greaterThan(variables.getVariables()[0], variables.getVariables()[1]))
            ).build());

    /**
     * Relational &gt; operator with two input integers and one output boolean.
     */
    public static final IOperator RELATIONAL_LT = REGISTRY.register(OperatorBuilders.RELATIONAL_2
            .inputTypes(2, ValueTypes.CATEGORY_NUMBER).symbol("<").operatorName("lt").interactName("lessThan")
            .function(
                variables -> ValueTypeBoolean.ValueBoolean.of(ValueTypes.CATEGORY_NUMBER.lessThan(variables.getVariables()[0], variables.getVariables()[1]))
            ).build());

    /**
     * Relational != operator with two inputs of any type (but equal) and one output boolean.
     */
    public static final IOperator RELATIONAL_NOTEQUALS = REGISTRY.register(
            new CompositionalOperator.AppliedOperatorBuilder(LOGICAL_NOT).apply(RELATIONAL_EQUALS).build(
                    "!=", "notequals", "notEquals", IConfigRenderPattern.INFIX, "relational"));

    /**
     * Relational &gt;= operator with two inputs of any type (but equal) and one output boolean.
     */
    public static final IOperator RELATIONAL_GE = REGISTRY.register(
            new CompositionalOperator.AppliedOperatorBuilder(LOGICAL_OR).apply(RELATIONAL_EQUALS, RELATIONAL_GT).build(
                    ">=", "ge", "greaterThanOrEquals", IConfigRenderPattern.INFIX, "relational"));

    /**
     * Relational &lt;= operator with two inputs of any type (but equal) and one output boolean.
     */
    public static final IOperator RELATIONAL_LE = REGISTRY.register(
            new CompositionalOperator.AppliedOperatorBuilder(LOGICAL_OR).apply(RELATIONAL_EQUALS, RELATIONAL_LT).build(
                    "<=", "le", "lessThanOrEquals", IConfigRenderPattern.INFIX, "relational"));

    /**
     * ----------------------------------- BINARY OPERATORS -----------------------------------
     */

    /**
     * Binary AND operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_AND = REGISTRY.register(OperatorBuilders.BINARY_2.symbol("&").operatorName("and").interactName("binaryAnd")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() & b.getRawValue());
            }).build());

    /**
     * Binary OR operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_OR = REGISTRY.register(OperatorBuilders.BINARY_2.symbol("|").operatorName("or").interactName("binaryOr")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() | b.getRawValue());
            }).build());

    /**
     * Binary XOR operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_XOR = REGISTRY.register(OperatorBuilders.BINARY_2.symbol("^").operatorInteract("xor")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() ^ b.getRawValue());
            }).build());

    /**
     * Binary COMPLEMENT operator with one input integers and one output integers.
     */
    public static final IOperator BINARY_COMPLEMENT = REGISTRY.register(OperatorBuilders.BINARY_1_PREFIX.symbol("~").operatorInteract("complement")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(~a.getRawValue());
            }).build());

    /**
     * Binary &lt;&lt; operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_LSHIFT = REGISTRY.register(OperatorBuilders.BINARY_2.symbol("<<").operatorName("lshift").interactName("leftShift")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() << b.getRawValue());
            }).build());

    /**
     * Binary &gt;&gt; operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_RSHIFT = REGISTRY.register(OperatorBuilders.BINARY_2.symbol(">>").operatorName("rshift").interactName("rightShift")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() >> b.getRawValue());
            }).build());

    /**
     * Binary &gt;&gt;&gt; operator with two input integers and one output integers.
     */
    public static final IOperator BINARY_RZSHIFT = REGISTRY.register(OperatorBuilders.BINARY_2.symbol(">>>").operatorName("rzshift").interactName("unsignedRightShift")
            .function(variables -> {
                ValueTypeInteger.ValueInteger a = variables.getValue(0, ValueTypes.INTEGER);
                ValueTypeInteger.ValueInteger b = variables.getValue(1, ValueTypes.INTEGER);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue() >>> b.getRawValue());
            }).build());

    /**
     * ----------------------------------- STRING OPERATORS -----------------------------------
     */

    /**
     * String length operator with one input string and one output integer.
     */
    public static final IOperator STRING_LENGTH = REGISTRY.register(OperatorBuilders.STRING_1_PREFIX.symbol("len").operatorInteract("length")
            .output(ValueTypes.INTEGER).function(variables -> {
                ValueTypeString.ValueString a = variables.getValue(0, ValueTypes.STRING);
                return ValueTypeInteger.ValueInteger.of(a.getRawValue().length());
            }).build());

    /**
     * String concat operator with two input strings and one output string.
     */
    public static final IOperator STRING_CONCAT = REGISTRY.register(OperatorBuilders.STRING_2.symbol("+").operatorInteract("concat")
            .function(variables -> {
                ValueTypeString.ValueString a = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString b = variables.getValue(1, ValueTypes.STRING);
                return ValueTypeString.ValueString.of(a.getRawValue() + b.getRawValue());
            }).build());

    /**
     * String contains operator which checks whether a given (literal) string is contained in the given string.
     */
    public static final IOperator STRING_CONTAINS = REGISTRY.register(OperatorBuilders.STRING_2.symbolOperatorInteract("contains")
        .output(ValueTypes.BOOLEAN).function(variables -> {
                ValueTypeString.ValueString search = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                return ValueTypeBoolean.ValueBoolean.of(str.getRawValue().contains(search.getRawValue()));
            }).build());

    /**
     * String match operator which checks whether a given regular expression is contained within a string.
     */
    public static final IOperator STRING_CONTAINS_REGEX = REGISTRY.register(OperatorBuilders.STRING_2_LONG.symbolOperator("contains_regex").interactName("containsRegex")
        .output(ValueTypes.BOOLEAN).function(variables -> {
                ValueTypeString.ValueString pattern = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                try {
                    Matcher m = Pattern.compile(pattern.getRawValue()).matcher(str.getRawValue());
                    return ValueTypeBoolean.ValueBoolean.of(m.find());
                } catch (PatternSyntaxException e) {
                    throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_REGEX_INVALID,
                            pattern.getRawValue()));
                }
            }).build());

    /**
     * String match operator which checks whether a given regular expression matches a string.
     */
    public static final IOperator STRING_MATCHES_REGEX = REGISTRY.register(OperatorBuilders.STRING_2_LONG.symbolOperator("matches_regex").interactName("matchesRegex")
            .output(ValueTypes.BOOLEAN).function(variables -> {
                ValueTypeString.ValueString pattern = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                try {
                    Matcher m = Pattern.compile(pattern.getRawValue()).matcher(str.getRawValue());
                    return ValueTypeBoolean.ValueBoolean.of(m.matches());
                } catch (PatternSyntaxException e) {
                    throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_REGEX_INVALID,
                            pattern.getRawValue()));
                }
            }).build());

    /**
     * String operator which returns the integral index of the first position where the search string appears in the given string.
     */
    public static final IOperator STRING_INDEX_OF = REGISTRY.register(OperatorBuilders.STRING_2_LONG.symbolOperator("index_of").interactName("indexOf")
        .output(ValueTypes.INTEGER).function(variables -> {
                ValueTypeString.ValueString search = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                return ValueTypeInteger.ValueInteger.of(str.getRawValue().indexOf(search.getRawValue()));
            }).build());

    /**
     * String operator which returns the integral index where the a substring matching the regular expression appears in the given string.
     */
    public static final IOperator STRING_INDEX_OF_REGEX = REGISTRY.register(OperatorBuilders.STRING_2_LONG.symbolOperator("index_of_regex").interactName("indexOfRegex")
        .output(ValueTypes.INTEGER).function(variables -> {
                ValueTypeString.ValueString pattern = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                try {
                    Matcher m = Pattern.compile(pattern.getRawValue()).matcher(str.getRawValue());
                    if (m.find()) {
                        return ValueTypeInteger.ValueInteger.of(m.start());
                    } else {
                        return ValueTypeInteger.ValueInteger.of(-1);
                    }
                } catch (PatternSyntaxException e) {
                    throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_REGEX_INVALID,
                            pattern.getRawValue()));
                }
            }).build());

    /**
     * String match operator which checks whether a given string matches the beginning of the given string.
     */
    public static final IOperator STRING_STARTS_WITH = REGISTRY.register(OperatorBuilders.STRING_2.symbolOperator("starts_with").interactName("startsWith")
        .output(ValueTypes.BOOLEAN).function(variables -> {
                ValueTypeString.ValueString search = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                return ValueTypeBoolean.ValueBoolean.of(str.getRawValue().startsWith(search.getRawValue()));
            }).build());

    /**
     * String match operator which checks whether a given string matches the end of the given string.
     */
    public static final IOperator STRING_ENDS_WITH = REGISTRY.register(OperatorBuilders.STRING_2.symbolOperator("ends_with").interactName("endsWith")
        .output(ValueTypes.BOOLEAN).function(variables -> {
                ValueTypeString.ValueString search = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                return ValueTypeBoolean.ValueBoolean.of(str.getRawValue().endsWith(search.getRawValue()));
            }).build());

    /**
     * String operator which splits on the given (literal) delimiter the input string .
     */
    public static final IOperator STRING_SPLIT_ON = REGISTRY.register(OperatorBuilders.STRING_2.symbolOperator("split_on").interactName("splitOn")
        .output(ValueTypes.LIST).function(variables -> {
                ValueTypeString.ValueString search = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                List<String> pieces = Arrays.asList(str.getRawValue().split(java.util.regex.Pattern.quote(search.getRawValue())));
                List<ValueTypeString.ValueString> values = Lists.newArrayList();
                for (String piece : pieces) {
                    values.add(ValueTypeString.ValueString.of(piece));
                }
                return ValueTypeList.ValueList.ofList(ValueTypes.STRING, values);
            }).build());

    /**
     * String operator which splits on the given (regular expression) delimiter the input string.
     */
    public static final IOperator STRING_SPLIT_ON_REGEX = REGISTRY.register(OperatorBuilders.STRING_2_LONG.symbolOperator("split_on_regex").interactName("splitOnRegex")
        .output(ValueTypes.LIST).function(variables -> {
                ValueTypeString.ValueString pattern = variables.getValue(0, ValueTypes.STRING);
                ValueTypeString.ValueString str = variables.getValue(1, ValueTypes.STRING);
                try {
                    List<String> pieces = Arrays.asList(Pattern.compile(pattern.getRawValue()).split(str.getRawValue()));
                    List<ValueTypeString.ValueString> values = Lists.newArrayList();
                    for (String piece : pieces) {
                        values.add(ValueTypeString.ValueString.of(piece));
                    }
                    return ValueTypeList.ValueList.ofList(ValueTypes.STRING, values);
                } catch (PatternSyntaxException e) {
                    throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_REGEX_INVALID,
                            pattern.getRawValue()));
                }
            }).build());

    /**
     * String operator which takes the substring of the given string between the two integer indices.
     */
    public static final IOperator STRING_SUBSTRING = REGISTRY.register(OperatorBuilders.STRING.symbolOperatorInteract("substring")
        .renderPattern(IConfigRenderPattern.PREFIX_3_LONG)
        .inputTypes(ValueTypes.INTEGER, ValueTypes.INTEGER, ValueTypes.STRING)
        .output(ValueTypes.STRING)
        .function(variables -> {
            ValueTypeInteger.ValueInteger from = variables.getValue(0, ValueTypes.INTEGER);
            ValueTypeInteger.ValueInteger to = variables.getValue(1, ValueTypes.INTEGER);
            ValueTypeString.ValueString str = variables.getValue(2, ValueTypes.STRING);
            if (from.getRawValue() > to.getRawValue()) {
                throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_SUBSTRING_TOGREATERTHANFROM));
            }
            if (from.getRawValue() < 0 || to.getRawValue() < 0) {
                throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_SUBSTRING_INDEXNEGATIVE));
            }
            int stringLength = str.getRawValue().length();
            if (from.getRawValue() > stringLength || to.getRawValue() > stringLength) {
                throw new EvaluationException(Component.translatable(L10NValues.OPERATOR_ERROR_SUBSTRING_LONGERTHANSTRING));
            }
            return ValueTypeString.ValueString.of(str.getRawValue().substring(from.getRawValue(), to.getRawValue()));
        }).build());


    /**
     * String operator which matches against a regex and takes the group at the index of the integer given (including zero), in the input string. It is invalid for the pattern to not match.
     */
    public static final IOperator STRING_REGEX_GROUP = REGISTRY.register(OperatorBuilders.STRING.symbolOperator("regex_group").interactName("regexGroup")
        .renderPattern(IConfigRenderPattern.PREFIX_3_LONG)
