package org.jruby.runtime;

import org.jruby.parser.BlockStaticScope;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.builtin.IRubyObject;

/**
 * Represents the the dynamic portion of scoping information.  The variableValues are the
 * values of assigned local or block variables.  The staticScope identifies which sort of
 * scope this is (block or local).
 * 
 * Properties of Dynamic Scopes:
 * 1. static and dynamic scopes have the same number of names to values
 * 2. size of variables (and thus names) is determined during parsing.  So those structured do
 *    not need to change
 *
 * FIXME: When creating dynamic scopes we sometimes accidentally pass in extra parents.  This
 * is harmless (other than wasting memory), but we should not do that.  We can fix this in two
 * ways:
 * 1. Fix all callers
 * 2. Check parent that is passed in and make if new instance is local, then its parent is not local
 */
public class DynamicScope {
    // Our values holder (name of variables are kept in staticScope)
    private IRubyObject[] variableValues;
    
    // Static scoping information for this scope
    private StaticScope staticScope;
    
    // Captured dyanmic scopes
    private DynamicScope parent;
    
    // A place to store that special hiding space that bindings need to implement things like:
    // eval("a = 1", binding); eval("p a").  All binding instances must get access to this
    // hidden shared scope.  We store it here.  This will be null if no binding has yet
    // been called.
    private DynamicScope bindingScope;

    public DynamicScope(StaticScope staticScope, DynamicScope parent) {
        this(staticScope);
        this.parent = parent;
    }

    public DynamicScope(StaticScope staticScope) {
        this.staticScope = staticScope;
        
    }

    private void lazy() {
        if(variableValues == null) {
            int size = staticScope.getNumberOfVariables();
            variableValues = new IRubyObject[size];
        }
    }
    
    public DynamicScope cloneScope() {
        return new DynamicScope(staticScope, parent);
    }
    
    /**
     * Get all variable names captured (visible) by this scope (sans $~ and $_).
     * 
     * @return a list of variable names
     */
    public String[] getAllNamesInScope() {
        return staticScope.getAllNamesInScope(this);
    }

    public IRubyObject[] getValues() {
        lazy();
        return variableValues;
    }
    
    /**
     * Get value from current scope or one of its captured scopes.
     * 
     * FIXME: block variables are not getting primed to nil so we need to null check those
     *  until we prime them properly.  Also add assert back in.
     * 
     * @param offset zero-indexed value that represents where variable lives
     * @param depth how many captured scopes down this variable should be set
     * @return the value here
     */
    public IRubyObject getValue(int offset, int depth) {
        if (depth > 0) {
            return parent.getValue(offset, depth - 1);
        }
        lazy();
        assert variableValues != null : "No variables in getValue for Off: " + offset + ", Dep: " + depth;
        assert offset < variableValues.length : "Index to big for getValue Off: " + offset + ", Dep: " + depth + ", O: " + this;
        // &foo are not getting set from somewhere...I want the following assert to be true though
        //assert variableValues[offset] != null : "Getting unassigned: " + staticScope.getVariables()[offset];
        return variableValues[offset];
    }

    /**
     * Set value in current dynamic scope or one of its captured scopes.
     * 
     * @param offset zero-indexed value that represents where variable lives
     * @param value to set
     * @param depth how many captured scopes down this variable should be set
     */
    public void setValue(int offset, IRubyObject value, int depth) {
        if (depth > 0) {
            assert parent != null : "If depth > 0, then parent should not ever be null";
            
            parent.setValue(offset, value, depth - 1);
        } else {
            lazy();

            assert offset < variableValues.length : "Setting " + offset + " to " + value + ", O: " + this; 

            variableValues[offset] = value;
        }
    }

    /**
     * Set all values which represent 'normal' parameters in a call list to this dynamic
     * scope.  Function calls bind to local scopes by assuming that the indexes or the
     * arg list correspond to that of the local scope (plus 2 since $_ and $~ always take
     * the first two slots).  We pass in a second argument because we sometimes get more
     * values than we are expecting.  The rest get compacted by original caller into 
     * rest args.
     * 
     * @param values up to size specified to be mapped as ordinary parm values
     * @param size is the number of values to assign as ordinary parm values
     */
    public void setArgValues(IRubyObject[] values, int size) {
        lazy();
        System.arraycopy(values, 0, variableValues, 2, size);
    }
    
    public void setBlockArgValues(IRubyObject[] blockArgValues, int size) {
        lazy();
        System.arraycopy(blockArgValues, 0, variableValues, 0, size);
    }

    /**
     * Copy variable values back for ZSuper call.
     */
    public void getArgValues(IRubyObject[] args, int size) {
        lazy();
        if(variableValues != null && args != null && variableValues.length>=(size+2)) {
            System.arraycopy(variableValues, 2, args, 0, size);
        }
    }

    /**
     * 
     * Make a larger dynamic scope if the static scope grew.
     * 
     * Eval's with bindings require us to possibly change the size of the dynamic scope if
     * things like 'eval "b = 2", binding' happens.
     *
     */
    public void growIfNeeded() {
        lazy();
        int dynamicSize = variableValues == null ? 0: variableValues.length;
        
        if (staticScope.getNumberOfVariables() > dynamicSize) {
            IRubyObject values[] = new IRubyObject[staticScope.getNumberOfVariables()];
            
            if (dynamicSize > 0) {
                System.arraycopy(variableValues, 0, values, 0, dynamicSize);
            }
            
            variableValues = values;
        }
    }

    // FIXME: Depending on profiling we may want to cache information on location and depth of
    // both $_ and/or $~ since in some situations they may happen a lot.  isDefined should be
    // fairly cheap, but you never know...
    
    public void setLastLine(IRubyObject value) {
        lazy();
        int location = staticScope.isDefined("$_");
        
        setValue(location & 0xffff, value, location >> 16);
    }
    
    public IRubyObject getLastLine() {
        lazy();
        int location = staticScope.isDefined("$_");

        return getValue(location & 0xffff, location >> 16);
    }

    public void setBackRef(IRubyObject value) {
        lazy();
        int location = staticScope.isDefined("$~");
        
        setValue(location & 0xffff, value, location >> 16);
    }
    
    public IRubyObject getBackRef() {
        lazy();
        int location = staticScope.isDefined("$~");
        
        return getValue(location & 0xffff, location >> 16); 
    }
    
    public DynamicScope getBindingScope() {
        return bindingScope;
    }
    
    public void setBindingScope(DynamicScope bindingScope) {
        this.bindingScope = bindingScope;
    }
    
    /**
     * Get next 'captured' scope.
     * 
     * @return the scope captured by this scope for implementing closures
     *
     */
    public DynamicScope getNextCapturedScope() {
        return parent;
    }

    /**
     * Get the static scope associated with this DynamicScope.
     * 
     * @return static complement to this scope
     */
    public StaticScope getStaticScope() {
        return staticScope;
    }
    
    public String toString() {
        return toString(new StringBuffer(), "");
    }

    // Helper function to give a good view of current dynamic scope with captured scopes
    private String toString(StringBuffer buf, String indent) {
        lazy();
        buf.append(indent).append("Static Type[" + hashCode() + "]: " + 
                (staticScope instanceof BlockStaticScope ? "block" : "local")+" [");
        int size = staticScope.getNumberOfVariables();
        
        if (size != 0) {
            String names[] = staticScope.getVariables();
            for (int i = 0; i < size-1; i++) {
                buf.append(names[i]).append("=");

                if (variableValues[i] == null) {
                    buf.append("null");
                } else {
                    buf.append(variableValues[i]);
                }
                
                buf.append(",");
            }
            buf.append(names[size-1]).append("=");
            
            assert variableValues.length == names.length : "V: " + variableValues.length + 
                " != N: " + names.length + " for " + buf;
            
            if (variableValues[size-1] == null) {
                buf.append("null");
            } else {
                buf.append(variableValues[size-1]);
            }
            
        }
        
        buf.append("]");
        if (parent != null) {
            buf.append("\n");
            parent.toString(buf, indent + "  ");
        }
        
        return buf.toString();
    }
}
