using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace Ink.Runtime
{
	public class Container : Runtime.Object, INamedContent
	{
		public string name { get; set; }

        public List<Runtime.Object> content { 
            get {
                return _content;
            }
            set {
                AddContent (value);
            }
        }
        List<Runtime.Object> _content;

		public Dictionary<string, INamedContent> namedContent { get; set; }

        public Dictionary<string, Runtime.Object> namedOnlyContent { 
            get {
                var namedOnlyContentDict = new Dictionary<string, Runtime.Object>();
                foreach (var kvPair in namedContent) {
                    namedOnlyContentDict [kvPair.Key] = (Runtime.Object)kvPair.Value;
                }

                foreach (var c in content) {
                    var named = c as INamedContent;
                    if (named != null && named.hasValidName) {
                        namedOnlyContentDict.Remove (named.name);
                    }
                }

                if (namedOnlyContentDict.Count == 0)
                    namedOnlyContentDict = null;

                return namedOnlyContentDict;
            } 
            set {
                var existingNamedOnly = namedOnlyContent;
                if (existingNamedOnly != null) {
                    foreach (var kvPair in existingNamedOnly) {
                        namedContent.Remove (kvPair.Key);
                    }
                }

                if (value == null)
                    return;
                
                foreach (var kvPair in value) {
                    var named = kvPair.Value as INamedContent;
                    if( named != null )
                        AddToNamedContentOnly (named);
                }
            }
        }
            
        public bool visitsShouldBeCounted { get; set; }
        public bool turnIndexShouldBeCounted { get; set; }
        public bool countingAtStartOnly { get; set; }

        [Flags]
        public enum CountFlags
        {
            Visits         = 1,
            Turns          = 2,
            CountStartOnly = 4
        }
                
        public int countFlags
        {
            get {
                CountFlags flags = 0;
                if (visitsShouldBeCounted)    flags |= CountFlags.Visits;
                if (turnIndexShouldBeCounted) flags |= CountFlags.Turns;
                if (countingAtStartOnly)      flags |= CountFlags.CountStartOnly;

                // If we're only storing CountStartOnly, it serves no purpose,
                // since it's dependent on the other two to be used at all.
                // (e.g. for setting the fact that *if* a gather or choice's
                // content is counted, then is should only be counter at the start)
                // So this is just an optimisation for storage.
                if (flags == CountFlags.CountStartOnly) {
                    flags = 0;
                }

                return (int)flags;
            }
            set {
                var flag = (CountFlags)value;
                if ((flag & CountFlags.Visits) > 0) visitsShouldBeCounted = true;
                if ((flag & CountFlags.Turns) > 0)  turnIndexShouldBeCounted = true;
                if ((flag & CountFlags.CountStartOnly) > 0) countingAtStartOnly = true;
            }
        }

		public bool hasValidName 
		{
			get { return name != null && name.Length > 0; }
		}

		public Path pathToFirstLeafContent
		{
			get {
                if( _pathToFirstLeafContent == null )
                    _pathToFirstLeafContent = path.PathByAppendingPath (internalPathToFirstLeafContent);

                return _pathToFirstLeafContent;
			}
		}
        Path _pathToFirstLeafContent;

        Path internalPathToFirstLeafContent
        {
            get {
				var components = new List<Path.Component>();
                var container = this;
                while (container != null) {
                    if (container.content.Count > 0) {
                        components.Add (new Path.Component (0));
                        container = container.content [0] as Container;
                    }
                }
				return new Path(components);
            }
        }

		public Container ()
		{
            _content = new List<Runtime.Object> ();
			namedContent = new Dictionary<string, INamedContent> ();
		}

		public void AddContent(Runtime.Object contentObj)
		{
			content.Add (contentObj);

            if (contentObj.parent) {
                throw new System.Exception ("content is already in " + contentObj.parent);
            }

			contentObj.parent = this;

			TryAddNamedContent (contentObj);
		}

        public void AddContent(IList<Runtime.Object> contentList)
        {
            foreach (var c in contentList) {
                AddContent (c);
            }
        }

        public void InsertContent(Runtime.Object contentObj, int index)
        {
            content.Insert (index, contentObj);

            if (contentObj.parent) {
                throw new System.Exception ("content is already in " + contentObj.parent);
            }

            contentObj.parent = this;

            TryAddNamedContent (contentObj);
        }
            
		public void TryAddNamedContent(Runtime.Object contentObj)
		{
			var namedContentObj = contentObj as INamedContent;
			if (namedContentObj != null && namedContentObj.hasValidName) {
				AddToNamedContentOnly (namedContentObj);
			}
		}

		public void AddToNamedContentOnly(INamedContent namedContentObj)
		{
			Debug.Assert (namedContentObj is Runtime.Object, "Can only add Runtime.Objects to a Runtime.Container");
			var runtimeObj = (Runtime.Object)namedContentObj;
			runtimeObj.parent = this;

			namedContent [namedContentObj.name] = namedContentObj;
		}

        public void AddContentsOfContainer(Container otherContainer)
        {
            content.AddRange (otherContainer.content);
            foreach (var obj in otherContainer.content) {
                obj.parent = this;
                TryAddNamedContent (obj);
            }
        }

		protected Runtime.Object ContentWithPathComponent(Path.Component component)
		{
            if (component.isIndex) {

                if (component.index >= 0 && component.index < content.Count) {
                    return content [component.index];
                }

				// When path is out of range, quietly return nil
				// (useful as we step/increment forwards through content)
				else {
                    return null;
                }

            } 

            else if (component.isParent) {
                return this.parent;
            }

            else {
                INamedContent foundContent = null;
                if (namedContent.TryGetValue (component.name, out foundContent)) {
                    return (Runtime.Object)foundContent;
                } else {
                    return null;
                }
			}
		}

        public SearchResult ContentAtPath(Path path, int partialPathStart = 0, int partialPathLength = -1)
		{
            if (partialPathLength == -1)
                partialPathLength = path.length;

            var result = new SearchResult ();
            result.approximate = false;

            Container currentContainer = this;
            Runtime.Object currentObj = this;

            for (int i = partialPathStart; i < partialPathLength; ++i) {
				var comp = path.GetComponent(i);

                // Path component was wrong type
                if (currentContainer == null) {
                    result.approximate = true;
                    break;
                }

                var foundObj = currentContainer.ContentWithPathComponent(comp);

                // Couldn't resolve entire path?
                if (foundObj == null) {
                    result.approximate = true;
                    break;
                } 

                currentObj = foundObj;
                currentContainer = foundObj as Container;
            }

            result.obj = currentObj;

            return result;
		}
         
        public void BuildStringOfHierarchy(StringBuilder sb, int indentation, Runtime.Object pointedObj)
        {
            Action appendIndentation = () => { 
                const int spacesPerIndent = 4;
                for(int i=0; i<spacesPerIndent*indentation;++i) { 
                    sb.Append(" "); 
                } 
            };

            appendIndentation ();
            sb.Append("[");

            if (this.hasValidName) {
                sb.AppendFormat (" ({0})", this.name);
            }

            if (this == pointedObj) {
                sb.Append ("  <---");
            }

            sb.AppendLine ();

            indentation++;
            
            for (int i=0; i<content.Count; ++i) {

                var obj = content [i];

                if (obj is Container) {

                    var container = (Container)obj;

                    container.BuildStringOfHierarchy (sb, indentation, pointedObj);

                } else {
                    appendIndentation ();
                    if (obj is StringValue) {
                        sb.Append ("\"");
                        sb.Append (obj.ToString ().Replace ("\n", "\\n"));
                        sb.Append ("\"");
                    } else {
                        sb.Append (obj.ToString ());
                    }
                }

                if (i != content.Count - 1) {
                    sb.Append (",");
                }

                if ( !(obj is Container) && obj == pointedObj ) {
                    sb.Append ("  <---");
                }
                    
                sb.AppendLine ();
            }
                

            var onlyNamed = new Dictionary<string, INamedContent> ();

            foreach (var objKV in namedContent) {
                if (content.Contains ((Runtime.Object)objKV.Value)) {
                    continue;
                } else {
                    onlyNamed.Add (objKV.Key, objKV.Value);
                }
            }

            if (onlyNamed.Count > 0) {
                appendIndentation ();
                sb.AppendLine ("-- named: --");

                foreach (var objKV in onlyNamed) {

                    Debug.Assert (objKV.Value is Container, "Can only print out named Containers");
                    var container = (Container)objKV.Value;
                    container.BuildStringOfHierarchy (sb, indentation, pointedObj);

                    sb.AppendLine ();

                }
            }


            indentation--;

            appendIndentation ();
            sb.Append ("]");
        }

        public virtual string BuildStringOfHierarchy()
        {
            var sb = new StringBuilder ();

            BuildStringOfHierarchy (sb, 0, null);

            return sb.ToString ();
        }

	}
}

