﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using IronJS;

namespace xmlTools
{
    /// <summary>
    /// This classes parses a .patch xml tools file and applies its content to the files which it specifies
    /// </summary>
    class XmlPatch
    {
        String fileName;
        String forceFiles = "";
        bool globalNoBackups = false;
        IronJS.Hosting.CSharp.Context jsEngine = null; // initialize only when necessary

        public XmlPatch(String file, bool noBackups)
        {
            fileName = file;
            globalNoBackups = noBackups;
        }

        public XmlPatch(String file, String forceInFiles, bool noBackups)
        {
            fileName = file;
            forceFiles = forceInFiles; //We support apply the operation in diverse forced files (NameOfFile parameter will be ignored)
            globalNoBackups = noBackups;
        }

        /// <summary>
        /// Applies the patch file. Returns true if successful otherwise returns false.
        /// </summary>
        /// <returns></returns>
        public bool startPatch()
        {
            string line;

            // Read the file and display it line by line.
            System.IO.StreamReader file = new System.IO.StreamReader(fileName);

            while ((line = file.ReadLine()) != null) //read while we don't reach the end of the file
            {
                if (line.StartsWith("@ADDTO "))
                {
                    string operation = line;
                    string xmlToInject = "";

                    file.ReadLine(); //ignore <xml> start header
                    while ((line = file.ReadLine()) != "</xml>")
                    {
                        xmlToInject += line + System.Environment.NewLine; //get all the xml that will be injected
                    }
                    if (!addOperation(operation, xmlToInject))
                    {
                        Program.printAppError(Program.appErrors.PATCH_ADDTO_PROCESS_ERROR, "Error while performing adding operation in patch file. Aborting...");
                        return false;
                    }
                }
                else if (line.StartsWith("@REMOVE "))
                {
                    if (!removeOperation(line))
                    {
                        Program.printAppError(Program.appErrors.PATCH_REMOVE_PROCESS_ERROR, "Error while performing remove operation in patch file. Aborting...");
                        return false;
                    }
                }
                else if (line.StartsWith("@COMMAND "))
                {
                    if (!executeCommand(line))
                    {
                        Program.printAppError(Program.appErrors.PATCH_COMMAND_PROCESS_ERROR, "Error while performing command operation in patch file. Aborting...");
                        return false;
                    }
                }
                else if (line.StartsWith("@CUSTOMCODE "))
                {
                    string operation = line;
                    string jsCode = "";

                    file.ReadLine(); //ignore <xml> start header
                    while ((line = file.ReadLine()) != "</code>")
                    {
                        jsCode += line + System.Environment.NewLine; //get all the xml that will be injected
                    }
                    if (!executeCode(operation, jsCode))
                    {
                        Program.printAppError(Program.appErrors.PATCH_CODE_PROCESS_ERROR, "Error while performing code operation in patch file. Aborting...");
                        return false;
                    }
                }
            }

            file.Close();

            return true;
        }

        /// <summary>
        /// Inserts xml in a desired Element. Returns true or false if it succeeds
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="xmlToInject"></param>
        /// <returns></returns>
        private bool addOperation(string operation, string xmlToInject)
        {
            //@ADDTO File "example.xml" ParentElement "Animation" Element "Lookup"

            string FileParam = "", ParentElementParam = "", ElementParam = "";

            //---------------------------------------------------Parse Operation command (start)
            try
            {
                if (String.IsNullOrEmpty(forceFiles))
                {
                    FileParam = getPatchParameter(operation, "File");
                }
                else
                {
                    FileParam = forceFiles;
                }

                ParentElementParam = getPatchParameter(operation, "ParentElement"); //Get the ParentElement

                ElementParam = getPatchParameter(operation, "Element"); //Get the Element
            }
            catch (Exception e)
            {
                Program.printAppError(Program.appErrors.PATCH_ADDTO_ERROR_PARSING_XML, "Error parsing addOperation in Patch file.\n" + e.ToString());
                return false;
            }

            if (String.IsNullOrEmpty(ElementParam))
            {
                return false;
            }

            //---------------------------------------------------Parse Operation command (end)
            List<String> filesToProcess = new List<String>();
            if (String.IsNullOrEmpty(FileParam))
            {
                filesToProcess = Util.getAllXmlFiles(); //no file specified, use all xml files found in same folder
            }
            else if (Util.containsWildcard(FileParam))
            {
                filesToProcess = Util.getXmlFilesWildcard(FileParam);
            }
            else
            {
                filesToProcess.Add(FileParam);
            }

            //---------------------------------------------------XML Injection (start)
            foreach (String currFile in filesToProcess)
            {
                if (!this.globalNoBackups && !Util.ContainsIgnoreCase(operation, "NoBackups")) // only skip backup if specified via global parameter or in patch file
                {
                    Util.backupFile(currFile);
                }

                XmlDocument xdoc = new XmlDocument();
                xdoc.Load(currFile);

                List<XmlNode> myElements = new List<XmlNode>();
                Util.getAllSpecificElements(xdoc.DocumentElement, ref myElements, ElementParam, ParentElementParam); //Returns all after "Oni" element

                if (myElements.Count == 0)
                {
                    Program.printAppError(Program.appErrors.PATCH_ELEMENT_NOT_FOUND, "Error in addOperation in Patch file: the element specified doesn't exist.");
                    return false;
                }

                try
                {
                    XmlNode newXml = xdoc.ImportNode(Util.stringToXmlNode(xmlToInject), true); //necessary to import node or ArgumentException will be thrown when appending

                    myElements[myElements.Count - 1].AppendChild(newXml); // Append the code after last element

                    xdoc.Save(currFile);
                }
                catch (XmlException e)
                {
                    Program.printAppError(Program.appErrors.PATCH_ADDTO_ERROR_PARSING_XML, "Error parsing xml to addOperation in Patch file.\n" + e.ToString());
                    return false;
                }
            }
            //---------------------------------------------------XML Injection (end)

            return true;
        }

        /// <summary>
        /// Removes a xml element, right now it removes the first element it finds with matchs "Element" and "ParentElement" parameters
        /// </summary>
        /// <param name="operation"></param>
        /// <returns>true or false depending if succeed or not</returns>
        private bool removeOperation(string operation)
        {
            //@REMOVE File "example.xml" ParentElement "Particles" Element "Particle"

            string FileParam = "", ParentElementParam = "", ElementParam = "";

            //---------------------------------------------------Parse Operation command (start)
            try
            {
                if (String.IsNullOrEmpty(forceFiles))
                {
                    FileParam = getPatchParameter(operation, "File");
                }
                else
                {
                    FileParam = forceFiles;
                }

                ParentElementParam = getPatchParameter(operation, "ParentElement"); //Get the ParentElement

                ElementParam = getPatchParameter(operation, "Element"); //Get the Element
            }
            catch (Exception e)
            {
                Program.printAppError(Program.appErrors.PATCH_REMOVE_PROCESS_ERROR, "Error parsing removeOperation in Patch file.\n" + e.ToString());
                return false;
            }

            if (String.IsNullOrEmpty(ElementParam))
            {
                return false;
            }

            //---------------------------------------------------Parse Operation command (end)

            List<String> filesToProcess = new List<String>();
            if (String.IsNullOrEmpty(FileParam))
            {
                filesToProcess = Util.getAllXmlFiles(); //no file specified, use all xml files found in same folder
            }
            else if (Util.containsWildcard(FileParam))
            {
                filesToProcess = Util.getXmlFilesWildcard(FileParam);
            }
            else
            {
                filesToProcess.Add(FileParam);
            }

            //---------------------------------------------------XML Remove (start)

            foreach (String currFile in filesToProcess)
            {

                if (!this.globalNoBackups && !Util.ContainsIgnoreCase(operation, "NoBackups")) // only skip backup if specified via global parameter or in patch file
                {
                    Util.backupFile(currFile);
                }

                XmlDocument xdoc = new XmlDocument();
                xdoc.Load(currFile);

                List<XmlNode> myElements = new List<XmlNode>();
                Util.getAllSpecificElements(xdoc.DocumentElement, ref myElements, ElementParam, ParentElementParam); //Returns all after "Oni" element

                if (myElements.Count == 0)
                {
                    Program.printAppError(Program.appErrors.PATCH_ELEMENT_NOT_FOUND, "Error in removeOperation in Patch file: the element specified doesn't exist.");
                    return false;
                }

                myElements[0].ParentNode.RemoveChild(myElements[0]); // Removes the first occurrence which matches the "Element" and "ParentElement" given

                xdoc.Save(currFile);

            }
            //---------------------------------------------------XML Remove (end)


            return true;
        }

        /// <summary>
        ///  Executes a command for xmlTools
        /// </summary>
        /// <param name="command"></param>
        /// <returns>true or false depending if succeed or not</returns>
        private bool executeCommand(string command)
        {
            //---------------------------------------------------Parse Operation command (start)

            command = command.Replace("@COMMAND ", ""); //get only the command to process

            if (String.IsNullOrEmpty(command.Trim()))
            {
                Program.printAppError(Program.appErrors.PATCH_COMMAND_NOT_FOUND, "Error parsing commandOperation in Patch file: Command is empty.");
                return false;
            }

            try
            {
                if (!String.IsNullOrEmpty(forceFiles))
                {
                    string paramType = "";

                    // Filename already exists?
                    if (Util.ContainsIgnoreCase(command, "filename:"))
                    {
                        paramType = "filename:";
                    }
                    else if (Util.ContainsIgnoreCase(command, "filename="))
                    {
                        paramType = "filename=";
                    }
                    // Add the filename if it doesn't exists
                    else
                    {
                        command = command.Insert(command.Length, " -filename:" + this.forceFiles);
                    }

                    if (!String.IsNullOrEmpty(paramType))
                    {
                        int startIdx = command.IndexOf(paramType) + paramType.Length;
                        int endIdx = command.IndexOf(" ", startIdx); // it may end with space
                        if (endIdx == -1)
                        {
                            endIdx = command.IndexOf("\n", startIdx); // or with endline
                            if (endIdx == -1)
                            { // Filename parameters is the last one in the file (file ends with this parameter)
                                endIdx = command.Length - 1;
                            }
                        }
                        string currFilename = command.Substring(startIdx, endIdx - startIdx);
                        command = command.Replace(currFilename, this.forceFiles);
                    }

                }

                if (this.globalNoBackups && !Util.ContainsIgnoreCase(command, "nobackups")) // add noBackup flag if provided as global parameter
                {
                    command = command.Insert(command.Length, " -nobackups");
                }

                Program.Main(Util.stringToArgsArray(command)); // use the current process is more efficient than start a new one
            }
            catch (Exception e)
            {
                Program.printAppError(Program.appErrors.PATCH_COMMAND_PROCESS_ERROR, "Error processing command in Patch file.\n" + e.ToString());
                return false;
            }

            return true;
        }

        /// <summary>
        /// Executes custom Javascript code over the xml file specified. Uses .NET JINT library.
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        private bool executeCode(string operation, string jsCode)
        {
            string FileParam = "";

            //---------------------------------------------------Parse Operation command (start)
            try
            {
                if (String.IsNullOrEmpty(forceFiles))
                {
                    FileParam = getPatchParameter(operation, "File");
                }
                else
                {
                    FileParam = forceFiles;
                }

            }
            catch (Exception e)
            {
                Program.printAppError(Program.appErrors.PATCH_CODE_PROCESS_ERROR, "Error parsing codeOperation in Patch file.\n" + e.ToString());
                return false;
            }

            //---------------------------------------------------Parse Operation command (end)
            List<String> filesToProcess = new List<String>();
            if (String.IsNullOrEmpty(FileParam))
            {
                filesToProcess = Util.getAllXmlFiles(); //no file specified, use all xml files found in same folder
            }
            else if (Util.containsWildcard(FileParam))
            {
                filesToProcess = Util.getXmlFilesWildcard(FileParam);
            }
            else
            {
                filesToProcess.Add(FileParam);
            }

            //---------------------------------------------------JS Code Proccess (start)
            foreach (String currXMLFile in filesToProcess)
            {
                if (!this.globalNoBackups && !Util.ContainsIgnoreCase(operation, "NoBackups")) // only skip backup if specified via global parameter or in patch file
                {
                    Util.backupFile(currXMLFile);
                }

                string xmlFileContent = File.ReadAllText(currXMLFile);

                // Initialize Jint Engine
                if (jsEngine == null)
                {
                    jsEngine = new IronJS.Hosting.CSharp.Context();

                    // Load XML libraries
                    jsEngine.Execute(xmlTools.Properties.Resources.tinyxmlsax);
                    jsEngine.Execute(xmlTools.Properties.Resources.tinyxmlw3cdom);
                }

                // Construct code to execute
                StringBuilder sourceCode = new StringBuilder();

                // give user the xml we needs to edit...
                sourceCode.Append("var $xmlData='").Append(xmlFileContent.Replace(System.Environment.NewLine, " \\" + System.Environment.NewLine)).Append("';").AppendLine(); // replace is for multine string in javascript (http://stackoverflow.com/questions/805107/creating-multiline-strings-in-javascript)
                // append the user js code...
                sourceCode.Append(jsCode).AppendLine();
                // return to .NET the new xml data
                sourceCode.Append("$xmlData;");

                try
                {
                    xmlFileContent = jsEngine.Execute(sourceCode.ToString()).ToString();
                }
                catch (Exception e)
                {
                    Program.printAppError(Program.appErrors.PATCH_CODE_PROCESS_ERROR, "Error parsing code in customCodeOperation in Patch file.\n" + e.ToString());
                    return false;
                }

                // Let's see if the returned result is valid xml...
                try
                {
                    XmlDocument xmlDoc = new XmlDocument();

                    xmlDoc.LoadXml(xmlFileContent);

                    xmlDoc.Save(currXMLFile); //saving the new xml with this method will auto ident it.

                }
                catch (Exception e)
                {
                    Program.printAppError(Program.appErrors.PATCH_CODE_PARSE_XML_OUTPUT_ERROR, "Error parsing result xml to customCodeOperation in Patch file.\n" + e.ToString());
                    return false;
                }
            }
            //---------------------------------------------------JS Code Proccess (end)

            return true;
        }

        private string getPatchParameter(string line, string parameterName)
        {
            string result = "";
            int startIdx = 0, endIdx = 0;

            string temp = parameterName + " \"";

            startIdx = line.IndexOf(temp);
            if (startIdx != -1) //we have Parameter specified
            {
                startIdx += temp.Length;
                endIdx = line.IndexOf("\"", startIdx);
                result = line.Substring(startIdx, endIdx - startIdx); //Get the parameter value
            }

            return result;
        }
    }
}