// == LICENSE INFORMATION ==
/*
 * First author tiritomato 2013.
 * This program is distributed under the GNU General Public License(GPL).
 * support blog (Japanese only) http://d.hatena.ne.jp/tiri_tomato/
 */
// == LICENSE INFORMATION ==

using System;
using System.Collections.Generic;
using System.Text;

namespace UVTexIntegra.Scripting
{
    //! @addtogroup UVTexIntegra-Scripting名前空間
    //! @{

    //! @brief ScriptObject,LoadedAssembly,またはCompiledAssemblyのコレクションを管理するクラス。
    public class AssemblyCollection : System.IDisposable
    {
		// public properties

        //! @brief 編集ターゲットの表示名配列を返します。 @details 要素の並びはEditTargetsと対応しています。GetDisplayNames( EditTargets ) と等しい実装です。
		public System.String[] DisplayNames { get { return GetDisplayNames( EditTargets ); } }

        //! @brief 編集ターゲットになるオブジェクト配列を返します。
		//! @details ひとつの要素（編集ターゲット）にはそれぞれScriptObject,LoadedAssembly,CompiledAssemblyのいずれかが格納されています。要素の並びはDisplayNamesと対応しています。
        public IScriptObject[] EditTargets
        {
            get
            {
                lock (m_assemblies)
                {
                    System.Collections.Generic.List<IScriptObject> ret = new System.Collections.Generic.List<IScriptObject>();
                    foreach (LoadedAssembly assembly in m_assemblies)
                    {
                        if (assembly == null) continue;
                        System.Collections.ObjectModel.ReadOnlyCollection<Scripting.ScriptMain> objects = assembly.Objects;
                        if (objects.Count <= 0) ret.Add(assembly);
                        else foreach (Scripting.ScriptMain instance in objects) ret.Add(new Scripting.ScriptObject(assembly, instance, m_config));
                    }
                    return ret.ToArray();
                }
            }
        }

        //! @brief コンストラクタ
        public AssemblyCollection(IBakeTextureConfig config)
        {
            m_config = config;
            m_assemblies = new SortedSet<LoadedAssembly>(new LoadedAssembly.FilePathComparer());
        }

		//! @brief コンストラクタ @details シリアライズ用の情報リストを元にコレクションを構築します。
		//! アセンブリ要素の生成時に、読み込みまたはコンパイルに失敗する場合は、例外をスルーすることなく、単にコレクションには加えずに無視します。
        public AssemblyCollection(IBakeTextureConfig config, System.Collections.Generic.IEnumerable<LoadedAssembly.SerializeInformation> src)
            : this(config)
        {
            if (src == null) return;
            foreach (LoadedAssembly.SerializeInformation info in src)
            {
                if (info == null) continue;
                LoadedAssembly load_assembly = null;
                try { load_assembly = info.Load(); }
                catch (Scripting.Exception) { load_assembly = null; }
                if (load_assembly == null)
                {
                    System.Windows.Forms.MessageBox.Show(info.FilePath + "のロードまたはコンパイルに失敗しました。");
                    continue;
                }
                if ((info.SerializedCollection != null) && (info.SerializedCollection.Length > load_assembly.Objects.Count))
                {
                    System.Text.StringBuilder sb = new StringBuilder();
                    sb.AppendLine(info.FilePath);
                    sb.AppendLine(String.Format("{0}個のロードできない型がありました。", info.SerializedCollection.Length - load_assembly.Objects.Count));
                    System.Windows.Forms.MessageBox.Show(sb.ToString());
                }
                m_assemblies.Add(load_assembly);
            }
        }

        //! @brief コンストラクタ @details シリアライズ文字列をもとにアセンブリコレクションの状態を復元します。
        //! シリアライズ文字列は、ToSerializedString() メソッドによって生成される文字列です。
        //! アセンブリ要素の生成時に、読み込みまたはコンパイルに失敗する場合は、コレクションには加えられません。
        public AssemblyCollection(IBakeTextureConfig config, System.String serialized)
            : this(config)
		{
			if (System.String.IsNullOrEmpty(serialized)) return;

			System.IO.MemoryStream stream = new System.IO.MemoryStream();
			LoadedAssembly.SerializeInformation[] src = null;
			try
            {
				System.Byte[] arry = System.Convert.FromBase64String(serialized);
				System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
				stream.Write( arry, 0, arry.Length );
                stream.Position = 0;
                src = bf.Deserialize(stream) as LoadedAssembly.SerializeInformation[];
			}
            catch (System.Exception) { src = null; }
			finally { stream.Dispose(); stream = null; }

			if (src == null) return;

			foreach (LoadedAssembly.SerializeInformation info in src)
            {
				if ( info == null ) continue;
                LoadedAssembly load_assembly = null;
                try { load_assembly = info.Load(); }
                catch (Scripting.Exception) { load_assembly = null; }
                if ( load_assembly == null ) {
					System.Windows.Forms.MessageBox.Show(info.FilePath + "のロードまたはコンパイルに失敗しました。");
					continue;
				}
                if ((info.SerializedCollection != null) && (info.SerializedCollection.Length > load_assembly.Objects.Count))
                {
					System.Text.StringBuilder sb = new StringBuilder();
					sb.AppendLine(info.FilePath);
                    sb.AppendLine(String.Format("{0}個のロードできない型がありました。", info.SerializedCollection.Length - load_assembly.Objects.Count));
					System.Windows.Forms.MessageBox.Show(sb.ToString());
				}
				m_assemblies.Add(load_assembly);
			}
		}

        // public methods

        //! @brief パスを指定してLoadedAssemblyを新規に生成します。
        //! @details ファイル（*.dll/*.cs/*.cpp/*.c/*.vb/*.js）を指定します。dllの場合はすぐにロードされますが、それ以外の場合は情報だけ生成され明示的なRebuild()メソッドを待機する状態になっています。
        //! @exception FileNotFoundException ファイルが存在しません。
        //! @exception ArgumentException 同じパスのアセンブリ情報が既にリストに登録されています。
        //! @exception LoadException 新規にDLLをロードしようとしましたが、エラーが生じています。
        //! @return 既存のアイテムまたは新規に作成されたLoadedAssembly（dllの場合はLoadedAssembly、それ以外の場合はCompiledAssembly）。
        public LoadedAssembly AddAssembly(System.String path)
        {
            lock (m_assemblies)
            {
                if (System.IO.File.Exists(path) == false) throw new FileNotFoundException(path);
                
                // check old instance
                LoadedAssembly ret = _GetAssembly(path);
                if (ret != null) throw new ArgumentException();
                
                m_assemblies.Add(ret = CompiledAssembly.From(path));
                return ret;
            }
        }

        //! @brief パスを指定して、ロード済みのLoadAssembly要素があるか判定します。
        public bool Contains(System.String path) { lock (m_assemblies) return _GetAssembly(path) != null; }

        //! @brief 内部のスクリプトインスタンスを全てDispose()します
        public void Dispose() { Dispose(true); System.GC.SuppressFinalize(this); }

        //! @brief インデックスを指定して編集ターゲットを取得します。
        //! @param [in] index インデックスは、EditTargetsの返すコレクションと同じ並び順でのインデックスです。
        //! @return IScriptObjectには、ScriptObject,LoadedAssembly,CompiledAssemblyのいずれかが格納されます。indexが範囲外の時はnullが返ります。
        public IScriptObject EditTarget(int index)
        {
            lock (m_assemblies)
            {
                int ct = -1;
                foreach (LoadedAssembly assembly in m_assemblies)
                {
                    if (assembly == null) continue;
                    System.Collections.ObjectModel.ReadOnlyCollection<Scripting.ScriptMain> objects = assembly.Objects;
                    if (objects.Count <= 0)
                    {
                        if (++ct == index) return assembly;
                    }
                    else
                    {
                        foreach (Scripting.ScriptMain instance in objects)
                        {
                            if (++ct == index) return new Scripting.ScriptObject(assembly, instance, m_config);
                        }
                    }
                }
                return null;
            }
        }

        //! @brief 編集ターゲットの表示名配列を取得します。
		//! @param src オブジェクト配列。ひとつの要素（編集ターゲット）にはIDisplayNameを実装するインスタンスを指定してください。
		//! @return IDisplayName::DisplayNameプロパティの返す値が格納されたString配列。srcがnullまたは空の場合、空のString配列が返ります
		//! @details 返されるString配列数はsrcの配列数と必ず同じです。IDisplayName::DisplayNameが取得できない時、返されるString配列の対応する要素にはnullがセットされています。
		public static System.String[] GetDisplayNames(IScriptObject[] src)
        {
			int ct = 0; if (src != null) ct = src.Length;
			System.Collections.Generic.List<System.String> ret = new System.Collections.Generic.List<System.String>(ct);
            if (src != null)
            {
                foreach (IScriptObject obj in src)
                {
                    if (obj == null) ret.Add(null); else ret.Add(obj.DisplayName);
                }
            }
			return ret.ToArray();
        }

        //! @brief パスを指定して、ロード済みのLoadedAssembly要素を検索、取得します。見つからない場合はnullを返します（例外はスローされません）。
        public LoadedAssembly GetAssembly(System.String path) { lock (m_assemblies) return _GetAssembly(path); }

        //! @brief ロード済みのLoadedAssembly要素を削除します。
        //! @return 見つかればtrue、見つからない時はfalseを返します。
        //! @details この処理をしてもDLLはロード状態が維持されます。例えばメモリ上にコンパイルされたソースコードであれば、
        //! たとえ一時ファイルが生成されていようとユーザーから見えない場所にある時点で実質的にアンロードする必要がありません。
        //! また参照を破棄して別の一時ファイルとして再コンパイルすることも可能です。
        //! しかし特定のDLLファイルからロードしている場合はロード状態の解除は出来ないため、メタセコの実行中はずっとDLLへのリンクが継続することを通達すべきケースがあり得ます。
        public bool RemoveAssembly(LoadedAssembly assembly) { return m_assemblies.Remove(assembly); }

        //! @brief 現在のアセンブリリストの状態をシリアライズリストに変換し、さらにひとつの文字列にシリアライズします。
        public System.String ToSerializedString()
		{
			LoadedAssembly.SerializeInformation[] arry = ToSerializeInfoArray();
			if (arry == null) return null;
            using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
            {
				System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
				bf.Serialize(stream, arry);
				return System.Convert.ToBase64String(stream.ToArray());
            }
		}

        //! @brief 現在のアセンブリリストからシリアライズリストを生成します。
		public LoadedAssembly.SerializeInformation[] ToSerializeInfoArray()
		{
			if (m_assemblies == null) return null;
			System.Collections.Generic.List<LoadedAssembly.SerializeInformation> ret = new System.Collections.Generic.List<LoadedAssembly.SerializeInformation>( m_assemblies.Count );
			foreach (LoadedAssembly loadedAsm in m_assemblies)
            {
				LoadedAssembly.SerializeInformation si = loadedAsm.SerializeInfo;
				if ( si != null ) ret.Add(si);
			}
			return ret.ToArray();
		}

        // protected implements
        ~AssemblyCollection() { Dispose(false); }
        protected virtual void Dispose(bool ExplicitDispose) {
			lock (m_assemblies)
            {
			    foreach (LoadedAssembly assembly in m_assemblies) if (assembly != null) assembly.Dispose();
			    m_assemblies.Clear();
            }
        }

        // private fields
        private readonly System.Collections.Generic.SortedSet<LoadedAssembly> m_assemblies;
        private readonly IBakeTextureConfig m_config;

        private LoadedAssembly _GetAssembly(System.String path)
        {
            System.String fullPath = null;
            try { fullPath = System.IO.Path.GetFullPath(path); }
            catch (System.Exception) { fullPath = path; }
            foreach (LoadedAssembly assembly in m_assemblies)
            {
                if (assembly == null) continue;
                if (System.String.Compare(fullPath, assembly.AbsPath, System.StringComparison.OrdinalIgnoreCase) == 0) return assembly;
            }
            return null;
        }
    }
    
    //! @}
}
