/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

#if !SILVERLIGHT // Remoting

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace Microsoft.Scripting.Hosting.Shell.Remote {
    /// <summary>
    /// Supports detecting the remote runtime being killed, and starting up a new one.
    /// 
    /// Threading model:
    /// 
    /// ConsoleRestartManager creates a separate thread on which to create and execute the consoles. 
    /// There are usually atleast three threads involved:
    /// 
    /// 1. Main app thread: Instantiates ConsoleRestartManager and accesses its APIs. This thread has to stay 
    ///    responsive to user input and so the ConsoleRestartManager APIs cannot be long-running or blocking.
    ///    Since the remote runtime process can terminate asynchronously, the current RemoteConsoleHost can 
    ///    change at any time (if auto-restart is enabled). The app should typically not care which instance of 
    ///    RemoteConsoleHost is currently being used. The flowchart of this thread is:
    ///        Create ConsoleRestartManager
    ///        ConsoleRestartManager.Start
    ///        Loop:
    ///            Respond to user input | Send user input to console for execution | BreakExecution | RestartConsole | GetMemberNames
    ///        ConsoleRestartManager.Terminate
    ///    TODO: Currently, BreakExecution and GetMemberNames are called by the main thread synchronously.
    ///    Since they execute code in the remote runtime, they could take arbitrarily long. We should change
    ///    this so that the main app thread can never be blocked indefinitely.
    ///
    /// 2. Console thread: Dedicated thread for creating RemoteConsoleHosts and executing code (which could
    ///    take a long time or block indefinitely).
    ///        Wait for ConsoleRestartManager.Start to be called
    ///        Loop:
    ///            Create RemoteConsoleHost
    ///            Wait for signal for:
    ///                 Execute code | RestartConsole | Process.Exited
    ///
    /// 3. CompletionPort async callbacks:
    ///        Process.Exited | Process.OutputDataReceived | Process.ErrorDataReceived
    /// 
    /// 4. Finalizer thred
    ///    Some objects may have a Finalize method (which possibly calls Dispose). Not many (if any) types
    ///    should have a Finalize method.
    /// 
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1012:AbstractTypesShouldNotHaveConstructors")] // TODO: This is public only because the test (RemoteConsole.py) needs it to be so. The test should be rewritten
    public abstract class ConsoleRestartManager {
        private RemoteConsoleHost _remoteConsoleHost;
        private Thread _consoleThread;
        private bool _exitOnNormalExit;
        private bool _terminating;

        /// <summary>
        /// Accessing _remoteConsoleHost from a thread other than console thread can result in race.
        /// If _remoteConsoleHost is accessed while holding _accessLock, it is guaranteed to be
        /// null or non-disposed.
        /// </summary>
        private object _accessLock = new object();

        /// <summary>
        /// This is created on the "creating thread", and goes on standby. Start needs to be called for activation.
        /// </summary>
        /// <param name="exitOnNormalExit">A host might want one of two behaviors:
        /// 1. Keep the REPL loop alive indefinitely, even when a specific instance of the RemoteConsoleHost terminates normally
        /// 2. Close the REPL loop when an instance of the RemoteConsoleHost terminates normally, and restart the loop
        ///    only if the instance terminates abnormally.</param>
        public ConsoleRestartManager(bool exitOnNormalExit) {
            _exitOnNormalExit = exitOnNormalExit;
            _consoleThread = new Thread(Run);
            _consoleThread.Name = "Console thread";
        }

        protected object AccessLock { get { return _accessLock; } }

        public Thread ConsoleThread { get { return _consoleThread; } }

        protected RemoteConsoleHost CurrentConsoleHost { get { return _remoteConsoleHost; } }

        public abstract RemoteConsoleHost CreateRemoteConsoleHost();

        /// <summary>
        /// Needs to be called for activation.
        /// </summary>
        public void Start() {
            Debug.Assert(Thread.CurrentThread != _consoleThread);

            if (_consoleThread.IsAlive) {
                throw new InvalidOperationException("Console thread is already running.");
            }
            _consoleThread.Start();
        }

        private void Run() {
#if DEBUG
            try {
                RunWorker();
            } catch (Exception e) {
                Debug.Assert(false, "Unhandled exception on console thread:\n\n" + e.ToString());
            }
#else
            RunWorker();
#endif
        }

        private void RunWorker() {
            Debug.Assert(Thread.CurrentThread == _consoleThread);

            while (true) {
                RemoteConsoleHost remoteConsoleHost = CreateRemoteConsoleHost();

                // Reading _terminating and setting of _remoteConsoleHost should be done atomically. 
                // Terminate() does the reverse operation (setting _terminating reading _remoteConsoleHost) atomically
                lock (_accessLock) {
                    if (_terminating) {
                        return;
                    }

                    _remoteConsoleHost = remoteConsoleHost;
                }

                try {
                    try {
                        int exitCode = remoteConsoleHost.Run(new string[0]);

                        if (_exitOnNormalExit && exitCode == 0) {
                            return;
                        }
                    } catch (RemoteRuntimeStartupException) {
                    }
                } finally {
                    lock (_accessLock) {
                        remoteConsoleHost.Dispose();
                        _remoteConsoleHost = null;
                    }
                }
            }
        }

        // TODO: We have to catch all exceptions as we are executing user code in the remote runtime, and we cannot control what 
        // exception it may throw. This could be fixed if we built our own remoting channel which returned an error code
        // instead of propagating exceptions back from the remote runtime.
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public IList<string> GetMemberNames(string expression) {
            Debug.Assert(Thread.CurrentThread != _consoleThread);

            lock (_accessLock) {
                if (_remoteConsoleHost == null) {
                    return null;
                }

                ScriptEngine engine = _remoteConsoleHost.Engine;
                try {
                    ScriptScope scope = _remoteConsoleHost.ScriptScope;
                    ObjectOperations operations = engine.CreateOperations(scope);
                    ScriptSource src = engine.CreateScriptSourceFromString(expression, SourceCodeKind.Expression);
                    return operations.GetMemberNames(src.ExecuteAndWrap(scope));
                } catch {
                    return null;
                }
            }
        }

        public void BreakExecution() {
            Debug.Assert(Thread.CurrentThread != _consoleThread);

            lock (_accessLock) {
                if (_remoteConsoleHost == null) {
                    return;
                }

                try {
                    _remoteConsoleHost.AbortCommand();
                } catch (System.Runtime.Remoting.RemotingException) {
                    // The remote runtime may be terminated or non-responsive
                }
            }
        }

        public void RestartConsole() {
            Debug.Assert(Thread.CurrentThread != _consoleThread);

            lock (_accessLock) {
                if (_remoteConsoleHost == null) {
                    return;
                }

                _remoteConsoleHost.Terminate(0);
            }
        }

        /// <summary>
        /// Request (from another thread) the console REPL loop to terminate
        /// </summary>
        public void Terminate() {
            Debug.Assert(Thread.CurrentThread != _consoleThread);

            lock (_accessLock) {
                _terminating = true;
                _remoteConsoleHost.Terminate(0);
            }
            
            _consoleThread.Join();
        }
    }
}

#endif