#region Disclaimer / License

// Copyright (C) 2009, Kenneth Skovhede
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//

#endregion Disclaimer / License

using ICSharpCode.SharpZipLib.Zip;
using OSGeo.MapGuide.MaestroAPI;
using OSGeo.MapGuide.ObjectModels;
using OSGeo.MapGuide.ObjectModels.Common;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Xml;

namespace Maestro.Packaging
{
    /// <summary>
    /// Enumeration used to signal the type of operation currently running
    /// </summary>
    public enum ProgressType
    {
        /// <summary>
        /// The file list is being fetched from MapGuide
        /// </summary>
        ReadingFileList,

        /// <summary>
        /// Files are downloaded in temporary folder
        /// </summary>
        PreparingFolder,

        /// <summary>
        /// Resource references are updated to use the new folder
        /// </summary>
        MovingResources,

        /// <summary>
        /// The files are being compressed
        /// </summary>
        Compressing,

        /// <summary>
        /// The package opertion has completed
        /// </summary>
        Done,

        /// <summary>
        /// The package is being uploaded
        /// </summary>
        Uploading,

        /// <summary>
        /// Extracting filenames from package
        /// </summary>
        ListingFiles,

        /// <summary>
        /// Setting resource content
        /// </summary>
        SetResource,

        /// <summary>
        /// Setting resource data
        /// </summary>
        SetResourceData
    }

    /// <summary>
    /// Defines the type of entry
    /// </summary>
    public enum EntryTypeEnum
    {
        /// <summary>
        /// The item already exists in the package
        /// </summary>
        Regular,

        /// <summary>
        /// The item is deleted from the package
        /// </summary>
        Deleted,

        /// <summary>
        /// The item is added to the package
        /// </summary>
        Added
    }

    /// <summary>
    /// A delegate for reporting package creation progress
    /// </summary>
    /// <param name="args">Package progress</param>
    public delegate void ProgressDelegate(ProgressEventArgs args);

    public class ProgressEventArgs : EventArgs
    {
        /// <summary>
        /// The progress type that is currently running
        /// </summary>
        public ProgressType Type { get; set; }

        /// <summary>
        /// The max value, meaning that when value equals maxValue, progress is equal to 100%
        /// </summary>
        public string ResourceId { get; set; }

        /// <summary>
        /// The current item being progressed
        /// </summary>
        public int MaxValue { get; set; }

        /// <summary>
        /// The name of the resource being processed, if any
        /// </summary>
        public double Value { get; set; }
    }

    /// <summary>
    /// A class to create MapGuide data packages
    /// </summary>
    public class PackageBuilder
    {
        /// <summary>
        /// The connection object
        /// </summary>
        private readonly IServerConnection m_connection;

        /// <summary>
        /// Constructs a new package builder instance
        /// </summary>
        /// <param name="connection">The connection used to serialize and fetch items</param>
        public PackageBuilder(IServerConnection connection)
        {
            if (connection == null)
                throw new ArgumentNullException(nameof(connection)); //NOXLATE
            m_connection = connection;
        }

        /// <summary>
        /// This event is invoked to report progress to the caller
        /// </summary>
        public event ProgressDelegate Progress;

        /// <summary>
        /// Keep track of the last pg sent, to avoid excessive events
        /// </summary>
        private long m_lastPg = -1;

        private void SignalProgress(ProgressType type, string file, int maxValue, double value)
            => this.Progress?.Invoke(new ProgressEventArgs() { Type = type, ResourceId = file, MaxValue = maxValue, Value = value });

        /// <summary>
        /// Uploads a package to the server
        /// </summary>
        /// <param name="sourceFile"></param>
        public void UploadPackage(string sourceFile)
        {
            SignalProgress(ProgressType.Uploading, sourceFile, 100, 0);

            m_lastPg = -1;
            m_connection.ResourceService.UploadPackage(sourceFile, new Utility.StreamCopyProgressDelegate(ProgressCallback_Upload));

            SignalProgress(ProgressType.Uploading, sourceFile, 100, 100);
        }

        /// <summary>
        /// Uploads a package to the server in a non-transactional fashion. Resources which fail to load are added to the specified list of
        /// failed resources. The upload is non-transactional in the sense that it can partially fail. Failed operations are logged.
        /// </summary>
        /// <param name="sourceFile">The source package file</param>
        /// <param name="result">An <see cref="T:Maestro.Packaging.UploadPackageResult"/> object containing an optional list of operations to skip. It will be populated with the list of operations that passed and failed as the process executes</param>
        public void UploadPackageNonTransactional(string sourceFile, UploadPackageResult result)
        {
            Dictionary<PackageOperation, PackageOperation> skipOps = new Dictionary<PackageOperation, PackageOperation>();
            if (result.SkipOperations.Count > 0)
            {
                foreach (var op in result.SkipOperations)
                {
                    skipOps[op] = op;
                }
            }

            ProgressDelegate progress = this.Progress;

            double step = 0.0;
            SignalProgress(ProgressType.ListingFiles, sourceFile, 100, step);

            //Process overview:
            //
            // 1. Extract the package to a temp directory
            // 2. Read the package manifest
            // 3. For each resource id in the manifest, if it is in the list of resource ids to skip
            //    then skip it. Otherwise process the directive that uses this id.

            ZipFile package = new ZipFile(sourceFile);
            ZipEntry manifestEntry = package.GetEntry("MgResourcePackageManifest.xml"); //NOXLATE
            XmlDocument doc = new XmlDocument();
            using (var s = package.GetInputStream(manifestEntry))
            {
                doc.Load(s);
            }
            XmlNodeList opNodes = doc.GetElementsByTagName("Operation"); //NOXLATE
            double unit = (100.0 / (double)opNodes.Count);
            foreach (XmlNode opNode in opNodes)
            {
                step += unit;
                string name = opNode["Name"].InnerText.ToUpper(); //NOXLATE

                PackageOperation op = ParseOperation(opNode);
                //TODO: A DELETERESOURCE would cause a null operation. Should we bother to support it?
                if (op == null)
                    continue;

                //Is a skipped operation?
                if (skipOps.ContainsKey(op))
                {
                    System.Diagnostics.Trace.TraceInformation("Skipping " + op.OperationName + " on " + op.ResourceId); //NOXLATE
                    continue;
                }

                switch (name)
                {
                    case "SETRESOURCE": //NOXLATE
                        {
                            SetResourcePackageOperation sop = (SetResourcePackageOperation)op;
                            if (sop.Content == null)
                            {
                                skipOps[sop] = sop;
                            }
                            else
                            {
                                ZipEntry contentEntry = package.GetEntry(sop.Content);
                                ZipEntry headerEntry = null;

                                if (!string.IsNullOrEmpty(sop.Header))
                                    headerEntry = package.GetEntry(sop.Header);

                                try
                                {
                                    using (var s = package.GetInputStream(contentEntry))
                                    {
                                        m_connection.ResourceService.SetResourceXmlData(op.ResourceId, s);
                                        SignalProgress(ProgressType.SetResource, op.ResourceId, 100, step);
                                    }

                                    if (headerEntry != null)
                                    {
                                        using (var s = package.GetInputStream(headerEntry))
                                        {
                                            using (var sr = new StreamReader(s))
                                            {
                                                ResourceDocumentHeaderType header = ResourceDocumentHeaderType.Deserialize(sr.ReadToEnd());
                                                m_connection.ResourceService.SetResourceHeader(op.ResourceId, header);
                                                SignalProgress(ProgressType.SetResource, op.ResourceId, 100, step);
                                            }
                                        }
                                    }

                                    result.Successful.Add(op);
                                }
                                catch (Exception ex)
                                {
                                    //We don't really care about the header. We consider failure if the
                                    //content upload did not succeed
                                    if (!m_connection.ResourceService.ResourceExists(op.ResourceId))
                                        result.Failed.Add(op, ex);
                                }
                            }
                        }
                        break;

                    case "SETRESOURCEDATA": //NOXLATE
                        {
                            SetResourceDataPackageOperation sop = (SetResourceDataPackageOperation)op;
                            ZipEntry dataEntry = package.GetEntry(sop.Data);

                            try
                            {
                                using (var s = package.GetInputStream(dataEntry))
                                {
                                    m_connection.ResourceService.SetResourceData(sop.ResourceId, sop.DataName, sop.DataType, s);
                                    SignalProgress(ProgressType.SetResourceData, sop.ResourceId, 100, step);
                                }

                                result.Successful.Add(op);
                            }
                            catch (Exception ex)
                            {
                                var resData = m_connection.ResourceService.EnumerateResourceData(sop.ResourceId);
                                bool found = false;

                                foreach (var data in resData.ResourceData)
                                {
                                    if (data.Name == sop.DataName)
                                    {
                                        found = true;
                                        break;
                                    }
                                }

                                if (!found)
                                    result.Failed.Add(sop, ex);
                            }
                        }
                        break;
                }
            }
        }

        private static PackageOperation ParseOperation(XmlNode opNode)
        {
            PackageOperation op = null;
            NameValueCollection p = new NameValueCollection();
            foreach (XmlNode paramNode in opNode["Parameters"].ChildNodes) //NOXLATE
            {
                p[paramNode["Name"].InnerText] = paramNode["Value"].InnerText; //NOXLATE
            }
            string resourceId = p["RESOURCEID"]; //NOXLATE
            switch (opNode["Name"].InnerText) //NOXLATE
            {
                case "SETRESOURCE": //NOXLATE
                    {
                        op = new SetResourcePackageOperation(resourceId, p["CONTENT"], p["HEADER"]); //NOXLATE
                    }
                    break;

                case "SETRESOURCEDATA": //NOXLATE
                    {
                        ResourceDataType rdt;
                        try
                        {
                            rdt = (ResourceDataType)Enum.Parse(typeof(ResourceDataType), p["DATATYPE"], true); //NOXLATE
                        }
                        catch { rdt = ResourceDataType.File; }
                        op = new SetResourceDataPackageOperation(resourceId, p["DATA"], p["DATANAME"], rdt); //NOXLATE
                    }
                    break;
            }
            return op;
        }

        private void ProgressCallback_Upload(long copied, long remain, long total)
        {
            if (Progress != null)
            {
                if (m_lastPg < 0 || remain == 0 || copied - m_lastPg > 1024 * 50)
                {
                    SignalProgress(ProgressType.Uploading, string.Empty, (int)(total / 1024), (int)(copied / 1024));
                    m_lastPg = copied;
                }
            }
        }

        /// <summary>
        /// Creates a package
        /// </summary>
        /// <param name="folderResourceId">The folder to create the package from</param>
        /// <param name="zipfilename">The name of the output file to create</param>
        /// <param name="allowedExtensions">A list of allowed extensions without leading dot, or null to include all file types. The special item &quot;*&quot; matches all unknown types.</param>
        /// <param name="removeExistingFiles">A value indicating if a delete operation is included in the package to remove existing files before restoring the package</param>
        /// <param name="alternateTargetResourceId">An optional target folder resourceId, use null or an empty string to restore the files at the original locations</param>
        public void CreatePackage(string folderResourceId, string zipfilename, IEnumerable<ResourceTypes> allowedExtensions, bool removeExistingFiles, string alternateTargetResourceId)
        {
            SignalProgress(ProgressType.ReadingFileList, folderResourceId, 100, 0);

            ResourceList items = m_connection.ResourceService.GetRepositoryResources(folderResourceId);

            CreatePackageInternal(folderResourceId, zipfilename, allowedExtensions, removeExistingFiles, alternateTargetResourceId, items.Children.Select(x => x.ResourceId));
        }

        /// <summary>
        /// Creates a package
        /// </summary>
        /// <param name="resourceIdsToPack">The list of resource ids to include into the package</param>
        /// <param name="zipfilename">The name of the output file to create</param>
        /// <param name="allowedExtensions">A list of allowed extensions without leading dot, or null to include all file types. The special item &quot;*&quot; matches all unknown types.</param>
        /// <param name="removeExistingFiles">A value indicating if a delete operation is included in the package to remove existing files before restoring the package</param>
        /// <param name="alternateTargetResourceId">An optional target folder resourceId, use null or an empty string to restore the files at the original locations</param>
        public void CreatePackage(IEnumerable<string> resourceIdsToPack, string zipfilename, IEnumerable<ResourceTypes> allowedExtensions, bool removeExistingFiles, string alternateTargetResourceId)
        {
            SignalProgress(ProgressType.ReadingFileList, string.Empty, 100, 0);

            var resourceIds = new List<string>(resourceIdsToPack);
            string folderId = GetCommonParent(resourceIds);

            CreatePackageInternal(folderId, zipfilename, allowedExtensions, removeExistingFiles, alternateTargetResourceId, resourceIds);
        }

        private static string GetCommonParent(ICollection<string> data)
        {
            if (data.Count > 0)
            {
                var firstResId = new ResourceIdentifier(data.ElementAt(0));
                if (data.Count == 1)
                {
                    if (firstResId.IsFolder)
                        return firstResId.ResourceId.ToString();
                    else
                        return firstResId.ParentFolder;
                }
                else
                {
                    int matches = 0;
                    string[] parts = firstResId.ResourceId.ToString()
                                               .Substring(StringConstants.RootIdentifier.Length)
                                               .Split('/'); //NOXLATE
                    string test = StringConstants.RootIdentifier;
                    string parent = test;
                    int partIndex = 0;
                    //Use first one as a sample to see how far we can go. Keep going until we have
                    //a parent that doesn't match all of them. The one we recorded before then will
                    //be the common parent
                    while (matches == data.Count)
                    {
                        parent = test;
                        partIndex++;
                        if (partIndex < parts.Length) //Shouldn't happen, but just in case
                            break;

                        test = test + parts[partIndex];
                        matches = data.Count(x => x.StartsWith(test));
                    }
                    return parent;
                }
            }
            else
            {
                return StringConstants.RootIdentifier;
            }
        }

        private void CreatePackageInternal(string folderResourceId, string zipfilename, IEnumerable<ResourceTypes> allowedExtensions, bool removeExistingFiles, string alternateTargetResourceId, IEnumerable<string> resourceIds)
        {
            ResourcePackageManifest manifest = new ResourcePackageManifest();
            manifest.Description = "MapGuide Package created with Maestro"; //NOXLATE
            manifest.Operations = new ResourcePackageManifestOperations();
            manifest.Operations.Operation = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperation>();

            var allowed = new List<string>();
            foreach (var rt in allowedExtensions)
            {
                allowed.Add(rt.ToString());
            }
            var files = new List<string>();
            var folders = new List<string>();
            var resourceData = new Dictionary<string, List<ResourceDataListResourceData>>();

            foreach (var resId in resourceIds)
            {
                if (!ResourceIdentifier.Validate(resId))
                    continue;

                var r = new ResourceIdentifier(resId);
                if (r.IsFolder)
                {
                    folders.Add(resId);
                }
                else
                {
                    var extension = r.ResourceType;
                    if (allowedExtensions == null || allowed.Count == 0)
                        files.Add(resId);
                    else if (m_connection.Capabilities.IsSupportedResourceType(extension) && allowed.Contains(extension))
                        files.Add(resId);
                }
            }

            SignalProgress(ProgressType.ReadingFileList, folderResourceId, 100, 100);
            SignalProgress(ProgressType.PreparingFolder, string.Empty, files.Count + folders.Count + 1, 0);

            string temppath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

            //All files have random names on disk, but a full path in the zip file
            List<KeyValuePair<string, string>> filemap = new List<KeyValuePair<string, string>>();

            try
            {
                Directory.CreateDirectory(temppath);
                int opno = 1;

                foreach (var folder in folders)
                {
                    SignalProgress(ProgressType.PreparingFolder, folder, files.Count + folders.Count + 1, opno);
                    AddFolderResource(manifest, temppath, folder, removeExistingFiles, m_connection, filemap);
                    SignalProgress(ProgressType.PreparingFolder, folder, files.Count + folders.Count + 1, opno++);
                }

                foreach (var doc in files)
                {
                    SignalProgress(ProgressType.PreparingFolder, doc, files.Count + folders.Count + 1, opno);
                    string filebase = CreateFolderForResource(doc, temppath);

                    resourceData[doc] = new List<ResourceDataListResourceData>();
                    ResourceDataList rdl = m_connection.ResourceService.EnumerateResourceData(doc);
                    foreach (ResourceDataListResourceData rd in rdl.ResourceData)
                        resourceData[doc].Add(rd);

                    int itemCount = resourceData[doc].Count + 1;

                    filemap.Add(new KeyValuePair<string, string>(filebase + "_CONTENT.xml", Path.Combine(temppath, Guid.NewGuid().ToString()))); //NOXLATE
                    using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.Create, FileAccess.Write, FileShare.None))
                    {
                        using (var s = m_connection.ResourceService.GetResourceXmlData(doc))
                        {
                            var data = Utility.StreamAsArray(s);
                            fs.Write(data, 0, data.Length);
                        }
                    }

                    AddFileResource(manifest, temppath, doc, filemap[filemap.Count - 1].Key, removeExistingFiles, m_connection, filemap);

                    foreach (ResourceDataListResourceData rd in rdl.ResourceData)
                    {
                        filemap.Add(new KeyValuePair<string, string>(filebase + "_DATA_" + EncodeFilename(rd.Name), Path.Combine(temppath, Guid.NewGuid().ToString())));
                        FileInfo fi = new FileInfo(filemap[filemap.Count - 1].Value);
                        using (FileStream fs = new FileStream(fi.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
                        {
                            Utility.CopyStream(m_connection.ResourceService.GetResourceData(doc, rd.Name), fs);
                        }

                        AddResourceData(manifest, temppath, doc, fi, filemap[filemap.Count - 1].Key, rd, m_connection);
                    }

                    SignalProgress(ProgressType.PreparingFolder, doc, files.Count + folders.Count + 1, opno++);
                }

                SignalProgress(ProgressType.PreparingFolder, Strings.ProgressDone, files.Count + folders.Count + 1, files.Count + folders.Count + 1);

                if (!string.IsNullOrEmpty(alternateTargetResourceId))
                {
                    SignalProgress(ProgressType.MovingResources, Strings.ProgressUpdatingReferences, 100, 0);
                    RemapFiles(m_connection, manifest, temppath, folderResourceId, alternateTargetResourceId, filemap);
                    SignalProgress(ProgressType.MovingResources, Strings.ProgressUpdatedReferences, 100, 100);
                }

                filemap.Add(new KeyValuePair<string, string>(Path.Combine(temppath, "MgResourcePackageManifest.xml"), Path.Combine(temppath, Guid.NewGuid().ToString()))); //NOXLATE
                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                    m_connection.ResourceService.SerializeObject(manifest, fs);

                SignalProgress(ProgressType.MovingResources, zipfilename, filemap.Count, 0);

                ZipDirectory(zipfilename, temppath, "MapGuide Package created by Maestro", filemap); //NOXLATE

                if (Progress != null)
                {
                    SignalProgress(ProgressType.MovingResources, zipfilename, filemap.Count, filemap.Count);
                    SignalProgress(ProgressType.Done, "", filemap.Count, filemap.Count);
                }
            }
            finally
            {
                try
                {
                    if (Directory.Exists(temppath))
                        Directory.Delete(temppath, true);
                }
                catch
                { }
            }
        }

        private void AddResourceData(ResourcePackageManifest manifest, string temppath, string docResourceId, FileInfo fi, string resourcePath, ResourceDataListResourceData rd, IServerConnection connection)
        {
            string contentType = "application/octet-stream"; //NOXLATE

            string name = rd.Name;
            string type = rd.Type.ToString();
            string resourceId = docResourceId;
            string filename = RelativeName(resourcePath, temppath).Replace('\\', '/'); //NOXLATE
            long size = fi.Length;

            AddResourceData(manifest, resourceId, contentType, type, name, filename, size);
        }

        private void AddResourceData(ResourcePackageManifest manifest, string resourceId, string contentType, string type, string name, string filename, long size)
        {
            ResourcePackageManifestOperationsOperation op = new ResourcePackageManifestOperationsOperation();
            op.Name = "SETRESOURCEDATA"; //NOXLATE
            op.Version = "1.0.0"; //NOXLATE
            op.Parameters = new ResourcePackageManifestOperationsOperationParameters();
            op.Parameters.Parameter = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperationParametersParameter>();

            ResourcePackageManifestOperationsOperationParametersParameter param = new ResourcePackageManifestOperationsOperationParametersParameter();

            param.Name = "DATA"; //NOXLATE
            param.Value = filename;
            param.ContentType = contentType;
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "DATALENGTH"; //NOXLATE
            param.Value = size.ToString();
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "DATANAME"; //NOXLATE
            param.Value = name;
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "DATATYPE";
            param.Value = type;
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "RESOURCEID"; //NOXLATE
            param.Value = resourceId;
            op.Parameters.Parameter.Add(param);

            manifest.Operations.Operation.Add(op);
        }

        private void AddFileResource(ResourcePackageManifest manifest, string temppath, string docResourceId, string contentfilename, bool eraseFirst, IServerConnection connection, List<KeyValuePair<string, string>> filemap)
        {
            string filebase = CreateFolderForResource(docResourceId, temppath);

            filemap.Add(new KeyValuePair<string, string>(filebase + "_HEADER.xml", Path.Combine(temppath, Guid.NewGuid().ToString()))); //NOXLATE
            using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.Create, FileAccess.Write, FileShare.None))
                connection.ResourceService.SerializeObject(connection.ResourceService.GetResourceHeader(docResourceId), fs);

            string headerpath = RelativeName(filemap[filemap.Count - 1].Key, temppath).Replace('\\', '/'); //NOXLATE
            string contentpath = RelativeName(contentfilename, temppath).Replace('\\', '/'); //NOXLATE
            AddFileResource(manifest, docResourceId, headerpath, contentpath, eraseFirst);
        }

        private void AddFileResource(ResourcePackageManifest manifest, string resourceId, string headerpath, string contentpath, bool eraseFirst)
        {
            if (eraseFirst)
            {
                ResourcePackageManifestOperationsOperation delop = new ResourcePackageManifestOperationsOperation();
                delop.Name = "DELETERESOURCE"; //NOXLATE
                delop.Version = "1.0.0"; //NOXLATE
                delop.Parameters = new ResourcePackageManifestOperationsOperationParameters();
                delop.Parameters.Parameter = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperationParametersParameter>();

                ResourcePackageManifestOperationsOperationParametersParameter delparam = new ResourcePackageManifestOperationsOperationParametersParameter();

                delparam.Name = "RESOURCEID"; //NOXLATE
                delparam.Value = resourceId;
                delop.Parameters.Parameter.Add(delparam);
                manifest.Operations.Operation.Add(delop);
            }

            ResourcePackageManifestOperationsOperation op = new ResourcePackageManifestOperationsOperation();
            op.Name = "SETRESOURCE"; //NOXLATE
            op.Version = "1.0.0"; //NOXLATE
            op.Parameters = new ResourcePackageManifestOperationsOperationParameters();
            op.Parameters.Parameter = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperationParametersParameter>();

            ResourcePackageManifestOperationsOperationParametersParameter param = new ResourcePackageManifestOperationsOperationParametersParameter();

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "CONTENT"; //NOXLATE
            param.Value = contentpath;
            param.ContentType = "text/xml"; //NOXLATE
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "HEADER"; //NOXLATE
            param.Value = headerpath;
            param.ContentType = "text/xml"; //NOXLATE
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "RESOURCEID"; //NOXLATE
            param.Value = resourceId;
            op.Parameters.Parameter.Add(param);

            manifest.Operations.Operation.Add(op);
        }

        private void AddFolderResource(ResourcePackageManifest manifest, string temppath, string folderResId, bool eraseFirst, IServerConnection connection, List<KeyValuePair<string, string>> filemap)
        {
            string filebase = Path.GetDirectoryName(CreateFolderForResource(folderResId + "dummy.xml", temppath)); //NOXLATE

            filemap.Add(new KeyValuePair<string, string>(Path.Combine(filebase, "_HEADER.xml"), Path.Combine(temppath, Guid.NewGuid().ToString()))); //NOXLATE
            using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.Create, FileAccess.Write, FileShare.None))
                connection.ResourceService.SerializeObject(connection.ResourceService.GetFolderHeader(folderResId), fs);

            if (!filebase.EndsWith(Path.DirectorySeparatorChar.ToString()))
                filebase += Path.DirectorySeparatorChar;

            string headerpath = RelativeName(filebase + "_HEADER.xml", temppath).Replace('\\', '/'); //NOXLATE

            AddFolderResource(manifest, folderResId, headerpath, eraseFirst);
        }

        private void AddFolderResource(ResourcePackageManifest manifest, string resourceId, string headerpath, bool eraseFirst)
        {
            if (eraseFirst)
            {
                ResourcePackageManifestOperationsOperation delop = new ResourcePackageManifestOperationsOperation();
                delop.Name = "DELETERESOURCE"; //NOXLATE
                delop.Version = "1.0.0"; //NOXLATE
                delop.Parameters = new ResourcePackageManifestOperationsOperationParameters();
                delop.Parameters.Parameter = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperationParametersParameter>();

                ResourcePackageManifestOperationsOperationParametersParameter delparam = new ResourcePackageManifestOperationsOperationParametersParameter();

                delparam.Name = "RESOURCEID"; //NOXLATE
                delparam.Value = resourceId;
                delop.Parameters.Parameter.Add(delparam);
                manifest.Operations.Operation.Add(delop);
            }

            ResourcePackageManifestOperationsOperation op = new ResourcePackageManifestOperationsOperation();
            if (resourceId.EndsWith("//")) //NOXLATE
                op.Name = "UPDATEREPOSITORY"; //NOXLATE
            else
                op.Name = "SETRESOURCE"; //NOXLATE
            op.Version = "1.0.0"; //NOXLATE
            op.Parameters = new ResourcePackageManifestOperationsOperationParameters();
            op.Parameters.Parameter = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperationParametersParameter>();

            ResourcePackageManifestOperationsOperationParametersParameter param = new ResourcePackageManifestOperationsOperationParametersParameter();

            param.Name = "HEADER"; //NOXLATE
            param.Value = headerpath;
            param.ContentType = "text/xml"; //NOXLATE
            op.Parameters.Parameter.Add(param);

            param = new ResourcePackageManifestOperationsOperationParametersParameter();
            param.Name = "RESOURCEID"; //NOXLATE
            param.Value = resourceId;
            op.Parameters.Parameter.Add(param);

            manifest.Operations.Operation.Add(op);
        }

        private string RelativeName(string filebase, string temppath)
        {
            if (!filebase.StartsWith(temppath))
                throw new Exception(string.Format(Strings.FilenameRelationInternalError, filebase, temppath));
            if (!temppath.EndsWith(Path.DirectorySeparatorChar.ToString()))
                temppath += Path.DirectorySeparatorChar;
            return filebase.Substring(temppath.Length);
        }

        private System.Text.RegularExpressions.Regex m_filenameTransformer = new System.Text.RegularExpressions.Regex(@"[^A-Za-z0-9\.-\/]", System.Text.RegularExpressions.RegexOptions.Compiled); //NOXLATE

        //There are some problems with the Zip reader in MapGuide and international characters :(
        private string EncodeFilename(string filename)
        {
            System.Text.RegularExpressions.Match m = m_filenameTransformer.Match(filename);
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            int previndex = 0;

            while (m != null && m.Success)
            {
                string replaceval = string.Format("-x{0:x2}-", (int)m.Value[0]); //NOXLATE

                sb.Append(filename.Substring(previndex, m.Index - previndex));
                sb.Append(replaceval);
                previndex = m.Index + m.Value.Length;

                m = m.NextMatch();
            }

            if (sb.Length == 0)
                return filename;
            else
            {
                sb.Append(filename.Substring(previndex));
                return sb.ToString();
            }
        }

        private string CreateFolderForResource(string resourceId, string temppath)
        {
            var rid = new ResourceIdentifier(resourceId);
            string filebase = EncodeFilename(rid.Name);
            string folder = "Library/" + EncodeFilename(rid.Path); //NOXLATE
            folder = folder.Substring(0, folder.Length - filebase.Length);
            filebase += resourceId.Substring(resourceId.LastIndexOf('.')); //NOXLATE

            folder = folder.Replace('/', Path.DirectorySeparatorChar); //NOXLATE
            folder = Path.Combine(temppath, folder);

            return Path.Combine(folder, filebase);
        }

        private void RemapFiles(IServerConnection connection, ResourcePackageManifest manifest, string tempdir, string origpath, string newpath, List<KeyValuePair<string, string>> filemap)
        {
            if (!newpath.EndsWith("/")) //NOXLATE
                newpath += "/"; //NOXLATE
            if (!origpath.EndsWith("/")) //NOXLATE
                origpath += "/"; //NOXLATE

            Dictionary<string, string> lookup = new Dictionary<string, string>();
            foreach (KeyValuePair<string, string> p in filemap)
                lookup.Add(p.Key, p.Value);

            foreach (ResourcePackageManifestOperationsOperation op in manifest.Operations.Operation)
            {
                op.Parameters.SetParameterValue("RESOURCEID", newpath + op.Parameters.GetParameterValue("RESOURCEID").Substring(origpath.Length)); //NOXLATE
                if (op.Parameters.GetParameterValue("CONTENT") != null) //NOXLATE
                {
                    string path = Path.Combine(tempdir, op.Parameters.GetParameterValue("CONTENT").Replace('/', Path.DirectorySeparatorChar)); //NOXLATE
                    XmlDocument doc = new XmlDocument();
                    doc.Load(lookup[path]);
                    ((PlatformConnectionBase)connection).UpdateResourceReferences(doc, origpath, newpath, true);
                    MemoryStream ms = new MemoryStream();
                    doc.Save(ms);
                    MemoryStream ms2 = Utility.RemoveUTF8BOM(ms);
                    if (ms2 != ms)
                        ms.Dispose();

                    ms2.Position = 0;
                    using (FileStream fs = new FileStream(lookup[path], FileMode.Create, FileAccess.Write, FileShare.None))
                    {
                        Utility.CopyStream(ms2, fs);
                    }
                    ms2.Dispose();
                }
            }
        }

        private void ZipDirectory(string zipfile, string folder, string comment, List<KeyValuePair<string, string>> filemap)
        {
            ZipConstants.DefaultCodePage = System.Text.Encoding.UTF8.CodePage;
            ICSharpCode.SharpZipLib.Checksums.Crc32 crc = new ICSharpCode.SharpZipLib.Checksums.Crc32();
            using (FileStream ofs = new FileStream(zipfile, FileMode.Create, FileAccess.Write, FileShare.None))
            using (ICSharpCode.SharpZipLib.Zip.ZipOutputStream zip = new ICSharpCode.SharpZipLib.Zip.ZipOutputStream(ofs))
            {
                try
                {
                    zip.SetLevel(9);
                    if (!string.IsNullOrEmpty(comment))
                        zip.SetComment(comment);

                    int i = 0;

                    foreach (KeyValuePair<string, string> f in filemap)
                    {
                        SignalProgress(ProgressType.Compressing, f.Key, filemap.Count, i);

                        FileInfo fi = new FileInfo(f.Value);
                        ICSharpCode.SharpZipLib.Zip.ZipEntry ze = new ICSharpCode.SharpZipLib.Zip.ZipEntry(RelativeName(f.Key, folder).Replace('\\', '/'));
                        ze.DateTime = fi.LastWriteTime;
                        ze.Size = fi.Length;
                        zip.PutNextEntry(ze);
                        using (FileStream fs = new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.None))
                            Utility.CopyStream(fs, zip);

                        SignalProgress(ProgressType.Compressing, f.Key, filemap.Count, i++);
                    }

                    zip.Finish();
                }
                finally
                {
                    try { zip.Close(); }
                    catch { }
                }
            }
        }

        private const string DEFAULT_HEADER =
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
            "<ResourceFolderHeader xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"ResourceFolderHeader-1.0.0.xsd\">\n" +
            "	<Security xsi:noNamespaceSchemaLocation=\"ResourceSecurity-1.0.0.xsd\">\n" +
            "		<Inherited>true</Inherited>\n" +
            "	</Security>\n" +
            "</ResourceFolderHeader>"; //NOXLATE

        private string MapResourcePathToFolder(string tempfolder, string resourcename)
        {
            return CreateFolderForResource(resourcename, tempfolder);
        }

        /// <summary>
        /// Builds a package with the specified content
        /// </summary>
        /// <param name="sourcePackageFile">The MGP file to read existing items from</param>
        /// <param name="items">The list of items that should be present in the new package</param>
        /// <param name="insertEraseCommands">True if each resource should have a delete operation inserted before the actual operation, false otherwise</param>
        /// <param name="targetfile">The output package filename</param>
        public void RebuildPackage(string sourcePackageFile, List<ResourceItem> items, string targetfile, bool insertEraseCommands)
        {
            string tempfolder = Path.GetTempPath();
            int opno = 1;
            try
            {
                SignalProgress(ProgressType.ReadingFileList, sourcePackageFile, 100, 0);

                //Step 1: Create the file system layout
                if (!Directory.Exists(tempfolder))
                    Directory.CreateDirectory(tempfolder);

                string zipfilecomment;

                List<KeyValuePair<string, string>> filemap = new List<KeyValuePair<string, string>>();

                ZipConstants.DefaultCodePage = System.Text.Encoding.UTF8.CodePage;
                using (ZipFile zipfile = new ZipFile(sourcePackageFile))
                {
                    zipfilecomment = zipfile.ZipFileComment;

                    SignalProgress(ProgressType.ReadingFileList, sourcePackageFile, 100, 100);

                    SignalProgress(ProgressType.PreparingFolder, string.Empty, items.Count, 0);

                    foreach (ResourceItem ri in items)
                    {
                        SignalProgress(ProgressType.PreparingFolder, ri.ResourcePath, items.Count, opno);

                        string filebase;
                        if (ri.IsFolder)
                        {
                            filebase = Path.GetDirectoryName(MapResourcePathToFolder(tempfolder, ri.ResourcePath + "dummy.xml")); //NOXLATE
                            if (!filebase.EndsWith(Path.DirectorySeparatorChar.ToString()))
                                filebase += Path.DirectorySeparatorChar;
                        }
                        else
                            filebase = MapResourcePathToFolder(tempfolder, ri.ResourcePath);

                        string headerpath = filebase + "_HEADER.xml"; //NOXLATE
                        string contentpath = filebase + "_CONTENT.xml"; //NOXLATE

                        if (ri.EntryType == EntryTypeEnum.Added)
                        {
                            if (string.IsNullOrEmpty(ri.Headerpath))
                            {
                                filemap.Add(new KeyValuePair<string, string>(headerpath, Path.Combine(tempfolder, ri.GenerateUniqueName())));
                                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                                {
                                    byte[] data = System.Text.Encoding.UTF8.GetBytes(DEFAULT_HEADER);
                                    fs.Write(data, 0, data.Length);
                                }
                            }
                            else if (!ri.IsFolder)
                            {
                                filemap.Add(new KeyValuePair<string, string>(headerpath, ri.Headerpath));
                                System.IO.File.Copy(ri.Headerpath, headerpath);
                            }

                            if (!string.IsNullOrEmpty(ri.Contentpath))
                                filemap.Add(new KeyValuePair<string, string>(contentpath, ri.Contentpath));
                        }
                        else if (ri.EntryType == EntryTypeEnum.Regular)
                        {
                            if (string.IsNullOrEmpty(ri.Headerpath))
                            {
                                filemap.Add(new KeyValuePair<string, string>(headerpath, Path.Combine(tempfolder, ri.GenerateUniqueName())));
                                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                                {
                                    byte[] data = System.Text.Encoding.UTF8.GetBytes(DEFAULT_HEADER);
                                    fs.Write(data, 0, data.Length);
                                }
                            }
                            else
                            {
                                int index = FindZipEntry(zipfile, ri.Headerpath);
                                if (index < 0)
                                    throw new Exception(string.Format(Strings.FileMissingError, ri.Headerpath));

                                filemap.Add(new KeyValuePair<string, string>(headerpath, Path.Combine(tempfolder, ri.GenerateUniqueName())));
                                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                                    Utility.CopyStream(zipfile.GetInputStream(index), fs);
                            }

                            if (!ri.IsFolder)
                            {
                                int index = FindZipEntry(zipfile, ri.Contentpath);
                                if (index < 0)
                                    throw new Exception(string.Format(Strings.FileMissingError, ri.Contentpath));

                                filemap.Add(new KeyValuePair<string, string>(contentpath, Path.Combine(tempfolder, ri.GenerateUniqueName())));
                                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                                    Utility.CopyStream(zipfile.GetInputStream(index), fs);
                            }
                        }

                        ri.Headerpath = headerpath;
                        ri.Contentpath = contentpath;

                        foreach (ResourceDataItem rdi in ri.Items)
                        {
                            string targetpath = filebase + "_DATA_" + EncodeFilename(rdi.ResourceName); //NOXLATE
                            if (rdi.EntryType == EntryTypeEnum.Added)
                            {
                                var tempFilePath = Path.Combine(tempfolder, ri.GenerateUniqueName());
                                filemap.Add(new KeyValuePair<string, string>(targetpath, tempFilePath));
                                if (File.Exists(rdi.Filename))
                                    File.Copy(rdi.Filename, tempFilePath);
                            }
                            else
                            {
                                int index = FindZipEntry(zipfile, rdi.Filename);
                                if (index < 0)
                                    throw new Exception(string.Format(Strings.FileMissingError, rdi.Filename));

                                filemap.Add(new KeyValuePair<string, string>(targetpath, Path.Combine(tempfolder, ri.GenerateUniqueName())));
                                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                                    Utility.CopyStream(zipfile.GetInputStream(index), fs);
                            }
                            rdi.Filename = targetpath;
                        }

                        SignalProgress(ProgressType.PreparingFolder, ri.ResourcePath, items.Count, opno++);
                    }
                }

                int i = 0;

                Dictionary<string, string> filemap_lookup = new Dictionary<string, string>();
                foreach (KeyValuePair<string, string> kv in filemap)
                    filemap_lookup[kv.Key] = kv.Value;

                //Step 2: Repoint all resources with respect to the update
                foreach (ResourceItem ri in items)
                {
                    SignalProgress(ProgressType.MovingResources, Strings.ProgressUpdatingResources, items.Count, i);

                    if (ri.OriginalResourcePath != ri.ResourcePath)
                    {
                        foreach (ResourceItem rix in items)
                        {
                            if (!rix.IsFolder)
                            {
                                XmlDocument doc = new XmlDocument();
                                doc.Load(filemap_lookup[rix.Contentpath]);
                                ((PlatformConnectionBase)m_connection).UpdateResourceReferences(doc, ri.OriginalResourcePath, ri.ResourcePath, ri.IsFolder);
                                MemoryStream ms = new MemoryStream();
                                doc.Save(ms);
                                MemoryStream ms2 = Utility.RemoveUTF8BOM(ms);
                                if (ms2 != ms)
                                    ms.Dispose();

                                ms2.Position = 0;
                                using (FileStream fs = new FileStream(filemap_lookup[rix.Contentpath], FileMode.Create, FileAccess.Write, FileShare.None))
                                {
                                    Utility.CopyStream(ms2, fs);
                                }
                                ms2.Dispose();
                            }
                        }
                    }
                    SignalProgress(ProgressType.MovingResources, Strings.ProgressUpdatingResources, items.Count, i++);
                }

                SignalProgress(ProgressType.MovingResources, Strings.ProgressUpdatedResources, items.Count, items.Count);

                //Step 3: Create an updated definition file
                ResourcePackageManifest manifest = new ResourcePackageManifest();
                manifest.Description = "MapGuide Package created by Maestro"; //NOXLATE
                manifest.Operations = new ResourcePackageManifestOperations();
                manifest.Operations.Operation = new System.ComponentModel.BindingList<ResourcePackageManifestOperationsOperation>();

                foreach (ResourceItem ri in items)
                {
                    if (ri.IsFolder)
                    {
                        AddFolderResource(
                            manifest,
                            ri.ResourcePath,
                            RelativeName(ri.Headerpath, tempfolder).Replace('\\', '/'), //NOXLATE
                            insertEraseCommands);
                    }
                    else
                    {
                        AddFileResource(
                            manifest,
                            ri.ResourcePath,
                            RelativeName(ri.Headerpath, tempfolder).Replace('\\', '/'), //NOXLATE
                            RelativeName(ri.Contentpath, tempfolder).Replace('\\', '/'), //NOXLATE
                            insertEraseCommands);

                        foreach (ResourceDataItem rdi in ri.Items)
                        {
                            AddResourceData(
                                manifest,
                                ri.ResourcePath,
                                rdi.ContentType,
                                rdi.DataType,
                                rdi.ResourceName,
                                RelativeName(rdi.Filename, tempfolder).Replace('\\', '/'), //NOXLATE
                                new FileInfo(filemap_lookup[rdi.Filename]).Length);
                        }
                    }
                }

                filemap.Add(new KeyValuePair<string, string>(Path.Combine(tempfolder, "MgResourcePackageManifest.xml"), Path.Combine(tempfolder, Guid.NewGuid().ToString()))); //NOXLATE
                using (FileStream fs = new FileStream(filemap[filemap.Count - 1].Value, FileMode.CreateNew, FileAccess.Write, FileShare.None))
                    m_connection.ResourceService.SerializeObject(manifest, fs);

                SignalProgress(ProgressType.Compressing, Strings.ProgressCompressing, 100, 0);

                //Step 4: Create the zip file
                ZipDirectory(targetfile, tempfolder, zipfilecomment, filemap);

                SignalProgress(ProgressType.Compressing, Strings.ProgressCompressed, 100, 100);
            }
            finally
            {
                try { Directory.Delete(tempfolder, true); }
                catch { }
            }
        }

        private int FindZipEntry(ZipFile file, string path)
        {
            string p = path.Replace('\\', '/'); //NOXLATE
            foreach (ICSharpCode.SharpZipLib.Zip.ZipEntry ze in file)
                if (ze.Name.Replace('\\', '/').Equals(p)) //NOXLATE
                    return (int)ze.ZipFileIndex;

            return -1;
        }

        /// <summary>
        /// Reads the contents of a package file
        /// </summary>
        /// <param name="packageFile">The file to read</param>
        /// <returns>A dictionary of items, the key is the resourceId</returns>
        public Dictionary<string, ResourceItem> ListPackageContents(string packageFile)
        {
            SignalProgress(ProgressType.ListingFiles, packageFile, 100, 0);

            Dictionary<string, ResourceItem> resourceList = new Dictionary<string, ResourceItem>();

            ResourcePackageManifest manifest;
            ZipConstants.DefaultCodePage = System.Text.Encoding.UTF8.CodePage;
            using (ZipFile zipfile = new ZipFile(packageFile))
            {
                int index = FindZipEntry(zipfile, "MgResourcePackageManifest.xml"); //NOXLATE
                if (index < 0)
                    throw new Exception(Strings.InvalidPackageFileError);

                manifest = m_connection.ResourceService.DeserializeObject<ResourcePackageManifest>(zipfile.GetInputStream(index));
            }

            int i = 0;
            SignalProgress(ProgressType.ListingFiles, packageFile, manifest.Operations.Operation.Count, i);

            //TODO: Much of this assumes that the package is correctly constructed, ea.: no SETRESOURCEDATA, before a SETRESOURCE and so on.
            foreach (ResourcePackageManifestOperationsOperation op in manifest.Operations.Operation)
            {
                SignalProgress(ProgressType.ListingFiles, packageFile, manifest.Operations.Operation.Count, i++);

                if (op.Name.ToLower().Equals("setresource")) //NOXLATE
                {
                    string id = op.Parameters.GetParameterValue("RESOURCEID"); //NOXLATE
                    string header;
                    if (op.Parameters.GetParameterValue("HEADER") != null) //NOXLATE
                        header = op.Parameters.GetParameterValue("HEADER"); //NOXLATE
                    else
                        header = null;
                    string content = op.Parameters.GetParameterValue("CONTENT") ?? null; //NOXLATE

                    resourceList.Add(id, new ResourceItem(id, header, content));
                }
                else if (op.Name.ToLower().Equals("setresourcedata")) //NOXLATE
                {
                    string id = op.Parameters.GetParameterValue("RESOURCEID"); //NOXLATE
                    ResourceItem ri = resourceList[id];
                    string name = op.Parameters.GetParameterValue("DATANAME"); //NOXLATE
                    string file = op.Parameters.GetParameterValue("DATA"); //NOXLATE
                    string contentType = op.Parameters.GetParameterValue("DATA"); //NOXLATE
                    string dataType = op.Parameters.GetParameterValue("DATATYPE"); //NOXLATE

                    ri.Items.Add(new ResourceDataItem(name, contentType, file, dataType));
                }

                //TODO: What to do with "DELETERESOURCE" ?
            }

            return resourceList;
        }
    }

    /// <summary>
    /// Base class of all package operations
    /// </summary>
    public abstract class PackageOperation
    {
        /// <summary>
        /// Gets or sets the resource id.
        /// </summary>
        /// <value>
        /// The resource id.
        /// </value>
        public string ResourceId { get; set; }

        /// <summary>
        /// Gets or sets the name of the operation.
        /// </summary>
        /// <value>
        /// The name of the operation.
        /// </value>
        public string OperationName { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="PackageOperation"/> class.
        /// </summary>
        /// <param name="resId">The res id.</param>
        protected PackageOperation(string resId)
        {
            this.ResourceId = resId;
        }
    }

    /// <summary>
    /// A SETRESOURCE package operation
    /// </summary>
    public class SetResourcePackageOperation : PackageOperation
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="SetResourcePackageOperation"/> class.
        /// </summary>
        /// <param name="resId">The res id.</param>
        /// <param name="content">The content.</param>
        /// <param name="header">The header.</param>
        public SetResourcePackageOperation(string resId, string content, string header)
            : base(resId)
        {
            this.OperationName = "SETRESOURCE"; //NOXLATE
            this.Content = content;
            this.Header = header;
        }

        /// <summary>
        /// Gets or sets the content.
        /// </summary>
        /// <value>
        /// The content.
        /// </value>
        public string Content { get; set; }

        /// <summary>
        /// Gets or sets the header.
        /// </summary>
        /// <value>
        /// The header.
        /// </value>
        public string Header { get; set; }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;

            if (!typeof(SetResourcePackageOperation).IsAssignableFrom(obj.GetType()))
                return false;

            SetResourcePackageOperation vi = (SetResourcePackageOperation)obj;
            return string.Compare(this.Content, vi.Content) == 0 &&
                   string.Compare(this.Header, vi.Header) == 0 &&
                   string.Compare(this.OperationName, vi.OperationName) == 0 &&
                   string.Compare(this.ResourceId, vi.ResourceId) == 0;
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        public override int GetHashCode()
        {
            //http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-systemobjectgethashcode
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + (this.Content ?? string.Empty).GetHashCode();
                if (this.Header != null)
                    hash = hash * 23 + this.Header.GetHashCode();
                hash = hash * 23 + this.OperationName.GetHashCode();
                hash = hash * 23 + this.ResourceId.GetHashCode();

                return hash;
            }
        }
    }

    /// <summary>
    /// A SETRESOURCEDATA package operation
    /// </summary>
    public class SetResourceDataPackageOperation : PackageOperation
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="SetResourceDataPackageOperation"/> class.
        /// </summary>
        /// <param name="resId">The res id.</param>
        /// <param name="data">The data.</param>
        /// <param name="dataName">Name of the data.</param>
        /// <param name="dataType">Type of the data.</param>
        public SetResourceDataPackageOperation(string resId, string data, string dataName, ResourceDataType dataType)
            : base(resId)
        {
            this.OperationName = "SETRESOURCEDATA"; //NOXLATE
            this.Data = data;
            this.DataName = dataName;
            this.DataType = dataType;
        }

        /// <summary>
        /// Gets or sets the data.
        /// </summary>
        /// <value>
        /// The data.
        /// </value>
        public string Data { get; set; }

        /// <summary>
        /// Gets or sets the name of the data.
        /// </summary>
        /// <value>
        /// The name of the data.
        /// </value>
        public string DataName { get; set; }

        /// <summary>
        /// Gets or sets the type of the data.
        /// </summary>
        /// <value>
        /// The type of the data.
        /// </value>
        public ResourceDataType DataType { get; set; }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;

            if (!typeof(SetResourceDataPackageOperation).IsAssignableFrom(obj.GetType()))
                return false;

            SetResourceDataPackageOperation vi = (SetResourceDataPackageOperation)obj;
            return this.Data.Equals(vi.Data) &&
                   this.DataName.Equals(vi.DataName) &&
                   this.OperationName.Equals(vi.OperationName) &&
                   this.ResourceId.Equals(vi.ResourceId) &&
                   this.DataType == vi.DataType;
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        public override int GetHashCode()
        {
            //http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-systemobjectgethashcode
            unchecked
            {
                int hash = 17;
                hash = hash * 23 + this.Data.GetHashCode();
                hash = hash * 23 + this.DataName.GetHashCode();
                hash = hash * 23 + this.OperationName.GetHashCode();
                hash = hash * 23 + this.ResourceId.GetHashCode();
                hash = hash * 23 + this.DataType.GetHashCode();

                return hash;
            }
        }
    }
}