// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.npc.util.expression;

import java.util.function.BooleanSupplier;
import java.util.function.DoubleSupplier;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.hypixel.hytale.common.util.ArrayUtil;
import java.util.Arrays;
import java.util.Objects;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;

public class ExecutionContext
{
    public static final int STACK_GROW_INCREMENT = 8;
    protected Scope scope;
    protected Operand[] operandStack;
    protected int stackTop;
    protected ValueType lastPushedType;
    protected String combatConfig;
    protected Map<String, String> interactionVars;
    public static final Instruction UNARY_PLUS;
    public static final Instruction UNARY_MINUS;
    public static final Instruction LOGICAL_NOT;
    public static final Instruction BITWISE_NOT;
    public static final Instruction EXPONENTIATION;
    public static final Instruction REMAINDER;
    public static final Instruction DIVIDE;
    public static final Instruction MULTIPLY;
    public static final Instruction MINUS;
    public static final Instruction PLUS;
    public static final Instruction GREATER_EQUAL;
    public static final Instruction GREATER;
    public static final Instruction LESS_EQUAL;
    public static final Instruction LESS;
    public static final Instruction NOT_EQUAL;
    public static final Instruction EQUAL;
    public static final Instruction NOT_EQUAL_BOOL;
    public static final Instruction EQUAL_BOOL;
    public static final Instruction BITWISE_AND;
    public static final Instruction BITWISE_XOR;
    public static final Instruction BITWISE_OR;
    public static final Instruction LOGICAL_AND;
    public static final Instruction LOGICAL_OR;
    
    public ExecutionContext(final Scope scope) {
        this.scope = scope;
        this.operandStack = new Operand[8];
        for (int i = 0; i < this.operandStack.length; ++i) {
            this.operandStack[i] = new Operand();
        }
    }
    
    public ExecutionContext() {
        this(null);
    }
    
    public ValueType execute(@Nonnull final List<Instruction> instructions, final Scope scope) {
        this.setScope(scope);
        return this.execute(instructions);
    }
    
    public ValueType execute(@Nonnull final List<Instruction> instructions) {
        Objects.requireNonNull(this.scope, "Scope not initialised executing instructions");
        Objects.requireNonNull(instructions, "Instruction sequence is null executing instructions");
        this.stackTop = -1;
        this.lastPushedType = ValueType.VOID;
        instructions.forEach(instruction -> instruction.execute(this));
        return this.getType();
    }
    
    public ValueType execute(@Nonnull final Instruction[] instructions, final Scope scope) {
        this.setScope(scope);
        return this.execute(instructions);
    }
    
    public ValueType execute(@Nonnull final Instruction[] instructions) {
        Objects.requireNonNull(this.scope, "Scope not initialised executing instructions");
        Objects.requireNonNull(instructions, "Instruction sequence is null executing instructions");
        try {
            this.stackTop = -1;
            this.lastPushedType = ValueType.VOID;
            for (final Instruction instruction : instructions) {
                instruction.execute(this);
            }
            return this.getType();
        }
        catch (final Throwable t) {
            throw new IllegalStateException("Failed to execute instruction sequence: ", t);
        }
    }
    
    public ValueType getType() {
        return this.lastPushedType;
    }
    
    public Operand top() {
        return this.get(0);
    }
    
    public Scope setScope(final Scope scope) {
        final Scope oldScope = this.getScope();
        this.scope = scope;
        return oldScope;
    }
    
    public Scope getScope() {
        return this.scope;
    }
    
    public String getCombatConfig() {
        return this.combatConfig;
    }
    
    public void setCombatConfig(final String combatConfig) {
        this.combatConfig = combatConfig;
    }
    
    public Map<String, String> getInteractionVars() {
        return this.interactionVars;
    }
    
    public void setInteractionVars(final Map<String, String> interactionVars) {
        this.interactionVars = interactionVars;
    }
    
    protected Operand push() {
        ++this.stackTop;
        if (this.operandStack.length <= this.stackTop) {
            int i = this.operandStack.length;
            this.operandStack = Arrays.copyOf(this.operandStack, i + 8);
            while (i < this.operandStack.length) {
                this.operandStack[i++] = new Operand();
            }
        }
        return this.operandStack[this.stackTop];
    }
    
    public void push(final String value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final double value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final int value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final boolean value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final String[] value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final double[] value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void push(final boolean[] value) {
        this.lastPushedType = this.push().set(value);
    }
    
    public void pushEmptyArray() {
        this.lastPushedType = this.push().setEmptyArray();
    }
    
    protected Operand popPush(final int popCount) {
        this.stackTop -= popCount - 1;
        return this.operandStack[this.stackTop];
    }
    
    public void popPush(final String value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final double value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final int value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final boolean value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final String[] value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final double[] value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPush(final boolean[] value, final int popCount) {
        this.lastPushedType = this.popPush(popCount).set(value);
    }
    
    public void popPushEmptyArray(final int popCount) {
        this.lastPushedType = this.popPush(popCount).setEmptyArray();
    }
    
    protected Operand pop() {
        this.lastPushedType = ValueType.VOID;
        return this.operandStack[this.stackTop--];
    }
    
    public double popNumber() {
        return this.pop().number;
    }
    
    public int popInt() {
        return (int)this.pop().number;
    }
    
    public String popString() {
        return this.pop().string;
    }
    
    public boolean popBoolean() {
        return this.pop().bool;
    }
    
    public double[] popNumberArray() {
        return (this.top().type != ValueType.EMPTY_ARRAY) ? this.pop().numberArray : ArrayUtil.EMPTY_DOUBLE_ARRAY;
    }
    
    @Nullable
    public String[] popStringArray() {
        return (this.top().type != ValueType.EMPTY_ARRAY) ? this.pop().stringArray : ArrayUtil.EMPTY_STRING_ARRAY;
    }
    
    public boolean[] popBooleanArray() {
        return (this.top().type != ValueType.EMPTY_ARRAY) ? this.pop().boolArray : ArrayUtil.EMPTY_BOOLEAN_ARRAY;
    }
    
    public String popAsString() {
        final Operand op = this.pop();
        return switch (op.type) {
            default -> throw new MatchException(null, null);
            case VOID -> "null";
            case STRING -> op.string;
            case NUMBER -> Double.toString(op.number);
            case BOOLEAN -> Boolean.toString(op.bool);
            case NUMBER_ARRAY -> Arrays.toString(op.numberArray);
            case STRING_ARRAY -> Arrays.toString(op.stringArray);
            case BOOLEAN_ARRAY -> Arrays.toString(op.boolArray);
            case EMPTY_ARRAY -> "[]";
        };
    }
    
    protected Operand get(final int index) {
        return this.operandStack[this.stackTop - index];
    }
    
    public double getNumber(final int index) {
        return this.get(index).number;
    }
    
    public int getInt(final int index) {
        return (int)this.get(index).number;
    }
    
    public String getString(final int index) {
        return this.get(index).string;
    }
    
    public boolean getBoolean(final int index) {
        return this.get(index).bool;
    }
    
    public double[] getNumberArray(final int index) {
        return this.get(index).numberArray;
    }
    
    @Nullable
    public String[] getStringArray(final int index) {
        return this.get(index).stringArray;
    }
    
    public boolean[] getBooleanArray(final int index) {
        return this.get(index).boolArray;
    }
    
    @Nonnull
    public static Instruction genPUSH(final String value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSH(final double value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSH(final boolean value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSH(final String[] value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSH(final double[] value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSH(final boolean[] value) {
        return context -> context.push(value);
    }
    
    @Nonnull
    public static Instruction genPUSHEmptyArray() {
        return ExecutionContext::pushEmptyArray;
    }
    
    @Nonnull
    public static Instruction genREAD(final String ident, @Nonnull final ValueType type, @Nullable final Scope scope) {
        if (scope == null) {
            return switch (type) {
                case STRING -> context -> context.push(context.scope.getString(ident));
                case NUMBER -> context -> context.push(context.scope.getNumber(ident));
                case BOOLEAN -> context -> context.push(context.scope.getBoolean(ident));
                case STRING_ARRAY -> context -> context.push(context.scope.getStringArray(ident));
                case NUMBER_ARRAY -> context -> context.push(context.scope.getNumberArray(ident));
                case BOOLEAN_ARRAY -> context -> context.push(context.scope.getBooleanArray(ident));
                default -> throw new RuntimeException("ExecutionContext: Invalid read type");
            };
        }
        return switch (type) {
            case STRING -> {
                final Supplier<String> supplier = scope.getStringSupplier(ident);
                yield context -> context.push(supplier.get());
            }
            case NUMBER -> {
                final DoubleSupplier supplier2 = scope.getNumberSupplier(ident);
                final Supplier<String> supplier;
                yield context -> context.push(supplier.getAsDouble());
            }
            case BOOLEAN -> {
                final BooleanSupplier supplier3 = scope.getBooleanSupplier(ident);
                final Supplier<String> supplier;
                yield context -> context.push(supplier.getAsBoolean());
            }
            case STRING_ARRAY -> {
                final Supplier<String[]> supplier4 = scope.getStringArraySupplier(ident);
                final Supplier<String> supplier;
                yield context -> context.push(supplier.get());
            }
            case NUMBER_ARRAY -> {
                final Supplier<double[]> supplier5 = scope.getNumberArraySupplier(ident);
                final Supplier<String> supplier;
                yield context -> context.push(supplier.get());
            }
            case BOOLEAN_ARRAY -> {
                final Supplier<boolean[]> supplier6 = scope.getBooleanArraySupplier(ident);
                final Supplier<String> supplier;
                yield context -> context.push(supplier.get());
            }
            default -> throw new RuntimeException("ExecutionContext: Invalid read type");
        };
    }
    
    @Nonnull
    public static Instruction genCALL(final String ident, final int numArgs, @Nullable final Scope scope) {
        if (scope == null) {
            return context -> context.scope.getFunction(ident).call(context, numArgs);
        }
        final Scope.Function function = scope.getFunction(ident);
        return context -> function.call(context, numArgs);
    }
    
    @Nonnull
    public static Instruction genNumberPACK(final int size) {
        return context -> {
            final double[] array = new double[size];
            for (int i = 0; i < size; ++i) {
                array[i] = context.getNumber(size - i);
            }
            context.popPush(array, size);
        };
    }
    
    @Nonnull
    public static Instruction genStringPACK(final int size) {
        return context -> {
            final String[] array = new String[size];
            for (int i = 0; i < size; ++i) {
                array[i] = context.getString(size - i);
            }
            context.popPush(array, size);
        };
    }
    
    @Nonnull
    public static Instruction genBooleanPACK(final int size) {
        return context -> {
            final boolean[] array = new boolean[size];
            for (int i = 0; i < size; ++i) {
                array[i] = context.getBoolean(size - i);
            }
            context.popPush(array, size);
        };
    }
    
    @Nonnull
    public static Instruction genPACK(@Nonnull final ValueType arrayType, final int size) {
        return switch (arrayType) {
            case NUMBER_ARRAY -> genNumberPACK(size);
            case STRING_ARRAY -> genStringPACK(size);
            case BOOLEAN_ARRAY -> genBooleanPACK(size);
            default -> throw new IllegalStateException("Cannot create PACK instruction for type " + String.valueOf(arrayType));
        };
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "ExecutionContext{scope=" + String.valueOf(this.scope) + ", operandStack=" + Arrays.toString(this.operandStack) + ", stackTop=" + this.stackTop + ", lastPushedType=" + String.valueOf(this.lastPushedType);
    }
    
    static {
        UNARY_PLUS = (context -> {});
        UNARY_MINUS = (context -> context.push(-context.popNumber()));
        LOGICAL_NOT = (context -> context.push(!context.popBoolean()));
        BITWISE_NOT = (context -> context.push(~context.popInt()));
        EXPONENTIATION = (context -> context.popPush(Math.pow(context.getNumber(1), context.getNumber(0)), 2));
        REMAINDER = (context -> context.popPush(context.getNumber(1) % context.getNumber(0), 2));
        DIVIDE = (context -> context.popPush(context.getNumber(1) / context.getNumber(0), 2));
        MULTIPLY = (context -> context.popPush(context.getNumber(1) * context.getNumber(0), 2));
        MINUS = (context -> context.popPush(context.getNumber(1) - context.getNumber(0), 2));
        PLUS = (context -> context.popPush(context.getNumber(1) + context.getNumber(0), 2));
        GREATER_EQUAL = (context -> context.popPush(context.getNumber(1) >= context.getNumber(0), 2));
        GREATER = (context -> context.popPush(context.getNumber(1) > context.getNumber(0), 2));
        LESS_EQUAL = (context -> context.popPush(context.getNumber(1) <= context.getNumber(0), 2));
        LESS = (context -> context.popPush(context.getNumber(1) < context.getNumber(0), 2));
        NOT_EQUAL = (context -> context.popPush(context.getNumber(1) != context.getNumber(0), 2));
        EQUAL = (context -> context.popPush(context.getNumber(1) == context.getNumber(0), 2));
        NOT_EQUAL_BOOL = (context -> context.popPush(context.getBoolean(1) != context.getBoolean(0), 2));
        EQUAL_BOOL = (context -> context.popPush(context.getBoolean(1) == context.getBoolean(0), 2));
        BITWISE_AND = (context -> context.popPush(context.getInt(1) & context.getInt(0), 2));
        BITWISE_XOR = (context -> context.popPush(context.getInt(1) ^ context.getInt(0), 2));
        BITWISE_OR = (context -> context.popPush(context.getInt(1) | context.getInt(0), 2));
        LOGICAL_AND = (context -> context.popPush(context.getBoolean(1) && context.getBoolean(0), 2));
        LOGICAL_OR = (context -> context.popPush(context.getBoolean(1) || context.getBoolean(0), 2));
    }
    
    public static class Operand
    {
        public ValueType type;
        public String string;
        public double number;
        public boolean bool;
        @Nullable
        public double[] numberArray;
        @Nullable
        public String[] stringArray;
        @Nullable
        public boolean[] boolArray;
        
        public ValueType set(final String value) {
            this.reInit(ValueType.STRING);
            this.string = value;
            return this.type;
        }
        
        public ValueType set(final double value) {
            this.reInit(ValueType.NUMBER);
            this.number = value;
            return this.type;
        }
        
        public ValueType set(final boolean value) {
            this.reInit(ValueType.BOOLEAN);
            this.bool = value;
            return this.type;
        }
        
        public ValueType set(final String[] value) {
            this.reInit(ValueType.STRING_ARRAY);
            this.stringArray = value;
            return this.type;
        }
        
        public ValueType set(final double[] value) {
            this.reInit(ValueType.NUMBER_ARRAY);
            this.numberArray = value;
            return this.type;
        }
        
        public ValueType set(final boolean[] value) {
            this.reInit(ValueType.BOOLEAN_ARRAY);
            this.boolArray = value;
            return this.type;
        }
        
        public ValueType setEmptyArray() {
            this.reInit(ValueType.EMPTY_ARRAY);
            return this.type;
        }
        
        private void reInit(final ValueType type) {
            this.type = type;
            this.numberArray = null;
            this.stringArray = null;
            this.boolArray = null;
        }
        
        @Nonnull
        @Override
        public String toString() {
            return "Operand{type=" + String.valueOf(this.type) + ", string='" + this.string + "', number=" + this.number + ", bool=" + this.bool + ", numberArray=" + Arrays.toString(this.numberArray) + ", stringArray=" + Arrays.toString(this.stringArray) + ", boolArray=" + Arrays.toString(this.boolArray);
        }
    }
    
    @FunctionalInterface
    public interface Instruction
    {
        void execute(final ExecutionContext p0);
    }
}
