using System;
using System.Collections.Generic;

namespace Ink.Runtime
{
    /// <summary>
    /// Encompasses all the global variables in an ink Story, and
    /// allows binding of a VariableChanged event so that that game
    /// code can be notified whenever the global variables change.
    /// </summary>
	public class VariablesState : IEnumerable<string>
    {
        public delegate void VariableChanged(string variableName, Runtime.Object newValue);
        public event VariableChanged variableChangedEvent;

        public StatePatch patch;

        public bool batchObservingVariableChanges 
        { 
            get {
                return _batchObservingVariableChanges;
            }
            set { 
                _batchObservingVariableChanges = value;
                if (value) {
                    _changedVariablesForBatchObs = new HashSet<string> ();
                } 

                // Finished observing variables in a batch - now send 
                // notifications for changed variables all in one go.
                else {
                    if (_changedVariablesForBatchObs != null) {
                        foreach (var variableName in _changedVariablesForBatchObs) {
                            var currentValue = _globalVariables [variableName];
                            variableChangedEvent (variableName, currentValue);
                        }
                    }

                    _changedVariablesForBatchObs = null;
                }
            }
        }
        bool _batchObservingVariableChanges;

        // Allow StoryState to change the current callstack, e.g. for
        // temporary function evaluation.
        public CallStack callStack {
            get {
                return _callStack;
            }
            set {
                _callStack = value;
            }
        }

        /// <summary>
        /// Get or set the value of a named global ink variable.
        /// The types available are the standard ink types. Certain
        /// types will be implicitly casted when setting.
        /// For example, doubles to floats, longs to ints, and bools
        /// to ints.
        /// </summary>
        public object this[string variableName]
        {
            get {
                Runtime.Object varContents;

                if (patch != null && patch.TryGetGlobal(variableName, out varContents))
                    return (varContents as Runtime.Value).valueObject;

                // Search main dictionary first.
                // If it's not found, it might be because the story content has changed,
                // and the original default value hasn't be instantiated.
                // Should really warn somehow, but it's difficult to see how...!
                if ( _globalVariables.TryGetValue (variableName, out varContents) || 
                     _defaultGlobalVariables.TryGetValue(variableName, out varContents) )
                    return (varContents as Runtime.Value).valueObject;
                else {
                    return null;
                }
            }
            set {
                if (!_defaultGlobalVariables.ContainsKey (variableName))
                    throw new StoryException ("Cannot assign to a variable ("+variableName+") that hasn't been declared in the story");
                
                var val = Runtime.Value.Create(value);
                if (val == null) {
                    if (value == null) {
                        throw new Exception ("Cannot pass null to VariableState");
                    } else {
                        throw new Exception ("Invalid value passed to VariableState: "+value.ToString());
                    }
                }

                SetGlobal (variableName, val);
            }
        }

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

        /// <summary>
        /// Enumerator to allow iteration over all global variables by name.
        /// </summary>
		public IEnumerator<string> GetEnumerator()
		{
			return _globalVariables.Keys.GetEnumerator();
		}

        public VariablesState (CallStack callStack, ListDefinitionsOrigin listDefsOrigin)
        {
            _globalVariables = new Dictionary<string, Object> ();
            _callStack = callStack;
            _listDefsOrigin = listDefsOrigin;
        }

        public void ApplyPatch()
        {
            foreach(var namedVar in patch.globals) {
                _globalVariables[namedVar.Key] = namedVar.Value;
            }

            if(_changedVariablesForBatchObs != null ) {
                foreach (var name in patch.changedVariables)
                    _changedVariablesForBatchObs.Add(name);
            }

            patch = null;
        }

        public void SetJsonToken(Dictionary<string, object> jToken)
        {
            _globalVariables.Clear();

            foreach (var varVal in _defaultGlobalVariables) {
                object loadedToken;
                if( jToken.TryGetValue(varVal.Key, out loadedToken) ) {
                    _globalVariables[varVal.Key] = Json.JTokenToRuntimeObject(loadedToken);
                } else {
                    _globalVariables[varVal.Key] = varVal.Value;
                }
            }
        }

        /// <summary>
        /// When saving out JSON state, we can skip saving global values that
        /// remain equal to the initial values that were declared in ink.
        /// This makes the save file (potentially) much smaller assuming that
        /// at least a portion of the globals haven't changed. However, it
        /// can also take marginally longer to save in the case that the 
        /// majority HAVE changed, since it has to compare all globals.
        /// It may also be useful to turn this off for testing worst case
        /// save timing.
        /// </summary>
        public static bool dontSaveDefaultValues = true;

        public void WriteJson(SimpleJson.Writer writer)
        {
            writer.WriteObjectStart();
            foreach (var keyVal in _globalVariables)
            {
                var name = keyVal.Key;
                var val = keyVal.Value;

                if(dontSaveDefaultValues) {
                    // Don't write out values that are the same as the default global values
                    Runtime.Object defaultVal;
                    if (_defaultGlobalVariables.TryGetValue(name, out defaultVal))
                    {
                        if (RuntimeObjectsEqual(val, defaultVal))
                            continue;
                    }
                }


                writer.WritePropertyStart(name);
                Json.WriteRuntimeObject(writer, val);
                writer.WritePropertyEnd();
            }
            writer.WriteObjectEnd();
        }

        public bool RuntimeObjectsEqual(Runtime.Object obj1, Runtime.Object obj2)
        {
            if (obj1.GetType() != obj2.GetType()) return false;

            // Perform equality on int/float/bool manually to avoid boxing
            var boolVal = obj1 as BoolValue;
            if( boolVal != null ) {
                return boolVal.value == ((BoolValue)obj2).value;
            }

            var intVal = obj1 as IntValue;
            if( intVal != null ) {
                return intVal.value == ((IntValue)obj2).value;
            }

            var floatVal = obj1 as FloatValue;
            if (floatVal != null)
            {
                return floatVal.value == ((FloatValue)obj2).value;
            }

            // Other Value type (using proper Equals: list, string, divert path)
            var val1 = obj1 as Value;
            var val2 = obj2 as Value;
            if( val1 != null ) {
                return val1.valueObject.Equals(val2.valueObject);
            }

            throw new System.Exception("FastRoughDefinitelyEquals: Unsupported runtime object type: "+obj1.GetType());
        }

        public Runtime.Object GetVariableWithName(string name)
        {
            return GetVariableWithName (name, -1);
        }

        public Runtime.Object TryGetDefaultVariableValue (string name)
        {
            Runtime.Object val = null;
            _defaultGlobalVariables.TryGetValue (name, out val);
            return val;
        }

		public bool GlobalVariableExistsWithName(string name)
		{
			return _globalVariables.ContainsKey(name) || _defaultGlobalVariables != null && _defaultGlobalVariables.ContainsKey(name);
		}

        Runtime.Object GetVariableWithName(string name, int contextIndex)
        {
            Runtime.Object varValue = GetRawVariableWithName (name, contextIndex);

            // Get value from pointer?
            var varPointer = varValue as VariablePointerValue;
            if (varPointer) {
                varValue = ValueAtVariablePointer (varPointer);
            }

            return varValue;
        }

        Runtime.Object GetRawVariableWithName(string name, int contextIndex)
        {
            Runtime.Object varValue = null;

            // 0 context = global
            if (contextIndex == 0 || contextIndex == -1) {
                if (patch != null && patch.TryGetGlobal(name, out varValue))
                    return varValue;

                if ( _globalVariables.TryGetValue (name, out varValue) )
                    return varValue;

                // Getting variables can actually happen during globals set up since you can do
                //  VAR x = A_LIST_ITEM
                // So _defaultGlobalVariables may be null.
                // We need to do this check though in case a new global is added, so we need to
                // revert to the default globals dictionary since an initial value hasn't yet been set.
                if( _defaultGlobalVariables != null && _defaultGlobalVariables.TryGetValue(name, out varValue) ) {
                    return varValue;
                }

                var listItemValue = _listDefsOrigin.FindSingleItemListWithName (name);
                if (listItemValue)
                    return listItemValue;
            } 

            // Temporary
            varValue = _callStack.GetTemporaryVariableWithName (name, contextIndex);

            return varValue;
        }

        public Runtime.Object ValueAtVariablePointer(VariablePointerValue pointer)
        {
            return GetVariableWithName (pointer.variableName, pointer.contextIndex);
        }

        public void Assign(VariableAssignment varAss, Runtime.Object value)
        {
            var name = varAss.variableName;
            int contextIndex = -1;

            // Are we assigning to a global variable?
            bool setGlobal = false;
            if (varAss.isNewDeclaration) {
                setGlobal = varAss.isGlobal;
            } else {
                setGlobal = GlobalVariableExistsWithName (name);
            }

            // Constructing new variable pointer reference
            if (varAss.isNewDeclaration) {
                var varPointer = value as VariablePointerValue;
                if (varPointer) {
                    var fullyResolvedVariablePointer = ResolveVariablePointer (varPointer);
                    value = fullyResolvedVariablePointer;
                }

            } 

            // Assign to existing variable pointer?
            // Then assign to the variable that the pointer is pointing to by name.
            else {

                // De-reference variable reference to point to
                VariablePointerValue existingPointer = null;
                do {
                    existingPointer = GetRawVariableWithName (name, contextIndex) as VariablePointerValue;
                    if (existingPointer) {
                        name = existingPointer.variableName;
                        contextIndex = existingPointer.contextIndex;
                        setGlobal = (contextIndex == 0);
                    }
                } while(existingPointer);
            }


            if (setGlobal) {
                SetGlobal (name, value);
            } else {
                _callStack.SetTemporaryVariable (name, value, varAss.isNewDeclaration, contextIndex);
            }
        }

        public void SnapshotDefaultGlobals ()
        {
            _defaultGlobalVariables = new Dictionary<string, Object> (_globalVariables);
        }

        void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue)
        {
            var oldList = oldValue as ListValue;
            var newList = newValue as ListValue;
            if (oldList && newList && newList.value.Count == 0)
                newList.value.SetInitialOriginNames (oldList.value.originNames);
        }

        public void SetGlobal(string variableName, Runtime.Object value)
        {
            Runtime.Object oldValue = null;
            if( patch == null || !patch.TryGetGlobal(variableName, out oldValue) )
                _globalVariables.TryGetValue (variableName, out oldValue);

            ListValue.RetainListOriginsForAssignment (oldValue, value);

            if (patch != null)
                patch.SetGlobal(variableName, value);
            else
                _globalVariables [variableName] = value;

            if (variableChangedEvent != null && !value.Equals (oldValue)) {

                if (batchObservingVariableChanges) {
                    if (patch != null)
                        patch.AddChangedVariable(variableName);
                    else if(_changedVariablesForBatchObs != null)
                        _changedVariablesForBatchObs.Add (variableName);
                } else {
                    variableChangedEvent (variableName, value);
                }
            }
        }

        // Given a variable pointer with just the name of the target known, resolve to a variable
        // pointer that more specifically points to the exact instance: whether it's global,
        // or the exact position of a temporary on the callstack.
        VariablePointerValue ResolveVariablePointer(VariablePointerValue varPointer)
        {
            int contextIndex = varPointer.contextIndex;

            if( contextIndex == -1 )
                contextIndex = GetContextIndexOfVariableNamed (varPointer.variableName);

            var valueOfVariablePointedTo = GetRawVariableWithName (varPointer.variableName, contextIndex);

            // Extra layer of indirection:
            // When accessing a pointer to a pointer (e.g. when calling nested or 
            // recursive functions that take a variable references, ensure we don't create
            // a chain of indirection by just returning the final target.
            var doubleRedirectionPointer = valueOfVariablePointedTo as VariablePointerValue;
            if (doubleRedirectionPointer) {
                return doubleRedirectionPointer;
            } 

            // Make copy of the variable pointer so we're not using the value direct from
            // the runtime. Temporary must be local to the current scope.
            else {
                return new VariablePointerValue (varPointer.variableName, contextIndex);
            }
        }

        // 0  if named variable is global
        // 1+ if named variable is a temporary in a particular call stack element
        int GetContextIndexOfVariableNamed(string varName)
        {
            if (GlobalVariableExistsWithName(varName))
                return 0;

            return _callStack.currentElementIndex;
        }

        Dictionary<string, Runtime.Object> _globalVariables;

        Dictionary<string, Runtime.Object> _defaultGlobalVariables;

        // Used for accessing temporary variables
        CallStack _callStack;
        HashSet<string> _changedVariablesForBatchObs;
        ListDefinitionsOrigin _listDefsOrigin;
    }
}

