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

#region Using directives

using System;
using System.Collections;
using System.Text;
using System.Reflection;
using System.IO;
using System.Diagnostics;

using IronPython.Compiler;
using IronPython.Modules;
using IronPython.Runtime;
using System.Threading;
using System.Collections.Generic;
#endregion

namespace IronPython.Hosting {
    public class PythonEngine {
        public const string Copyright = "Copyright (c) Microsoft Corporation. All rights reserved.";

        private IConsole _console = null;
        private PythonModule _module;
        private SystemState sys = new SystemState();
        private Frame topFrame;
        private CompilerContext context = new CompilerContext();

        static internal Options options;

        //!!! design right post PyCon
        public static object ExecWrapper = null;

        public IConsole MyConsole {
            get {
                if (_console == null) {
                    _console = new BasicConsole();
                }
                return _console;
            }
            set {
                _console = value;
            }
        }

        public static Version Version {
            get {
                return typeof(PythonEngine).Assembly.GetName().Version;
            }
        }

        public static string VersionString {
            get {
                Version pythonVersion = Version;
                string version = String.Format("IronPython {0}.{1}.{2} (Beta) on .NET {3}",
                                  pythonVersion.Major, pythonVersion.Minor, pythonVersion.Build,
                                  Environment.Version);
                return version;
            }
        }

        public SystemState Sys {
            get {
                return sys;
            }
        }

        public PythonEngine() {
            object tmp = OutputGenerator.Snippets; //!!! makes sure cctor for OutputGenerator has run
            _module = new PythonModule("__main__", new Dict());
            topFrame = new Frame(_module);
            Ops.systemState = sys;
            options = new Options();
        }

        public void InitializeModules(string prefix, string executable, string version) {
            sys.version = version;
            sys.prefix = prefix;
            sys.executable = executable;
            sys.exec_prefix = prefix;
        }

        public PythonEngine(Options opts)
            : this() {
            if (opts == null) throw new ArgumentNullException("No options specified for PythonEngine");
            // Save the options. Clone it first to prevent the client from unexpectedly mutating it
            options = opts.Clone();
        }

        public void AddToPath(string dirName) {
            sys.path.Append(dirName);
        }

        public void LoadAssembly(Assembly assem) {
            ReflectedPackage.LoadAssembly(assem);
        }

        public void SetStderr(Stream stream) {
            sys.__stderr__ = new PythonFile(stream, "HostedStderr", "w");
            sys.stderr = sys.__stderr__;
        }

        public void SetStdout(Stream stream) {
            sys.__stdout__ = new PythonFile(stream, "HostedStdout", "w");
            sys.stdout = sys.__stdout__;
        }

        public void SetStdin(Stream stream) {
            sys.__stdin__ = new PythonFile(stream, "HostedStdin", "w");
            sys.stdin = sys.__stdin__;
        }
        public void SetVariable(string name, object val) {
            val = Ops.ToPython(val);
            Ops.SetAttr(_module, _module, SymbolTable.StringToId(name), val);
        }

        public object GetVariable(string name) {
            return Ops.GetAttr(_module, _module, SymbolTable.StringToId(name));
        }

        public void ImportSite() {
            try {
                Import("site");
            } catch (Exception e) {
                if (options.Verbose) {
                    DumpException(e);
                } else {
                    MyConsole.WriteLine("'import site' failed; use -v for traceback", Style.Error);
                }
            }
        }

        public object Import(string module) {
            object mod = Importer.ImportModule(DefaultContext.Default, _module, module, true);
            if (mod != null) {
                string[] names = module.Split('.');
                topFrame.SetGlobal(names[names.Length - 1], mod);
            }
            return mod;
        }

        public object Evaluate(string expr) {
            return Builtin.Eval(_module, expr);
        }

        public T Evaluate<T>(string expr) {
            return Converter.Convert<T>(Builtin.Eval(_module, expr));
        }

        /// <summary>
        /// Executes script that is passed in as a string.
        /// </summary>
        public int Execute(string text) {
            Parser p = Parser.FromString(context, text);
            Stmt s = p.ParseFileInput();

            FrameCode code = OutputGenerator.GenerateSnippet(context, s, "input", true);

            try {
                code.Run(new Frame(_module));
            } catch (PythonSystemExit e) {
                return e.ExitCode;
            }
            return 0;
        }

        /// <summary>
        /// Executes code that has been compiled with PythonEngine.Compile.
        /// </summary>
        public void Execute(object code) {
            FrameCode fc = code as FrameCode;
            if (fc != null) {
                fc.Run(new Frame(_module));
            } else if(code is string) {
                Execute((string)code);
            } else {
                throw new ArgumentException("code object must be string or have been generated via PythonEngine.Compile");
            }
        }

        public object Compile(string text) {
            return Compile(text, false);
        }

        /// <summary>
        /// Compiles a script into an object that can be executed using PythonEngine.Execute.
        ///
        /// If printExprStatements is true then the result of expressions is printed to the
        /// console and stored in the _ variable for later evaluation.
        /// </summary>
        public object Compile(string text, bool printExprStatements) {
            Parser p = Parser.FromString(context, text);
            Stmt s = p.ParseFileInput();

            return OutputGenerator.GenerateSnippet(context, s, "input", printExprStatements);
        }

        /// <summary>
        /// This executes the given module
        /// </summary>
        /// <param name="fileName">Name of the module</param>
        /// <param name="commandLineArgs">command-line arguments, excluding module name</param>
        /// <param name="introspection">Inspect interactively after running script</param>
        /// <param name="skipLine"></param>
        /// <returns>0 on success. Otherwise, the exit code</returns>
        public int RunFileInNewModule(string fileName, ArrayList commandLineArgs, bool introspection, bool skipLine) {
            // Publish the command line arguments
            sys.argv = new List(commandLineArgs);

            if (introspection) {
                bool continueInteraction;
                int result = TryInteractiveAction(
                    delegate(out bool continueInteractionArgument) {
                        bool exitRaised;
                        int res = RunFileInNewModule(fileName, "__main__", skipLine, out exitRaised);
                        continueInteractionArgument = !exitRaised;
                        return res;
                    },
                    out continueInteraction);

                if (continueInteraction) {
                    topFrame = new Frame(_module);
                    return RunInteractiveLoop();
                } else {
                    return result;
                }
            } else {
                bool exitRaised;
                return RunFileInNewModule(fileName, "__main__", skipLine, out exitRaised);
            }
        }

        public int RunFileInNewModule(string fileName, string moduleName) {
            bool exitRaised;
            return RunFileInNewModule(fileName, moduleName, false, out exitRaised);
        }

        /// <summary>
        /// Run the given module. sys.exit() will be caught and handled.
        /// Any other exception will be passed through.
        /// </summary>
        /// <param name="exitRaised">Was sys.exit called? Note that the return code can be 0 even in this case</param>
        /// <returns>The exit code of the module</returns>
        private int RunFileInNewModule(string fileName, string moduleName, bool skipLine, out bool exitRaised) {
            CompilerContext context = new CompilerContext();
            Parser p = Parser.FromFile(context, fileName, skipLine);
            Stmt s = p.ParseFileInput();

            PythonModule mod = OutputGenerator.GenerateModule(context, s, moduleName, fileName);

            sys.modules[mod.ModuleName] = mod;
            mod.SetAttr(DefaultContext.Default, SymbolTable.File, fileName);
            _module = mod;
            exitRaised = false;

            try {
                mod.Initialize();
            } catch (PythonSystemExit e) {
                exitRaised = true;
                return e.ExitCode;
            }

            return 0;
        }

        public void ExecuteFile(string fileName) {
            Parser p = Parser.FromFile(context, fileName);
            Stmt s = p.ParseFileInput();

            Frame topFrame = new Frame(_module);
            FrameCode code = OutputGenerator.GenerateSnippet(context, s, "input", false);
            code.Run(topFrame);
        }

        static int counter = 0;
        public int RunFile(string fileName) {
            Parser p = Parser.FromFile(context, fileName);
            Stmt s = p.ParseFileInput();

            string moduleName = "tmp" + counter++;

            PythonModule mod = OutputGenerator.GenerateModule(context, s, moduleName, fileName);
            foreach (SymbolId name in _module.__dict__.Keys) {
                mod.SetAttr(_module, name, _module.__dict__[name]);
            }
            mod.SetAttr(_module, SymbolTable.File, fileName);

            try {
                mod.Initialize();
            } catch (PythonSystemExit e) {
                return e.ExitCode;
            }

            return 0;
        }

        public int RunInteractive() {
            topFrame.SetGlobal("__doc__", null);
            sys.modules[_module.ModuleName] = _module;
            return RunInteractiveLoop();
        }

        /// <summary>
        /// Perform an interactive action. Most exceptions should be caught and the interactive loop
        /// should continue. A few exception types should terminate the interactive loop.
        /// </summary>
        /// <param name="continueInteraction">Should the interactive loop continue?</param>
        /// <returns>Exit code of the action</returns>
        delegate int InteractiveAction(out bool continueInteraction);

        private int TryInteractiveAction(InteractiveAction interactiveAction, out bool continueInteraction) {
            int result = 1; // assume failure
            continueInteraction = false;

            try {
                result = interactiveAction(out continueInteraction);
            } catch (PythonSystemExit se) {
                return se.ExitCode;
            } catch (ThreadAbortException tae) {
                PythonKeyboardInterrupt pki = tae.ExceptionState as PythonKeyboardInterrupt;
                if (pki != null) {
                    Thread.ResetAbort();
                    bool endOfMscorlib = false;
                    DumpException(tae, ExceptionConverter.ToPython(pki), delegate(StackFrame sf) {
                        // filter out mscorlib methods that show up on the stack initially, 
                        // for example ReadLine / ReadBuffer etc...
                        if (!endOfMscorlib && 
                            sf.GetMethod().DeclaringType != null &&
                            sf.GetMethod().DeclaringType.Assembly == typeof(string).Assembly) {
                            return false;
                        }
                        endOfMscorlib = true;
                        return true;
                    });
                    continueInteraction = true;                    
                }
            } catch (Exception e) {
                // There should be no unhandled exceptions in the interactive session
                // We catch all exceptions here, and just display it,
                // and keep on going
                DumpException(e);
                continueInteraction = true;
            }

            return result;
        }

        private int RunInteractiveLoop() {
            bool continueInteraction = true;
            int result = 0;
            while (continueInteraction) {
                result = TryInteractiveAction(
                    delegate(out bool continueInteractionArgument) {
                        continueInteractionArgument = DoOneInteractive(topFrame);
                        return 0;
                    },
                    out continueInteraction);
            }

            return result;
        }

        /// <summary>
        /// This method reads a single statement. The statement could be a multi-line compound statement,
        // containing nested suite of statements.
        /// </summary>
        /// <param name="continueInteraction">Should the interactive loop continue?</param>
        /// <returns>The parsed statement tree. 
        /// null if there is nothing to execute (only input is NEWLINE).</returns>
        private Stmt ReadStatement(out bool continueInteraction) {
            Stmt s = null;
            StringBuilder b = new StringBuilder();
            bool isMultiLine = false;
            int autoIndentSize = 0;
            int numberOfBlankLines = 0;

            MyConsole.Write(sys.ps1.ToString(), Style.Prompt);

            while (true) {
                string line = MyConsole.ReadLine(autoIndentSize);

                if (line == null) {
                    if (ExecWrapper != null) {
                        Ops.Call(ExecWrapper, new object[] { null });
                    }
                    continueInteraction = false;
                    return null;
                }

                b.Append(line);
                b.Append("\n");

                bool endOfInput = (line.Length == 0);
                bool parsingMultiLineString;
                s = ParseText(b.ToString(), endOfInput, out parsingMultiLineString, out isMultiLine);

                // Break the loop if we have successfully parsed a single-line Stmt
                if ((s != null) && !isMultiLine) break;

                if (Options.AutoIndentSize != 0 && line.Trim().Length == 0)
                    numberOfBlankLines++;
                else
                    numberOfBlankLines = 0;

                if (!parsingMultiLineString) {
                    // Note that splitting a string literal over multiple lines does not 
                    // constitute a multi-line statement.
                    isMultiLine = true;

                    // Break the loop when we get an empty line
                    // Or if we get two white-space lines (of auto-indentation)
                    if (endOfInput || (numberOfBlankLines == 2)) break;
                }

                autoIndentSize = Parser.GetNextAutoIndentSize(b.ToString());

                // Keep on reading input
                MyConsole.Write(sys.ps2.ToString(), Style.Prompt);
            }

            continueInteraction = true;
            return s;
        }

        /// <summary>
        /// Read input from console and execute it
        /// </summary>
        /// <returns>Should the interactive loop continue</returns>
        public bool DoOneInteractive(Frame topFrame) {
            bool continueInteraction;
            Stmt s = ReadStatement(out continueInteraction);
            if (continueInteraction == false)
                return false;

            //  's' is null when we parse a line composed only of a NEWLINE (interactive_input grammar);
            //  we don't generate anything when 's' is null
            if (s != null) {
                FrameCode code = OutputGenerator.GenerateSnippet(context, s, "input", true);

                if (ExecWrapper != null) {
                    CallTarget0 t = delegate() {
                        try { code.Run(topFrame); } catch (Exception e) { DumpException(e); }
                        return null;
                    };
                    object callable = new Function0(topFrame.__module__, "wrapper", t, new string[0], new object[0]);
                    Ops.Call(ExecWrapper, callable);
                } else {
                    code.Run(topFrame);
                }
            }

            return true;
        }

        public void DefaultExceptionHandler(object sender, UnhandledExceptionEventArgs args) {
            MyConsole.WriteLine("Unhandled exception: ", Style.Error);
            DumpException((Exception)args.ExceptionObject);
        }

        /// <summary>
        /// Dumps an exception to the console.  This will display all
        /// nested stack traces, the python exception information, and
        /// the CLR exception if that option is enabled.
        /// </summary>
        public void DumpException(Exception e) {
            object pythonEx = ExceptionConverter.ToPython(e);

            DumpStackTraces(e);
            DumpPythonException(pythonEx);
            if (PythonEngine.options.ShowCLSExceptions) {
                DumpCLSException(e);
            }
        }

        public delegate bool FilterStackFrame(StackFrame frame);

        public void DumpException(Exception e, object pythonException, FilterStackFrame fsf) {
            Debug.Assert(pythonException != null);
            Debug.Assert(e != null);

            bool printedHeader = false;
            DumpStackTraces(e, fsf, ref printedHeader);
            DumpPythonException(pythonException);
            if (PythonEngine.options.ShowCLSExceptions) {
                DumpCLSException(e);
            }
        }

        /// <summary>
        /// Dumps a python exception to the console.
        /// </summary>
        private void DumpPythonException(object python) {
            // dump the python exception.
            if (python != null) {
                string str = python as string;
                if (str != null) {
                    MyConsole.WriteLine(str, Style.Error);
                } else {
                    string className = "";
                    object val;
                    if (Ops.TryGetAttr(python, SymbolTable.Class, out val)) {
                        if (Ops.TryGetAttr(val, SymbolTable.Name, out val)) {
                            className = val.ToString();
                        }
                    }
                    MyConsole.WriteLine(className + ": " + python.ToString(), Style.Error);
                }
            }
        }

        /// <summary>
        /// Dumps a CLS exception to the console.  Only called if ShowCLSExceptions
        /// option is enabled.
        /// </summary>
        private void DumpCLSException(Exception e) {
            MyConsole.WriteLine("CLR Exception: ", Style.Error);

            while (e != null) {
                if (!String.IsNullOrEmpty(e.Message)) {
                    MyConsole.WriteLine("    " + e.GetType().Name + ": " + e.Message, Style.Error);
                } else {
                    MyConsole.WriteLine("    " + e.GetType().Name, Style.Error);
                }

                e = e.InnerException;
            }
        }

        private void DumpStackTraces(Exception e) {
            bool printedHeader = false;

            DumpStackTraces(e, ref printedHeader);
        }

        private void DumpStackTraces(Exception e, ref bool printedHeader) {
            DumpStackTraces(e, null, ref printedHeader);
        }

        /// <summary>
        /// Dumps all the stack traces associated with an exception to the console.  This
        /// includes stack traces from catch & rethrows that were appended.
        /// </summary>
        private void DumpStackTraces(Exception e, FilterStackFrame fsf, ref bool printedHeader) {            
            if (PythonEngine.options.ExceptionDetail) {
                if (!printedHeader) {
                    MyConsole.WriteLine(e.Message, Style.Error);
                    printedHeader = true;
                }
                List<System.Diagnostics.StackTrace> traces = ExceptionConverter.GetExceptionStackTraces(e);

                if (traces != null) {   
                    for (int i = 0; i <traces.Count; i++) {
                        for (int j = 0; j < traces[i].FrameCount; j++) {
                            StackFrame curFrame = traces[i].GetFrame(j);
                            if(fsf == null || fsf(curFrame))
                                MyConsole.WriteLine(curFrame.ToString(), Style.Error);
                        }
                    }
                }

                MyConsole.WriteLine(e.StackTrace.ToString(), Style.Error);
                if (e.InnerException != null) DumpStackTraces(e.InnerException, ref printedHeader);
            } else {
                // dump inner most exception first, followed by outer most.
                if (e.InnerException != null) DumpStackTraces(e.InnerException, ref printedHeader);

                if (!printedHeader) {
                    MyConsole.WriteLine("Traceback (most recent call last):", Style.Error);
                    printedHeader = true;
                }
                DumpStackTrace(new StackTrace(e, true), fsf);
                List<StackTrace> traces = ExceptionConverter.GetExceptionStackTraces(e);
                if (traces != null && traces.Count > 0) {
                    for (int i = 0; i < traces.Count; i++) {
                        DumpStackTrace(traces[i], fsf);
                    }
                }
            }            
        }
        
        /// <summary>
        /// Dumps an individual StackTrace to the console
        /// </summary>
        private void DumpStackTrace(StackTrace st, FilterStackFrame fsf) {
            StackFrame[] frames = st.GetFrames();
            if (frames == null) return;

            for (int i = frames.Length - 1; i >= 0; i--) {
                StackFrame frame = frames[i];
                Type parentType = frame.GetMethod().DeclaringType;
                if (parentType != null) {
                    string typeName = parentType.FullName;
                    if (typeName.StartsWith("IronPython.") ||
                        typeName.StartsWith("ReflectOpt.") ||
                        typeName.StartsWith("System.Reflection.") ||
                        typeName.StartsWith("System.Runtime") ||
                        typeName.StartsWith("IronPythonConsole.")) {
                        continue;
                    }
                }

                if (fsf != null && !fsf(frame)) continue;

                MyConsole.WriteLine(FrameToString(frame), Style.Error);
            }            
        }

        /// <summary>
        /// Converts an individual stack frame into a user-displayable format.
        /// </summary>
        private string FrameToString(StackFrame frame) {
            if (frame.GetMethod().DeclaringType != null &&
                frame.GetMethod().DeclaringType.Assembly == OutputGenerator.Snippets.myAssembly) {
                string methodName;
                int dollar;

                if (frame.GetMethod().Name == "Run") methodName = "-toplevel-";
                else if ((dollar = frame.GetMethod().Name.IndexOf('$')) == -1) methodName = frame.GetMethod().Name;
                else methodName = frame.GetMethod().Name.Substring(0, dollar);
                
                return String.Format("  File {0}, line {1}, in {2}",
                    frame.GetFileName(),
                    frame.GetFileLineNumber(),
                    methodName);
            } else {
                string methodName;
                int dollar;

                if ((dollar = frame.GetMethod().Name.IndexOf('$')) == -1) methodName = frame.GetMethod().Name;
                else methodName = frame.GetMethod().Name.Substring(0, dollar);

                string filename = frame.GetFileName();
                string line = frame.GetFileLineNumber().ToString();
                if (String.IsNullOrEmpty(filename)) {
                    if (frame.GetMethod().DeclaringType != null) {
                        filename = frame.GetMethod().DeclaringType.Assembly.GetName().Name;
                        line = "unknown";
                    }
                }

                return String.Format("  File {0}, line {1}, in {2}",
                    filename,
                    line,
                    methodName);
            }
        }

        public void DumpDebugInfo() {
            if (PythonEngine.options.EngineDebug) {
                PerfTrack.DumpStats();
                try {
                    OutputGenerator.DumpSnippets();
                } catch (NotSupportedException) { } //!!! usually not important info...
            }
        }

        private Stmt ParseText(string text, bool endOfInput, out bool parsingMultiLineString, out bool parsingMultiLineCmpdStmt) {
            Parser p = Parser.FromString(context, "<stdin>", text);

            parsingMultiLineString = false;
            parsingMultiLineCmpdStmt = false;

            try {
                return p.ParseInteractiveInput(out parsingMultiLineCmpdStmt);
            } catch (PythonSyntaxError se) {
                // Check if its a real syntax error, or if its just an incomplete multi-line statement

                if (se.Message.IndexOf("<eof>") > 0) {
                    if (se.Message.IndexOf("while reading string") > 0)
                        parsingMultiLineString = true;
                    return null;
                }

                if (se.Severity >= Severity.Error || 
                    endOfInput && !parsingMultiLineString && !parsingMultiLineCmpdStmt) {
                    throw;
                }

                if (se.Message.IndexOf("<newline>") > 0) return null;
                if (se.Message.IndexOf("<dedent>") > 0) return null;

                // It looks like a real syntax error. Rethrow the exception
                throw;
            }
        }

        public static int ExecuteCompiled(CustomFieldIdDict compiled, InitializeModule init, string fullName) {
            PythonEngine engine = new PythonEngine();
            engine.AddToPath(Environment.CurrentDirectory);

            engine.LoadAssembly(compiled.GetType().Assembly);
            PythonModule module = new PythonModule(fullName, compiled);
            string executable = compiled.GetType().Assembly.Location;
            module.InitializeBuiltins();
            engine.sys.prefix = System.IO.Path.GetDirectoryName(executable);
            engine.sys.executable = executable;
            engine.sys.exec_prefix = engine.sys.prefix;

            // first arg is EXE 
            List args = new List();
            string [] fullArgs = Environment.GetCommandLineArgs();
            args.Add(Path.GetFullPath(fullArgs[0]));
            for (int i = 1; i < fullArgs.Length; i++)
                args.Add(fullArgs[i]);
            engine.sys.argv = args;

            try {
                init();
            } catch (PythonSystemExit x) {
                return x.ExitCode;
            } catch (Exception e) {
                engine.DumpException(e);
                return -1;
            }
            return 0;
        }

        public void AssociateComInterface(object o, object i) {
            ComObject co = o as ComObject;
            Type t = Converter.ConvertToType(i);
            if (co != null) {
                co.AddInterface(t);
            }
        }

        public void AssociateHiddenComInterface(Type visibleInterface, Type hiddenInterface) {
            lock (ComObject.HiddenInterfaces) {
                if (!ComObject.HiddenInterfaces.ContainsKey(visibleInterface))
                    ComObject.HiddenInterfaces[visibleInterface] = new List<Type>();

                ComObject.HiddenInterfaces[visibleInterface].Add(hiddenInterface);
            }
        }

        public void AssociateHiddenComAddInterface(Type visibleInterface, IList<Type> hiddenInterfaces) {
            lock (ComObject.HiddenInterfaces) {
                if (!ComObject.HiddenInterfaces.ContainsKey(visibleInterface))
                    ComObject.HiddenInterfaces[visibleInterface] = new List<Type>();

                foreach (Type hiddenInterface in hiddenInterfaces)
                    ComObject.HiddenInterfaces[visibleInterface].Add(hiddenInterface);
            }
        }

        public void Shutdown() {
            object callable;
            if (Sys.TryGetAttr(DefaultContext.Default, SymbolTable.SysExitFunc, out callable)) {
                try {
                    Ops.Call(callable);
                } catch (Exception e) {
                    MyConsole.WriteLine("",Style.Error);
                    MyConsole.WriteLine("Error in sys.exitfunc:", Style.Error);
                    DumpException(e);
                }
            }
        }
    }

}
