// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

namespace Microsoft.Samples.WindowsAzureMmc.Powershell.Host
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IO;
    using System.Management.Automation;
    using System.Management.Automation.Host;
    using System.Management.Automation.Runspaces;
    using System.Threading;
    using Microsoft.Samples.WindowsAzureMmc.Model;
    using Microsoft.Samples.WindowsAzureMmc.Model.Storage;
    using Microsoft.Samples.WindowsAzureMmc.Powershell.Host.Helpers;

    /// <summary>
    /// A class that is used to host PowerShell as well as wrap the CloudDrive CmdLets.
    /// </summary>
    public class AzurePSHost : PSHost, IAzurePSHost, IDisposable
    {
        private const string QUEUEDRIVEMONIKER = "queue:";
        private const string BLOBDRIVEMONIKER = "blob:";

        private const string PATHSEPERATOR = "/";
        private const string WILDCARDSTRING = "*";

        private const string FOLDERTYPENAME = ".FolderInfo";
        private const string BLOBTYPENAME = ".BlobInfo";

        private Guid instanceId;
        private PSHostUserInterface powershellHostUserInterface;
        private string accountName;
        private bool disposed;

        private bool usingDevStorage;

        public AzurePSHost(string account, string key, bool isLocal, Uri blobEndpoint, Uri queueEndpoint, PSHostUserInterface ui)
        {
            this.instanceId = Guid.NewGuid();
            this.powershellHostUserInterface = ui;
            this.accountName = account;
            this.usingDevStorage = isLocal;

            this.State = RunspaceState.BeforeOpen;
            
            this.Credentials = new AzureStorageCredentials
            {
                AccountName = account,
                AccountKey = key,
                BlobServiceEndpoint = blobEndpoint,
                QueueServiceEndpoint = queueEndpoint
            };
        }

        public event ProgressEventHandler OnProgress;

        public override PSHostUserInterface UI
        {
            get { return this.powershellHostUserInterface; }
        }

        public override Version Version
        {
            get { return new Version(1, 0, 0, 0); }
        }

        public override string Name
        {
            get { return "AzureBlobsAndQueuesPSHost"; }
        }

        public override Guid InstanceId
        {
            get { return this.instanceId; }
        }

        public override CultureInfo CurrentCulture
        {
            get { return Thread.CurrentThread.CurrentCulture; }
        }

        public override CultureInfo CurrentUICulture
        {
            get { return Thread.CurrentThread.CurrentUICulture; }
        }

        public RunspaceState State { get; set; }

        public AzureStorageCredentials Credentials { get; set; }

        public Collection<AzureBlobContainer> RetrieveBlobContainers()
        {
            Collection<AzureBlobContainer> result = new Collection<AzureBlobContainer>();
            int count = 0;

            this.ReportProgress("Fetching BLOB containers");

            Command cmd = new Command("dir");
            cmd.Parameters.Add("Path", BLOBDRIVEMONIKER);
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            string script = "$p = $c.IsPublic()";

            foreach (PSObject ps in powershellResult)
            {
                this.ReportProgress("Fetching BLOB container properties", ++count);

                var container = new AzureBlobContainer(ps);

                var variables = new Dictionary<string, object>
                {
                    { "c", container.PSObject },
                    { "p", null }
                };

                PSHelper.ExecuteScript(this, script, variables);

                bool isPublic = (bool)variables["p"];
                container.IsPublic = isPublic;
                
                result.Add(container);
            }

            return result;
        }

        public AzureBlobContainer RetrieveBlobContainer(string containerName)
        {
            //this.ReportProgress("Fetching BLOB container");

            Command cmd = new Command("get-item");
            cmd.Parameters.Add("Path", BuildCloudPath(BLOBDRIVEMONIKER, containerName, PATHSEPERATOR));
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            if (powershellResult != null && powershellResult.Count > 0)
            {
                var container = new AzureBlobContainer(powershellResult[0]);

                //this.ReportProgress("Fetching BLOB container properties");

                string script = "$p = $c.IsPublic()";
                var variables = new Dictionary<string, object>
                {
                    { "c", container.PSObject },
                    { "p", null }
                };

                PSHelper.ExecuteScript(this, script, variables);

                bool isPublic = (bool)variables["p"];
                container.IsPublic = isPublic;

                return container;
            }

            return null;
        }

        public void CreateBlobContainer(string containerName, bool isPrivate)
        {
            Command cmd;

            if (string.IsNullOrEmpty(containerName))
            {
                throw new ArgumentException("containerName must be specified");
            }

            this.ReportProgress("Creating container");

            if (isPrivate)
            {
                cmd = new Command("mkdir");
            }
            else
            {
                cmd = new Command("new-item");
                cmd.Parameters.Add("Public");
            }

            cmd.Parameters.Add("path", BuildCloudPath(BLOBDRIVEMONIKER, containerName));
            cmd.Parameters.Add("ErrorAction", "stop");

            PSHelper.ExecuteCommand(this, cmd);
        }

        public void UploadBlob(string containerName, string blobName, string fileName)
        {
            //this.ReportProgress("Uploading BLOB");

            Command cmd = new Command("copy-cd");
            cmd.Parameters.Add("Path", fileName);
            cmd.Parameters.Add("Destination", BuildCloudPath(BLOBDRIVEMONIKER, containerName, blobName));
            cmd.Parameters.Add("ErrorAction", "stop");
            
            PSHelper.ExecuteCommand(this, cmd);
        }

        public AzureBlobContainer UploadContainer(string localPath, string containerName, bool isPrivate)
        {
            this.ReportProgress("Starting upload");
            DirectoryInfo dir = new DirectoryInfo(localPath);

            // Create the top level container
            this.CreateBlobContainer(containerName, isPrivate);
            AzureBlobContainer container = this.RetrieveBlobContainer(containerName);

            // Get all the files 
            FileInfo[] files = dir.GetFiles("*.*", SearchOption.AllDirectories);

            foreach (FileInfo file in files)
            {
                // Ignore hidden files - the Cmdlet Copy-CD does not like them
                if ((file.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                {
                    string folder = file.DirectoryName;
                    folder = folder.Replace(dir.FullName, string.Empty).ToLowerInvariant() + PATHSEPERATOR;

                    this.UploadBlob(containerName, folder + file.Name, file.FullName);

                    this.ReportProgress("Uploaded " + file.Name);
                }
            }

            return container;
        }

        public void DownloadBlobs(string containerName, Collection<AzureBlob> blobs, string localPath)
        {
            AzureBlobContainer container = null;
            string destination;

            this.ReportProgress("Starting download");

            // Remove the trailing slash if there is one
            if (localPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.OrdinalIgnoreCase))
            {
                localPath = localPath.Remove(localPath.Length - 1, 1);
            }

            // Make sure the container exists
            container = this.RetrieveBlobContainer(containerName);
            if (container != null)
            {
                // Create the base folder 
                destination = localPath + container.Url.LocalPath + Path.DirectorySeparatorChar.ToString();
                destination = destination.Replace(PATHSEPERATOR, Path.DirectorySeparatorChar.ToString());
                DirectoryInfo dir = new DirectoryInfo(destination);
                dir.Create();

                foreach (AzureBlob blob in blobs)
                {
                    // swap / for \
                    destination = blob.Url.LocalPath.Replace(PATHSEPERATOR, Path.DirectorySeparatorChar.ToString());

                    // add the folders + file name
                    destination = localPath + destination;

                    // make sure the destination path exists first 
                    dir = new DirectoryInfo(Path.GetDirectoryName(destination));
                    dir.Create();

                    Command cmd = new Command("Copy-cd");
                    cmd.Parameters.Add("path", BuildCloudPath(BLOBDRIVEMONIKER, blob.Url.LocalPath.Replace(PATHSEPERATOR + this.accountName, string.Empty)));
                    cmd.Parameters.Add("destination", destination);
                    cmd.Parameters.Add("ErrorAction", "stop");

                    PSHelper.ExecuteCommand(this, cmd);

                    this.ReportProgress("Downloaded " + destination);
                }
            }
        }

        public void DownloadContainer(string containerName, string path)
        {
            AzureBlobContainer container = null;
            string destination;

            this.ReportProgress("Starting download");

            // Remove the trailing slash if there is one
            if (path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.OrdinalIgnoreCase))
            {
               path = path.Remove(path.Length - 1, 1);
            }

            // Make sure the container exists
            container = this.RetrieveBlobContainer(containerName);
            if (container != null)
            {
                Collection<AzureBlob> blobs = this.RetrieveBlobs(containerName, WILDCARDSTRING);
                DirectoryInfo dir;

                // Create the base folder 
                destination = path + container.Url.LocalPath + Path.DirectorySeparatorChar.ToString();
                destination = destination.Replace(PATHSEPERATOR, Path.DirectorySeparatorChar.ToString());
                dir = new DirectoryInfo(destination);
                dir.Create();

                foreach (AzureBlob blob in blobs)
                {
                    // Swap / for \
                    destination = blob.Url.LocalPath.Replace(PATHSEPERATOR, Path.DirectorySeparatorChar.ToString());

                    // Add the folders + file name
                    destination = path + destination;

                    // Make sure the destination path exists first 
                    dir = new DirectoryInfo(Path.GetDirectoryName(destination));
                    dir.Create();

                    Command cmd = new Command("Copy-cd");
                    cmd.Parameters.Add("path", BuildCloudPath(BLOBDRIVEMONIKER, blob.Url.LocalPath.Replace(PATHSEPERATOR + this.accountName, string.Empty)));
                    cmd.Parameters.Add("destination", destination);
                    cmd.Parameters.Add("ErrorAction", "stop");

                    PSHelper.ExecuteCommand(this, cmd);

                    this.ReportProgress("Downloaded " + destination);
                }
            }
        }

        public Collection<AzureBlob> RetrieveBlobs(string containerName, string blobName)
        {
            Collection<AzureBlob> result = new Collection<AzureBlob>();
            bool recursive = blobName.EndsWith(WILDCARDSTRING, StringComparison.OrdinalIgnoreCase);

            //this.ReportProgress("Fetching BLOBs");

            Command cmd;

            if (!recursive)
            {
                cmd = new Command("get-item");
                cmd.Parameters.Add("Path", BuildCloudPath(BLOBDRIVEMONIKER, containerName, blobName));
            }
            else
            {
                cmd = new Command("get-childitem");
                cmd.Parameters.Add("Path", BuildCloudPath(BLOBDRIVEMONIKER, containerName, blobName));
                cmd.Parameters.Add("Recurse");
            }

            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            if (powershellResult != null)
            {
                //this.ReportProgress("Fetching BLOB properties");
                AzureBlobContainer container = this.RetrieveBlobContainer(containerName);

                foreach (PSObject ps in powershellResult)
                {
                    // If we find a folder then go recursive
                    if (ps.TypeNames != null && ps.TypeNames.Count > 0)
                    {
                        string mainType = ps.TypeNames[0];
                        if (mainType.EndsWith(BLOBTYPENAME, StringComparison.OrdinalIgnoreCase))
                        {
                            // The path of blobs in dev storeage is not right - we need to recompose them 
                            if (this.usingDevStorage)
                            {
                                result.Add(new AzureBlob(container, ps, new Uri(this.Credentials.BlobServiceEndpoint + PATHSEPERATOR + this.accountName)));
                            }
                            else
                            {
                                result.Add(new AzureBlob(container, ps, this.Credentials.BlobServiceEndpoint));
                            }
                        }
                    }
                }
            }

            return result;
        }

        public void DeleteBlobContainer(string containerName)
        {
            this.ReportProgress("Deleting container");

            Command cmd = new Command("rd");
            cmd.Parameters.Add("path", BuildCloudPath(BLOBDRIVEMONIKER, containerName));
            cmd.Parameters.Add("force");
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            PSHelper.ExecuteCommand(this, cmd);
        }

        public void DeleteBlob(string blobName)
        {
            //this.ReportProgress("Deleting BLOB");

            Command cmd = new Command("del");
            if (this.usingDevStorage)
            {
                // Remove the account name from the path
                if (blobName.StartsWith(PATHSEPERATOR + this.accountName, StringComparison.OrdinalIgnoreCase))
                {
                    blobName = blobName.Remove(0, this.accountName.Length + 1);
                }
            }

            cmd.Parameters.Add("Path", BuildCloudPath(BLOBDRIVEMONIKER, blobName));
            cmd.Parameters.Add("ErrorAction", "Stop");

            PSHelper.ExecuteCommand(this, cmd);
        }

        public AzureQueue CreateQueue(string queueName)
        {
            this.ReportProgress("Creating queue");

            Command cmd = new Command("new-item");
            cmd.Parameters.Add("path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            //// cmd.Parameters.Add("type", "directory");
            cmd.Parameters.Add("ErrorAction", "stop");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            if (powershellResult != null)
            {
                return this.RetrieveQueue(queueName);
            }

            return null;
        }

        public AzureQueue RetrieveQueue(string queueName)
        {
            AzureQueue result = null;

            this.ReportProgress("Retrieving queue");

            Command cmd = new Command("get-item");
            cmd.Parameters.Add("Path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            if (powershellResult != null && powershellResult.Count > 0)
            {
                string script = "$s = $q.ApproximateMessageCount";
                var queue = new AzureQueue(powershellResult[0]);

                // Need to get the size/length of queue seperatly as Size is a method property
                this.ReportProgress("Fetching queue properties");

                var variables = new Dictionary<string, object> 
                {
                    { "s", 0 },
                    { "q", queue.PSObject }
                };

                PSHelper.ExecuteScript(this, script, variables);

                queue.Length = Convert.ToInt32(variables["s"].ToString(), CultureInfo.InvariantCulture);

                result = queue;
            }

            return result;
        }

        public Collection<AzureQueue> RetrieveQueues()
        {
            Collection<AzureQueue> result = new Collection<AzureQueue>();

            this.ReportProgress("Fetching queues");

            Command cmd = new Command("get-item");
            cmd.Parameters.Add("path", BuildCloudPath(QUEUEDRIVEMONIKER, "*"));
            cmd.Parameters.Add("ErrorAction", "stop");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            string script = "$s = $q.ApproximateMessageCount";
            int count = 0;

            foreach (PSObject ps in powershellResult)
            {
                var queue = new AzureQueue(ps);
                
                // Need to get the size/length of each queue seperatly as Size is a method property
                this.ReportProgress("Fetching queue properties", ++count);

                var variables = new Dictionary<string, object> 
                {
                    { "s", 0 },
                    { "q", queue.PSObject }
                };

                PSHelper.ExecuteScript(this, script, variables);

                queue.Length = Convert.ToInt32(variables["s"].ToString(), CultureInfo.InvariantCulture);

                result.Add(queue);
            }
            
            return result;
        }

        public void DeleteQueue(string queueName)
        {
            this.ReportProgress("Deleting queue");

            Command cmd = new Command("del");
            cmd.Parameters.Add("path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            cmd.Parameters.Add("confirm");
            cmd.Parameters.Add("ErrorAction", "stop");

            PSHelper.ExecuteCommand(this, cmd);
        }

        public void PurgeQueue(string queueName)
        {
            this.ReportProgress("Purging queue");

            Command cmd = new Command("clear-item");
            cmd.Parameters.Add("Path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            cmd.Parameters.Add("ErrorAction", "stop");

            PSHelper.ExecuteCommand(this, cmd);
        }

        public Collection<AzureMessage> PeekMessages(string queueName, int maxMessages, bool getContent)
        {
            Collection<AzureMessage> messages = new Collection<AzureMessage>();

            this.ReportProgress("Peeking queue messages");

            // Get the queue as a PSObject so it can be used for peeking
            Command cmd = new Command("get-item");
            cmd.Parameters.Add("Path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");
            
            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            this.ReportProgress("Getting message properties");

            // Peek the messages on the queue
            if (powershellResult != null && powershellResult.Count > 0)
            {
                string script = "$q.PeekMessages(" + maxMessages.ToString(CultureInfo.InvariantCulture) + ")";
                var variables = new Dictionary<string, object>
                {
                    { "q", powershellResult[0] }
                };

                powershellResult = PSHelper.ExecuteScript(this, script, variables);

                if (powershellResult != null && powershellResult.Count > 0)
                {
                    script = "$content = $message.AsString";
                    int count = 0;

                    foreach (PSObject ps in powershellResult)
                    {
                        // If there are no messages in the queue we get an array with a null
                        if (ps != null)
                        {
                            var message = new AzureMessage(ps);

                            // Look again for the content
                            if (getContent)
                            {
                                this.ReportProgress("Getting message content", ++count);

                                variables = new Dictionary<string, object>
                                {
                                    { "message", message.PSObject },
                                    { "content", string.Empty }
                                };

                                powershellResult = PSHelper.ExecuteScript(this, script, variables);

                                if (powershellResult != null)
                                {
                                    message.ContentAsString = variables["content"].ToString();
                                }
                            }

                            messages.Add(message);
                        }
                    }
                }
            }

            return messages;
        }

        public bool CreateMessage(string queueName, string message)
        {
            this.ReportProgress("Creating message");

            Command cmd = new Command("get-item");
            cmd.Parameters.Add("Path", BuildCloudPath(QUEUEDRIVEMONIKER, queueName));
            cmd.Parameters.Add("ErrorAction", "SilentlyContinue");

            Collection<PSObject> powershellResult = PSHelper.ExecuteCommand(this, cmd);

            if (powershellResult != null && powershellResult.Count > 0)
            {
                string script = "$q.AddMessage(\"" + message + "\")";
                var variables = new Dictionary<string, object>
                {
                    { "q", powershellResult[0] }
                };

                powershellResult = PSHelper.ExecuteScript(this, script, variables);

                if (powershellResult != null && powershellResult.Count > 0)
                {
                    return powershellResult[0].Equals(true);
                }
            }

            return false;
        }

        public override void SetShouldExit(int exitCode)
        {
        }

        public override void EnterNestedPrompt()
        {
            throw new NotImplementedException();
        }

        public override void ExitNestedPrompt()
        {
            throw new NotImplementedException();
        }

        public override void NotifyBeginApplication()
        {
            throw new NotImplementedException();
        }

        public override void NotifyEndApplication()
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        internal void RunspaceStateChanged(object sender, RunspaceStateEventArgs e)
        {
            this.State = e.RunspaceStateInfo.State;
        }

        private static string BuildCloudPath(string moniker, params string[] pathSegments)
        {
            string path = moniker;

            for (int i = 0; i < pathSegments.Length; i++)
            {
                // Make sure we have the correct path seperators
                pathSegments[i] = pathSegments[i].Replace(Path.DirectorySeparatorChar.ToString(), PATHSEPERATOR);

                path += pathSegments[i];
                if (i < pathSegments.Length - 1)
                {
                    path += PATHSEPERATOR;
                }
            }

            return path;
        }

        private void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources.
                }

                // Note disposing has been done.
                this.disposed = true;
            }
        }

        private void ReportProgress(string message)
        {
            if (this.OnProgress != null)
            {
                this.OnProgress(message);
            }
        }

        private void ReportProgress(string message, int count)
        {
            if (count % 10 == 0)
            {
                if (this.OnProgress != null)
                {
                    this.OnProgress(message + " ... " + count.ToString(CultureInfo.InvariantCulture));
                }
            }
        }
    }
}