using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Xml;

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 = "";

        public XmlPatch(String file)
        {
            fileName = file;
        }

        public XmlPatch(String file, String forceInFiles)
        {
            fileName = file;
            forceFiles = forceInFiles; //We support apply the operation in diverse forced files (NameOfFile parameter will be ignored)
        }

        /// <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 + "\n"; //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;
                    }
                }
            }

            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 File = "", ParentElement = "", Element = "";

            //---------------------------------------------------Parse Operation command (start)
            try
            {
                if (forceFiles == null)
                {
                    File = getPatchParameter(operation, "File");
                }
                else
                {
                    File = forceFiles;
                }

                ParentElement = getPatchParameter(operation, "ParentElement"); //Get the ParentElement

                Element = 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 (Element == "")
            {
                return false;
            }

            //---------------------------------------------------Parse Operation command (end)
            List<String> filesToProcess = new List<String>();
            if (File == "")
            {
                filesToProcess = Util.getAllXmlFiles(); //no file specified, use all xml files found in same folder
            }
            else if (Util.containsWildcard(File))
            {
                filesToProcess = Util.getXmlFilesWildcard(File);
            }
            else
            {
                filesToProcess.Add(File);
            }

            //---------------------------------------------------XML Injection (start)
            foreach (String currFile in filesToProcess)
            {

                Util.backupFile(currFile);

                XmlDocument xdoc = new XmlDocument();
                xdoc.Load(currFile);

                List<XmlNode> myElements = new List<XmlNode>();
                Util.getAllSpecificElements(xdoc.DocumentElement, ref myElements, Element, ParentElement); //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 File = "", ParentElement = "", Element = "";

            //---------------------------------------------------Parse Operation command (start)
            try
            {
                if (forceFiles == null)
                {
                    File = getPatchParameter(operation, "File");
                }
                else
                {
                    File = forceFiles;
                }

                ParentElement = getPatchParameter(operation, "ParentElement"); //Get the ParentElement

                Element = 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 (Element == "")
            {
                return false;
            }

            //---------------------------------------------------Parse Operation command (end)

            List<String> filesToProcess = new List<String>();
            if (File == "")
            {
                filesToProcess = Util.getAllXmlFiles(); //no file specified, use all xml files found in same folder
            }
            else if (Util.containsWildcard(File))
            {
                filesToProcess = Util.getXmlFilesWildcard(File);
            }
            else
            {
                filesToProcess.Add(File);
            }

            //---------------------------------------------------XML Remove (start)

            foreach (String currFile in filesToProcess)
            {

                Util.backupFile(currFile);

                XmlDocument xdoc = new XmlDocument();
                xdoc.Load(currFile);

                List<XmlNode> myElements = new List<XmlNode>();
                Util.getAllSpecificElements(xdoc.DocumentElement, ref myElements, Element, ParentElement); //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>
        /// 
        /// </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 (command.Trim() == "")
            {
                Program.printAppError(Program.appErrors.PATCH_COMMAND_NOT_FOUND, "Error parsing commandOperation in Patch file: Command is empty.");
                return false;
            }

            try
            {
                if (forceFiles != null)
                {
                    string paramType = "";

                    // Filename already exists?
                    if (command.IndexOf("filename:") != -1)
                    {
                        paramType = "filename:";
                    }
                    else if (command.IndexOf("filename=") != -1)
                    {
                        paramType = "filename=";
                    }
                    // Add the filename if it doesn't exists
                    else
                    {
                        command = command.Insert(command.Length-1," -filename:" + this.forceFiles); // -2 to be inside quotes
                    }

                    if (paramType != "")
                    {
                        int startIdx = command.IndexOf(paramType) + paramType.Length;
                        int endIdx = command.IndexOf(" ", startIdx); // it may end with space
                        if (endIdx == -1)
                        {
                            endIdx = command.IndexOf("\"", startIdx); // or with quotes
                        }
                        string currFilename = command.Substring(startIdx, endIdx - startIdx);
                        command = command.Replace(currFilename, this.forceFiles);
                    }

                }

                command = command.Replace("\"", ""); // remove quotes

                ProcessStartInfo startInfo = new ProcessStartInfo();
                if (!Util.IsRunningOnMono())
                {
                    startInfo.FileName = Util.getExeFileName();
                }
                else{
                    startInfo.FileName = "mono";
                }
                if (!Util.IsRunningOnMono())
                {
                    startInfo.Arguments = command;
                }
                else{
                    startInfo.Arguments = Util.getExeFileName() + " " + command;
                }
                startInfo.UseShellExecute = false; // necessary to redirect output
                startInfo.RedirectStandardOutput = true;
                startInfo.RedirectStandardError = true;
                startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; // hide new process window

                Process p = System.Diagnostics.Process.Start(startInfo);
                p.OutputDataReceived += commandStdOutputReceived;
                p.ErrorDataReceived += commandStdErrorReceived;
                p.BeginOutputReadLine();
                p.BeginErrorReadLine();
                p.WaitForExit();
            }
            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>
        /// Reads asynchronously output from the new process where the command will be executed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void commandStdOutputReceived(object sender, DataReceivedEventArgs e)
        {

            string myData = e.Data;

            if (myData != null)
            {
                if (myData.EndsWith("\n"))
                {
                    Console.Write(myData);
                }
                else
                {
                    Console.WriteLine(myData);
                }
            }

        }

        private void commandStdErrorReceived(object sender, DataReceivedEventArgs e)
        {

            string myData = e.Data;

            if (myData != null)
            {
                if (myData.EndsWith("\n"))
                {
                    Console.Error.Write(myData);
                }
                else
                {
                    Console.Error.WriteLine(myData);
                }
            }

        }

        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;
        }
    }
}