//------------------------------------------------------------------------------
// <copyright file="HPCExcelAddIn.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      HPC Excel Add-In Sample for ExcelRunner
// </summary>
//------------------------------------------------------------------------------

namespace HPCExcelAddIn
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.ServiceModel;
    using System.Threading;
    using System.Windows.Forms;

    using Extensibility;
    using Microsoft.Hpc.Excel;
    using Microsoft.Hpc.Scheduler.Properties;
    using Microsoft.Office.Core;
    using Microsoft.Office.Interop.Excel;

    /// <summary>
    ///   The object for implementing an Add-in.
    /// </summary>
    [GuidAttribute("F0C6BA3D-6A58-4DD1-A90C-097057ADAAC3"), ProgId("HPCExcelAddIn.Connect")]
    public class HPCExcel : object, Extensibility.IDTExtensibility2, IRibbonExtensibility
    {
        #region private_members

        /// <summary>
        /// Path to workbook share
        /// </summary>
        private string workbookSavePath;

        /// <summary>
        /// Cluster Head Node Name
        /// </summary>
        private string headNode;

        /// <summary>
        /// Minimum number of cores requested
        /// </summary>
        private int minCores;

        /// <summary>
        /// Maximum number of cores requested
        /// </summary>
        private int maxCores;

        /// <summary>
        /// Number of iterations of Monte Carlo
        /// </summary>
        private int maxIterations;

        /// <summary>
        /// Ribbon hosting HPC UI
        /// </summary>
        private IRibbonUI ribbon;

        /// <summary>
        /// Thread coordination lock
        /// </summary>
        private object monitor;

        /// <summary>
        /// Reporting interval
        /// </summary>
        private int chunkSize;

        /// <summary>
        /// Thread coordination lock
        /// </summary>
        private AutoResetEvent done = new AutoResetEvent(false);

        /// <summary>
        /// Generic Engine handling SOA and ExcelDriver
        /// </summary>
        private ExcelClient engine;

        /// <summary>
        /// Excel Application that owns the Ribbon
        /// </summary>
        private Microsoft.Office.Interop.Excel.Application runningApplication;

        /// <summary>
        /// Enabled or disable config changes based on session state
        /// </summary>
        private bool configChangesEnabled;

        /// <summary>
        /// Open Session or Close Session depending on session state
        /// </summary>
        private string openCloseSessionLabel;

        /// <summary>
        /// OpenClose Session button enabled/disabled depending on run state
        /// </summary>
        private bool openCloseSessionEnabled;

        /// <summary>
        /// Enabled the run buttons and disable the cancel button
        /// </summary>
        private bool newRunEnabled;

        /// <summary>
        /// Flag to handle cancellation properly in multiple threads
        /// </summary>
        private bool justCancelled;

        #endregion

        /// <summary>
        /// Initializes a new instance of the HPCExcel class
        /// </summary>
        public HPCExcel()
        {
            // Initialize these variables to your default save location and head node
            this.workbookSavePath = @"\\<server>\<share>\<filename>";
            this.headNode = "<headnode>";
            this.maxCores = 8;
            this.configChangesEnabled = true;
            this.openCloseSessionLabel = "Open Session";
            this.openCloseSessionEnabled = true;
            this.newRunEnabled = true;
            this.justCancelled = false;
        }

        #region IDTExtensibility2 members
        /// <summary>
        ///      Implements the OnConnection method of the IDTExtensibility2 interface.
        ///      Receives notification that the Add-in is being loaded.
        /// </summary>
        /// <param name='application'>
        ///      Root object of the host application.
        /// </param>
        /// <param name='connectMode'>
        ///      Describes how the Add-in is being loaded.
        /// </param>
        /// <param name='addInInst'>
        ///      Object representing this Add-in.
        /// </param>
        /// <param name='custom'>
        ///      Custom array reference
        /// </param>
        /// <seealso class='IDTExtensibility2' />
        public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom)
        {
            this.monitor = new object();
            this.runningApplication = (Microsoft.Office.Interop.Excel.Application)application;
        }

        /// <summary>
        ///     Implements the OnDisconnection method of the IDTExtensibility2 interface.
        ///     Receives notification that the Add-in is being unloaded.
        /// </summary>
        /// <param name='disconnectMode'>
        ///      Describes how the Add-in is being unloaded.
        /// </param>
        /// <param name='custom'>
        ///      Array of parameters that are host application specific.
        /// </param>
        /// <seealso class='IDTExtensibility2' />
        public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom)
        {
            if (this.engine != null)
            {
                this.engine.Dispose();
            }

            // Try to release the running application
            try
            {
                Marshal.FinalReleaseComObject(this.runningApplication);
            }
            catch
            {
            }
            this.runningApplication = null;

            // Force garbage collection of COM objects. 
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        /// <summary>
        ///      Implements the OnAddInsUpdate method of the IDTExtensibility2 interface.
        ///      Receives notification that the collection of Add-ins has changed.
        /// </summary>
        /// <param name='custom'>
        ///      Array of parameters that are host application specific.
        /// </param>
        /// <seealso class='IDTExtensibility2' />
        public void OnAddInsUpdate(ref System.Array custom)
        {
        }

        /// <summary>
        ///      Implements the OnStartupComplete method of the IDTExtensibility2 interface.
        ///      Receives notification that the host application has completed loading.
        /// </summary>
        /// <param name='custom'>
        ///      Array of parameters that are host application specific.
        /// </param>
        /// <seealso class='IDTExtensibility2' />
        public void OnStartupComplete(ref System.Array custom)
        {
        }

        /// <summary>
        ///      Implements the OnBeginShutdown method of the IDTExtensibility2 interface.
        ///      Receives notification that the host application is being unloaded.
        /// </summary>
        /// <param name='custom'>
        ///      Array of parameters that are host application specific.
        /// </param>
        /// <seealso class='IDTExtensibility2' />
        public void OnBeginShutdown(ref System.Array custom)
        {
        }
        #endregion

        #region IRibbonExtensibility members

        /// <summary>
        /// Load the UI spec for the Ribbon 
        /// </summary>
        /// <param name="ribbonID">Name of Ribbon</param>
        /// <returns>Resource requested from manifest</returns>
        string IRibbonExtensibility.GetCustomUI(string ribbonID)
        {
            return this.GetResource("customUI.xml");
        }

        /// <summary>
        /// Acquire reference to ribbon
        /// </summary>
        /// <param name="ribbon">Ribbon UI object</param>
        public void OnRibbonLoad(IRibbonUI ribbon)
        {
            this.ribbon = ribbon;
        }

        /// <summary>
        /// Unused Callback function
        /// </summary>
        /// <param name="control">Ribbon UI control object</param>
        public void Calculate(IRibbonControl control)
        {
        }

        /// <summary>
        /// UI Callback to edit save location
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New workbook save location</param>
        public void WorkbookSaveLocationChanged(IRibbonControl control, string text)
        {
            this.workbookSavePath = text;
        }

        /// <summary>
        /// UI Callback to edit headnode name
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New head node name</param>
        public void HeadNodeChanged(IRibbonControl control, string text)
        {
            this.headNode = text;
        }

        /// <summary>
        /// UI Callback to edit minimum cores
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New min cores</param>
        public void MinCoresChanged(IRibbonControl control, string text)
        {
            try
            {
                this.minCores = Int32.Parse(text, System.Globalization.NumberStyles.None);
            }
            catch
            {
                this.minCores = 0;
            }
        }

        /// <summary>
        /// UI Callback to edit maximum cores
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New max cores</param>
        public void MaxCoresChanged(IRibbonControl control, string text)
        {
            try
            {
                this.maxCores = Int32.Parse(text, System.Globalization.NumberStyles.None);
            }
            catch
            {
                this.maxCores = 0;
            }
        }

        /// <summary>
        /// UI Callback to edit maximum iterations
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New max iterations</param>
        public void MaxIterationsChanged(IRibbonControl control, string text)
        {
            try
            {
                this.maxIterations = Int32.Parse(text, System.Globalization.NumberStyles.None);
            }
            catch
            {
                this.maxIterations = 0;
            }
        }

        /// <summary>
        /// UI Callback to edit chunk size
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <param name="text">New chunk size</param>
        public void ChunkSizeChanged(IRibbonControl control, string text)
        {
            try
            {
                this.chunkSize = Int32.Parse(text, System.Globalization.NumberStyles.None);
            }
            catch
            {
                this.chunkSize = 1;
            }
        }

        /// <summary>
        /// UI Call to get default headnode name
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Head node name</returns>
        public string HPCHeadNode_GetText(IRibbonControl control)
        {
            return this.headNode;
        }

        /// <summary>
        /// UI Call to get default minimum cores
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>min number of cores</returns>
        public string MinCores_GetText(IRibbonControl control)
        {
            return this.minCores.ToString();
        }

        /// <summary>
        /// UI Call to get default maximum cores
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>max number of cores</returns>
        public string MaxCores_GetText(IRibbonControl control)
        {
            return this.maxCores.ToString();
        }

        /// <summary>
        /// UI Call to get default maximum iterations
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>max iterations</returns>
        public string MaxIterations_GetText(IRibbonControl control)
        {
            return this.maxIterations.ToString();
        }

        /// <summary>
        /// UI Call to get default maximum iterations
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>max iterations</returns>
        public string ChunkSize_GetText(IRibbonControl control)
        {
            return this.chunkSize.ToString();
        }

        /// <summary>
        /// UI Call to get default workbook share path
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Workbook save location</returns>
        public string WorkbookSaveLocation_GetText(IRibbonControl control)
        {
            return this.workbookSavePath;
        }

        /// <summary>
        /// UI Call to get Enabled state of workbook save location
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool WorkbookSaveLocation_GetEnabled(IRibbonControl control)
        {
            return this.configChangesEnabled;
        }

        /// <summary>
        /// UI Call to get Enabled state of head node
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool HPCHeadNode_GetEnabled(IRibbonControl control)
        {
            return this.configChangesEnabled;
        }

        /// <summary>
        /// UI Call to get Enabled state of core count
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool MaxCores_GetEnabled(IRibbonControl control)
        {
            return this.configChangesEnabled;
        }

        /// <summary>
        /// UI Call to get name of open/close session button
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Either "Open Session" or "Close Session" depending on program state</returns>
        public string OpenCloseSession_GetLabel(IRibbonControl control)
        {
            return this.openCloseSessionLabel;
        }

        /// <summary>
        /// UI Call to get Enabled state of open/close session button
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool OpenCloseSession_GetEnabled(IRibbonControl control)
        {
            return this.openCloseSessionEnabled;
        }

        /// <summary>
        /// UI Call to get Enabled state of run on cluster button
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool RunOnCluster_GetEnabled(IRibbonControl control)
        {
            return this.newRunEnabled;
        }

        /// <summary>
        /// UI Call to get Enabled state of run locally button
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool RunLocal_GetEnabled(IRibbonControl control)
        {
            return this.newRunEnabled;
        }

        /// <summary>
        /// UI Call to get Enabled state of cancel calculation button
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        /// <returns>Enabled/Disabled State</returns>
        public bool CancelRun_GetEnabled(IRibbonControl control)
        {
            return !this.newRunEnabled;
        }

        #endregion

        /// <summary>
        /// UI Callback to begin calculation on cluster spins up new thread 
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        public void RunOnCluster(IRibbonControl control)
        {
            // Disable configuration changes while running
            this.EnableConfigChanges(false);
            this.EnableOpenCloseSession(false);
            this.EnableNewRuns(false);

            ThreadPool.QueueUserWorkItem(new WaitCallback(this.RunOnCluster_Thread));
        }

        /// <summary>
        /// UI Callback to begin calculation on cluster spins up new thread 
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        public void RunLocal(IRibbonControl control)
        {
            // Disable new runs and changing session state
            this.EnableNewRuns(false);
            this.EnableOpenCloseSession(false);

            ThreadPool.QueueUserWorkItem(new WaitCallback(this.RunLocal_Thread));
        }

        /// <summary>
        /// UI Callback to begin calculation on cluster spins up new thread 
        /// </summary>
        /// <param name="control">Ribbon UI object</param>
        public void CancelRun(IRibbonControl control)
        {
            // Disable making any new runs
            this.EnableNewRuns(false);

            ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(object stateInfo)
            {
                try
                {
                    this.engine.Cancel();
                    this.justCancelled = true;
                }
                catch (Exception ex)
                {
                    // Let user know if there are any errors
                    MessageBox.Show(ex.Message, "Error during cancellation");
                }
            }));
        }

        /// <summary>
        /// Opens or closes the session based on the program state
        /// </summary>
        /// <param name="control">UI control object</param>
        public void RunOpenCloseSession(IRibbonControl control)
        {
            // Disallow changes to configuration while opening or closing
            this.EnableConfigChanges(false);
            this.EnableOpenCloseSession(false);
            this.EnableNewRuns(false);

            // Open or close the session in a separate thread to free the UI
            ThreadPool.QueueUserWorkItem(new WaitCallback(delegate(object stateInfo)
            {
                try
                {
                    // If engine is null, then we're opening the session, otherwise we're closing it
                    if (this.engine == null)
                    {
                        // create a new ExcelClient, initialize it, open a session, and deploy the workbook
                        this.engine = new ExcelClient();
                        this.engine.Initialize(this.runningApplication, this.workbookSavePath);
                        this.engine.OpenSession(this.headNode, this.maxCores, this.maxCores, JobUnitType.Core, null);

                        // Attempt to deploy workbook to share specified in ribbon
                        this.engine.DeployWorkbook();

                        this.openCloseSessionLabel = "Close Session";
                    }
                    else
                    {
                        // close down the ExcelClient instance
                        this.engine.Dispose();
                        this.engine = null;

                        this.configChangesEnabled = true;
                        this.openCloseSessionLabel = "Open Session";
                    }
                }
                catch (Exception ex)
                {
                    // Let user know if there are any errors
                    MessageBox.Show(ex.Message, "Error while opening or closing the session");

                    // If there are any problems, just kill the ExcelClient instance
                    if (this.engine != null)
                    {
                        this.engine.Dispose();
                        this.engine = null;
                        this.configChangesEnabled = true;
                        this.openCloseSessionLabel = "Open Session";
                    }
                }
                finally
                {
                    // Reallow changes
                    this.EnableConfigChanges(this.configChangesEnabled);
                    this.EnableOpenCloseSession(true);
                    this.EnableNewRuns(true);
                }
            }));
        }

        /// <summary>
        /// Checks input parameters, creates session and client, 
        /// registers response handler, and sends calculation requests
        /// </summary>
        /// <param name="stateInfo">Thread state parameter</param>
        private void RunOnCluster_Thread(object stateInfo)
        {
            try
            {
                // First time using 
                if (this.engine == null)
                {
                    this.engine = new ExcelClient();
                    this.engine.Initialize(this.runningApplication, this.workbookSavePath);
                    this.engine.OpenSession(this.headNode, this.maxCores, this.maxCores, JobUnitType.Core, null);

                    // Attempt to deploy workbook to share specified in ribbon
                    this.engine.DeployWorkbook();
                    this.openCloseSessionLabel = "Close Session";
                }

                this.engine.Run(false);
            }
            catch (Exception ex)
            {
                if (!this.justCancelled)
                {
                    // Let user know if there are any errors
                    MessageBox.Show(ex.Message, "Error while calculating on the cluster");

                    // If this hasn't already been cancelled, close the session and allow configuration changes
                    if (this.engine != null)
                    {
                        this.engine.Dispose();
                        this.engine = null;
                        this.configChangesEnabled = true;
                        this.openCloseSessionLabel = "Open Session";
                    }
                }
            }
            finally
            {
                // Set Open/Close session button to "Close Session", and reenable it.
                this.EnableConfigChanges(this.configChangesEnabled);
                this.EnableOpenCloseSession(true);
                this.EnableNewRuns(true);
                this.justCancelled = false;
            }
        }

        /// <summary>
        /// Registers engine and runs calculations locally
        /// </summary>
        /// <param name="stateInfo">Thread state parameter</param>
        private void RunLocal_Thread(object stateInfo)
        {
            try
            {
                // If engine hasn't been configured for cluster computation
                // use new engine. Otherwise use existing engine.
                if (this.engine == null)
                {
                    using (this.engine = new ExcelClient())
                    {
                        this.engine.Initialize(this.runningApplication);
                        this.engine.Run(true);
                    }

                    this.engine = null;
                }
                else
                {
                    this.engine.Run(true);
                }
            }
            catch (Exception ex)
            {
                if (!this.justCancelled)
                {
                    // Let user know if there are any errors
                    MessageBox.Show(ex.Message, "Error while calculating locally");

                    // If this hasn't already been cancelled, close the session and allow configuration changes
                    if (this.engine != null)
                    {
                        this.engine.Dispose();
                        this.engine = null;
                        this.configChangesEnabled = true;
                        this.openCloseSessionLabel = "Open Session";
                    }
                }
            }
            finally
            {
                this.EnableConfigChanges(this.configChangesEnabled);
                this.EnableNewRuns(true);
                this.EnableOpenCloseSession(true);
                this.justCancelled = false;
            }
        }

        /// <summary>
        /// Return resource from assembly manifest
        /// </summary>
        /// <param name="resourceName">Name of resource to return</param>
        /// <returns>Path to Resource requested</returns>
        private string GetResource(string resourceName)
        {
            // Load custom UI XML
            Assembly asm = Assembly.GetExecutingAssembly();
            foreach (string name in asm.GetManifestResourceNames())
            {
                if (name.EndsWith(resourceName))
                {
                    System.IO.TextReader tr = new System.IO.StreamReader(asm.GetManifestResourceStream(name));
                    string resource = tr.ReadToEnd();

                    tr.Close();
                    return resource;
                }
            }

            return null;
        }

        /// <summary>
        /// Set config changes to enabled or disabled based on parameter
        /// </summary>
        /// <param name="enable">Enabled or disable based on true/false</param>
        private void EnableConfigChanges(bool enable)
        {
            // Enable configuration changes and reload the affected UI
            this.configChangesEnabled = enable;
            this.ribbon.InvalidateControl("hpcWorkbook");
            this.ribbon.InvalidateControl("hpcHeadNode");
            this.ribbon.InvalidateControl("maxCores");
        }

        /// <summary>
        /// Set open/close session to enabled/disabled based on parameter
        /// </summary>
        /// <param name="enable">Enabled or disable based on true/false</param>
        private void EnableOpenCloseSession(bool enable)
        {
            // Enable open/close session and reload the affected UI
            this.openCloseSessionEnabled = enable;
            this.ribbon.InvalidateControl("btnCloseSession");
        }

        /// <summary>
        /// Set new runs to enabled/disabled based on parameter
        /// </summary>
        /// <param name="enable">Enabled or disable based on true/false</param>
        private void EnableNewRuns(bool enable)
        {
            // Enable calculate local/cluster/cancel and reload the affected UI
            this.newRunEnabled = enable;
            this.ribbon.InvalidateControl("btnCalcCluster");
            this.ribbon.InvalidateControl("btnCalcLocal");
            this.ribbon.InvalidateControl("btnCancel");
        }
    } // Class
} // Namespace