using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Wayne.Lib.StateEngine.Generic
{
    /// <summary>
    /// Class that is used as base class for State Data objects. The idea is that the descendant classes 
    /// should define strongly typed properties for the data that should be accessed from the states. The 
    /// storage of the data however is in a weakly typed dictionary that is managed by the class through the 
    /// Define/Get/Set data methods.
    /// Typically an enumeration should be used to define the data.
    /// </summary>
    public abstract class StateData : IDisposable
    {
        #region Fields

        private readonly StateData parentStateData;
        private readonly Dictionary<object, object> dataDict = new Dictionary<object, object>();
        private readonly Dictionary<object, object> dataDefinitionDict = new Dictionary<object, object>();
        private readonly Dictionary<object, Type> dataDefinitionType = new Dictionary<object, Type>();

        #endregion

        #region Construction

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="parentStateData">Reference to the parent composite state's state data object.</param>
        protected StateData(StateData parentStateData)
        {
            this.parentStateData = parentStateData;
        }

        /// <summary>
        /// Finalizer
        /// </summary>
        ~StateData()
        {
            Dispose(false);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Defines a parameter and specifies the type and initial value for it.
        /// </summary>
        /// <param name="dataDefinition"></param>
        /// <param name="initialValue"></param>
        protected void DefineData<TDefinitionType, TDataType>(TDefinitionType dataDefinition, TDataType initialValue)
        {
            if (!DataIsDefined(dataDefinition))
            {
                dataDict[dataDefinition] = initialValue;
                dataDefinitionDict[dataDefinition] = initialValue;
                dataDefinitionType[dataDefinition] = typeof(TDataType);
            }
        }


        /// <summary>
        /// Checks if the data is defined already in a parent state data.
        /// </summary>
        /// <param name="dataDefinition"></param>
        /// <returns></returns>
        private bool DataIsDefined(object dataDefinition)
        {
            if (dataDict.ContainsKey(dataDefinition))
                return true;
            else if (parentStateData != null)
                return parentStateData.DataIsDefined(dataDefinition);
            else
                return false;
        }

        /// <summary>
        /// Sets a new value to a field that is defined by dataDefinition.
        /// </summary>
        /// <param name="dataDefinition"></param>
        /// <param name="dataField"></param>
        protected void SetData<TDefinitionType, TDataType>(TDefinitionType dataDefinition, TDataType dataField)
        {
            //TDataType oldValue = GetData<TDefinitionType, TDataType>(dataDefinition);
//            Console.WriteLine("{0}++++ {1}.SetData ({2})={3}=>{4}", DateTime.Now.ToString("HH:mm:ss fff"), this.GetType(), dataDefinition, oldValue, dataField);

            if (!InternalSetData(dataDefinition, dataField))
                throw new StateEngineException("Unable to set data " + dataDefinition);
        }

        /// <summary>
        /// Gets data from the definition.
        /// </summary>
        /// <param name="dataDefinition"></param>
        /// <returns></returns>
        public TDataType GetData<TDefinitionType, TDataType>(TDefinitionType dataDefinition)
        {
            TDataType result;
            if (InternalGetData(dataDefinition, out result))
            {
                //Console.WriteLine("---- {0}.GetData ({1})={2}", this.GetType(), dataDefinition, result);
                return result;
            }
            else
                throw new StateEngineException("Unable to get data " + dataDefinition);
        }

        #endregion

        #region Internal Methods

        private bool InternalSetData<TDefinitionType, TDataType>(TDefinitionType dataDefinition, TDataType dataField)
        {
            if (dataDict.ContainsKey(dataDefinition))
            {
                //Validate the type of the value that is already there.
                //It may 
                //  1. Be of the exact same type as the data was defined
                //  2. It may be a descendent class to the defined. 
                object initialValue = dataDefinitionDict[dataDefinition];
                if (dataDefinitionType[dataDefinition].IsAssignableFrom(typeof(TDataType)))
                    dataDict[dataDefinition] = dataField;
                else
                    throw new InvalidCastException(string.Concat("Cannot set data with definition ", dataDefinition, " as type ", typeof(TDataType).Name, "; should be ", initialValue.GetType().Name));

                return true;
            }
            else if (parentStateData != null)
            {
                return parentStateData.InternalSetData(dataDefinition, dataField);
            }
            else
            {
                return false;
            }
        }

        private bool InternalGetData<TDefinitionType, TDataType>(TDefinitionType dataDefinition, out TDataType dataField)
        {
            if (dataDict.ContainsKey(dataDefinition))
            {
                object value = dataDict[dataDefinition];
                Type type = dataDefinitionType[dataDefinition];

                //It's ok to get data as long the data is of the requested type.
                if (type.Equals(typeof(TDataType)))
                {
                    dataField = (TDataType)value;
                    return true;
                }
                else
                {
                    throw new InvalidCastException(string.Concat("Cannot get data with definition ", dataDefinition,
                                                         " as type ", typeof(TDataType).Name, "; is ",
                                                         value.GetType().Name));
                }
            }
            else if (parentStateData != null)
            {
                return parentStateData.InternalGetData(dataDefinition, out dataField);
            }
            else
            {
                dataField = default(TDataType);
                return false;
            }
        }

        /// <summary>
        /// Used from the different generic data states to handle fetch the Composite state that contains the state data that this state should use.
        /// </summary>
        /// <typeparam name="TData"></typeparam>
        /// <param name="state"></param>
        /// <returns></returns>
        internal static IStateWithData GetParentCompositeStateWithStateData<TData>(State state) where TData : class
        {
            CompositeState compositeState = state.ParentState;
            while (compositeState != null)
            {
                IStateWithData stateWithData = compositeState as IStateWithData;
                if (stateWithData != null)
                {
                    if (stateWithData.StateData is TData)
                    {
                        return stateWithData;
                    }
                }

                //Search down the inheritance line.
                compositeState = compositeState.ParentState;
            }

            throw new StateEngineException("State with state data of type " + typeof(TData) + " is not contained in any matching composite state.");
        }
        #endregion

        /// <summary>
        /// Dispose 
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {

        }

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}