/*
 * PackageOps.cs --
 *
 * Copyright (c) 2007-2012 by Joe Mistachkin.  All rights reserved.
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: $
 */

using System;
using System.IO;
using Eagle._Attributes;
using Eagle._Components.Private.Delegates;
using Eagle._Components.Public;
using Eagle._Constants;
using Eagle._Containers.Private;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;

namespace Eagle._Components.Private
{
    [ObjectId("e6e6c799-cbfd-4aa3-9017-c9944322a81c")]
    internal static class PackageOps
    {
        #region Version Checking Methods
        public static int VersionCompare(
            Version version1,
            Version version2
            )
        {
            if ((version1 != null) && (version2 != null))
                return version1.CompareTo(version2);
            else if ((version1 == null) && (version2 == null))
                return 0;        // x (null) is equal to y (null)
            else if (version1 == null)
                return -1;       // x (null) is less than y (non-null)
            else
                return 1;        // x (non-null) is greater than y (null)
        }

        ///////////////////////////////////////////////////////////////////////

        public static bool VersionSatisfies(
            Version version1,
            Version version2,
            bool exact
            )
        {
            if (exact)
                return (VersionCompare(version1, version2) == 0);
            else
                return (VersionCompare(version1, version2) >= 0);
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Class Factory Methods
        public static IPackage NewCore(
            string name,
            string group,
            string description,
            IClientData clientData,
            string indexFileName,
            string provideFileName,
            PackageFlags flags,
            Version loaded,
            VersionStringDictionary ifNeeded
            )
        {
            return new _Packages.Core(new PackageData(
                name, group, description, clientData, indexFileName,
                provideFileName, flags, loaded, ifNeeded, 0));
        }
        #endregion

        ///////////////////////////////////////////////////////////////////////

        #region Package Support Methods
        public static string GetRelativeFileName(
            Interpreter interpreter,               /* in */
            string name,                           /* in, script name */
            PathComparisonType pathComparisonType, /* in */
            bool verbose                           /* in */
            )
        {
            string fileName = null;
            Result error = null;

            if (GetRelativeFileName(
                    interpreter, name, pathComparisonType,
                    ref fileName, ref error) == ReturnCode.Ok)
            {
                return fileName;
            }
            else if (verbose)
            {
                TraceOps.DebugTrace(String.Format(
                    "{0}: GetRelativeFileName: interpreter = {1}, " +
                    "name = {2}, pathComparisonType = {3}, verbose = {4}, " +
                    "fileName = {5}, error = {6}{7}",
                    GlobalState.GetCurrentThreadId(),
                    FormatOps.WrapOrNull(interpreter),
                    FormatOps.WrapOrNull(name),
                    FormatOps.WrapOrNull(pathComparisonType), verbose,
                    FormatOps.WrapOrNull(fileName),
                    FormatOps.WrapOrNull(error),
                    Environment.NewLine), typeof(PackageOps).Name);
            }

            return null;
        }

        ///////////////////////////////////////////////////////////////////////

        public static ReturnCode GetRelativeFileName(
            Interpreter interpreter,               /* in */
            string name,                           /* in, script name */
            PathComparisonType pathComparisonType, /* in */
            ref string fileName,                   /* out */
            ref Result error                       /* out */
            )
        {
            if (interpreter == null)
            {
                error = "invalid interpreter";
                return ReturnCode.Error;
            }

            if (String.IsNullOrEmpty(name))
            {
                error = "invalid script name";
                return ReturnCode.Error;
            }

            StringList packageIndexFileNames;

            PackageIndexDictionary packageIndexes =
                interpreter.CopyPackageIndexes();

            if (packageIndexes == null)
            {
                error = "package indexes not available";
                return ReturnCode.Error;
            }

            //
            // NOTE: Sort the package index file names in order so
            //       that the deepest directories are listed first.
            //
            packageIndexFileNames = new StringList(packageIndexes.Keys);
            packageIndexFileNames.Sort(new _Comparers.FileName(
                pathComparisonType));

            string localFileName = PathOps.ResolveFullPath(interpreter, name);

            if (localFileName == null)
            {
                error = String.Format(
                    "failed to resolve full path of \"{0}\"",
                    name);

                return ReturnCode.Error;
            }

            string directory = PathOps.GetDirectoryName(localFileName);

            if (directory == null)
            {
                error = String.Format(
                    "failed to get directory name for \"{0}\"",
                    localFileName);

                return ReturnCode.Error;
            }

            directory = PathOps.AppendSeparator(directory);

            foreach (string packageIndexFileName in packageIndexFileNames)
            {
                string packageDirectory = PathOps.GetDirectoryName(
                    packageIndexFileName);

                if (String.IsNullOrEmpty(packageDirectory))
                    continue;

#if MONO || MONO_HACKS
                //
                // HACK: *MONO* The Mono call to Path.GetDirectoryName does not
                //       appear to convert the forward slashes in the directory
                //       name to backslashes as the .NET does; therefore, force
                //       a conversion by fully resolving the directory name, but
                //       only when running on Mono.
                //
                if (CommonOps.Runtime.IsMono())
                {
                    packageDirectory = PathOps.ResolveFullPath(interpreter,
                        packageDirectory);
                }
#endif

                packageDirectory = PathOps.AppendSeparator(packageDirectory);

                if (PathOps.IsEqualFileName(
                        packageDirectory, directory, packageDirectory.Length))
                {
                    fileName = PathOps.GetUnixPath(localFileName.Substring(
                        packageDirectory.Length));

                    return ReturnCode.Ok;
                }
            }

            error = String.Format(
                "package index matching directory \"{0}\" not found",
                directory);

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode FindHost(
            Interpreter interpreter,
            StringList paths, /* NOT USED */
            PackageIndexCallback callback,
            bool resolve,
            bool refresh,
            ref PackageIndexDictionary packageIndexes, /* in, out */
            ref Result error
            )
        {
            if (interpreter == null)
            {
                error = "invalid interpreter";
                return ReturnCode.Error;
            }

            if (interpreter.HasNoPackageIndexes())
                return ReturnCode.Ok;

            if (paths == null) /* NOT USED */
            {
                error = "invalid paths";
                return ReturnCode.Error;
            }

            //
            // NOTE: Initialize the package index collection if
            //       necessary.
            //
            if (packageIndexes == null)
                packageIndexes = new PackageIndexDictionary();

            //
            // NOTE: The file name we are interested in is the host
            //       package index, named "pkgIndex".
            //
            string fileName = FileName.PackageIndex;

            //
            // NOTE: Initially mark the host package index as
            //       "not found".  After the main search (below), if
            //       the host package index is still marked "not found"
            //       it will be purged.
            //
            if (MarkIndex(
                    packageIndexes, fileName, PackageIndexFlags.Host,
                    PackageIndexFlags.Normal, PackageIndexFlags.Found,
                    true, false, false, ref error) != ReturnCode.Ok)
            {
                return ReturnCode.Error;
            }

            //
            // NOTE: Have we seen this package index before?
            //
            bool exists = packageIndexes.ContainsKey(fileName);

            //
            // NOTE: If we are refreshing package indexes or we have
            //       never seen this package index before, notify the
            //       caller.
            //
            if (refresh || !exists)
            {
                interpreter.SetNoPackageIndexes(true);

                try
                {
                    Result result = null;

                    if ((callback != null) &&
                        (callback(interpreter, fileName,
                            true, resolve, refresh,
                            ref result) != ReturnCode.Ok))
                    {
                        error = result;
                        return ReturnCode.Error;
                    }
                }
                finally
                {
                    interpreter.SetNoPackageIndexes(false);
                }
            }

            //
            // NOTE: If we have not seen this package index before add
            //       it to the resulting collection now; otherwise,
            //       mark it as "found" so that it will not be purged.
            //
            if (exists)
                packageIndexes[fileName] |=
                    PackageIndexFlags.Host | PackageIndexFlags.Found;
            else
                packageIndexes.Add(fileName,
                    PackageIndexFlags.Host | PackageIndexFlags.Found);

            //
            // NOTE: Purge the host package index from the list if it
            //       is still marked as "not found".
            //
            if (PurgeIndex(
                    packageIndexes, fileName, PackageIndexFlags.Host,
                    PackageIndexFlags.Normal | PackageIndexFlags.Found,
                    true, false, ref error) != ReturnCode.Ok)
            {
                return ReturnCode.Error;
            }

            return ReturnCode.Ok;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode FindFile(
            Interpreter interpreter,
            StringList paths,
            PackageIndexCallback callback,
            bool resolve,
            bool refresh,
            ref PackageIndexDictionary packageIndexes, /* in, out */
            ref Result error
            )
        {
            if (interpreter == null)
            {
                error = "invalid interpreter";
                return ReturnCode.Error;
            }

            if (interpreter.HasNoPackageIndexes())
                return ReturnCode.Ok;

            if (paths == null)
            {
                error = "invalid paths";
                return ReturnCode.Error;
            }

            //
            // NOTE: Initialize the package index collection if
            //       necessary.
            //
            if (packageIndexes == null)
                packageIndexes = new PackageIndexDictionary();

            //
            // NOTE: Initially mark all package indexes as "not found".
            //       After the main search loop (below), any remaining
            //       package indexes that are still marked "not found"
            //       will be purged.
            //
            if (MarkIndexes(
                    packageIndexes, PackageIndexFlags.Normal,
                    PackageIndexFlags.Host, PackageIndexFlags.Found,
                    false, false, false, ref error) != ReturnCode.Ok)
            {
                return ReturnCode.Error;
            }

            //
            // NOTE: Find all the package index files in the specified
            //       paths.
            //
            foreach (string path in paths)
            {
                //
                // NOTE: Normalize the path prior to using it or adding
                //       it to the dictionary.
                //
                string newPath = PathOps.ResolveFullPath(interpreter, path);

                //
                // NOTE: Make sure the directory exists prior to
                //       attempting to find any files in it; otherwise,
                //       we just ignore it to reduce the burden on the
                //       caller to validate that a given path actually
                //       exists (which could be especially burdensome
                //       if it is constructed dynamically from
                //       environment variables, etc).
                //
                if (!String.IsNullOrEmpty(newPath) &&
                    Directory.Exists(newPath))
                {
                    //
                    // NOTE: Find all package index files in the
                    //       specified directory.
                    //
                    StringList fileNames = new StringList(
                        Directory.GetFiles(newPath,
                            FileName.PackageIndex,
                            SearchOption.AllDirectories));

                    //
                    // NOTE: For each package index file, notify the
                    //       callback if it is new and/or add it to the
                    //       resulting collection.
                    //
                    foreach (string fileName in fileNames)
                    {
                        //
                        // NOTE: Have we seen this package index before?
                        //
                        bool exists = packageIndexes.ContainsKey(fileName);

                        //
                        // NOTE: If we are refreshing package indexes or
                        //       we have never seen this package index
                        //       before, notify the caller.
                        //
                        if (refresh || !exists)
                        {
                            interpreter.SetNoPackageIndexes(true);

                            try
                            {
                                Result result = null;

                                if ((callback != null) &&
                                    (callback(interpreter, fileName,
                                        false, resolve, refresh,
                                        ref result) != ReturnCode.Ok))
                                {
                                    error = result;
                                    return ReturnCode.Error;
                                }
                            }
                            finally
                            {
                                interpreter.SetNoPackageIndexes(false);
                            }
                        }

                        //
                        // NOTE: If we have not seen this package index
                        //       before add it to the resulting
                        //       collection now; otherwise, mark it as
                        //       "found" so that it will not be purged.
                        //
                        if (exists)
                            packageIndexes[fileName] |=
                                PackageIndexFlags.Normal |
                                PackageIndexFlags.Found;
                        else
                            packageIndexes.Add(fileName,
                                PackageIndexFlags.Normal |
                                PackageIndexFlags.Found);
                    }
                }
            }

            //
            // NOTE: Purge any package indexes from the list that are
            //       still marked as "not found".
            //
            if (PurgeIndexes(
                    packageIndexes, PackageIndexFlags.None,
                    PackageIndexFlags.Host | PackageIndexFlags.Found,
                    false, false, ref error) != ReturnCode.Ok)
            {
                return ReturnCode.Error;
            }

            return ReturnCode.Ok;
        }

        ///////////////////////////////////////////////////////////////////////

        public static ReturnCode FindAll(
            Interpreter interpreter,
            StringList paths,
            bool host,
            bool normal,
            bool resolve,
            bool refresh,
            ref PackageIndexDictionary packageIndexes,
            ref Result error
            )
        {
            if ((!host || (FindHost(
                    interpreter, paths, SourceIndexCallback, resolve,
                    refresh, ref packageIndexes, ref error) == ReturnCode.Ok)) &&
                (!normal || (FindFile(
                    interpreter, paths, SourceIndexCallback, resolve,
                    refresh, ref packageIndexes, ref error) == ReturnCode.Ok)))
            {
                return ReturnCode.Ok;
            }

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode MarkIndex(
            PackageIndexDictionary packageIndexes,
            string fileName,
            PackageIndexFlags hasFlags,
            PackageIndexFlags notHasFlags,
            PackageIndexFlags markFlags,
            bool hasAll,
            bool notHasAll,
            bool mark,
            ref Result error
            )
        {
            if (packageIndexes != null)
            {
                if (fileName != null)
                {
                    if (packageIndexes.ContainsKey(fileName))
                    {
                        PackageIndexFlags flags = packageIndexes[fileName];

                        if (FlagOps.HasFlags(flags, hasFlags, hasAll) &&
                            !FlagOps.HasFlags(flags, notHasFlags, notHasAll))
                        {
                            if (mark)
                                flags |= markFlags;
                            else
                                flags &= ~markFlags;

                            packageIndexes[fileName] = flags;
                        }
                    }

                    return ReturnCode.Ok;
                }
                else
                {
                    error = "invalid file name";
                }
            }
            else
            {
                error = "invalid package indexes";
            }

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode MarkIndexes(
            PackageIndexDictionary packageIndexes,
            PackageIndexFlags hasFlags,
            PackageIndexFlags notHasFlags,
            PackageIndexFlags markFlags,
            bool hasAll,
            bool notHasAll,
            bool mark,
            ref Result error
            )
        {
            if (packageIndexes != null)
            {
                StringList list = new StringList(packageIndexes.Keys);

                for (int index = 0; index < list.Count; index++)
                {
                    PackageIndexFlags flags = packageIndexes[list[index]];

                    if (FlagOps.HasFlags(flags, hasFlags, hasAll) &&
                        !FlagOps.HasFlags(flags, notHasFlags, notHasAll))
                    {
                        if (mark)
                            flags |= markFlags;
                        else
                            flags &= ~markFlags;

                        packageIndexes[list[index]] = flags;
                    }
                }

                return ReturnCode.Ok;
            }
            else
            {
                error = "invalid package indexes";
            }

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode PurgeIndex(
            PackageIndexDictionary packageIndexes,
            string fileName,
            PackageIndexFlags hasFlags,
            PackageIndexFlags notHasFlags,
            bool hasAll,
            bool notHasAll,
            ref Result error
            )
        {
            if (packageIndexes != null)
            {
                if (fileName != null)
                {
                    if (packageIndexes.ContainsKey(fileName))
                    {
                        PackageIndexFlags flags = packageIndexes[fileName];

                        if (FlagOps.HasFlags(flags, hasFlags, hasAll) &&
                            !FlagOps.HasFlags(flags, notHasFlags, notHasAll))
                        {
                            packageIndexes.Remove(fileName);
                        }
                    }

                    return ReturnCode.Ok;
                }
                else
                {
                    error = "invalid file name";
                }
            }
            else
            {
                error = "invalid package indexes";
            }

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode PurgeIndexes(
            PackageIndexDictionary packageIndexes,
            PackageIndexFlags hasFlags,
            PackageIndexFlags notHasFlags,
            bool hasAll,
            bool notHasAll,
            ref Result error
            )
        {
            if (packageIndexes != null)
            {
                StringList list = new StringList(packageIndexes.Keys);

                for (int index = list.Count - 1; index >= 0; index--)
                {
                    PackageIndexFlags flags = packageIndexes[list[index]];

                    if (FlagOps.HasFlags(flags, hasFlags, hasAll) &&
                        !FlagOps.HasFlags(flags, notHasFlags, notHasAll))
                    {
                        packageIndexes.Remove(list[index]);
                    }
                }

                return ReturnCode.Ok;
            }
            else
            {
                error = "invalid package indexes";
            }

            return ReturnCode.Error;
        }

        ///////////////////////////////////////////////////////////////////////

        private static ReturnCode SourceIndexCallback(
            Interpreter interpreter,
            string fileName,
            bool host,
            bool resolve,
            bool refresh, /* IGNORED */
            ref Result result
            )
        {
            ReturnCode code;

            if (interpreter != null)
            {
                bool directory = false;

                try
                {
                    bool trace = FlagOps.HasFlags(interpreter.InterpreterFlags,
                        InterpreterFlags.TracePackageIndex, true);

                    if (host)
                    {
                        //
                        // NOTE: It is important to note here that currently
                        //       there may only be a maximum of ONE package
                        //       index file provided by the host.
                        //
                        ScriptFlags scriptFlags =
                            ScriptFlags.PackageLibraryOptionalFile;

                        string text = interpreter.GetScript(
                            FileName.PackageIndex, ref scriptFlags);

                        if (!String.IsNullOrEmpty(text))
                        {
                            if (FlagOps.HasFlags(scriptFlags, ScriptFlags.File, true))
                            {
                                bool remoteUri = PathOps.IsRemoteUri(text);

                                if (remoteUri || File.Exists(text))
                                {
                                    string newText = text;

                                    if (resolve && !remoteUri)
                                    {
                                        //
                                        // NOTE: Attempt to resolve the file name to a
                                        //       fully qualified one.
                                        //
                                        newText = PathOps.ResolveFullPath(interpreter, newText);

                                        //
                                        // NOTE: Failing that, fallback to the original
                                        //       file name which has already been
                                        //       "validated".
                                        //
                                        if (String.IsNullOrEmpty(newText))
                                            newText = text;
                                    }

                                    //
                                    // NOTE: The host for the interpreter seems
                                    //       to indicate we should be able to
                                    //       find the package index on the native
                                    //       file system?  Ok, fine.  Setup the
                                    //       directory variable properly.
                                    //
                                    code = interpreter.SetVariableValue( /* EXEMPT */
                                        VariableFlags.None, TclVars.Directory,
                                        PathOps.GetDirectoryName(newText),
                                        ref result);

                                    if (code == ReturnCode.Ok)
                                    {
                                        directory = true;

                                        code = interpreter.EvaluateFile(newText, ref result);

                                        if (trace)
                                            TraceOps.DebugTrace(String.Format(
                                                "{0}: SourceIndexCallback: interpreter = {1}, " +
                                                "fileName = {2}, host = {3}, resolve = {4}, " +
                                                "refresh = {5}, newText = {6}, code = {7}, " +
                                                "result = {8}{9}", GlobalState.GetCurrentThreadId(),
                                                FormatOps.WrapOrNull(interpreter),
                                                FormatOps.WrapOrNull(fileName), host, resolve,
                                                refresh, FormatOps.WrapOrNull(newText), code,
                                                FormatOps.WrapOrNull(true, true, result),
                                                Environment.NewLine), typeof(PackageOps).Name);
                                    }
                                }
                                else
                                {
                                    result = String.Format(
                                        "the provided \"{0}\" script file \"{1}\" is not " +
                                        "a valid remote uri and does not exist locally",
                                        ScriptTypes.PackageIndex, text);

                                    code = ReturnCode.Error;
                                }
                            }
                            else
                            {
                                code = interpreter.SetVariableValue( /* EXEMPT */
                                    VariableFlags.None, TclVars.Directory,
                                    null, ref result);

                                if (code == ReturnCode.Ok)
                                {
                                    directory = true;

                                    code = interpreter.EvaluateScript(text, ref result); /* EXEMPT */

                                    if (trace)
                                        TraceOps.DebugTrace(String.Format(
                                            "{0}: SourceIndexCallback: interpreter = {1}, " +
                                            "fileName = {2}, host = {3}, resolve = {4}, " +
                                            "refresh = {5}, text = {6}, code = {7}, result = {8}{9}",
                                            GlobalState.GetCurrentThreadId(),
                                            FormatOps.WrapOrNull(interpreter),
                                            FormatOps.WrapOrNull(fileName), host, resolve,
                                            refresh, FormatOps.WrapOrNull(true, true, text), code,
                                            FormatOps.WrapOrNull(true, true, result),
                                            Environment.NewLine), typeof(PackageOps).Name);
                                }
                            }
                        }
                        else
                        {
                            //
                            // NOTE: This is optional; therefore, success.
                            //
                            code = ReturnCode.Ok;
                        }
                    }
                    else
                    {
                        if (!String.IsNullOrEmpty(fileName))
                        {
                            bool remoteUri = PathOps.IsRemoteUri(fileName);

                            if (remoteUri || File.Exists(fileName))
                            {
                                string newFileName = fileName;

                                if (resolve && !remoteUri)
                                {
                                    //
                                    // NOTE: Attempt to resolve the file name to a
                                    //       fully qualified one.
                                    //
                                    newFileName = PathOps.ResolveFullPath(
                                        interpreter, newFileName);

                                    //
                                    // NOTE: Failing that, fallback to the original
                                    //       file name which has already been
                                    //       "validated".
                                    //
                                    if (String.IsNullOrEmpty(newFileName))
                                        newFileName = fileName;
                                }

                                code = interpreter.SetVariableValue( /* EXEMPT */
                                    VariableFlags.None, TclVars.Directory,
                                    PathOps.GetDirectoryName(newFileName),
                                    ref result);

                                if (code == ReturnCode.Ok)
                                {
                                    directory = true;

                                    code = interpreter.EvaluateFile(newFileName, ref result);

                                    if (trace)
                                        TraceOps.DebugTrace(String.Format(
                                            "{0}: SourceIndexCallback: interpreter = {1}, " +
                                            "fileName = {2}, host = {3}, resolve = {4}, " +
                                            "refresh = {5}, newFileName = {6}, code = {7}, " +
                                            "result = {8}{9}", GlobalState.GetCurrentThreadId(),
                                            FormatOps.WrapOrNull(interpreter),
                                            FormatOps.WrapOrNull(fileName), host, resolve,
                                            refresh, FormatOps.WrapOrNull(newFileName), code,
                                            FormatOps.WrapOrNull(true, true, result),
                                            Environment.NewLine), typeof(PackageOps).Name);
                                }
                            }
                            else
                            {
                                result = String.Format(
                                    "the provided \"{0}\" script file \"{1}\" is not " +
                                    "a valid remote uri and does not exist locally",
                                    ScriptTypes.PackageIndex, fileName);

                                code = ReturnCode.Error;
                            }
                        }
                        else
                        {
                            result = String.Format(
                                "the provided \"{0}\" script is invalid",
                                ScriptTypes.PackageIndex);

                            code = ReturnCode.Error;
                        }
                    }
                }
                catch (Exception e)
                {
                    result = String.Format(
                        "caught exception while sourcing package index: {0}",
                        e);

                    code = ReturnCode.Error;
                }
                finally
                {
                    //
                    // NOTE: If we set the directory variable, be sure to unset
                    //       it.
                    //
                    if (directory)
                    {
                        ReturnCode unsetCode;
                        Result unsetResult = null;

                        unsetCode = interpreter.UnsetVariable( /* EXEMPT */
                            VariableFlags.None, TclVars.Directory,
                            ref unsetResult);

                        if (unsetCode != ReturnCode.Ok)
                            DebugOps.Complain(interpreter, unsetCode, unsetResult);
                    }
                }
            }
            else
            {
                result = "invalid interpreter";
                code = ReturnCode.Error;
            }

            return code;
        }
        #endregion
    }
}
