//Copyright (C) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Diagnostics;
using NUnit.Framework;

namespace SampleSupport
{
    public class SampleHarness : IEnumerable<Sample>
    {
        private readonly IDictionary<int, Sample> samples = new Dictionary<int, Sample>();

        private readonly string _Title;
        private readonly StreamWriter _OutputStreamWriter = new StreamWriter(new MemoryStream());
        private bool runTestsUningNUnit = false;

        public SampleHarness()
        {
            Type samplesType = this.GetType();

            _Title = "Samples";
            string prefix = "Sample";
            string codeFile = samplesType.Name + ".cs";

            foreach (Attribute a in samplesType.GetCustomAttributes(false))
            {
                if (a is TitleAttribute)
                    _Title = ((TitleAttribute)a).Title;
                else if (a is PrefixAttribute)
                    prefix = ((PrefixAttribute)a).Prefix;
            }

            String allCode = readFile(Application.StartupPath + @"\..\..\" + codeFile);
            int m = 1;

            MethodInfo[] methods = samplesType.GetMethods(BindingFlags.Public|BindingFlags.Instance|
                                                          BindingFlags.DeclaredOnly|BindingFlags.Static);
            foreach(MethodInfo method in methods)
            {
                if (method.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {

                    string methodCategory = "Miscellaneous";
                    string methodTitle = prefix + " Sample " + m;
                    string methodDescription = "See code.";
                    List<MethodInfo> linkedMethods = new List<MethodInfo>();
                    List<Type> linkedClasses = new List<Type>();

                    foreach (Attribute a in method.GetCustomAttributes(false))
                    {
                        if (a is CategoryAttribute)
                            methodCategory = ((CategoryAttribute)a).Category;
                        else if (a is TitleAttribute)
                            methodTitle = ((TitleAttribute)a).Title;
                        else if (a is DescriptionAttribute)
                            methodDescription = ((DescriptionAttribute)a).Description;
                        else if (a is LinkedMethodAttribute) {
                            MethodInfo linked = samplesType.GetMethod(((LinkedMethodAttribute)a).MethodName,
                                                                      (BindingFlags.Public | BindingFlags.NonPublic) |
                                                                      (BindingFlags.Static | BindingFlags.Instance));
                            if (linked != null)
                                linkedMethods.Add(linked);
                        }
                        else if (a is LinkedClassAttribute) {
                            Type linked = samplesType.GetNestedType(((LinkedClassAttribute)a).ClassName);
                            if (linked != null)
                                linkedClasses.Add(linked);
                            else
                            {
                                // added by TM to support showing Extension Methods with their examples
                                linked = this.GetType().Assembly.GetType(((LinkedClassAttribute)a).ClassName);
                                if (linked != null)
                                    linkedClasses.Add(linked);

                            }
                        }
                    }

                    StringBuilder methodCode = new StringBuilder();
                    methodCode.Append(getCodeBlock(allCode, "void " + method.Name));
                    
                    foreach (MethodInfo lm in linkedMethods)
                    {
                        methodCode.Append(Environment.NewLine);

                        methodCode.Append(getCodeBlock(allCode, shortTypeName(lm.ReturnType.FullName) + " " + lm.Name));
                    }

                    foreach (Type lt in linkedClasses)
                    {
                        methodCode.Append(Environment.NewLine);
                        methodCode.Append(getCodeBlock(allCode, "class " + lt.Name));
                    }
                    
                    Sample sample = new Sample(this, method, methodCategory, methodTitle, methodDescription, methodCode.ToString());
                    
                    samples.Add(m, sample);
                    m++;
                }
            }
        }

        private static String readFile(string path)
        {
            String fileContents;
            if (File.Exists(path))
                using (StreamReader reader = File.OpenText(path))
                    fileContents = reader.ReadToEnd();
            else
                fileContents = "";
            
            return fileContents;
        }

        private static string shortTypeName(string typeName)
        {
            bool isAssemblyQualified = typeName[0] == '[';
            if (isAssemblyQualified)
            {
                int commaPos = typeName.IndexOf(',');
                return shortTypeName(typeName.Substring(1, commaPos - 1));
            }
            else
            {
                bool isGeneric = typeName.Contains("`");
                if (isGeneric)
                {
                    int backTickPos = typeName.IndexOf('`');
                    int leftBracketPos = typeName.IndexOf('[');
                    string typeParam = shortTypeName(typeName.Substring(leftBracketPos + 1, typeName.Length - leftBracketPos - 2));
                    return shortTypeName(typeName.Substring(0, backTickPos)) + "<" + typeParam + ">";
                }
                else
                {
                    switch (typeName)
                    {
                        case "System.Void":     return "void";
                        case "System.Int16":    return "short";
                        case "System.Int32":    return "int";
                        case "System.Int64":    return "long";
                        case "System.Single":   return "float";
                        case "System.Double":   return "double";
                        case "System.String":   return "string";
                        case "System.Char":     return "char";
                        case "System.Boolean":  return "bool";
                        
                        /* other primitive types omitted */

                        default:
                            int lastDotPos = typeName.LastIndexOf('.');
                            int lastPlusPos = typeName.LastIndexOf('+');
                            int startPos = Math.Max(lastDotPos, lastPlusPos) + 1;
                            return typeName.Substring(startPos, typeName.Length - startPos);
                    }
                }
            }
        }

        private static string getCodeBlock(string allCode, string blockName)
        {
            int blockStart = allCode.IndexOf(blockName, StringComparison.OrdinalIgnoreCase);
            
            if (blockStart == -1)
                return "// " + blockName + " code not found";
            blockStart = allCode.LastIndexOf(Environment.NewLine, blockStart, StringComparison.OrdinalIgnoreCase);
            if (blockStart == -1)
                blockStart = 0;
            else
                blockStart += Environment.NewLine.Length;

            int pos = blockStart;
            int braceCount = 0;
            char c;
            do
            {
                pos++;

                c = allCode[pos];
                switch (c)
                {
                    case '{':
                        braceCount++;
                        break;

                    case '}':
                        braceCount--;
                        break;
                }
            } while (pos < allCode.Length && !(c == '}' && braceCount == 0));

            int blockEnd = pos;
            
            string blockCode = allCode.Substring(blockStart, blockEnd - blockStart + 1);

            return removeIndent(blockCode);
        }

        private static string removeIndent(string code)
        {
            int indentSpaces = 0;
            while (code[indentSpaces] == ' ')
            {
                indentSpaces++;
            }

            StringBuilder builder = new StringBuilder();
            string[] codeLines = code.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
            foreach (string line in codeLines)
            {
                if (indentSpaces < line.Length)
                    builder.AppendLine(line.Substring(indentSpaces));
                else
                    builder.AppendLine();
            }

            return builder.ToString();
        }


        public virtual void InitSample() {}

        public virtual void HandleException(Exception e) {
            Console.Write(e);
        }


        public string Title
        {
            get { return _Title; }
        }
        
        public StreamWriter OutputStreamWriter
        {
            get { return _OutputStreamWriter; }
        }        

        
        public void RunAllSamples() {
            TextWriter oldConsoleOut = Console.Out;
            Console.SetOut(StreamWriter.Null);

            foreach (Sample sample in this) {
                sample.Invoke();
            }

            Console.SetOut(oldConsoleOut);
        }


        public Sample this[int index]
        {
            get { return samples[index]; }
        }

        IEnumerator<Sample> IEnumerable<Sample>.GetEnumerator()
        {
            return samples.Values.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return samples.Values.GetEnumerator();
        }

        // added by TM to allow NUnit to be called. 
        protected void RunUnitTestFixture(string fixture)
        {
            if (runTestsUningNUnit)
            {
                ProcessStartInfo startInfo = new ProcessStartInfo();
                startInfo.FileName = Application.StartupPath + @"\..\..\nunit\nunit-console.exe";

                string args = "\"{0}\" /fixture=\"{1}\" /labels";

                startInfo.Arguments = string.Format(args, Application.ExecutablePath, fixture);
                startInfo.UseShellExecute = false;
                startInfo.RedirectStandardOutput = true;
                startInfo.CreateNoWindow = true;

                Process p = System.Diagnostics.Process.Start(startInfo);
                string output = p.StandardOutput.ReadToEnd();
                p.WaitForExit();

                Console.WriteLine(output);

                Console.WriteLine(startInfo.Arguments);
            }
            else
            {
                LightNUnitRunner.RunTests(fixture);
            }
        }


        // TM: couldn't get NUnit Console to run under Vista....Quick workaround, build our own runner!
        public static class LightNUnitRunner
        {
            public static void RunTests(string fixtureName)
            {
                object instance = Assembly.GetExecutingAssembly().CreateInstance(fixtureName);
                Type fixtureType = Assembly.GetExecutingAssembly().GetType(fixtureName);

                MethodInfo[] tests = fixtureType.GetMethods();
                foreach (var method in tests)
                {
                    if (method.GetCustomAttributes(typeof(TestAttribute), false).Length > 0)
                        RunTest(instance, fixtureType, method.Name);

                }
            }

            public static void RunTest(object instance, Type fixtureType, string testName)
            {
                MethodInfo method = fixtureType.GetMethod(testName);
                object[] excAtt = method.GetCustomAttributes(typeof(ExpectedExceptionAttribute), false);
                bool exceptionThrown = false;
                if (excAtt.Length == 0)
                {
                    // no attribute, just execute the method.
                    try
                    {
                        method.Invoke(instance, new object[] { });
                    }
                    catch (Exception e)
                    {
                        exceptionThrown = true;
                        Console.WriteLine("FAIL: {0} test failed with message -", testName);
                        Console.WriteLine(e.InnerException.Message);
                    }

                    if (exceptionThrown == false)
                        Console.WriteLine("PASS: {0}", testName);
                }
                else
                {
                    // expects exception. try/catch trap
                    ExpectedExceptionAttribute et = excAtt[0] as ExpectedExceptionAttribute;
                    try
                    {
                        method.Invoke(instance, new object[] { });
                    }
                    catch (Exception e)
                    {
                        exceptionThrown = true;
                        if (e.InnerException.GetType().ToString() != et.ExpectedExceptionName)
                            Console.WriteLine("FAIL: {0} threw an exception {1} rather than {2}.",
                                testName, e.InnerException.GetType().ToString(), et.ExpectedExceptionName);
                        else
                            Console.WriteLine("PASS: {0}", testName);
                    }

                    if (exceptionThrown == false)
                        Console.WriteLine("FAIL: {0} didn't throw expected exception {1}.",
                            testName, et.ExpectedExceptionName);
                }
            }
        }

    }

    public class Sample
    {
        private readonly SampleHarness _Harness;
        private readonly MethodInfo _Method;
        private readonly string _Category;
        private readonly string _Title;
        private readonly string _Description;
        private readonly string _Code;


        public Sample(SampleHarness harness, MethodInfo method, string category, string title,
                      string description, string code)
        {
            this._Harness = harness;
            this._Method = method;
            this._Category = category;
            this._Title = title;
            this._Description = description;
            this._Code = code;
        }
        

        public SampleHarness Harness
        {
            get { return _Harness; }
        }
        
        public MethodInfo Method
        {
            get { return _Method; }
        }

        public string Category
        {
            get { return _Category; }
        }
        
        public string Title
        {
            get { return _Title; }
        }
        
        public string Description
        {
            get { return _Description; }
        }
        
        public string Code
        {
            get { return _Code; }
        }
        

        public void Invoke()
        {
            _Harness.InitSample();
            _Method.Invoke(this._Harness, null);
        }

        public void InvokeSafe()
        {
            try
            {
                Invoke();
            }
            catch (TargetInvocationException e)
            {
                _Harness.HandleException(e.InnerException);
            }
        }

        public override string ToString()
        {
            return Title;
        }

        public string WikiCode
        {
            get
            {
                StringBuilder result = new StringBuilder();

                result.AppendLine("====" + Title + "====");
                result.AppendLine();
                result.AppendLine(Description);
                result.AppendLine();
                result.AppendLine("<code>" + Code.Replace('-', '–') + "</code>");
                                
                StreamWriter writer = this._Harness.OutputStreamWriter;
                TextWriter oldConsoleOut = Console.Out;
                Console.SetOut(writer);
                MemoryStream stream = (MemoryStream)writer.BaseStream;
                stream.SetLength(0);

                Stopwatch watch = new Stopwatch();
                watch.Start();
                InvokeSafe();
                long time = watch.ElapsedMilliseconds;

                writer.Flush();
                Console.SetOut(oldConsoleOut);
                string thisConsole = writer.Encoding.GetString(stream.ToArray());
                if (!string.IsNullOrEmpty(thisConsole) && !string.IsNullOrWhiteSpace(thisConsole))
                {
                    thisConsole = thisConsole.Replace('-', '–');

                    result.Append(String.Format("Console output (Execution time: {0}ms):", time));
                    string id = this.Method.Name;
                    result.AppendLine();
                    result.AppendLine("<nowiki>[<a href=\"#\" onclick=\"javascript:if(document.getElementById('" + id + "').style['display']=='none') document.getElementById('" + id + "').style['display']=''; else document.getElementById('" + id + "').style['display']='none'; return false;\">Hide/Show</a>]</nowiki>");
                    result.AppendLine("<br/>");
                    result.AppendLine("<div id='" + id + "' style='overflow:hidden;display:none' >");
                    result.AppendLine("<code>" + thisConsole + "</code>");
                    result.AppendLine("</div>");
                }
                result.AppendLine("<br/>"); 
                result.AppendLine("<br/>");
                result.AppendLine("{TOP}");
                result.AppendLine("----");

                return result.ToString();
            }
        }
    }

    [global::System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public sealed class TitleAttribute : Attribute
    {
        public TitleAttribute(string title)
        {
            this.Title = title;
        }

        public string Title {get; set;}
    }

    [global::System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public sealed class PrefixAttribute : Attribute
    {
        public PrefixAttribute(string prefix)
        {
            this.Prefix = prefix;
        }

        public string Prefix {get; set;}
    }

    [global::System.AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class CategoryAttribute : Attribute
    {
        public CategoryAttribute(string category)
        {
            this.Category = category;
        }

        public string Category { get; set; }
    }

    [global::System.AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class DescriptionAttribute : Attribute
    {
        public DescriptionAttribute(string description)
        {
            this.Description = description;
        }

        public string Description {get; set;}
    }

    [global::System.AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class LinkedMethodAttribute : Attribute
    {

        public LinkedMethodAttribute(string methodName)
        {
            this.MethodName = methodName;
        }

        public string MethodName { get; set; }
    }

    [global::System.AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class LinkedClassAttribute : Attribute
    {
        public LinkedClassAttribute(string className)
        {
            this.ClassName = className;
        }

        public string ClassName { get; set; }
    }
}
