//MIT License
//
//Copyright(c) 2016-2019 Peter Kirmeier
//
//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.

using IFR;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using static IFR.IFRHelper;

namespace IfrViewer
{
    public partial class MainForm : Form
    {
        private const string EmptyDetails = "No data available";
        private readonly BackgroundWorker DragDropWorker;

        public MainForm()
        {
            InitializeComponent();
            IFRHelper.log = log; // Use local window as logging window
            DragDropWorker = new BackgroundWorker();
            DragDropWorker.DoWork += DragDropWorker_DoWork;
        }

        #region GUI

        private void MainForm_Load(object sender, EventArgs e)
        {
            // Update Version
            Text += " - v" + Application.ProductVersion + " (UEFI 2.6)";

            // Set size of window to 80 percent by default
            MainForm_SizeChanged(sender, e);
            Width = (int)(Screen.GetWorkingArea(DesktopLocation).Width * 0.80);
            Height = (int)(Screen.GetWorkingArea(DesktopLocation).Height * 0.80);
            CenterToScreen();
            Show();

            // Load project from command line argument (when available)
            List<string> Files = new List<string>();
            Boolean DoTranslate = false;
            Boolean DoHTML = false;
            foreach (string arg in Environment.GetCommandLineArgs().SubArray(1, Environment.GetCommandLineArgs().Length-1))
            {
                if (arg.StartsWith("-")) // is option?
                {
                    if (arg.Equals("-P")) // Start package
                    {
                        if (0 < Files.Count) // Parse previous package before loading next one..
                        {
                            LoadFiles(Files.ToArray()); // Load files of current package
                            Files.Clear();
                        }
                    }
                    else if (arg.StartsWith("-L=")) // Set display language
                    {
                        ts_parse_lang.Text = arg.Substring(3);
                    }
                    else if (arg.Equals("-T")) // Do Translation (Parsing logical tree)
                    {
                        DoTranslate = true;
                    }
                    else if (arg.Equals("-H")) // Do HTML output
                    {
                        DoHTML = true;
                    }
                    else CreateLogEntry(LogSeverity.WARNING, "Main", "Argument unkown \"" + arg + "\"");
                }
                else Files.Add(arg); // argument is a file
            }
            LoadFiles(Files.ToArray()); // Load last package

            if (0 == tv_tree.Nodes.Count)
                tv_details.Nodes.Add(EmptyDetails);

            TreeNode EmptyTree = tv_logical.Nodes.Add("No parsed packages available");
            EmptyTree.Tag = EmptyDetails;

            if (DoTranslate) // Parse logical tree
                parseLogicalViewToolStripMenuItem_Click(null, null);

            if (DoHTML) // Create HTML output
                createHTMLToolStripMenuItem_Click(null, null);
        }

        /// <summary>
        /// Creates a log entry for the "Main" module
        /// </summary>
        /// <param name="severity">Severity of message</param>
        /// <param name="msg">Message string</param>
        private void CreateLogEntryMain(LogSeverity severity, string msg)
        {
            CreateLogEntry(severity, "Main", msg);

            log.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
            log.AutoResizeRows(DataGridViewAutoSizeRowsMode.AllCells);
            log.FirstDisplayedScrollingRowIndex = log.Rows[log.RowCount - 1].Index; // Scroll to bottom

            Update();
        }

        private void MainForm_SizeChanged(object sender, EventArgs e)
        {
            // Check if window got minimized then stop changing sizes!
            // When form gets maximized (means ClientSize changes back to normal) then SizeChanged event doesn't get fired   (;゚︵゚;)
            if ((ClientSize.Width == 0) || (ClientSize.Height == 0))
                return;

            splitContainer1.Width = ClientSize.Width - 24;
            splitContainer1.Height = ClientSize.Height - 24;
            splitContainer1_SplitterMoved(sender, null);
        }

        private void splitContainer1_SplitterMoved(object sender, SplitterEventArgs e)
        {
            tabControl1.Width = splitContainer1.Panel1.Width - 9;
            tabControl1.Height = splitContainer1.Panel1.Height - 6;
            tabControl1_SizeChanged(sender, null);
            splitContainer2.Width = splitContainer1.Panel2.Width - 6;
            splitContainer2.Height = splitContainer1.Panel2.Height - 6;
            splitContainer2_SplitterMoved(sender, null);
        }

        private void splitContainer2_SplitterMoved(object sender, SplitterEventArgs e)
        {
            tv_details.Width = splitContainer2.Panel1.Width - 6;
            tv_details.Height = splitContainer2.Panel1.Height - 6;
            log.Width = splitContainer2.Panel2.Width - 6;
            log.Height = splitContainer2.Panel2.Height - 6;
        }

        private void tabControl1_SizeChanged(object sender, EventArgs e)
        {
            Control ActiveParent = tabControl1.SelectedTab;
            tv_tree.Width = ActiveParent.Width;
            tv_tree.Height = ActiveParent.Height;
            tv_logical.Width = ActiveParent.Width;
            tv_logical.Height = ActiveParent.Height;
        }

        /// <summary>
        /// Adds a text-object pair to a tree
        /// </summary>
        /// <param name="root">Node which the new node is added to</param>
        /// <param name="text">Displayed text of new node</param>
        /// <param name="obj">Object added to node for reference</param>
        /// <returns>New created tree node</returns>
        private TreeNode AddTreeNode(TreeNode root, string text, object obj)
        {
            TreeNode leaf = root.Nodes.Add(text);
            leaf.Tag = obj;
            return leaf;
        }

        /// <summary>
        /// Updates the details window when a tree node gets selected by user
        /// </summary>
        private void tv_tree_AfterSelect(object sender, TreeViewEventArgs e)
        {
            ShowAtDetails(e.Node.Tag, e.Node.Name);
        }

        /// <summary>
        /// Handles drag and drop operation in order to load further files
        /// </summary>
        /// <param name="e">Argument must contain DragEventArgs</param>
        void DragDropWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            DragEventArgs drag_args = (DragEventArgs)e.Argument;
            string[] DroppedPathList = (string[])drag_args.Data.GetData(DataFormats.FileDrop);
            List<string> DroppedFiles = new List<string>();

            // get all files of the dropped object(s) and add them..
            foreach (string path in DroppedPathList)
            {
                if (Directory.Exists(path))
                    DroppedFiles.AddRange(Directory.GetFiles(path, "*.*", SearchOption.AllDirectories));
                else if (File.Exists(path))
                    DroppedFiles.Add(path);
            }

            Invoke(Delegate.CreateDelegate(typeof(DragDropFilesFunc), this, "DragDropFiles"), DroppedFiles.ToArray(), drag_args);
        }

        private void MainForm_DragDrop(object sender, DragEventArgs e)
        {
            DragDropWorker.RunWorkerAsync(e);
        }

        private void tv_tree_DragOver(object sender, DragEventArgs e)
        {
            if (null != GetTreeNodeAtPoint(sender, e.X, e.Y)) // Is draging onto a tree node?
                e.Effect = DragDropEffects.Link; // Allow dropping files into existing package
            else
                e.Effect = DragDropEffects.Copy; // Allow dropping files into separate package
        }

        private void MainForm_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy; // Allow dopping files
        }

        private void parseLogicalViewToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ParseAllFiles(ts_parse_lang.Text);
            tabControl1.SelectedIndex = 1;
        }

        private void createHTMLToolStripMenuItem_Click(object sender, EventArgs e)
        {
            CreateHTMLFiles(ts_parse_lang.Text, printDetailsIntoHtmlToolStripMenuItem.Checked);
        }

        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void helpToolStripMenuItem_Click(object sender, EventArgs e)
        {
            (new About()).ShowDialog(this);
        }

        /// <summary>
        /// Retrieves the TreeNode object of a TreeView at a given position
        /// </summary>
        /// <param name="sender">TreeView object to search</param>
        /// <param name="x">Position X</param>
        /// <param name="x">Position Y</param>
        private TreeNode GetTreeNodeAtPoint(object sender, int x, int y)
        {
            if (!(sender is TreeView))
                return null;

            TreeView tv_sender = (TreeView)sender;
            Point pt = tv_sender.PointToClient(new Point(x, y));
            return tv_sender.GetNodeAt(pt);
        }

        private delegate void DragDropFilesFunc(string[] filepaths, DragEventArgs e);
        /// <summary>
        /// Loads a bunch of files via drag and drop
        /// </summary>
        /// <param name="filepaths">List of files to load</param>
        /// <param name="e">Drag and drop event arguments</param>
        private void DragDropFiles(string[] filepaths, DragEventArgs e)
        {
            TreeNode RootNode = null;
            if (DragDropEffects.Link == e.Effect) // object needs to be linked with an existing node?
            {
                RootNode = GetTreeNodeAtPoint(tv_tree, e.X, e.Y);
                while (null != RootNode.Parent) RootNode = RootNode.Parent;
            }
            LoadFiles(filepaths, RootNode);
        }
        #endregion

        /// <summary>
        /// Loads a bunch of files which are referring the same "package"
        /// </summary>
        /// <param name="filepaths">List of files to load</param>
        /// <param name="ParentNode">Optional node the loaded packages are added to</param>
        private void LoadFiles(string[] filepaths, TreeNode ParentNode = null)
        {
            Cursor previousCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;

            List<HiiPackageBase> Packages = new List<HiiPackageBase>();

            TreeNode PkgNodeRaw = ParentNode;
            if (null == PkgNodeRaw) // use given parent or create it
            {
                PkgNodeRaw = tv_tree.Nodes.Add("Package");
                PkgNodeRaw.Tag = EmptyDetails;
            }

            CreateLogEntryMain(LogSeverity.INFO, "Loading files...");

            // Load HPKs into memory and build tree view
            foreach (string filename in filepaths)
            {
                CreateLogEntryMain(LogSeverity.INFO, "Loading file \"" + filename + "\" ...");

                HPKfile hpk = null;
                try
                {
                    hpk = new HPKfile(filename);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                    CreateLogEntryMain(LogSeverity.ERROR, "Loading file failed!" + Environment.NewLine + ex.ToString());
                    MessageBox.Show("Loading file failed!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }

                if (null != hpk) // Loaded successfully?
                {
                    tv_tree.BeginUpdate();
                    TreeNode root = PkgNodeRaw.Nodes.Add(hpk.Name);
                    root.Tag = hpk;
                    ShowAtRawTree(hpk, root);
                    root.Expand();
                    tv_tree.EndUpdate();

                    // Collect all new packages of this file
                    foreach (HiiPackageBase hpkpkg in hpk.Childs)
                        Packages.Add(hpkpkg);
                }
            }

            CreateLogEntryMain(LogSeverity.SUCCESS, "Loading files completed!");

            Cursor.Current = previousCursor;
        }

        /// <summary>
        /// Parses a bunch of files which are referring the same "package" using a given default language
        /// </summary>
        /// <param name="Language">Primary language</param>
        private void ParseAllFiles(string Language)
        {
            Cursor previousCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;

            CreateLogEntryMain(LogSeverity.INFO, "Parsing packages...");

            tv_logical.Parent.Text = "Logical Tree (\"" + Language + "\")";
            tv_logical.Nodes.Clear();

            foreach (TreeNode node_files in tv_tree.Nodes)
            {
                List<HiiPackageBase> Packages = new List<HiiPackageBase>();

                TreeNode PkgNodeLogical = tv_logical.Nodes.Add("Package");
                PkgNodeLogical.Tag = EmptyDetails;

                // Collect all HII packages of the files that are building a logical package
                foreach (TreeNode node_pkg in node_files.Nodes)
                    foreach (HiiPackageBase hpk in (node_pkg.Tag as HPKfile).Childs)
                        Packages.Add(hpk);

                ParsedHpkStringContainer HpkStrings = new ParsedHpkStringContainer(Packages, Language);
                ParsedHpkContainer ParsedHpkContainer = new ParsedHpkContainer(Packages, HpkStrings);

                // Since HPKs interact with each other, build logical tree after loading is completely done
                foreach (ParsedHpkNode pkg in ParsedHpkContainer.HpkPackages)
                {
                    CreateLogEntryMain(LogSeverity.INFO, "Parsing \"" + pkg.Name + "\" ...");

                    tv_logical.BeginUpdate();
                    TreeNode root = PkgNodeLogical.Nodes.Add(pkg.Name);
                    root.Tag = pkg.Origin;
                    ShowAtLogicalTree(pkg, root);
                    root.Expand();
                    tv_logical.EndUpdate();
                }

                PkgNodeLogical.Expand();
            }

            if (0 == tv_logical.Nodes.Count)
            {
                TreeNode EmptyTree = tv_logical.Nodes.Add("No parsed packages available");
                EmptyTree.Tag = EmptyDetails;
            }

            CreateLogEntryMain(LogSeverity.SUCCESS, "Parsing packages completed!");

            Cursor.Current = previousCursor;
        }

        /// <summary>
        /// Creates HTML files foreach formset including all their forms
        /// </summary>
        /// <param name="Language">Primary language</param>
        /// <param name="bShowDetails">Printing details into HTML</param>
        private void CreateHTMLFiles(string Language, bool bShowDetails)
        {
            Cursor previousCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;

            CreateLogEntryMain(LogSeverity.INFO, "Creating HTML output...");

            foreach (TreeNode node_files in tv_tree.Nodes)
            {
                List<HiiPackageBase> Packages = new List<HiiPackageBase>();

                // Collect all HII packages of the files that are building a logical package
                foreach (TreeNode node_pkg in node_files.Nodes)
                    foreach (HiiPackageBase hpk in (node_pkg.Tag as HPKfile).Childs)
                        Packages.Add(hpk);

                ParsedHpkStringContainer HpkStrings = new ParsedHpkStringContainer(Packages, Language);
                HtmlBuilder HtmlBuilder = new HtmlBuilder(Packages, HpkStrings, bShowDetails, printCompactHtmlToolStripMenuItem.Checked);
            }

            // TODO!
            //if (0 == tv_logical.Nodes[0].Nodes.Count)
            //    CreateLogEntryMain(LogSeverity.ERROR, "Creating HTML output failed!\nNo parsed package available");
            //else
            CreateLogEntryMain(LogSeverity.SUCCESS, "Creating HTML output completed!");

            Cursor.Current = previousCursor;
        }

        /// <summary>
        /// Adds a subtree according to the given HPK tree
        /// </summary>
        /// <param name="elem">Root node of HPK tree</param>
        /// <param name="root">Root node of target tree</param>
        private void ShowAtRawTree(HPKElement elem, TreeNode root)
        {
            // add all child elements to the tree..
            if (elem.Childs.Count > 0)
            {
                foreach (HPKElement child in elem.Childs)
                {
                    ShowAtRawTree(child, AddTreeNode(root, child.Name + " [" + child.UniqueID + "]", child));
                }
                root.Expand();
            }
        }

        /// <summary>
        /// Adds a subtree according to the given parsed HPK tree
        /// </summary>
        /// <param name="node">Root node of parsed HPK tree</param>
        /// <param name="root">Root node of target tree</param>
        private void ShowAtLogicalTree(ParsedHpkNode node, TreeNode root)
        {
            // add all child elements to the tree..
            if (node.Childs.Count > 0)
            {
                foreach (ParsedHpkNode child in node.Childs)
                {
                    ShowAtLogicalTree(child, AddTreeNode(root, child.Name, child.Origin));
                }
                root.Expand();
            }
        }

        /// <summary>
        /// Shows object's data at details window
        /// </summary>
        /// <param name="obj">Object to be displayed</param>
        /// <param name="name">Name of the displayed object (in case of error)</param>
        private void ShowAtDetails(object obj, string name)
        {
            Cursor previousCursor = Cursor.Current;
            Cursor.Current = Cursors.WaitCursor;
            tv_details.BeginUpdate();

            tv_details.Nodes.Clear();
            if (obj == null)
            {
                // should not happen because every node should have an object!
                tv_details.Nodes.Add(EmptyDetails);
                CreateLogEntryMain(LogSeverity.WARNING, "No data found for \"" + name + "\"!");
            }
            else if (obj is HPKElement)
            {
                HPKElement elem = (HPKElement)obj;
                const uint BytesPerLine = 16;

                // add common elements..
                tv_details.Nodes.Add("UniqueID = " + elem.UniqueID);

                // add all header fields to the tree..
                byte[] HeaderRaw = elem.HeaderRaw;
                if ((elem.Header != null) || (HeaderRaw != null))
                {
                    TreeNode branch = tv_details.Nodes.Add("Header");
                    // handle raw..
                    if ((HeaderRaw != null) && (showRawInDetailsWindowToolStripMenuItem.Checked))
                    {
                        TreeNode leaf = branch.Nodes.Add("__RAW");
                        foreach (string line in HeaderRaw.HexDump(BytesPerLine).Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                            leaf.Nodes.Add(line);
                    }
                    // handle managed..
                    if (elem.Header != null)
                    {
                        foreach (System.Collections.Generic.KeyValuePair<string, object> pair in elem.GetPrintableHeader(BytesPerLine))
                        {
                            branch.Nodes.Add(pair.Key + " = " + pair.Value.ToString());
                        }
                    }
                    //branch.Expand();
                }
                // add all payload fields to the tree..
                byte[] PayloadRaw = elem.PayloadRaw;
                if ((elem.Payload != null) || (PayloadRaw != null))
                {
                    TreeNode branch = tv_details.Nodes.Add("Payload");
                    // handle raw..
                    if ((PayloadRaw != null) && (showRawInDetailsWindowToolStripMenuItem.Checked))
                    {
                        TreeNode leaf = branch.Nodes.Add("__RAW");
                        foreach (string line in PayloadRaw.HexDump(BytesPerLine).Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                            leaf.Nodes.Add(line);
                    }
                    // handle managed..
                    if (elem.Payload != null)
                    {
                        foreach (System.Collections.Generic.KeyValuePair<string, object> pair in elem.GetPrintablePayload(BytesPerLine))
                        {
                            branch.Nodes.Add(pair.Key + " = " + pair.Value.ToString());
                        }
                    }
                    //branch.Expand();
                }
                tv_details.ExpandAll();
            }
            else if (obj is String)
            {
                tv_details.Nodes.Add((string)obj);
            }
            else // Unknown data type
            {
                tv_details.Nodes.Add(EmptyDetails);
                CreateLogEntryMain(LogSeverity.UNIMPLEMENTED, "Unkown data found for \"" + name + "\"!");
            }

            tv_details.EndUpdate();
            Cursor.Current = previousCursor;
        }

        /// <summary>
        /// Handler for CTRL+C copy shortcut on tree view nodes
        /// Copies the tree node's text to clipboard
        /// </summary>
        private void tv_KeyDown(object sender, KeyEventArgs e)
        {
            if (sender is TreeView)
            {
                TreeView tv = (TreeView)sender;
                if (e.KeyData == (Keys.Control | Keys.C))
                {
                    if (tv.SelectedNode != null)
                    {
                        Clipboard.SetText(tv.SelectedNode.Text);
                    }
                    e.SuppressKeyPress = true;
                }
            }
        }

    }

    static class IfrViewer
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }
}