using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;
using nft.util;
using nft.framework.loader;

namespace nft.framework.plugin
{
	public delegate void ParseEventHandler(Plugin p, ParamsReader e);
    public delegate void InitCompleteEventHandler();

	/// <summary>
	/// Loads plug-ins.
	/// </summary>
	public class PluginManager
	{
        public static readonly int SkipTooMenyErrorsCount = 5;

        /// <summary> The singleton instance. </summary>
		public static PluginManager theInstance;

		/// <summary>
		/// Called before a contribution parsed.
		/// </summary>
		public ParseEventHandler BeforeContributionParse;

		/// <summary>
		/// Plugins keyed by their names.
		/// </summary>
        private readonly Dictionary<VPluginId, Plugin> pluginMap = new Dictionary<VPluginId, Plugin>();
		
		/// <summary>
		/// Contribution factories that are used to load contributions.
		/// </summary>
        private readonly Dictionary<string, IContributionFactory> contributionFactories = new Dictionary<string, IContributionFactory>();

		/// <summary>
		/// Contributions keyed by their IDs.
		/// </summary>
        private readonly Dictionary<VContributionId, Contribution> contributionMap = new Dictionary<VContributionId, Contribution>();

		/// <summary>
		/// Contribution Types keyed by their CtbTypes (string specified a Type of Contribution).
		/// </summary>
		private readonly Hashtable ctbTypeMap = new Hashtable();

		private int errCount=0;

		public PluginManager() {
			theInstance = this;
            object binder = PluginSerializationBinder.theInstance; // initialize
		}

		/// <summary>
		/// This method should be called after the object is created.
		/// </summary>
		/// <param name="dirs">
		/// collection of strings (directory names)
		/// for each directory in this collection, its sub-directories
		/// are scanned for plugin.xml
		/// </param>
		public void init( ICollection dirs, ProgressMonitor monitor ) 
		{
			errCount=0;
			IList<Plugin> pluginSet=SeekPluginDirectories(dirs,monitor);

            Plugin[] plugins = SolveDependency(pluginSet, monitor);
            LoadContributions(plugins, monitor);
            InitContributions(monitor);
            DisposeReaders();
//			if( errCount!=0 ) {
//				// error during the initialization
//				Environment.Exit(errCount);
//			}
		}

		// called from Plugin on initialization
		internal void NotifyStartParse(Plugin p, ParamsReader e)
		{
			if( BeforeContributionParse!=null )
				BeforeContributionParse(p,e);
		}
	
		#region initialization processes
        private IList<Plugin> SeekPluginDirectories(ICollection directories, ProgressMonitor monitor) {
            List<Plugin> pluginSet = new List<Plugin>();
            monitor.Progress(1, 1, "プラグインを検索中");
            string[][] subdirs = new string[directories.Count][];
            int n = 0;
            int count = 0;
            // pre-search subdirectories to count total numbers.
            foreach (string dir in directories) {
                subdirs[n] = Directory.GetDirectories(dir);
                count += subdirs[n++].Length;
            }
            monitor.SetMaximum(2, count);

            n = 0;
            foreach (string dir in directories) {
                ProcessSubDirectories(pluginSet, dir, subdirs[n++], monitor);
            }
            return pluginSet;
        }
        private void ProcessSubDirectories(IList<Plugin> pluginSet, string parent, string[] subdirs, ProgressMonitor monitor)
		{
			foreach( string dir in subdirs )
			{								
				monitor.Progress(2,1,dir);
                string path = Path.Combine(parent, dir);
                string filepath = Path.Combine(path, Plugin.PluginFileName);
                FileInfo info = new FileInfo(filepath);
                if (!info.Exists)
					continue;	// this directory doesn't have the plugin.xml file.

				Plugin p = null;
				try 
				{                    
                    p = new Plugin(new Uri(path),info.LastWriteTime);
                    XmlDocument doc = XmlUtil.LoadFile(filepath);
                    XmlParamParser parser = new XmlParamParser(doc);
                    ParamsReader reader = new ParamsReader(path, parser);
					pluginSet.Add( p );
                    p.LoadParams(reader["plug-in"]);
					if( pluginMap.ContainsKey(p.PluginID) ) 
					{
						p._state = InstallationState.FatalError;
						// loaded more than once
						// maybe same subdir name in different plugin dirs.
						throw new Exception( string.Format(
							"プラグイン「{0}」は{1}と{2}の二箇所からロードされています",
							p.ID, p.Uri, (pluginMap[p.PluginID]).Uri) );
					}
					pluginMap.Add( p.PluginID, p );
				} 
				catch( Exception e ) 
				{
					Debug.WriteLine(e.Message);
					if(p!=null)
						p._state = InstallationState.FatalError;

                    string msg = I18n.F("Failed to load plug-in:{0}.",Path.GetFileName(dir));
					if(ReportError(msg+"\n"+e.Message,e))
                        throw;
				}
			}			
		}

        private Plugin[] SolveDependency(IList<Plugin> pluginSet, ProgressMonitor monitor)
		{// convert it to an array by sorting them in the order of dependency
			monitor.Progress(1,1,"依存関係を整理中");
			monitor.SetMaximum(2,pluginSet.Count);

			Plugin[] plugins = new Plugin[pluginSet.Count];
			int ptr=0;
            
            // Add system default plugin to the order-list at first.
            Plugin p = GetPlugin(VPluginId.System);
            monitor.Progress(2, 1, p.ID.AsString);
            plugins[ptr++] = p;
            pluginSet.Remove(p);
            // Loop until pluginSet becomes empty.
			while( pluginSet.Count>0 ) 
			{
				p = pluginSet[0];
				monitor.Progress(2,1,p.ID.AsString);
				try 
				{
                    Plugin pr;
                    while(true) 
					{
                        List<Plugin> deps = GetDependencies(p, p._reader);
                        pr = null;
                        // Pick a dependent plugin from remaining-list (if exist).
                        foreach (Plugin p2 in deps) {
                            if (pluginSet.Contains(p2)) {
                                pr = p2;
                                break;
                            }
                        }
                        if (pr == null) {
                            // exit inner while loop to add current 'p' to the orde-list.
                            break;
                        } else {
                            // 'pr' is the one to load earlier, so check dependency of 'pr' next.
                            p = pr;
                        }
					}
				} 
				catch( Exception e ) 
				{					
					if(ReportError(e.Message,e))
                        throw;
				}
                // Add 'p' to order-list and remove from remaining-list
				pluginSet.Remove(p);
				plugins[ptr++] = p;
			}
            return plugins;
		}

		private void LoadContributions(Plugin[] plugins, ProgressMonitor monitor)
		{
			//	 load all the contributions
			monitor.Progress(1,1,"コントリビューションをロード中");
			monitor.SetMaximum(2,plugins.Length*2);

			foreach( Plugin p in plugins ) 
			{
				monitor.Progress(2,1,p.ID.AsString);
				try 
				{
					LoadBinaries(p, p._reader);
                    LoadCtbDeclarations(p, p._reader);
				} 
				catch( Exception e ) 
				{
					if(ReportError(e.Message,e))
                        throw;
				}
			}

			foreach( Plugin p in plugins ) 
			{
				monitor.Progress(2,1,p.ID.AsString);
				try 
				{
					// this will call AddContribution method
					LoadContributions(p, p._reader);
                } 
				catch( Exception e ) 
				{
                    p._state = InstallationState.FatalError;
					if(ReportError(e.Message,e))
                        throw;
				}
			}
		}

		private void InitContributions(ProgressMonitor monitor)
		{
			monitor.Progress(1,1,"コントリビューションを初期化中");
            IList nonPrimitives = new List<Contribution>(contributionMap.Count);
            monitor.SetMaximum(2, contributionMap.Count);
            Hashtable handlers = new Hashtable();
            // Initialize all primitive contirbutions before Non primitive contributions.
            foreach (Contribution contrib in contributionMap.Values) {
                if(!contrib.IsPrimitive){
                    nonPrimitives.Add(contrib);
                    continue;
                }
                try {
                    InitCompleteEventHandler h = contrib.Initialize();
                    if (h != null) {
                        handlers.Add(contrib, h);
                    } else {
                        contrib.PrepareCacheData(false);
                        contrib._state = InstallationState.Ready;
                        monitor.Progress(2, 1, contrib.ID.AsString);
                    }
                } catch (Exception e) {
                    contrib._state = InstallationState.FatalError;
                    string msg = I18n.F("Failed to initialize contribution[{1}] in the plug-in:{0}.",
                        contrib.Parent.ID, "contrib.name", contrib.ID);
                    if(ReportError(msg, e))
                        throw;
                }
            }
            foreach (Contribution contrib in nonPrimitives) 
			{
				try 
				{
                    InitCompleteEventHandler h = contrib.Initialize();
                    if (h != null) {
                        handlers.Add(contrib,h);
                    } else {
                        contrib.PrepareCacheData(false);
                        contrib._state = InstallationState.Ready;
                        monitor.Progress(2, 1, contrib.ID.AsString);
                    }
				} 
				catch( Exception e ) 
				{
					contrib._state = InstallationState.FatalError;
                    string msg = I18n.F("Failed to initialize contribution[{1}] in the plug-in:{0}.",
                        contrib.Parent.ID, "contrib.name", contrib.ID);
					if(ReportError(msg,e))
                        throw;
				}			
			}
            foreach (Contribution contrib in handlers.Keys) {
                InitCompleteEventHandler h = handlers[contrib] as InitCompleteEventHandler;
                try {
                    h();
                    contrib.PrepareCacheData(false);
                    contrib._state = InstallationState.Ready;
                    monitor.Progress(2, 1, contrib.ID.AsString);
                } catch (Exception e) {
                    contrib._state = InstallationState.FatalError;
                    string msg = I18n.F("Failed to initialize contribution[{1}] in the plug-in:{0}.",
                        contrib.Parent.ID, "contrib.name", contrib.ID);
                    if(ReportError(msg, e))
                        throw;
                }
            }
		}

        private void DisposeReaders() {
            foreach (Plugin p in pluginMap.Values) {
                IDisposable pr = p._reader as IDisposable;
                p._reader = null;
                if (pr!=null) {
                    pr.Dispose();
                }
            }
        }

        /// <summary>
        /// Show Plugin Error Form. Should throw again when true returns.
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="e"></param>
        /// <returns></returns>
		internal bool ReportError(string msg, Exception e)
		{
            errCount++;
            if (errCount < SkipTooMenyErrorsCount) {
                return UIUtil.ShowException(msg, e, UIInformLevel.minor);
            } else {
                if (errCount == SkipTooMenyErrorsCount) {
                    string templ = I18n.T("Too many plug-in load errors! Check out remaining errors in the 'Plug-in List'.");
                    Exception ex = new Exception(templ,e);
                    UIUtil.ShowException(templ, ex, UIInformLevel.minor);
                }
                return false; // skip too many
            }
		}
		#endregion

		/// <summary>
		/// Gets the default plug-in directory.
		/// </summary>
		/// <returns></returns>
		public static string getDefaultPluginDirectory() 
		{
			return Directories.PluginDir;
		}

		#region Loading contributions for eacn Plugins.
        /// <summary>
        /// Get all the dependent plug-ins.
        /// called from PluginManager before initialize this plugin.
        /// </summary>
        public List<Plugin> GetDependencies(Plugin p, ParamsReader _reader) {
            List<Plugin> a = new List<Plugin>();
            foreach (ParamsReader depend in _reader.EnumChildren("depend")) {
                string name = depend["on"].InnerText;
                Plugin p2 = GetPlugin(VPluginId.FromString(name));
                if (p2 == null) {
                    string msg = I18n.F("Dependent plug-in:{1} (required for the plug-in:{0}) is not found.", p.ID, name);
                    throw new Exception(msg);
                }
                a.Add(p2);
            }
            return a;
        }

        /// <summary>
        /// Load declaration of contributions from this plug-in
        /// </summary>
        internal void LoadCtbDeclarations(Plugin p, ParamsReader _reader) {
            // locate contribution factories first,
            // because we'll need them to load contributions.
            foreach (ParamsReader ctb_reader in _reader.EnumChildren("declare-contribution")) {
                //string type = ctb_reader["type"].InnerText;
                try {
                    Contribution cb = null;
                    // load a contribution factory
                    NotifyStartParse(p, ctb_reader);
                    string regtype= ctb_reader["type|name"].InnerText;
                    object[] args = new object[] { ctb_reader };
                    // create and register contribution factory
                    IContributionFactory factory =
                        PluginUtil.createCtbFactory(p, ctb_reader);
                    AddContributionFactory(regtype, factory);
                    cb = factory as Contribution;
                    // Register dummy contribution (in order to list on Dialog).
                    if (cb == null) {
                        cb = new CtbContributionDefiner(p, ctb_reader);
                    }
                    
                    cb.Attach();
                    cb._state = InstallationState.Ready;
                } catch (Exception e) {
                    p._state = InstallationState.FatalError;
                    string msg = MakeContribExceptionMessage(p, ctb_reader);
                    throw new Exception(msg, e);
                }
            }
        }

        /// <summary>
        /// Loads class type contributions from this plug-in
        /// </summary>
        internal void LoadBinaries(Plugin p, ParamsReader _reader) {
            // locate contribution factories first,
            // because we'll need them to load contributions.
            foreach (ParamsReader ctb_reader in _reader.EnumChildren("contribution")) {
                string type = ctb_reader["type"].InnerText;
                try {
                    Contribution cb = null;
                    // load binary module contribution.
                    if ("binary".Equals(type)) {
                        NotifyStartParse(p, ctb_reader);
                        cb = PluginUtil.createContributionObject(p, ctb_reader) as BinaryModule;
                        if (cb == null) {
                            // create dummy module for list Dialog
                            cb = new BinaryModule(p, ctb_reader);
                        }
                    } else {
                        continue;
                    }
                    cb.Attach();
                    cb._state = InstallationState.Ready;
                } catch (Exception e) {
                    p._state = InstallationState.FatalError;
                    string msg = MakeContribExceptionMessage(p, ctb_reader);
                    throw new Exception(msg, e);
                }
            }
        }

        /// <summary>
        /// Loads contributions from this plug-in
        /// </summary>
        internal void LoadContributions(Plugin p, ParamsReader _reader) {
            Contribution c = null;
            int count = 0;
            int errors = 0;
            // load contributions
            foreach (ParamsReader ctb_reader in _reader.EnumChildren("contribution")) {
                try {
                    count++;
                    string type = ctb_reader["type"].InnerText;
                    if ("binary".Equals(type) /* || "factory".Equals(type)*/) continue;	// ignore

                    NotifyStartParse(p, ctb_reader);
                    IContributionFactory factory = GetContributionFactory(type);
                    c = factory.load(p, ctb_reader);
                    c.Attach();
                    if (c.IsDetachable) {
                        p.AssociateAddableContribution(c);
                    }
                    AddContribution(c);
                    c._state = InstallationState.Ready;
                } catch (Exception e) {
                    errors++;
                    Debug.WriteLine(e.Message);
                    Debug.WriteLine(e.StackTrace);
                    if (e.InnerException != null) {
                        Debug.WriteLine(e.InnerException.Message);
                        Debug.WriteLine(e.InnerException.StackTrace);
                    }
                    if (c != null)
                        c._state = InstallationState.FatalError;
                    string msg = MakeContribExceptionMessage(p, ctb_reader);
                    if (p._state != InstallationState.FatalError)
                        p._state = InstallationState.PartialError;
                    if(ReportError(msg, e))
                        throw;
                }
            }
            if (p._state != InstallationState.FatalError && errors == count) {
                p._state = InstallationState.FatalError;
            } else if (p._state == InstallationState.Uninitialized) {
                p._state = InstallationState.Ready;
            }
        }

        private string MakeContribExceptionMessage(Plugin p, ParamsReader pr) {
            string _id = pr["id"].InnerTextOr("unknown");
            string _name = pr["name"].InnerTextOr("unknown");
            string msg = I18n.F("Failed to load contribution[{1}] (ID={2}) from the file '{0}'.",
                p.Uri.AbsoluteUri, _name, _id);
            return msg;
        }

		/// <summary>
		/// Registers a <c>ContributionFactory</c>.
		/// This method has to be called before the initialization.
		/// Normally, this method is called by <c>Plugin</c> but the caller
		/// can invoke this method before calling the init method.
		/// </summary>
		public void AddContributionFactory( string name, IContributionFactory factory ) 
		{
            string anam = AdjustFactoryName(name);
            if (contributionFactories.ContainsKey(anam)) {
                string msg;
                if (anam.Equals(name)) {
                    msg = string.Format(
                    "contribution type \"{0}\" is already registered.", anam);
                } else {
                    msg = string.Format(
                    "contribution type \"{0}\" is already registered as \"{1}\".", name, anam);
                }
                throw new Exception(msg);
            }
            contributionFactories.Add(anam, factory);
		}

		// 
		public IContributionFactory GetContributionFactory( string name ) {
            string anam = AdjustFactoryName(name);
            IContributionFactory factory = null;
			if(contributionFactories.TryGetValue(anam,out factory))
                return factory;
            else
                throw new Exception(name + "は未知のコントリビューションです");
		}

        static string AdjustFactoryName(string orignam) {
            if(orignam==null) return null;
            return orignam.Trim().ToLower();
        }
		#endregion

		/// <summary>
		/// Lists up contributions of the given type.
		/// </summary>
        [Obsolete("Use EnumContributions")]
		public Array ListContributions( Type contributionType, bool hideDisabled ) 
		{
			ArrayList list = new ArrayList();
			foreach( Contribution contrib in contributionMap.Values) {
				if( contributionType.IsInstanceOfType(contrib) )
					if( !hideDisabled || contrib.IsAttached )
						list.Add(contrib);
			}
			return list.ToArray(contributionType);
		}

        public IEnumerable<U> EnumContributions<U>(bool hideDisabled) where U:Contribution {
            return new ContributionEnumerator<U>(hideDisabled);
        }
        public IEnumerable<U> EnumContributions<U>() where U : Contribution {
            return EnumContributions<U>(true);
        }

        public IEnumerable<Contribution> EnumContributions(Type t, bool hideDisabled) {
            if (!t.IsSubclassOf(typeof(Contribution))) {
                throw new ArgumentException("Mast be a subclass of Contribution.","t");
            }
            Type t2 = typeof(ContributionEnumerator<>);
            t2 = t2.MakeGenericType(t);
            return Activator.CreateInstance(t2, hideDisabled) as IEnumerable<Contribution>;
        }

		/// <summary>
		/// Gets all contributions.
		/// </summary>
		public IEnumerable<Contribution> Contributions {
			get {
				return new ContributionEnumerator<Contribution>(false);
			}
		}

        public IEnumerable<Plugin> Plugins {
            get {
                return new PluginEnumerator();
            }
        }

        public IEnumerable<KeyValuePair<string,IContributionFactory>> Factories {
            get {
                return new FactoryEnumerator();
            }
        }

		public void AddContribution( Contribution contrib ) {
			if(contributionMap.ContainsKey(contrib.ContributionID))
			{
				// TODO:
				Debug.WriteLine("Duplicate contribution id found:"+contrib.ID);
			}
			contributionMap.Add( contrib.ContributionID, contrib );
		}

		/// <summary>
		/// Gets the contribution with a given ID, or null if not found.
		/// </summary>
		public Contribution GetContribution( VContributionId id ) {
            Contribution ctb;
            if(contributionMap.TryGetValue(id, out ctb)){
                return ctb;
            } else {
                Debug.WriteLine("Contribution not found for the id = "+ id);
                return null;
            }
		}

		/// <summary>
		/// Get the plug-in of the specified name, or null if not found.
		/// </summary>
		public Plugin GetPlugin( VPluginId id ) 
		{
			return pluginMap[id];
		}

		public Type GetDefinedType( string ctbType )
		{
			IContributionFactory factory = GetContributionFactory(ctbType);
			if( factory!=null )
				return factory.OutputType;
			else
				return null;
		}

		public string GetInstallInfo()
		{
            if (pluginMap.Count == 0)
				return "";
			string output = "Installed Plugins (except for system plugins).";
			foreach(Plugin p in pluginMap.Values)
			{
				if(!p.ID.AsString.StartsWith("system"))
				{
					output+=Environment.NewLine;
					output+=string.Format("{0}[{1}] {2:yyyyMMdd-HHmm}",p.Title,p.ID,p.lastModifiedTime);
				}
			}
			return output;
		}


        internal class FactoryEnumerator : IEnumerable<KeyValuePair<string, IContributionFactory>>
        {
            public FactoryEnumerator() {
            }

            public IEnumerator<KeyValuePair<string, IContributionFactory>> GetEnumerator() {
                foreach (KeyValuePair<string,IContributionFactory> fct in PluginManager.theInstance.contributionFactories)
                    yield return fct;
            }

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

        internal class PluginEnumerator : IEnumerable<Plugin>
        {
            public PluginEnumerator() {
            }

            public IEnumerator<Plugin> GetEnumerator() {
                foreach (Plugin p in PluginManager.theInstance.pluginMap.Values)
                    yield return p;
            }

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

        internal class ContributionEnumerator<T> : IEnumerable<T> where T : Contribution
        {
            readonly Type enumtype;
            readonly bool hideDisabled;

            public ContributionEnumerator() : this(true) { }

            public ContributionEnumerator(bool hideDisable) {
                this.enumtype = this.GetType().GetGenericArguments()[0];
                this.hideDisabled = hideDisable;
            }

            public IEnumerator<T> GetEnumerator() {
                foreach (Contribution c in PluginManager.theInstance.contributionMap.Values) {
                    if (enumtype.IsInstanceOfType(c)) {
                        if (!hideDisabled || c.IsAttached)
                            yield return (T)c;
                    }
                }
            }

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

}
