/*
 * 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.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Vector;

import flash.localization.LocalizationManager;
import flash.tools.debugger.Bootstrap;
import flash.tools.debugger.NoResponseException;
import flash.tools.debugger.NotConnectedException;
import flash.tools.debugger.NotSuspendedException;
import flash.tools.debugger.PlayerDebugException;
import flash.tools.debugger.Session;
import flash.tools.debugger.Value;
import flash.tools.debugger.ValueAttribute;
import flash.tools.debugger.Variable;
import flash.tools.debugger.VariableAttribute;
import flash.tools.debugger.VariableType;
import flash.tools.debugger.concrete.DValue;
import flash.tools.debugger.expression.ASTBuilder;
import flash.tools.debugger.expression.IASTBuilder;
import flash.tools.debugger.expression.NoSuchVariableException;
import flash.tools.debugger.expression.PlayerFaultException;
import flash.tools.debugger.expression.ValueExp;

public class ExpressionCache
{
	Session				m_session;
	IASTBuilder			m_builder;
	Vector<Object>		m_expressions;
	IntProperties		m_props;
	DebugCLI			m_cli;

	/**
	 * Returned by evaluate().
	 */
	public static class EvaluationResult
	{
		/**
		 * The value to which the expression evaluated.
		 */
		public Object value;

		/**
		 * The context that was used to evaluate the expression. Sometimes used
		 * to convert the <code>value</code> field to a <code>Value</code>
		 * with <code>context.toValue()</code>.
		 */
		public ExpressionContext context;
	}

	/**
	 * We can get at files by name or module id, eventually we will put functions in here too
	 */

	public ExpressionCache(DebugCLI cli)
	{
		m_builder = new ASTBuilder(true); // allow fdb's "*x" and "x." indirection operators
		m_expressions = new Vector<Object>();
		m_props = new IntProperties();
		m_cli = cli;
	}

	public void			clear()			{ m_expressions.clear(); }
	public void			unbind()		{ m_session = null; }
	public int			size()			{ return m_expressions.size(); }
	public Object		at(int i)		{ return m_expressions.elementAt(i); }

	void setSession(Session s)	{ m_session = s; }

	public Session		getSession()			{ return m_session; }
	public String		getPackageName(int id)	{ return m_cli.module2ClassName(id); }

	public void bind(Session s)
	{ 
		setSession(s); 

		// propagates our properties to the session / non-critical if fails
		try { ((flash.tools.debugger.concrete.PlayerSession)s).setPreferences(m_props.map()); } catch(Exception e) {}
	}

	public EvaluationResult evaluate(ValueExp e, int isolateId) throws NumberFormatException, NoSuchVariableException, PlayerFaultException, PlayerDebugException
	{
		EvaluationResult result = new EvaluationResult();
		result.context = new ExpressionContext(this);
		result.context.setIsolateId(isolateId);
		result.value = e.evaluate(result.context);
		return result;
	}

	public ValueExp parse(String s) throws IOException, ParseException
	{
		return m_builder.parse(new StringReader(s));
	}

	public int add(Object e)
	{
		int at = m_expressions.size();
		m_expressions.add(e);
		return at+1;
	}

	//
	// Interface for accessing previous expression values and also the properties
	//
	public boolean propertyEnabled(String which)
	{
		boolean enabled = false;
		try
		{
			Number number = (Number) get(which);
			if (number != null)
				enabled = (number.intValue() != 0);
		}
		catch (Exception e)
		{
			// nothing; leave 'enabled' as false
		}
		return enabled;
	}

	// this goes in properties
	public void put(String s, int value) { m_props.put(s, value); setSessionProperty(s, value); }
	public Set<String>  keySet() { return m_props.keySet(); }

	/**
	 * Allow the session to receive property updates 
	 */
	void setSessionProperty(String s, int value)
	{
		Session sess = getSession();
	    if (sess != null)
			sess.setPreference(s, value);
		Bootstrap.sessionManager().setPreference(s, value);
	}

	/**
	 * We are able to fetch properties or expressions (i.e previous expression)
	 * using this single call, despite the fact that each of these types of 
	 * results lie in different data structures m_expressions and m_props.
	 * This allows us to easily perform expression evaluation without
	 * need or concern over which 'type' of $ reference we are dealing with
	 */
	public Object get(String s) throws NumberFormatException, ArrayIndexOutOfBoundsException, NoSuchElementException
	{
		Object exp = null;

		// should be of form '$n' where n is a number 0..size()
		if (s.charAt(0) != '$')
			throw new NoSuchElementException(s);

	    String num = s.substring(1);
		if (num == null || num.length() == 0)
			exp = at(size()-1);
		else if (num.equals("$")) //$NON-NLS-1$
			exp = at(size()-2);
		else
		{
			try
			{
				int index = Integer.parseInt(num);
				exp = at(index-1);
			}
			catch(NumberFormatException nfe)
			{
				// must be in the property list 
				exp = m_props.getInteger(s);
			}
		}
		return exp;
	}

	//
	// Statics for formatting stuff
	//

	/**
	 * Formatting function for variable
	 */
	public void appendVariable(StringBuilder sb, Variable v, int isolateId)
	{
		//sb.append('\'');
		String name = v.getName();
		sb.append(name);
		//sb.append('\'');
		sb.append(" = "); //$NON-NLS-1$
		appendVariableValue(sb, v.getValue(), name, isolateId);
		//appendVariableAttributes(sb, v);
	}

	/**
	 * Given any arbitrary constant value, such as a Double, a String, etc.,
	 * format its value appropriately. For example, strings will be quoted.
	 * 
	 * @param sb
	 *            a StringBuilder to which the formatted value will be appended.
	 * @param o
	 *            the value to format.
	 */
	public void appendVariableValue(StringBuilder sb, final Object o, final int isolateId)
	{
		Value v;

		if (o instanceof Value) {
			v = (Value) o;
		} else {
			v = new Value() {
				public int getAttributes() {
					return 0;
				}

				public String[] getClassHierarchy(boolean allLevels) {
					return new String[0];
				}

				public String getClassName() {
					return ""; //$NON-NLS-1$
				}

				public long getId() {
					return UNKNOWN_ID;
				}

				public int getMemberCount(Session s) throws NotSuspendedException,
						NoResponseException, NotConnectedException {
					return 0;
				}

				public Variable getMemberNamed(Session s, String name)
						throws NotSuspendedException, NoResponseException,
						NotConnectedException {
					return null;
				}

				public Variable[] getMembers(Session s)
						throws NotSuspendedException, NoResponseException,
						NotConnectedException {
					return new Variable[0];
				}

				public int getType() {
					if (o instanceof Number)
						return VariableType.NUMBER;
					else if (o instanceof Boolean)
						return VariableType.BOOLEAN;
					else if (o instanceof String)
						return VariableType.STRING;
					else if (o == Value.UNDEFINED)
						return VariableType.UNDEFINED;
					else if (o == null)
						return VariableType.NULL;

					assert false;
					return VariableType.UNKNOWN;
				}

				public String getTypeName() {
					return ""; //$NON-NLS-1$
				}

				public Object getValueAsObject() {
					return o;
				}

				public String getValueAsString() {
					return DValue.getValueAsString(o);
				}

				public boolean isAttributeSet(int variableAttribute) {
					return false;
				}

				public Variable[] getPrivateInheritedMembers() {
					return new Variable[0];
				}

				public Variable[] getPrivateInheritedMemberNamed(String name) {
					return new Variable[0];
				}
				
				public int getIsolateId() {
					return isolateId;
				}
			};
		}

		appendVariableValue(sb, v, isolateId);
	}

	public void appendVariableValue(StringBuilder sb, Value val, final int isolateId) { appendVariableValue(sb,val,"", isolateId); } //$NON-NLS-1$

	public void appendVariableValue(StringBuilder sb, Value val, String variableName, final int isolateId)
	{
		int type = val.getType();
		String typeName = val.getTypeName();
		String className = val.getClassName();

		// if no string or empty then typeName is blank
		if (typeName != null && typeName.length() == 0)
			typeName = null;

        switch (type)
        {
            case VariableType.NUMBER:
            {
				double value = ((Number)val.getValueAsObject()).doubleValue();
				long longValue = (long) value;
				// The value is stored as a double; however, in practice most values are
				// actually integers.  Check to see if this is the case, and if it is,
				// then display it:
				//    - without a fraction, and
				//    - with its hex equivalent in parentheses.
				// Note, we use 'long' instead of 'int', in order to deal with the
				// ActionScript type 'uint'.
				if (longValue == value)
				{
					sb.append(longValue);
					sb.append(" (0x"); //$NON-NLS-1$
					sb.append(Long.toHexString(longValue));
					sb.append(")"); //$NON-NLS-1$
				}
				else
				{
					sb.append(value);
				}
                break;
            }

            case VariableType.BOOLEAN:
            {
                Boolean b = (Boolean)val.getValueAsObject();
                if (b.booleanValue())
                    sb.append("true"); //$NON-NLS-1$
                else
                    sb.append("false"); //$NON-NLS-1$
                break;
            }

            case VariableType.STRING:
            {
            	// Exceptions are displayed in angle brackets, e.g.
            	//     foo = <Text of exception here>
            	// Strings are displayed quoted:
            	//     foo = "Value of string here"
            	//
            	// Note that quotation marks within the string are not escaped.  This
            	// is sort of weird, but it's what we want to do, at least for now;
            	// the debugger's output is intended to be human-readable, not
            	// machine-readable, and it's easier for a person to read the string
            	// if there is no escaping of quotation marks.
            	//
            	// As a small step in the direction of avoiding that weirdness, if
            	// the string contains double-quotes but no single-quotes, we will
            	// quote it in single quotes.
            	String s = val.getValueAsString();
            	char start, end;

				if (val.isAttributeSet(ValueAttribute.IS_EXCEPTION))
				{
					start = '<';
					end = '>';
				}
				else if (s.indexOf('"') != -1 && s.indexOf('\'') == -1)
				{
					start = end = '\'';
				}
				else
				{
					start = end = '"';
				}

                sb.append(start);
                sb.append(escapeIfIde(s));
                sb.append(end);
                break;
            }

            case VariableType.OBJECT:
            {
                sb.append("["); //$NON-NLS-1$
				sb.append(className);

				// Normally, we include the object id after the class name.
				// However, when running fdbunit, don't show object IDs, so that
				// results can reproduce consistently from one run to the next.
				if (System.getProperty("fdbunit") == null) //$NON-NLS-1$
				{
					sb.append(" "); //$NON-NLS-1$
					sb.append(escapeIfIde(String.valueOf(val.getValueAsObject()))); // object id
				}
                if (typeName != null && !typeName.equals(className))
                {
                    sb.append(", class='"); //$NON-NLS-1$

					// Often the typename is of the form 'classname@hexaddress',
					// but the hex address is the same as the object id which
					// is returned by getValue() -- we don't want to display it
					// here.
					int at = typeName.indexOf('@');
					if (at != -1)
						typeName = typeName.substring(0, at);

                    sb.append(typeName);
                    sb.append('\'');
                }
                sb.append(']');
                break;
            }

            case VariableType.FUNCTION:
            {
				// here we have a special case for getters/setters which 
				// look like functions to us, except the attribute is set.
				sb.append('[');
				if (val.isAttributeSet(VariableAttribute.HAS_GETTER))
					sb.append(getLocalizationManager().getLocalizedTextString("getterFunction")); //$NON-NLS-1$
				else if (val.isAttributeSet(VariableAttribute.HAS_SETTER))
					sb.append(getLocalizationManager().getLocalizedTextString("setterFunction")); //$NON-NLS-1$
				else
					sb.append(getLocalizationManager().getLocalizedTextString("function")); //$NON-NLS-1$
				sb.append(' ');

				sb.append(escapeIfIde(String.valueOf(val.getValueAsObject())));
                if (typeName != null && !typeName.equals(variableName))
                {
                    sb.append(", name='"); //$NON-NLS-1$
                    sb.append(typeName);
                    sb.append('\'');
                }
                sb.append(']');
                break;
            }

            case VariableType.MOVIECLIP:
            {
                sb.append("["); //$NON-NLS-1$
				sb.append(className);
				sb.append(" "); //$NON-NLS-1$
				sb.append(escapeIfIde(String.valueOf(val.getValueAsObject())));
                if (typeName != null && !typeName.equals(className))
                {
                    sb.append(", named='"); //$NON-NLS-1$
                    sb.append(typeName);
                    sb.append('\'');
                }
                sb.append(']');
                break;
            }

            case VariableType.NULL:
            {
                sb.append("null"); //$NON-NLS-1$
                break;
            }

            case VariableType.UNDEFINED:
            {
                sb.append("undefined"); //$NON-NLS-1$
                break;
            }

            case VariableType.UNKNOWN:
            {
                sb.append(getLocalizationManager().getLocalizedTextString("unknownVariableType")); //$NON-NLS-1$
                break;
            }
        }
	}

	private static LocalizationManager getLocalizationManager()
	{
		return DebugCLI.getLocalizationManager();
	}

	public static void appendVariableAttributes(StringBuilder sb, Variable v)
	{
		if (v.getAttributes() == 0)
			return;

		sb.append("  "); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.DONT_ENUMERATE))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_dontEnumerate")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.READ_ONLY))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_readOnly")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.IS_LOCAL))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_localVariable")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.IS_ARGUMENT))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_functionArgument")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.HAS_GETTER))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_getterFunction")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.HAS_SETTER))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_setterFunction")); //$NON-NLS-1$ //$NON-NLS-2$

		if (v.isAttributeSet(VariableAttribute.IS_DYNAMIC))
			sb.append(", dynamic"); //$NON-NLS-1$
		
		if (v.isAttributeSet(VariableAttribute.IS_STATIC))
			sb.append(", static"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.IS_CONST))
			sb.append(", const"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.PRIVATE_SCOPE))
			sb.append(", private"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.PUBLIC_SCOPE))
			sb.append(", public"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.PROTECTED_SCOPE))
			sb.append(", protected"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.INTERNAL_SCOPE))
			sb.append(", internal"); //$NON-NLS-1$

		if (v.isAttributeSet(VariableAttribute.NAMESPACE_SCOPE))
			sb.append(", " + getLocalizationManager().getLocalizedTextString("variableAttribute_hasNamespace")); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
    private String escapeIfIde(String s)
    {
        return m_cli != null && m_cli.isIde() ? escape(s) : s;
    }

    public static String escape(final String str) {
        final StringBuilder buffer = new StringBuilder();

        for (int idx = 0; idx < str.length(); idx++) {
            char ch = str.charAt(idx);
            switch (ch) {
                case '\b':
                    buffer.append("\\b");
                    break;

                case '\t':
                    buffer.append("\\t");
                    break;

                case '\n':
                    buffer.append("\\n");
                    break;

                case '\f':
                    buffer.append("\\f");
                    break;

                case '\r':
                    buffer.append("\\r");
                    break;

                case '\\':
                    buffer.append("\\\\");
                    break;

                default:
                    if (Character.isISOControl(ch)) {
                        String hexCode = Integer.toHexString(ch).toUpperCase();
                        buffer.append("\\u");
                        int paddingCount = 4 - hexCode.length();
                        while (paddingCount-- > 0) {
                            buffer.append(0);
                        }
                        buffer.append(hexCode);
                    } else {
                        buffer.append(ch);
                    }
            }
        }
        return buffer.toString();
    }
}
