#region <<< Using Directives >>>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Windows.Forms;

using Volpe.Cafe.EspressoUI.Charts;
using Volpe.Cafe.EspressoUI.LegacyControls;
using Volpe.Cafe.EspressoUI.ModelCharts;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.Model;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.UI;
using Volpe.Cafe.Utils;
#endregion

namespace Volpe.Cafe.EspressoUI
{
    /// <summary>
    /// Provides a graphical user interface (GUI) for a single session of the CAFE Model.
    /// </summary>
    public class SessionPanel : UserControl, ISession
    {

        #region <<< Nested Types >>>

        sealed class SessionMessages
        {
            public const string CAFEModelError = "CAFE Model Error";
            public const string CAFEModel      = "CAFE Model";

            public const string NEW_STATUS_SessionCreated = "New session created.";

            public const string OPEN_FileNotFound = "Error opening session file.\n\nThe specified file cannot be found on disk.";
            public const string OPEN_SessionNotValid = "Error opening session file.\n\nThe specified file may not represent a valid session, or the session format is no longer supported.";
            public const string OPEN_STATUS_SelectSessionFile = "Please select the session file you would like to open ...";
            public const string OPEN_STATUS_LoadingSession = "Loading session ...";
            public const string OPEN_STATUS_SessionLoadingFailed = "Session loading failed.";
            public const string OPEN_STATUS_SessionNotLoaded = "Session not loaded.";
            public const string OPEN_STATUS_SessionLoaded = "Session loaded.";
            public const string OPEN_STATUS_SessionLoadedReadOnly = "Session loaded as read-only.";

            public const string SAVE_SessionRunning = "The session is currently running.\n\nPlease wait for modeling to complete, or stop the session before saving again.";
            public const string SAVE_STATUS_SavingSession = "Saving session ...";
            public const string SAVE_STATUS_SessionSaved = "Session saved.";

            public const string CLOSE_SessionRunning = "The session is currently running.\n\nPlease wait for modeling to complete, or stop the session before closing.";
            public const string CLOSE_SessionNotSaved = "The session has not been saved yet.\n\nWould you like to save the changes before closing?";
            public const string CLOSE_CloseSession = "A CAFE Model session is currently open and will be closed.\n\nAre you sure you would like to proceed?";
            public const string CLOSE_STATUS_SessionClosed = "Session closed.";

            public const string START_SessionRunning = "The session is already running.";
            public const string START_SessionReadOnly = "The session cannot be run because it was opened as read-only.";
            public const string START_NoComplianceModel = "The session cannot be run since no valid compliance model has been selected.\n\nPlease open the Modeling Settings window to select a compliance model you wish to run.";
            public const string START_NoModelData = "The session cannot be run since industry data has not been loaded.\n\nPlease open the Modeling Settings window and select a valid market-data input file.";
            public const string START_NoModelSettings = "The session cannot be run since some or all of the input files have not been loaded.\n\nPlease open the Modeling Settings window and select the appropriate input files.";
            public const string START_OutputPathLockError =
                "The session cannot be run since an exclusive lock on the output path could not be obtained. " +
                "Either the path is locked by another session, or you do not have write permission.\n\n" +
                "Please choose a different location and try again.";

            public const string STOP_SessionNotRunning = "The session is not currently running.";

            public const string COMPLIANCE_STATUS_StartRequested = "Starting the Modeling Process ...";
            public const string COMPLIANCE_STATUS_Running        = "Model Running ...";
            public const string COMPLIANCE_STATUS_StopRequested  = "Stopping the Modeling Process ...";
            public const string COMPLIANCE_STATUS_Stopped        = "Modeling Stopped.";
            public const string COMPLIANCE_STATUS_Completed      = "Modeling Completed!";
            public const string COMPLIANCE_STATUS_Unstarted      = "The Modeling Process has not Started.";
            public const string COMPLIANCE_STATUS_CompressOutputs = "Compressing model outputs ...";

        }

        #endregion

        #region <<< Events >>>

        /// <summary>Occurs when the session state and/or progress is being updated.</summary>
        [Browsable(true)]
        [Category("Behavior")]
        public event EventHandler SessionUpdating;
        /// <summary>Occurs when the name of the session changes.</summary>
        [Browsable(true)]
        [Category("Behavior")]
        public event EventHandler SessionNameChanged;

        #endregion

        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new empty CAFE Model session.
        /// </summary>
        public SessionPanel()
        {
            this.InitializeComponent();
            this.InitializeCharts();
        }
        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }

        #endregion

        #region <<< Methods >>>

        #region < Windows Form Designer generated code >

        void InitializeComponent()
        {
            this.ux_progressPanel = new System.Windows.Forms.Panel();
            this.ux_sideChartPanel1 = new System.Windows.Forms.Panel();
            this.ux_sideChartPanel2 = new System.Windows.Forms.Panel();
            this.ux_mainChartPanel = new System.Windows.Forms.Panel();
            this.SuspendLayout();
            // 
            // ux_progressPanel
            // 
            this.ux_progressPanel.Location = new System.Drawing.Point(0, 0);
            this.ux_progressPanel.Name = "ux_progressPanel";
            this.ux_progressPanel.Size = new System.Drawing.Size(240, 160);
            this.ux_progressPanel.TabIndex = 0;
            // 
            // ux_sideChartPanel1
            // 
            this.ux_sideChartPanel1.Location = new System.Drawing.Point(0, 160);
            this.ux_sideChartPanel1.Name = "ux_sideChartPanel1";
            this.ux_sideChartPanel1.Size = new System.Drawing.Size(240, 160);
            this.ux_sideChartPanel1.TabIndex = 1;
            // 
            // ux_sideChartPanel2
            // 
            this.ux_sideChartPanel2.Location = new System.Drawing.Point(0, 320);
            this.ux_sideChartPanel2.Name = "ux_sideChartPanel2";
            this.ux_sideChartPanel2.Size = new System.Drawing.Size(240, 160);
            this.ux_sideChartPanel2.TabIndex = 2;
            // 
            // ux_mainChartPanel
            // 
            this.ux_mainChartPanel.Location = new System.Drawing.Point(240, 0);
            this.ux_mainChartPanel.Name = "ux_mainChartPanel";
            this.ux_mainChartPanel.Size = new System.Drawing.Size(240, 480);
            this.ux_mainChartPanel.TabIndex = 3;
            // 
            // SessionPanel
            // 
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
            this.Controls.Add(this.ux_sideChartPanel2);
            this.Controls.Add(this.ux_sideChartPanel1);
            this.Controls.Add(this.ux_mainChartPanel);
            this.Controls.Add(this.ux_progressPanel);
            this.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.Name = "SessionPanel";
            this.Size = new System.Drawing.Size(480, 480);
            this.ResumeLayout(false);

        }

        #endregion

        #region < Overridden Methods >

        /// <summary>
        /// Raises the <see cref="Control.SizeChanged"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected override void OnSizeChanged(EventArgs e)
        {
            this.UpdateLayout();
            base.OnSizeChanged(e);
        }
        void UpdateLayout()
        {
            const int MIN_W  = 240; // min allowed size of each chart element
            const int MIN_H  = 160;

            int w = this.ClientSize.Width;
            int h = this.ClientSize.Height;

            // calc the preferred size of the progress panel (everything is derived from this)
            int pw = Math.Max(MIN_W, w * 1 / 3);
            int ph = Math.Max(MIN_H, h * 1 / 3);

            // update bounds of panels
            this.BeginUpdate();
            this.ux_progressPanel  .SetBounds(0 , 0     , pw, ph);
            this.ux_sideChartPanel1.SetBounds(0 , ph    , pw, ph);
            this.ux_sideChartPanel2.SetBounds(0 , ph * 2, pw, ph);
            this.ux_mainChartPanel .SetBounds(pw, 0, Math.Max(MIN_W * 2, w - pw), Math.Max(MIN_H * 2, h));
            this.EndUpdate();
        }

        #endregion

        #region < Methods for raising events >

        /// <summary>
        /// Raises the <see cref="SessionUpdating"/> event.
        /// </summary>
        protected virtual void OnSessionUpdating()
        {
            this.SessionUpdating?.Invoke(this, EventArgs.Empty);
        }
        /// <summary>
        /// Raises the <see cref="SessionNameChanged"/> event.
        /// </summary>
        protected virtual void OnSessionNameChanged()
        {
            this.SessionNameChanged?.Invoke(this, EventArgs.Empty);
        }

        #endregion

        void InitializeCharts()
        {
            // initialize progress chart
            this._progressChart = new ModelingProgressChart() { Dock = DockStyle.Fill, TabStop = true };

            // initialize all other charts
            var indSummary = new SessionSummaryChart("Industry Summary");
            var ecoSummary = new CompliancePerformanceChart("Compliance Performance");
            var vehScatter = new VehicleScatterPlot();

            this._allCharts.Add(indSummary);
            this._allCharts.Add(ecoSummary);
            this._allCharts.Add(vehScatter);

            foreach (ModelChart chart in this._allCharts)
            {
                chart.Dock = DockStyle.Fill;
                chart.TabStop = true;
                chart.AutoExpand = true;
                chart.AutoExpandTopLevel = true;
            }

            // add charts to view panels
            this.SuspendLayout();
            this.ux_progressPanel  .Controls.Add(this._progressChart);
            this.ux_mainChartPanel .Controls.Add(indSummary);
            this.ux_sideChartPanel1.Controls.Add(vehScatter);
            this.ux_sideChartPanel2.Controls.Add(ecoSummary);
            this.ResumeLayout(false);
        }

        /// <summary>
        /// Creates a new empty session.
        /// </summary>
        /// <param name="makeVisible">true, to make the new session visible in the parent; false, otherwise.</param>
        public void CreateNewSession(bool makeVisible = true)
        {
            if (this.IsOpen && !this.CloseSession(true)) { return; }

            this._compliance     = null;
            this._data           = null;
            this._settings       = new ModelingSettings();
            //
            this.Index           = SessionCount++;
            this.SessionName     = null;
            this.SessionPath     = null;
            this.IsOpen          = true;
            this.IsReadOnly      = false;
            this.StopRequested   = false;
            this.Status          = null;
            this.RefreshInterval = RefreshIntervalNormal;
            this.SaveRequired    = true;

            // update gui ...
            this.UpdateSessionName();
            this.ResetSessionProgress();
            this.RefreshSession();
            if (makeVisible) { this.Visible = true; }
            this.UpdateStatus(SessionMessages.NEW_STATUS_SessionCreated);
        }

        /// <summary>
        /// Displays an <see cref="OpenFileDialog"/>, and opens the session selected by the user.
        /// </summary>
        /// <param name="silent">true, to silently accept all prompts; false, otherwise.</param>
        /// <returns>true, if the session was opened; false, otherwise.</returns>
        public bool OpenSession(bool silent = false)
        {
            return this.OpenSession_Internal(null, true, silent);
        }
        /// <summary>
        /// Opens the session at the specified path.
        /// </summary>
        /// <param name="path">The path of the session to load.</param>
        /// <param name="silent">true, to silently accept all prompts; false, otherwise.</param>
        /// <returns>true, if the session was opened; false, otherwise.</returns>
        public bool OpenSession(string path, bool silent = false)
        {
            return this.OpenSession_Internal(path, false, silent);
        }
        /// <summary>
        /// Opens the session at the specified path, or displays an <see cref="OpenFileDialog"/>, and opens the session
        /// selected by the user.
        /// </summary>
        /// <param name="path">The path to open, or null, if displayOpen is true.</param>
        /// <param name="displayOpen">true, to display the "Open" dialog.</param>
        /// <param name="silent">true, to silently accept all prompts; false, otherwise.</param>
        /// <returns>true, if the session was opened; false, otherwise.</returns>
        bool OpenSession_Internal(string path, bool displayOpen, bool silent)
        {
            if (this.IsOpen && !this.CloseSession(true)) { return false; }

            if (displayOpen)
            {   // prompt for session file to open
                this.UpdateStatus(SessionMessages.OPEN_STATUS_SelectSessionFile);
                var dlg = new OpenFileDialog()
                {
                    Filter = "All Files (*.*)|*.*|CAFE Model Session Data (*.cmsd)|*.cmsd",
                    FilterIndex = 2,
                    Title = "Please select the session file you would like to open:"
                };
                if (dlg.ShowDialog() == DialogResult.OK) { this.SessionPath = dlg.FileName; }
                else { this.UpdateStatus(SessionMessages.OPEN_STATUS_SessionNotLoaded); return false; }
            }
            else
            {   // use specified session file path
                this.SessionPath = path;
            }

            // perform some error-checking ...
            if (!File.Exists(this.SessionPath))
            {
                this.UpdateStatus(SessionMessages.OPEN_STATUS_SessionLoadingFailed);
                if (!silent) { this.ShowErrorPrompt(SessionMessages.OPEN_FileNotFound); }
                return false;
            }

            // -------------------------------------------------- \\
            // open the session file ...
            this.UpdateStatus(SessionMessages.OPEN_STATUS_LoadingSession);
            //
            var bf = new BinaryFormatter();
            Stream dataStream = null;
            GZipStream gzip = null;
            try
            {   // decompress the session file
                dataStream = new MemoryStream();
                gzip = new GZipStream(new FileStream(this.SessionPath, FileMode.Open, FileAccess.Read, FileShare.Read), CompressionMode.Decompress);
                byte[] gzipBuf = new byte[4096];
                int gzipBufCount = 0;
                do
                {
                    dataStream.Write(gzipBuf, 0, gzipBufCount);
                    gzipBufCount = gzip.Read(gzipBuf, 0, gzipBuf.Length);
                } while (gzipBufCount > 0);

                // deserialize session data
                dataStream.Seek(0, SeekOrigin.Begin);
                object[] sessionData = (object[])bf.Deserialize(dataStream);

                // copy session data to local vars
                this._compliance      = (ICompliance     )sessionData[0];
                this._data            = (IIndustry       )sessionData[1];
                this._settings        = (ModelingSettings)sessionData[2];
                this._refreshInterval = (int             )sessionData[3];

                // update runtime varaibles ...
                // (if session was opened read-only, a save will be required prior to running the model)
                var  fileInfo      = new FileInfo(this.SessionPath);
                this.IsOpen        = true;
                this.IsReadOnly    = fileInfo.IsReadOnly;
                this.SaveRequired  = this.IsReadOnly;
                this.StopRequested = false;

                // if session was opened as read/write, mark session file as read-only to prevent other instances from taking control
                if (!this.IsReadOnly) { fileInfo.IsReadOnly = true; }

                // update gui ...
                this.UpdateSessionName();
                this.ResetSessionProgress();
                this.RefreshSession();
                this.UpdateStatus((this.IsReadOnly) ?
                    SessionMessages.OPEN_STATUS_SessionLoadedReadOnly :
                    SessionMessages.OPEN_STATUS_SessionLoaded);

                // add to recent items lists
                RecentItems.AddEntry(this.SessionPath);
                return true;
            }
            catch (Exception ex)
            {   // update gui
                this.UpdateStatus(SessionMessages.OPEN_STATUS_SessionLoadingFailed);
                // write exception to console and show error prompt
                Console.WriteLine(ex.ToString());
                if (!silent) { this.ShowErrorPrompt(SessionMessages.OPEN_SessionNotValid); }
                return false;
            }
            finally
            {   // close any opened streams
                if (dataStream != null) { dataStream.Close(); }
                if (gzip       != null) { gzip      .Close(); }
            }
        }

        /// <summary>
        /// Closes the session by cleaning up runtime variables. If the session is running or needs saving, a
        /// user-prompt will be generated.
        /// </summary>
        /// <param name="confirmClose">Whether to display a close confirmation dialog to the user before closing the 
        ///     session.</param>
        /// <returns>true, if the session was closed; false, otherwise.</returns>
        public bool CloseSession(bool confirmClose = false)
        {
            if (!this.IsOpen) { return true; }

            if (this.IsRunning)
            {   // the session is running
                MessageBox.Show(SessionMessages.CLOSE_SessionRunning, SessionMessages.CAFEModel, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return false;
            }

            if (this.SaveRequired || confirmClose)
            {   // the session hasn't been saved or close-confirmation is required -- prompt user
                DialogResult dr;
                if (this.SaveRequired)
                {   // session needs saving
                    dr = MessageBox.Show(SessionMessages.CLOSE_SessionNotSaved, SessionMessages.CAFEModel,
                        MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button3);
                }
                else
                {   // session needs close confirmation
                    dr = MessageBox.Show(SessionMessages.CLOSE_CloseSession, SessionMessages.CAFEModel,
                        MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
                }
                //
                if (dr == DialogResult.Cancel || (!this.SaveRequired && dr == DialogResult.No))
                {   // user selected to cancel closing
                    return false;
                }
                else if (this.SaveRequired && dr == DialogResult.Yes)
                {   // user selected to save the session before closing
                    if (!this.SaveSession()) { return false; }
                }
            }

            // continue with session closing
            //
            // clear out read-only setting on the session file, but only if session was opened as read/write
            if (!this.IsReadOnly && File.Exists(this.SessionPath))
            {
                (new FileInfo(this.SessionPath)).IsReadOnly = false;
            }
            //
            // clear certain elements ...
            this._compliance     = null;
            this._data           = null;
            this._settings       = null;
            //
            this.Index           = -1;
            this.SessionName     = null;
            this.SessionPath     = null;
            this.IsOpen          = false;
            this.IsReadOnly      = false;
            this.StopRequested   = false;
            this.Status          = null;
            this.RefreshInterval = RefreshIntervalPaused;
            this.SaveRequired    = false;

            // reset chart states
            foreach (ModelChart chart in this._allCharts)
            {
                chart.ResetChart();
            }
            this._progressChart.ResetChart();

            // force garbage collection to ensure resources held by this session are released
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            // update gui ...
            this.UpdateSessionName();
            //this.RefreshSession();
            this.UpdateStatus(SessionMessages.CLOSE_STATUS_SessionClosed);

            // session closed
            return true;
        }

        /// <summary>
        /// Saves the session to disk at the default location. If the session has not been previously saved, a
        /// <see cref="SaveFileDialog"/> will be displayed.
        /// </summary>
        /// <returns>true, if the session was saved; false, otherwise.</returns>
        public bool SaveSession()
        {
            return this.SaveSession_Internal(false, null);
        }
        /// <summary>
        /// Saves the session to disk at the default location, using the specified session name. If the session has not
        /// been previously saved, a <see cref="SaveFileDialog"/> will be displayed.
        /// </summary>
        /// <param name="name">The name to assign to the session.</param>
        /// <returns>true, if the session was saved; false, otherwise.</returns>
        public bool SaveSession(string name)
        {
            if (name != null && name.Trim() != string.Empty && name != this.SessionName)
            {
                this.SaveRequired = true;
            }
            return this.SaveSession_Internal(false, name);
        }
        /// <summary>
        /// Displays a <see cref="SaveFileDialog"/>, and saves the session to the location selected by the user.
        /// </summary>
        /// <returns>true, if the session was saved; false, otherwise.</returns>
        public bool SaveSessionAs()
        {
            return this.SaveSession_Internal(true, null);
        }
        /// <summary>
        /// Saves the session to disk at the default location, or displays a <see cref="SaveFileDialog"/>, and saves
        /// the session to the location selected by the user.
        /// </summary>
        /// <param name="displaySaveAs">true, to display the "Save As" dialog.</param>
        /// <param name="name">The name to assign to the session file.</param>
        /// <returns>true, if the session was saved; false, otherwise.</returns>
        bool SaveSession_Internal(bool displaySaveAs, string name)
        {
            if (!this.IsOpen || (!displaySaveAs && !this.SaveRequired)) { return false; }
            if (this.IsRunning)
            {
                MessageBox.Show(SessionMessages.SAVE_SessionRunning, SessionMessages.CAFEModel, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return false;
            }

            // append path with the new session name
            bool silent = false;
            if (name != null)
            {
                name = name.Trim();
                if (File.Exists(this.SessionPath) && name != string.Empty)
                {
                    this.SessionPath = Path.GetDirectoryName(this.SessionPath) + "\\" + name + ".cmsd";
                    silent = true;
                }
            }

            // remove read-only flag if saving to existing session file
            if (File.Exists(this.SessionPath) && !this.IsReadOnly)
            {
                (new FileInfo(this.SessionPath)).IsReadOnly = false;
            }

            // display the save as dialog, if required
            if (displaySaveAs || this.SessionPath == null || (!silent && (!File.Exists(this.SessionPath) ||
                (File.GetAttributes(this.SessionPath) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)))
            {
                var dlg = new SaveFileDialog()
                {
                    FileName = File.Exists(this.SessionPath) ? this.SessionPath : ((name ?? this.SessionName) + ".cmsd"),
                    Filter = "CAFE Model Session Data (*.cmsd)|*.cmsd",
                    Title = $"Please specify the location where you would like to save the '{(name ?? this.SessionName)}' session:"
                };
                if (dlg.ShowDialog() == DialogResult.OK)
                {   // update the session path -- save is performed below ...
                    this.SessionPath = dlg.FileName;
                }
                else
                {   // user canceled save -- mark the session file as read-only to prevent other instances from taking control
                    if (File.Exists(this.SessionPath) && !this.IsReadOnly)
                    {
                        File.SetAttributes(this.SessionPath, FileAttributes.ReadOnly);
                    }
                    return false;
                }
            }

            // -------------------------------------------------- \\
            // save the session ...
            this.UpdateStatus(SessionMessages.SAVE_STATUS_SavingSession);
            //
            // copy local vars to the session data object
            object[] sessionData = new object[4];
            ICompliance complianceModel = null;
            if (this._compliance != null)
            {
                complianceModel              = this._compliance.CreateNew();
                complianceModel.UserNotes    = this._compliance.UserNotes;
                complianceModel.UserKeywords = this._compliance.UserKeywords;
            }
            sessionData[0] = complianceModel;
            sessionData[1] = this._data;
            sessionData[2] = this._settings;
            sessionData[3] = this._refreshInterval;

            // serialize the session to memory
            var dataStream = new MemoryStream();
            var bf = new BinaryFormatter();
            bf.Serialize(dataStream, sessionData);
            dataStream.Seek(0, SeekOrigin.Begin);
            // compress the serialized session data
            var gzip = new GZipStream(new FileStream(this.SessionPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), CompressionMode.Compress);
            byte[] gzipBuf = new byte[4096];
            int gzipBufCount = 0;
            do
            {
                gzip.Write(gzipBuf, 0, gzipBufCount);
                gzipBufCount = dataStream.Read(gzipBuf, 0, gzipBuf.Length);
            } while (gzipBufCount > 0);
            gzip.Close();
            dataStream.Close();
            // mark the saved session file as read-only to prevent other instances from taking control
            File.SetAttributes(this.SessionPath, FileAttributes.ReadOnly);

            // update gui ...
            this.SaveRequired = false;
            this.IsReadOnly   = false;
            //
            this.UpdateSessionName();
            this.RefreshSession();
            this.UpdateStatus(SessionMessages.SAVE_STATUS_SessionSaved);

            // add to recent items lists
            RecentItems.AddEntry(this.SessionPath);
            //
            return true;
        }

        /// <summary>
        /// Starts the compliance modeling process for the current session.
        /// </summary>
        public void StartModeling()
        {
            if (!this.IsOpen || this.IsInitializing) { return; }

            // perform some error-checking ...
            string msg = string.Empty;
            var msgIcon = MessageBoxIcon.Exclamation;

            if (this.IsRunning) {
                msg = SessionMessages.START_SessionRunning;
                msgIcon = MessageBoxIcon.Information;
            }
            else if (this.IsReadOnly) {
                msg = SessionMessages.START_SessionReadOnly;
                msgIcon = MessageBoxIcon.Information;
            }
            else if (this._compliance == null) { msg = SessionMessages.START_NoComplianceModel; }
            else if (this._data == null) { msg = SessionMessages.START_NoModelData; }
            else if (this._settings == null) { msg = SessionMessages.START_NoModelSettings; }

            if (msg != string.Empty)
            {
                MessageBox.Show(msg,
                    (msgIcon == MessageBoxIcon.Exclamation) ? SessionMessages.CAFEModelError : SessionMessages.CAFEModel,
                    MessageBoxButtons.OK, msgIcon);
                return;
            }

            // Check for existing results data and warn user if exists.
            var csvReportPath = $"{this.Settings.IOSettings.BaseOutputPath}\\{this.SessionName}\\reports-csv";

            if (Directory.Exists(csvReportPath)) {
                var userInput = this.ShowOverwriteWarningDialogue();
                if (userInput != DialogResult.Yes) {
                    return;
                }
            }

            // initialize vehicle simulation database -- DO NOT START modeling on failure
            this.IsInitializing = true;
            this.UpdateStatus("Initializing vehicle simulation database ...");
            bool isInitialized = false;
            var thread = new Thread(() => {
                isInitialized = Program.InitializeSimulationDB();
            });
            thread.Start();
            while (thread.IsAlive)
            {
                Application.DoEvents();
                Thread.Sleep(31);
            }
            this.IsInitializing = false;
            if (!isInitialized)
            {
                this.UpdateStatus("Vehicle simulation database failed to initialize.");
                return;
            }

            // create copy of data for compliance --
            // do not use session level data, in case user stops and restarts the model
            // we want to preserve original copies
            this.UpdateStatus("Preparing to start compliance modeling ...");
            var data = this._settings.RuntimeSettings.ExcludeBevFcvFromAnalysis
                     ? this._data.CreateNewWithoutBevFcv()
                     : this._data.CreateNew();
            this._settings.IOSettings.SessionName = this.SessionName;

            // initialize progress charts
            this._progressChart.InitializeChart(this._compliance, data, this._settings);
            foreach (ModelChart chart in this._allCharts)
            {
                chart.InitializeChart(this._compliance, data, this._settings);
            }
            this.ResetSessionProgress();

            // start the modeling process ...
            this.StopRequested = false;
            this._compliance.ModelingChanged += this.Compliance_ModelingChanged;
            this._compliance.Prompt          += this.Compliance_Prompt;
            this._compliance.Start(data, this._settings);

            this.RefreshSession();
            this.InitRefreshSessionThread();

            // paint demo
            if (this._allCharts.Count > 0)
            {
                this._allCharts[0].PaintTrackingDemo();
            }
        }
        /// <summary>
        /// Stops the compliance modeling process for the current session.
        /// </summary>
        public void StopModeling()
        {
            if (!this.IsOpen || this.IsInitializing) { return; }

            if (!this.IsRunning)
            {
                MessageBox.Show(SessionMessages.STOP_SessionNotRunning, SessionMessages.CAFEModel,
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }

            // stop the modeling process ...
            if (this.StopRequested)
            {
                this._compliance.Stop(false);
                this.StopRequested = false;
            }
            else
            {
                this._compliance.Stop(true);
                this.StopRequested = true;
                this.InitWaitToStopThread();
            }
        }

        //----- event handlers from the compliance model -----
        void Compliance_ModelingChanged(object sender, ModelingEventArgs e)
        {
            ModelingState state = e.State;

            // update status
            string status = string.Empty;

            if      (state == ModelingState.StartRequested) { status = SessionMessages.COMPLIANCE_STATUS_StartRequested; }
            else if (state == ModelingState.Running       ) { status = SessionMessages.COMPLIANCE_STATUS_Running;        }
            else if (state == ModelingState.StopRequested ) { status = SessionMessages.COMPLIANCE_STATUS_StopRequested;  }
            else if (state == ModelingState.Stopped       ) { status = SessionMessages.COMPLIANCE_STATUS_Stopped;        }
            else if (state == ModelingState.Completed     ) { status = SessionMessages.COMPLIANCE_STATUS_Completed;      }
            else if (state == ModelingState.Unstarted     ) { status = SessionMessages.COMPLIANCE_STATUS_Unstarted;      }
            else if (state == ModelingState.CompressOutputs) {
                status = SessionMessages.COMPLIANCE_STATUS_CompressOutputs;
            }

            if (state == ModelingState.Stopped || state == ModelingState.Completed) {
                this._compliance.ModelingChanged -= new ModelingEventHandler(this.Compliance_ModelingChanged);
                this._compliance.Prompt -= new PromptEventHandler(this.Compliance_Prompt);
            }

            if (this._allCharts.Count > 0) {
                this._allCharts[0].CancelTrackingDemo();
            }

            this.RefreshSession();
            this.UpdateStatus(status);
        }
        void Compliance_Prompt(object sender, PromptEventArgs e)
        {
            MessageBoxButtons buttons =
                (e.PromptOption == PromptOption.OkCancel   ) ? MessageBoxButtons.OKCancel    :
                (e.PromptOption == PromptOption.YesNoCancel) ? MessageBoxButtons.YesNoCancel :
                (e.PromptOption == PromptOption.YesNo      ) ? MessageBoxButtons.YesNo       : MessageBoxButtons.OK;
            MessageBoxDefaultButton defaultButton =
                (e.PromptOption == PromptOption.YesNoCancel) ? MessageBoxDefaultButton.Button3 :
                (e.PromptOption == PromptOption.YesNo ||
                 e.PromptOption == PromptOption.OkCancel   ) ? MessageBoxDefaultButton.Button2 : MessageBoxDefaultButton.Button1;

            DialogResult dr =  MessageBox.Show(e.Message, e.Title, buttons, MessageBoxIcon.Information, defaultButton);
            e.PromptResult =
                (dr == DialogResult.OK    ) ? PromptResult.Ok     :
                (dr == DialogResult.Cancel) ? PromptResult.Cancel :
                (dr == DialogResult.Yes   ) ? PromptResult.Yes    :
                (dr == DialogResult.No    ) ? PromptResult.No     : PromptResult.None;
        }

        //----- threading methods for UI support -----
        void InitRefreshSessionThread()
        {
            var refreshThread = new Thread(new ThreadStart(this.RefreshSessionThread))
            {
                Name = $"Session-{this.SessionName}::SessionRefreshThread"
            };
            refreshThread.Start();
        }
        void RefreshSessionThread()
        {
            while (this.IsRunning || this.StartRequested)
            {
                for (int i = 0; i < this._refreshInterval / MinRefreshInterval; i++)
                {
                    Thread.Sleep(MinRefreshInterval);
                    if (!this.IsRunning) { break; }
                }
                // if refresh interval is -1 (paused), sleep for half a second, and loop again
                if (this._refreshInterval == -1) { Thread.Sleep(500); } else { this.RefreshSession(); }
            }
            //
            // final refresh upon completion
            this.RefreshSession();
        }
        void InitWaitToStopThread()
        {
            var waitToStopThread = new Thread(new ThreadStart(this.WaitToStopThread))
            {
                Name = $"Session-{this.SessionName}::WaitToStopThread"
            };
            waitToStopThread.Start();
        }
        void WaitToStopThread()
        {
            // wait for 5 seconds before canceling a forced stop request
            // how this works:
            // - if the user clicks a stop button again within 5 seconds, the compliance
            //   model will be forced to stop (via ICompliance.Stop(false) call)
            // - after 5 seconds, the forced stop is canceled and if the user clicks on
            //   stop again, a ICompliance.Stop(true) call is repeated, and this 5 sec
            //   timer restarts -- the compliance model will then terminate when ready
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(500);
                if (!this.StopRequested || !this.IsRunning) { return; }
            }
            this.StopRequested = false;
        }

        //----- support methods -----
        /// <summary>
        /// Refreshes the session progress and status information.
        /// </summary>
        public void RefreshSession()
        {
            if (this.InvokeRequired)
            {
                this.Invoke(new Action(this.RefreshSession));
            }
            else if (this.IsRunning)
            {
                try
                {
                    IModelingProgress progress = this._compliance.Progress;

                    // refresh progress panel
                    this._progressChart.RefreshChartData(progress);
                    // refresh chart data
                    foreach (var chart in this._allCharts)
                    {
                        chart.RefreshChartData(progress);
                    }

                    // update progress
                    string additionalInfo = string.Empty;
                    if (progress.AdditionalInfo != null)
                    {
                        additionalInfo = progress.AdditionalInfo.ToString().Trim();
                        if (additionalInfo != string.Empty)
                        {
                            additionalInfo = $" ({additionalInfo})";
                        }
                    }
                    this.UpdateStatus(SessionMessages.COMPLIANCE_STATUS_Running + additionalInfo);
                }
                catch (Exception ex)
                {
                    this.Compliance?.LogWriter.LogWarning(ex.ToString());
                }
            }
        }
        /// <summary>
        /// Updates the status bar in the parent form.
        /// </summary>
        void UpdateStatus(string value)
        {
            if (this.Status != value)
            {
                this.Status = value;
                this.OnSessionUpdating();
            }
        }
        /// <summary>
        /// Clears progress and chart information for a new run.
        /// </summary>
        void ResetSessionProgress()
        {
            //this.ux_progress.Text = "Begin modeling to display\nprogress information ...";
            this._progressChart.ClearDataSeriesValues();
            foreach (Chart chart in this._allCharts)
            {
                chart.ClearDataSeriesValues();
            }
        }

        void UpdateSessionName()
        {
            if (this.IsOpen)
            {
                this.SessionName = (this.SessionPath == null) ? this.GetGenericSessionName() : Path.GetFileNameWithoutExtension(this.SessionPath);
                this.Text = this.SessionName + ((this.IsReadOnly) ? " [Read Only]" : string.Empty);
            }
            else { this.Text = this.SessionName = string.Empty; }
            //
            this.OnSessionNameChanged();
        }
        string GetGenericSessionName()
        {
            return "Session " + this.Index.ToString();
        }

        void ShowErrorPrompt(string message)
        {
            MessageBox.Show(message, SessionMessages.CAFEModelError, MessageBoxButtons.OK, MessageBoxIcon.Error);
        }

        DialogResult ShowOverwriteWarningDialogue()
        {
            string message = "Data already exists at the current output location.\nProceeding will result in " +
                "loss of previous model results.\n\nDo you wish to continue?";
            string header = "Data Overwrite Warning";
            var buttons = MessageBoxButtons.YesNo;
            var icon = MessageBoxIcon.Warning;
            var defaultButton = MessageBoxDefaultButton.Button2;

            return MessageBox.Show(message, header, buttons, icon, defaultButton);
        }

        /// <summary>
        /// Sets the zoom factor to apply to all charts of this session panel.
        /// </summary>
        /// <param name="zoomFactor">The zoom factor to apply to all charts of this session panel.</param>
        /// <param name="recurseChildren">true, to recursively set zoom factors for all child charts;
        ///     false, to only zoom the to top level charts.</param>
        public void ZoomCharts(float zoomFactor, bool recurseChildren = false)
        {
            this.ZoomChart(this._progressChart, zoomFactor, recurseChildren);
            foreach (var chart in this._allCharts)
            {
                this.ZoomChart(chart, zoomFactor, recurseChildren);
            }
        }
        void ZoomChart(Chart chart, float zoomFactor, bool recurseChildren)
        {
            chart.ZoomFactor = zoomFactor;
            if (recurseChildren)
            {
                var children = chart.Controls.OfType<Chart>();
                foreach (var child in children)
                {
                    this.ZoomChart(child, zoomFactor, true);
                }
            }
        }

        //----- begin/end update support methods -----
        void BeginUpdate()
        {
            Win32.BeginUpdate(this, ref this._updateCount, ref this._eventMask);
        }
        void EndUpdate()
        {
            if (Win32.EndUpdate(this, ref this._updateCount, ref this._eventMask))
            {
                base.Refresh();
            }
        }

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets or sets the index assigned to a new session.</summary>
        [Browsable(false)]
        int Index { get; set; }
        /// <summary>Gets the name of the session.</summary>
        [Browsable(false)]
        public string SessionName { get; private set; }
        /// <summary>Gets the location of the session file, where the session data was loaded from or can be saved to.</summary>
        [Browsable(false)]
        public string SessionPath { get; private set; }

        /// <summary>Gets whether the session is open.</summary>
        [Browsable(false)]
        public bool IsOpen { get; private set; }
        /// <summary>Gets whether the session was open as read-only.</summary>
        [Browsable(false)]
        public bool IsReadOnly { get; private set; }
        /// <summary>Gets or sets whether the session needs to be saved.</summary>
        [Browsable(false)]
        public bool SaveRequired { get; set; }

        /// <summary>Gets whether the session is currently preparing for a new compliance model simulation run.</summary>
        [Browsable(false)]
        public bool IsInitializing { get; private set; }
        /// <summary>Gets whether the session is currently executing the compliance model simulation.</summary>
        [Browsable(false)]
        public bool IsRunning { get { return this.IsInitializing || (this._compliance != null && this._compliance.Running); } }
        /// <summary>Gets whether the user has requested to start analysis of the compliance model simulation.</summary>
        [Browsable(false)]
        public bool StartRequested { get { return (this._compliance != null && this._compliance.StartRequested); } }
        /// <summary>Gets whether the user has requested to stop analysis of the currently executing compliance model
        ///     simulation.</summary>
        [Browsable(false)]
        public bool StopRequested { get; private set; }
        /// <summary>Gets the status of the session, which should be displayed in the parent form's status bar.</summary>
        [Browsable(false)]
        public string Status { get; private set; }

        /// <summary>Gets or set how frequently, in milliseconds, the session refreshes itself based on the latest
        ///     progress information of the compliance model.  Use -1 to pause refreshing; otherwise, the refresh
        ///     interval will be converted to be a minimum <see cref="MinRefreshInterval"/> or a multiple of
        ///     <see cref="MinRefreshInterval"/>.</summary>
        [Browsable(false)]
        public int RefreshInterval
        {
            get => this._refreshInterval;
            set
            {
                if (value != this.RefreshInterval)
                {
                    if (value == RefreshIntervalPaused) { this._refreshInterval = RefreshIntervalPaused; }
                    else
                    {   // make multiple of MinRefreshInterval
                        this._refreshInterval = (value < MinRefreshInterval) ? MinRefreshInterval : value - (value % MinRefreshInterval);
                    }
                    this.SaveRequired = true;
                }
            }
        }

        /// <summary>Gets or sets the compliance model that will be used during the modeling process.</summary>
        /// <remarks>Selecting a different compliance model will force the current session to restart.</remarks>
        [Browsable(false)]
        public ICompliance Compliance
        {
            get { return this._compliance; }
            set { this._compliance = value; this.SaveRequired = true; }
        }
        /// <summary>Gets or sets the modeling data associated with the current session.</summary>
        [Browsable(false)]
        public IIndustry Data
        {
            get { return this._data; }
            set { this._data = value; this.SaveRequired = true; }
        }
        /// <summary>Gets or sets the modeling settings associated with the current session.</summary>
        [Browsable(false)]
        public ModelingSettings Settings => this._settings;

        #endregion

        #region <<< Variables >>>

        //----- constants -----
        /// <summary>Represents a realtime refresh interval of 125 ms, or eight times per second.</summary>
        public const int RefreshIntervalRealtime = 125;
        /// <summary>Represents a high refresh interval of 500 ms, or twice per second.</summary>
        public const int RefreshIntervalHigh = 500;
        /// <summary>Represents a normal refresh interval of 1000 ms, or once per second.</summary>
        public const int RefreshIntervalNormal = 1000;
        /// <summary>Represents a low refresh interval of 2000 ms, or once every two seconds.</summary>
        public const int RefreshIntervalLow = 2000;
        /// <summary>Specifies that refreshing is paused.</summary>
        public const int RefreshIntervalPaused = -1;
        /// <summary>Represents the smallest allowed refresh interval, in milliseconds.</summary>
        public const int MinRefreshInterval = 125;

        /// <summary>Specifies the name to be used to produce the run status output file.</summary>
        public const string RunStatusFileName = "run-status.txt";

        // keep track of number of sessions opened since model launch
        static int SessionCount = 1;

        //----- UI variables -----
        Panel ux_progressPanel;
        Panel ux_mainChartPanel;
        Panel ux_sideChartPanel1;
        Panel ux_sideChartPanel2;

        //----- backing variables for some properties -----
        ICompliance      _compliance;
        IIndustry        _data;
        ModelingSettings _settings;
        int              _refreshInterval;

        // available and active charts
        ModelingProgressChart _progressChart;
        List<ModelChart>      _allCharts = new List<ModelChart>();

        //----- support variables for begin/end update methods -----
        int _updateCount = 0;
        int _eventMask   = 0;

        #endregion

    }
}
