#region License
//
// The Open Toolkit Library License
//
// Copyright (c) 2006 - 2009 the Open Toolkit library.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights to 
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
#endregion

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Text;
using System.Reflection;
using System.Windows.Forms;
using OpenTK.Examples.Properties;
using System.Threading;
using System.IO;

namespace Examples
{
    public partial class ExampleBrowser : Form
    {
        #region Fields

        //PrivateFontCollection font_collection = new PrivateFontCollection();

        bool show_warning = true;

        static readonly string SourcePath = FindSourcePath();

        #endregion

        #region Constructors

        public ExampleBrowser()
        {
            Font = SystemFonts.DialogFont;

            InitializeComponent();
            Icon = Resources.App;

            // Windows 6 (Vista) and higher come with Consolas, a high-quality monospace font. Use that or fallback to
            // the generic monospace font on other systems.
            if (System.Environment.OSVersion.Platform == PlatformID.Win32NT &&
                System.Environment.OSVersion.Version.Major >= 6)
            {
                textBoxOutput.Font = richTextBoxSource.Font = new Font("Consolas", 10.0f, FontStyle.Regular);
            }
            else
            {
                textBoxOutput.Font = richTextBoxSource.Font =
                    new Font(FontFamily.GenericMonospace, 10.0f, FontStyle.Regular);
            }
        }

        #endregion

        #region Protected Members

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            // Add those by hand, because using the designer results in an empty
            // image list when cross-compiling on Mono.
            imageListSampleCategories.Images.Add("OpenAL", Resources.OpenAL);
            imageListSampleCategories.Images.Add("OpenGL", Resources.OpenGL);
            imageListSampleCategories.Images.Add("OpenGLES", Resources.OpenGLES);
            imageListSampleCategories.Images.Add("OpenCL", Resources.OpenCL);
            imageListSampleCategories.Images.Add("OpenTK", Resources.OpenTK);
            imageListSampleCategories.Images.Add("1.x", Resources.v1x);
            imageListSampleCategories.Images.Add("2.x", Resources.v2x);
            imageListSampleCategories.Images.Add("3.x", Resources.v3x);
            imageListSampleCategories.Images.Add("4.x", Resources.v4x);

            Debug.Listeners.Add(new TextBoxTraceListener(textBoxOutput));
            treeViewSamples.TreeViewNodeSorter = new SamplesTreeViewSorter();

            LoadSamplesFromAssembly(Assembly.GetExecutingAssembly());
        }

        protected override void OnShown(EventArgs e)
        {
            if (show_warning)
            {
                //MessageBox.Show("The new Sample Browser is not complete. Please report any issues at http://www.opentk.com/project/issues.",
                //    "Work in Progress", MessageBoxButtons.OK, MessageBoxIcon.Information);
                show_warning = false;
            }
        }

        #endregion

        #region Private Members

        #region Events

        #region TreeView

        private void treeViewSamples_AfterSelect(object sender, TreeViewEventArgs e)
        {
            const string no_docs = "Documentation has not been entered.";
            const string no_source = "Source code has not been entered.";
            
            if (e.Node.Tag != null && !String.IsNullOrEmpty(((ExampleInfo)e.Node.Tag).Attribute.Documentation))
            {
                string docs = null;
                string source = null;

                ExampleInfo einfo = (ExampleInfo)e.Node.Tag;
                string sample = einfo.Attribute.Documentation;
                string category = einfo.Attribute.Category.ToString();
                string subcategory = einfo.Attribute.Subcategory;

                string path = Path.Combine(Path.Combine(Path.Combine(SourcePath, category), subcategory), sample);
                string sample_rtf = Path.ChangeExtension(path, "rtf");
                string sample_cs = Path.ChangeExtension(path, "cs");

                if (File.Exists(sample_rtf))
                {
                    docs = File.ReadAllText(sample_rtf);
                }

                if (File.Exists(sample_cs))
                {
                    source = File.ReadAllText(sample_cs);
                }
                
                if (String.IsNullOrEmpty(docs))
                    richTextBoxDescription.Text = String.Format("File {0} not found.", sample_rtf);
                else
                    richTextBoxDescription.Rtf = docs;

                if (String.IsNullOrEmpty(source))
                    richTextBoxSource.Text = String.Format("File {0} not found.", sample_cs);
                else
                    richTextBoxSource.Text = source;
            }
            else
            {
                richTextBoxDescription.Text = no_docs;
                richTextBoxSource.Text = no_source;
            }
        }

        private void treeViewSamples_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
        {
            if (e.Node.Tag != null)
            {
                ActivateNode(e.Node);
            }
        }

        private void treeViewSamples_KeyDown(object sender, KeyEventArgs e)
        {
            // The enter key activates a node (either expands/collapses or executes its sample).
            switch (e.KeyCode)
            {
                case Keys.Enter:
                    ActivateNode(treeViewSamples.SelectedNode);
                    e.Handled = true;
                    e.SuppressKeyPress = true;
                    break;
            }
        }

        private void treeViewSamples_MouseDown(object sender, MouseEventArgs e)
        {
            // Make sure that right-clicking a new node will select that node before displaying
            // the context menu. Without this, right-clicking a node does not select it, which
            // is completely disorienting.
            // As a bonus, make any mouse button select the underlying node,
            TreeNode node = treeViewSamples.HitTest(e.Location).Node;
            if (node != null)
                treeViewSamples.SelectedNode = node;

            // Middle click selects and activates a node (either expands/collapses or executes its sample).
            // Right button displays the context menu.
            // All other mouse buttons simply select the underlying node.
            switch (e.Button)
            {
                case MouseButtons.Middle:
                    ActivateNode(node);
                    break;

                case MouseButtons.Right:
                    treeViewSamples.ContextMenuStrip.Show(sender as Control, e.Location);
                    break;
            }
        }

        private void treeViewSamples_AfterExpand(object sender, TreeViewEventArgs e)
        {
            foreach (TreeNode child in e.Node.Nodes)
                child.EnsureVisible();
        }

        private void contextMenuStripSamples_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            switch (e.ClickedItem.Text)
            {
                case "&Run Sample": RunSample(this, (ExampleInfo)treeViewSamples.SelectedNode.Tag); break;
                case "View Description": tabControlSample.SelectedTab = tabDescription; break;
                case "View Source Code": tabControlSample.SelectedTab = tabSource; break;
            }
        }

        #endregion

        #region Description

        private void richTextBoxDescription_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                richTextBoxDescription.ContextMenuStrip.Show(sender as Control, e.X, e.Y);
            }
        }

        private void contextMenuStripDescription_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            if (e.ClickedItem.Text == "&Copy")
            {
                Clipboard.SetText(richTextBoxDescription.SelectedRtf, TextDataFormat.Rtf);
            }
        }

        #endregion

        #region Source Code

        private void richTextBoxSource_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                richTextBoxSource.ContextMenuStrip.Show(sender as Control, e.X, e.Y);
            }
        }

        private void contextMenuStripSource_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            if (e.ClickedItem.Text == "&Copy")
            {
                Clipboard.SetText(richTextBoxSource.SelectedText, TextDataFormat.Text);
            }
        }

        #endregion

        #endregion

        #region Actions

        void LoadSamplesFromAssembly(Assembly assembly)
        {
            if (assembly == null)
                throw new ArgumentNullException("assembly");

            Type[] types = assembly.GetTypes();
            foreach (Type type in types)
            {
                object[] attributes = type.GetCustomAttributes(false);
                ExampleAttribute example = null;
                foreach (object attr in attributes)
                {
                    if (attr is ExampleAttribute)
                    {
                        example = (ExampleAttribute)attr;

                        if (example.Visible)
                        {
                            // Add this example to the sample TreeView.
                            // First check whether the ExampleCategory exists in the tree (and add it if it doesn't).
                            // Then add the example as a child node on this category.

                            if (!treeViewSamples.Nodes.ContainsKey(example.Category.ToString()))
                            {
                                int category_index = GetImageIndexForSample(imageListSampleCategories, example.Category.ToString(), String.Empty);
                                treeViewSamples.Nodes.Add(example.Category.ToString(), String.Format("{0} samples", example.Category),
                                    category_index, category_index);
                            }

                            int image_index = GetImageIndexForSample(imageListSampleCategories, example.Category.ToString(), example.Subcategory);
                            TreeNode node = new TreeNode(example.Title, image_index, image_index);
                            node.Name = example.Title;
                            node.Tag = new ExampleInfo(type, example);
                            treeViewSamples.Nodes[example.Category.ToString()].Nodes.Add(node);
                        }
                    }
                }
            }

            treeViewSamples.Sort();
        }

        void ActivateNode(TreeNode node)
        {
            if (node == null)
                return;

            if (node.Tag == null)
            {
                if (node.IsExpanded)
                    node.Collapse();
                else
                    node.Expand();
            }
            else
            {
                tabControlSample.SelectedTab = tabPageOutput;
                textBoxOutput.Clear();
                RunSample(node.TreeView.TopLevelControl, (ExampleInfo)node.Tag);
            }
        }

        static int GetImageIndexForSample(ImageList list, string category, string subcategory)
        {
            if (list == null)
                throw new ArgumentNullException("list");

            foreach (string extension in new string[] { "", ".png", ".jpg" })
            {
                string name = subcategory.ToString() + extension;
                if (list.Images.ContainsKey(name))
                    return list.Images.IndexOfKey(name);

                name = category.ToString() + extension;
                if (list.Images.ContainsKey(name))
                    return list.Images.IndexOfKey(name);
            }
            
            return -1;
        }

        static void RunSample(Control parent, ExampleInfo e)
        {
            if (e == null)
                return;

            MethodInfo main =
                e.Example.GetMethod("Main", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ??
                e.Example.GetMethod("Main", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { typeof(object), typeof(object) }, null);
            if (main != null)
            {
                try
                {
                    if (parent != null)
                    {
                        parent.Visible = false;
                        Application.DoEvents();
                    }
                    Trace.WriteLine(String.Format("Launching sample: \"{0}\"", e.Attribute.Title));
                    Trace.WriteLine(String.Empty);

                    Thread thread = new Thread((ThreadStart)delegate
                    {
                        try
                        {
                            main.Invoke(null, null);
                        }
                        catch (TargetInvocationException expt)
                        {
                            string ex_info;
                            if (expt.InnerException != null)
                                ex_info = expt.InnerException.ToString();
                            else
                                ex_info = expt.ToString();
                            MessageBox.Show(ex_info, "An OpenTK example encountered an error.", MessageBoxButtons.OK, MessageBoxIcon.Warning);

                            Debug.Print(expt.ToString());
                        }
                        catch (NullReferenceException expt)
                        {
                            MessageBox.Show(expt.ToString(), "The Example launcher failed to load the example.", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                        }
                    });
                    thread.IsBackground = true;
                    thread.Start();
                    thread.Join();
                }
                finally
                {
                    if (parent != null)
                    {
                        parent.Visible = true;
                        Application.DoEvents();
                    }
                }
            }
            else
            {
                MessageBox.Show("The selected example does not define a Main method", "Entry point not found", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
        }

        // Tries to detect the path that contains the source for the examples.
        static string FindSourcePath()
        {
            string current_dir = Directory.GetCurrentDirectory();

            // Typically, our working directory is either "[opentk]/Binaries/OpenTK/[config]" or "[opentk]".
            // The desired source path is "[opentk]/Source/Examples/[ExampleCategory]"

            string guess = current_dir;
            if (CheckPath(ref guess))
                return guess; // We were in [opentk] after all

            guess = current_dir;
            for (int i = 0; i < 3; i++)
            {
                DirectoryInfo dir = Directory.GetParent(guess);
                if (!dir.Exists)
                    break;
                guess = dir.FullName;
            }
            
            if (CheckPath(ref guess))
                return guess; // We were in [opentk]/Binaries/OpenTK/[config] after all

            throw new DirectoryNotFoundException();
        }

        static bool CheckPath(ref string path)
        {
            string guess = path;
            if (Directory.Exists(guess))
            {
                guess = Path.Combine(guess, "Source");
                if (Directory.Exists(guess))
                {
                    guess = Path.Combine(guess, "Examples");
                    if (Directory.Exists(guess))
                    {
                        // We are have found [opentk]/Source/Examples
                        path = guess;
                        return true;
                    }
                }
            }
            return false;
        }

       #endregion

        #endregion
    }
}
