package org.jruby.ir.interpreter;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Supplier;

import org.jruby.RubySymbol;
import org.jruby.ir.IRClosure;
import org.jruby.ir.IRFlags;
import org.jruby.ir.IRMetaClassBody;
import org.jruby.ir.IRScope;
import org.jruby.ir.instructions.ExceptionRegionEndMarkerInstr;
import org.jruby.ir.instructions.ExceptionRegionStartMarkerInstr;
import org.jruby.ir.instructions.Instr;
import org.jruby.ir.instructions.LabelInstr;
import org.jruby.ir.representations.CFG;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.ThreadContext;

public class InterpreterContext {

    private final static Instr[] NO_INSTRUCTIONS = new Instr[0];

    private final static InterpreterEngine DEFAULT_INTERPRETER = new InterpreterEngine();
    private final static InterpreterEngine STARTUP_INTERPRETER = new StartupInterpreterEngine();

    protected int temporaryVariableCount;

    // startup interp will mark this at construction and not change but full interpreter will write it
    // much later after running compiler passes.  JIT will not use this field at all.
    protected volatile Instr[] instructions;

    // Contains pairs of values.  The first value is number of instrs in this range + number of instrs before
    // this range.  The second number is the rescuePC.  getRescuePC(ipc) will walk this list and first odd value
    // less than this value will be the rpc.
    protected volatile int[] rescueIPCs = null;

    // Cached computed fields
    protected boolean hasExplicitCallProtocol; // Only can be true in Full+
    protected boolean dynamicScopeEliminated; // Only can be true in Full+
    private boolean reuseParentDynScope; // Only can be true in Full+
    private final boolean metaClassBodyScope;
    private final boolean isEND;

    private InterpreterEngine engine;
    public final Supplier<List<Instr>> instructionsCallback;
    private final EnumSet<IRFlags> flags;

    private final IRScope scope;

    public InterpreterContext(IRScope scope, List<Instr> instructions, int temporaryVariableCount, EnumSet<IRFlags> flags) {
        this.scope = scope;

        // FIXME: Hack null instructions means coming from FullInterpreterContext but this should be way cleaner
        // For impl testing - engine = determineInterpreterEngine(scope);
        this.engine = instructions == null ? DEFAULT_INTERPRETER : STARTUP_INTERPRETER;

        this.metaClassBodyScope = scope instanceof IRMetaClassBody;
        setInstructions(instructions);
        this.instructionsCallback = null; // engine != null
        this.temporaryVariableCount = temporaryVariableCount;
        this.flags = flags;
        this.isEND = scope instanceof IRClosure && ((IRClosure) scope).isEND();
    }

    public InterpreterContext(IRScope scope, Supplier<List<Instr>> instructions, int temporaryVariableCount, EnumSet<IRFlags> flags) {
        this.scope = scope;

        this.metaClassBodyScope = scope instanceof IRMetaClassBody;
        this.instructionsCallback = instructions;
        this.temporaryVariableCount = temporaryVariableCount;
        this.flags = flags;
        this.isEND = scope instanceof IRClosure && ((IRClosure) scope).isEND();
    }

    protected void initialize() {
        if (instructions == null) getEngine();
    }

    public InterpreterEngine getEngine() {
        if (engine == null) {
            setInstructions(instructionsCallback.get());

            // FIXME: Hack null instructions means coming from FullInterpreterContext but this should be way cleaner
            // For impl testing - engine = determineInterpreterEngine(scope);
            this.engine = instructions == null ? DEFAULT_INTERPRETER : STARTUP_INTERPRETER;
        }
        return engine;
    }

    public Instr[] getInstructions() {
        initialize();

        return instructions == null ? NO_INSTRUCTIONS : instructions;
    }

    private synchronized void setInstructions(final List<Instr> instructions) {
        this.instructions = instructions != null ? prepareBuildInstructions(instructions) : null;
    }

    private Instr[] prepareBuildInstructions(List<Instr> instructions) {
        int length = instructions.size();
        Instr[] linearizedInstrArray = instructions.toArray(new Instr[length]);

        for (int ipc = 0; ipc < length; ipc++) {
            Instr i = linearizedInstrArray[ipc];

            if (i instanceof LabelInstr) ((LabelInstr) i).getLabel().setTargetPC(ipc + 1);
        }

        Deque<Integer> markers = new ArrayDeque<>(8);
        int[] rescueIPCs = new int[length];
        int rpc = -1;

        for (int ipc = 0; ipc < length; ipc++) {
            Instr i = linearizedInstrArray[ipc];

            if (i instanceof ExceptionRegionStartMarkerInstr) {
                rpc = ((ExceptionRegionStartMarkerInstr) i).getFirstRescueBlockLabel().getTargetPC();
                markers.push(rpc);
            } else if (i instanceof ExceptionRegionEndMarkerInstr) {
                markers.pop();
                rpc = markers.isEmpty() ? -1 : markers.peek().intValue();
            }

            rescueIPCs[ipc] = rpc;
        }

        this.rescueIPCs = rescueIPCs;

        return linearizedInstrArray;
    }

    public int[] getRescueIPCs() {
        return rescueIPCs;
    }

    public int getRequiredArgsCount() {
        return getStaticScope().getSignature().required();
    }

    public IRScope getScope() {
        return scope;
    }

    public CFG getCFG() {
        return null;
    }

    public int getTemporaryVariableCount() {
        return temporaryVariableCount;
    }

    public Object[] allocateTemporaryVariables() {
        return temporaryVariableCount > 0 ? new Object[temporaryVariableCount] : null;
    }

    public boolean[] allocateTemporaryBooleanVariables() {
        return null;
    }

    public long[] allocateTemporaryFixnumVariables() {
        return null;
    }

    public double[] allocateTemporaryFloatVariables() {
        return null;
    }

    public StaticScope getStaticScope() {
        return scope.getStaticScope();
    }

    public String getFileName() {
        return scope.getFile();
    }

    public int getLine() {
        return scope.getLine();
    }

    public RubySymbol getName() {
        return scope.getManager().getRuntime().newSymbol(scope.getId());
    }

    public void computeScopeFlagsFromInstructions() {
        for (Instr instr : getInstructions()) {
            instr.computeScopeFlags(scope, getFlags());
        }
    }

    /**
     * Get a new dynamic scope.  Note: This only works for method scopes (ClosureIC will throw).
     */
    public DynamicScope newDynamicScope(ThreadContext context) {
        // Add a parent-link to current dynscope to support non-local returns cheaply. This doesn't
        // affect variable scoping since local variables will all have the right scope depth.
        if (metaClassBodyScope) return DynamicScope.newDynamicScope(getStaticScope(), context.getCurrentScope());

        return DynamicScope.newDynamicScope(getStaticScope());
    }

    public boolean hasExplicitCallProtocol() {
        initialize();

        return hasExplicitCallProtocol;
    }

    public void setExplicitCallProtocol(boolean callProtocol) {
        this.hasExplicitCallProtocol = callProtocol;
    }

    public boolean isDynamicScopeEliminated() {
        return dynamicScopeEliminated;
    }

    public void setDynamicScopeEliminated(boolean dynamicScopeEliminated) {
        this.dynamicScopeEliminated = dynamicScopeEliminated;
    }

    public boolean isEND() {
        return isEND;
    }

    public boolean pushNewDynScope() {
        initialize();

        return !dynamicScopeEliminated && !reuseParentDynScope();
    }

    public boolean reuseParentDynScope() {
        initialize();

        return reuseParentDynScope || isEND;
    }

    public void setReuseParentDynScope(boolean reuseParentDynScope) {
        this.reuseParentDynScope = reuseParentDynScope;
    }

    public boolean popDynScope() {
        initialize();

        return pushNewDynScope() || this.reuseParentDynScope();
    }

    public boolean receivesKeywordArguments() {
        return scope.receivesKeywordArgs();
    }

    public boolean isRuby2Keywords() {
        return scope.isRuby2Keywords();
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();

        buf.append(getFileName()).append(':').append(scope.getLine());
        if (getName() != null) buf.append(' ').append(getName()).append("\n");

        if (getInstructions() == null) {
            buf.append("  No Instructions.  Full Build before linearizeInstr?");
        } else {
            buf.append(toStringInstrs()).append("\n");
        }

        return buf.toString();
    }

    public String toStringInstrs() {
        StringBuilder b = new StringBuilder();
        int length = instructions.length;

        var indent = 0;
        for (int i = 0; i < length; i++) {
            if (i > 0) b.append("\n");
            var instr = instructions[i];
            if (instr instanceof ExceptionRegionEndMarkerInstr) {
                indent -= 1;
            }
            b.append(String.format("%6d",i+1)).append('\t').append("  ".repeat(indent)).append(instr);
            if (instr instanceof ExceptionRegionStartMarkerInstr) {
                indent += 1;
            }
        }

        /* ENEBO: I this this is too much output espectially for ic and not fic
        Collection<IRClosure> nestedClosures = scope.getClosures();
        if (nestedClosures != null && !nestedClosures.isEmpty()) {
            b.append("\n\n------ Closures encountered in this scope ------\n");
            for (IRClosure c: nestedClosures)
                b.append(c.toStringBody());
            b.append("------------------------------------------------\n");
        }*/

        return b.toString();
    }

    public EnumSet<IRFlags> getFlags() {
        return flags;
    }
}
