/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package flex.tools.debugger.cli;

import java.util.StringTokenizer;
import java.util.Vector;

import flash.tools.debugger.Isolate;
import flash.tools.debugger.PlayerDebugException;
import flash.tools.debugger.Session;
import flash.tools.debugger.SessionManager;
import flash.tools.debugger.Value;
import flash.tools.debugger.ValueAttribute;
import flash.tools.debugger.Variable;
import flash.tools.debugger.VariableType;
import flash.tools.debugger.concrete.DValue;
import flash.tools.debugger.events.ExceptionFault;
import flash.tools.debugger.events.FaultEvent;
import flash.tools.debugger.expression.Context;
import flash.tools.debugger.expression.ExpressionEvaluatorException;
import flash.tools.debugger.expression.NoSuchVariableException;
import flash.tools.debugger.expression.PlayerFaultException;

public class ExpressionContext implements Context
{
	ExpressionCache		m_cache;
	Object				m_current;
	boolean				m_createIfMissing;  // set if we need to create a variable if it doesn't exist
	Vector<String>		m_namedPath;
	boolean				m_nameLocked;
	String				m_newline = System.getProperty("line.separator"); //$NON-NLS-1$
	int m_isolateId;

	// used when evaluating an expression
	public ExpressionContext(ExpressionCache cache)
	{
		m_cache = cache;
		m_current = null;
		m_createIfMissing = false;
		m_namedPath = new Vector<String>();
		m_nameLocked = false;
		m_isolateId = Isolate.DEFAULT_ID;
	}
	
	public void setIsolateId(int id) {
		m_isolateId = id;
	}

	void		setContext(Object o)	{ m_current = o; }

	void		pushName(String name)	{ if (m_nameLocked || name.length() < 1) return; m_namedPath.add(name);  }
	boolean		setName(String name)	{ if (m_nameLocked) return true; m_namedPath.clear(); pushName(name); return true; }
	void		lockName()				{ m_nameLocked = true; }

	public String getName()
	{
		int size = m_namedPath.size();
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<size; i++)
		{
			String s = m_namedPath.get(i);
			if (i > 0)
				sb.append('.');
			sb.append(s);
		}
		return ( sb.toString() );
	}

	String getCurrentPackageName()
	{ 
		String s = null;
		try
		{
			Integer o = (Integer)m_cache.get(DebugCLI.LIST_MODULE);
			s = m_cache.getPackageName(o.intValue());
		}
		catch(NullPointerException npe)
		{
		}
		catch(ClassCastException cce)
		{
		}
		return s; 
	}

	//
	//
	// Start of Context API implementation
	//
	//
	public void createPseudoVariables(boolean oui) { m_createIfMissing = oui; }

	// create a new context object by combining the current one and o 
	public Context createContext(Object o)
	{
		ExpressionContext c = new ExpressionContext(m_cache);
		c.setContext(o);
		c.createPseudoVariables(m_createIfMissing);
		c.m_namedPath.addAll(m_namedPath);
		c.setIsolateId(m_isolateId);
		return c;
	}

	// assign the object o, the value v
	public void assign(Object o, Value v) throws NoSuchVariableException, PlayerFaultException
	{
		try
		{
			// first see if it is an internal property (avoids player calls)
			InternalProperty prop = resolveToInternalProperty(o);

			// we expect that o is a variable that can be resolved or is a specially marked internal variable
			if (prop != null)
			{
				assignInternal(prop, v);
			}
			else
			{
				boolean wasCreateIfMissing = m_createIfMissing;
				createPseudoVariables(true);
				Variable var = null;
				try {
					var = resolveToVariable(o);
				} finally {
					createPseudoVariables(wasCreateIfMissing);
				}

				if (var == null)
					throw new NoSuchVariableException(m_current);

				// set the value, for the case of a variable that does not exist it will not have a type
				// so we try to glean one from v.
				FaultEvent faultEvent = var.setValue(getSession(), v.getType(), v.getValueAsString());
				if (faultEvent != null)
					throw new PlayerFaultException(faultEvent);
			}
		}
		catch(PlayerDebugException pde)
		{
			throw new ExpressionEvaluatorException(pde);
		}
	}

	/**
	 * The Context interface which goes out and gets values from the session
	 * Expressions use this interface as a means of evaluation.
	 * 
	 * We also use this to create a reference to internal variables.
	 */
	public Object lookup(Object o) throws NoSuchVariableException, PlayerFaultException
	{
		Object result = null;
		try
		{
			// first see if it is an internal property (avoids player calls)
			if ( (result = resolveToInternalProperty(o)) != null)
				;

			// attempt to resolve to a player variable
			else if ( (result = resolveToVariable(o)) != null)
				;

			// or value
			else if ( (result = resolveToValue(o)) != null)
				;

			else
				throw new NoSuchVariableException(o);

			// take on the path to the variable; so 'what' command prints something nice
			if (result instanceof VariableFacade)
			{
				((VariableFacade)result).setPath(getName());
			}

			// if the attempt to get the variable's value threw an exception inside the
			// player (most likely because the variable is actually a getter, and the
			// getter threw something), then throw something here
			Value resultValue = null;

			if (result instanceof Variable)
			{
				if (result instanceof VariableFacade && ((VariableFacade)result).getVariable() == null)
					resultValue = null;
				else
					resultValue = ((Variable)result).getValue();
			}
			else if (result instanceof Value)
			{
				resultValue = (Value) result;
			}

			if (resultValue != null)
			{
				if (resultValue.isAttributeSet(ValueAttribute.IS_EXCEPTION))
				{
					String value = resultValue.getValueAsString();
					throw new PlayerFaultException(new ExceptionFault(value, false, resultValue, resultValue.getIsolateId()));
				}
			}
		}
		catch(PlayerDebugException pde)
		{
			result = Value.UNDEFINED;
		}
		return result;
	}

	/* returns a string consisting of formatted member names and values */
	public Object lookupMembers(Object o) throws NoSuchVariableException
	{
		Variable var = null;
		Value val = null;
  		Variable[] mems = null;
		try
		{
			var = resolveToVariable(o);
			if (var != null)
				val = var.getValue();
			else
				val = resolveToValue(o);
			mems = val.getMembers(getSession());
		}
		catch(NullPointerException npe)
		{
			throw new NoSuchVariableException(o);
		}
		catch(PlayerDebugException pde)
		{
			throw new NoSuchVariableException(o); // not quite right...
		}

  		StringBuilder sb = new StringBuilder();

  		if (var != null)
  			m_cache.appendVariable(sb, var, m_isolateId);
  		else
  			m_cache.appendVariableValue(sb, val, m_isolateId);

		boolean attrs = m_cache.propertyEnabled(DebugCLI.DISPLAY_ATTRIBUTES);
		if (attrs && var != null)
			ExpressionCache.appendVariableAttributes(sb, var);

		// [mmorearty] experimenting with hierarchical display of members
		String[] classHierarchy = val.getClassHierarchy(false);
		if (classHierarchy != null && getSession().getPreference(SessionManager.PREF_HIERARCHICAL_VARIABLES) != 0)
		{
			for (int c=0; c<classHierarchy.length; ++c)
			{
				String classname = classHierarchy[c];
				sb.append(m_newline + "(Members of " + classname + ")"); //$NON-NLS-1$ //$NON-NLS-2$
				for (int i=0; i<mems.length; ++i)
				{
					if (classname.equals(mems[i].getDefiningClass()))
					{
			  			sb.append(m_newline + " "); //$NON-NLS-1$
			  			m_cache.appendVariable(sb, mems[i], m_isolateId);
						if (attrs)
							ExpressionCache.appendVariableAttributes(sb, mems[i]);
					}
				}
			}
		}
		else
		{
	  		for(int i=0; i<mems.length; i++)
	  		{
	  			sb.append(m_newline + " "); //$NON-NLS-1$
	  			m_cache.appendVariable(sb, mems[i], m_isolateId);
				if (attrs)
					ExpressionCache.appendVariableAttributes(sb, mems[i]);
	  		}
		}

  		return sb.toString();
  	}

	//
	//
	// End of Context API implementation 
	//
	//

	// used to assign a value to an internal variable 
	private void assignInternal(InternalProperty var, Value v) throws NoSuchVariableException, NumberFormatException, PlayerDebugException
	{
		// otherwise set it
		if (v.getType() != VariableType.NUMBER)
			throw new NumberFormatException(v.getValueAsString());
		long l = Long.parseLong(v.getValueAsString());
		m_cache.put(var.getName(), (int)l);
	}

	InternalProperty resolveToInternalProperty(Object o)
	{
		if (o instanceof String && ((String)o).charAt(0) == '$')
		{
			String key = (String)o;
			Object value = null;

			try { value = m_cache.get(key); } catch(Exception e) {}
			return new InternalProperty(key, value);
		}

		return null;
	}

	/**
	 * Resolve the object into a variable by various means and 
	 * using the current context.
	 * @return variable, or <code>null</code>
	 */
	Variable resolveToVariable(Object o) throws PlayerDebugException
	{
		Variable v = null;

		// if o is a variable already, then we're done!
		if (o instanceof Variable)
			return (Variable)o;

		/**
		 * Resolve the name to something
		 */
		{
			// not an id so try as name 
			String name = o.toString();
			long id = nameAsId(name);

			/**
			 * if #N was used just pick up the variable, otherwise
			 * we need to use the current context to resolve 
			 * the name to a member
			 */
			if (id != Value.UNKNOWN_ID)
			{
				// TODO what here?
			}
			else
			{
				// try to resolve as a member of current context (will set context if null)
				id = determineContext(name);
				v = locateForNamed(id, name, true);
				if (v != null)
					v = new VariableFacade(v, id, m_isolateId);
				else if (m_createIfMissing && name.charAt(0) != '$')
					v = new VariableFacade(id, name, m_isolateId);
			}
		}

		/* return the variable */
		return v;
	}

	/*
	 * Resolve the object into a variable by various means and 
	 * using the current context.
	 */
	Value resolveToValue(Object o) throws PlayerDebugException
	{
		Value v = null;

		// if o is a variable or a value already, then we're done!
		if (o instanceof Value)
			return (Value)o;
		else if (o instanceof Variable)
			return ((Variable)o).getValue();
		else if (o instanceof InternalProperty)
			return DValue.forPrimitive(((InternalProperty)o).m_value, m_isolateId);

		/**
		 * Resolve the name to something
		 */
		if (m_current == null)
		{
			// not an id so try as name 
			String name = o.toString();
			long id = nameAsId(name);

			/**
			 * if #N was used just pick up the variable, otherwise
			 * we need to use the current context to resolve 
			 * the name to a member
			 */
			if (id != Value.UNKNOWN_ID)
			{
				v = getSession().getWorkerSession(m_isolateId).getValue((int)id);
			}
			else if (name.equals("undefined")) //$NON-NLS-1$
			{
				v = DValue.forPrimitive(Value.UNDEFINED, m_isolateId);
			}
			else
			{
				// Ask the player to find something, anything, on the scope chain
				// with this name.  We'll end up here, for example, when resolving
				// things like MyClass, String, Number, etc.
				v = getSession().getWorkerSession(m_isolateId).getGlobal(name);
			}
		}

		/* return the value */
		return v;
	}

	// special code for #N support. I.e. naming a variable via an ID
	long nameAsId(String name)
	{
		long id = Value.UNKNOWN_ID;
		try
		{
			if (name.charAt(0) == '#')
				id = Long.parseLong(name.substring(1));
		}
		catch(Exception e) 
		{
			id = Value.UNKNOWN_ID;
		}
		return id;
	}

	/**
	 * Using the given id as a parent find the member named
	 * name.
	 * @throws NoSuchVariableException if id is UNKNOWN_ID
	 */
	Variable memberNamed(long id, String name) throws NoSuchVariableException, PlayerDebugException
	{
		Variable v = null;
		Value parent = getSession().getWorkerSession(m_isolateId).getValue(id);

		if (parent == null)
			throw new NoSuchVariableException(name);

		/* got a variable now return the member if any */
		v = parent.getMemberNamed(getSession(), name);

		return v;
	}

	/**
	 * All the really good stuff about finding where name exists goes here!
	 * 
	 * If name is not null, then it implies that we use the existing
	 * m_current to find a member of m_current.  If m_current is null
	 * Then we need to probe variable context points attempting to locate
	 * name.  When we find a match we set the m_current to this context
	 *
	 * If name is null then we simply return the current context.
	 */
	long determineContext(String name) throws PlayerDebugException
	{
		long id = Value.UNKNOWN_ID;

		// have we already resolved our context...
		if (m_current != null)
		{
			id = toValue().getId();
		}

		// nothing to go on, so we're done
		else if (name == null)
			;

		// use the name and try and resolve where we are...
		else
		{
			// Each stack frame has a root variable under (BASE_ID-depth)
			// where depth is the depth of the stack.
			// So we query for our current stack depth and use that 
			// as the context for our base computation
			long baseId = Value.BASE_ID;
			int depth = ((Integer)m_cache.get(DebugCLI.DISPLAY_FRAME_NUMBER)).intValue();
			baseId -= depth;

			// obtain data about our current state 
			Variable contextVar = null;
			Value contextVal = null;
			Value val = null;

			// look for 'name' starting from local scope
			if ( (val = locateParentForNamed(baseId, name, false)) != null)
				;

			// get the this pointer, then look for 'name' starting from that point
			else if ( ( (contextVar = locateForNamed(baseId, "this", false)) != null ) &&  //$NON-NLS-1$
					  ( setName("this") && (val = locateParentForNamed(contextVar.getValue().getId(), name, true)) != null ) ) //$NON-NLS-1$
				;

			// now try to see if 'name' exists off of _root
			else if ( setName("_root") && (val = locateParentForNamed(Value.ROOT_ID, name, true)) != null ) //$NON-NLS-1$
				;

			// now try to see if 'name' exists off of _global
			else if ( setName("_global") && (val = locateParentForNamed(Value.GLOBAL_ID, name, true)) != null ) //$NON-NLS-1$
				;

			// now try off of class level, if such a thing can be found
			else if ( ( (contextVal = locate(Value.GLOBAL_ID, getCurrentPackageName(), false)) != null ) && 
					  ( setName("_global."+getCurrentPackageName()) && (val = locateParentForNamed(contextVal.getId(), name, true)) != null ) ) //$NON-NLS-1$
				;

			// if we found it then stake this as our context!
			if (val != null)
			{
				id = val.getId();
				pushName(name);
				lockName();
			}
		}
		
		return id;
	}

	/**
	 * Performs a search for a member with the given name using the
	 * given id as the parent variable.
	 * 
	 * If a match is found then, we return the parent variable of
	 * the member that matched.  The proto chain is optionally traversed.
	 * 
	 * No exceptions are thrown
	 */
	Value locateParentForNamed(long id, String name, boolean traverseProto) throws PlayerDebugException
	{
		StringBuilder sb = new StringBuilder();

		Variable var = null;
		Value val = null;
		try
		{
			var = memberNamed(id, name);

			// see if we need to traverse the proto chain
			while (var == null && traverseProto)
			{
				// first attempt to get __proto__, then resolve name
				Variable proto = memberNamed(id, "__proto__"); //$NON-NLS-1$
 				sb.append("__proto__"); //$NON-NLS-1$
				if (proto == null)
					traverseProto = false;
				else
				{
					id = proto.getValue().getId();
					var = memberNamed(id, name);
					if (var == null)
						sb.append('.');
				}
			}
		}
		catch(NoSuchVariableException nsv)
		{
			// don't worry about this one, it means variable with id couldn't be found
		}
		catch(NullPointerException npe)
		{
			// probably no session
		}

		// what we really want is the parent not the child variable
		if (var != null)
		{
			pushName(sb.toString());
			val = getSession().getWorkerSession(m_isolateId).getValue(id);
		}

		return val;
	}

	// variant of locateParentForNamed, whereby we return the child variable
	Variable locateForNamed(long id, String name, boolean traverseProto) throws PlayerDebugException
	{
		Variable var = null;
		Value v = locateParentForNamed(id, name, traverseProto);
		if (v != null)
		{
			try
			{
				var = memberNamed(v.getId(), name);
			}
			catch(NoSuchVariableException nse)
			{
				v = null;
			}
		}

		return var;
	}

	/**
	 * Locates the member via a dotted name starting at the given id.
	 * It will traverse any and all proto chains if necc. to find the name.
	 */
	Value locate(long startingId, String dottedName, boolean traverseProto) throws PlayerDebugException
	{
		if (dottedName == null)
			return null;

		// first rip apart the dottedName
		StringTokenizer names = new StringTokenizer(dottedName, "."); //$NON-NLS-1$
		Value val = getSession().getWorkerSession(m_isolateId).getValue(startingId);

		while(names.hasMoreTokens() && val != null)
			val = locateForNamed(val.getId(), names.nextToken(), traverseProto).getValue();

		return val;
	}

	/*
	 * @see flash.tools.debugger.expression.Context#toValue(java.lang.Object)
	 */
	public Value toValue(Object o)
	{
		// if o is a variable or a value already, then we're done!
		if (o instanceof Value)
			return (Value)o;
		else if (o instanceof Variable)
			return ((Variable)o).getValue();
		else if (o instanceof InternalProperty)
			return DValue.forPrimitive(((InternalProperty)o).m_value, m_isolateId);
		else
			return DValue.forPrimitive(o, m_isolateId);
	}

	public Value toValue()
	{
		return toValue(m_current);
	}

	public Session getSession()
	{
		return m_cache.getSession();
	}

	@Override
	public int getIsolateId() {
		return m_isolateId;
	}
}
