/* Copyright 2018 Siddhartha Das (bablu.boy@gmail.com)
*
* This file is part of Bookworm and handles all xml related parsing
*
* Bookworm is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Bookworm is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with Bookworm. If not, see http://www.gnu.org/licenses/.
*/

using Xml;
using Gee;

public class BookwormApp.XMLData {
    public bool shouldExtractionStart = false;
    public bool useLocalTagName = true;
    public bool isContainerTagMatched = false;
    public string currentTagName;
    public StringBuilder charBuffer = new StringBuilder ("");

    public string containerTagName;
    public string inputTagName;
    public string inputAttributeName;
    public bool isXMLExtraction = false;
    public bool enforceAttributeData = false;
    public ArrayList<string> extractedTagValues = new ArrayList<string> ();
    public ArrayList<string> extractedTagAttributes = new ArrayList<string> ();
}

public class BookwormApp.XmlParser {
    private static XMLData thisXMLData;

    public XmlParser () {
        thisXMLData = null;
    }

    /*public static int main (string[] args) {
        Environment.set_variable ("G_MESSAGES_DEBUG", "all", true);
        Log.set_handler ("", GLib.LogLevelFlags.LEVEL_DEBUG, GLib.Log.default_handler);

        string pathToXMLFile = "/home/sid/Documents/Projects/misc/xmlFiles/toc.ncx";
        //string pathToXMLFile = "/home/sid/Documents/Projects/misc/xmlFiles/test.opf";
        //string pathToXMLFile = "/home/sid/Documents/Projects/misc/xmlFiles/container.xml";
        //string pathToXMLFile = "/home/sid/.config/bookworm/books/The Da Vinci Code.epub/OEBPS/Content.opf";

        ArrayList<XMLData> inputDataList = new ArrayList<XMLData> ();
        inputDataList.add (new XMLData () {
            containerTagName = "rootfiles",
            inputTagName = "rootfile",
            inputAttributeName="full-path"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            inputAttributeName="id"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            inputAttributeName="href"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "spine",
            inputTagName = "itemref",
            inputAttributeName="idref"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "spine",
            inputAttributeName="toc"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "subject",
            inputAttributeName=""
        });
        inputDataList.add (new XMLData () {
            containerTagName = "navLabel",
            inputTagName = "text",
            inputAttributeName=""
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "content",
            inputAttributeName="src"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "content",
            inputAttributeName="src",
            isXMLExtraction = true //obtain all xml content as extracted data
        });

        XmlParser thisParser = new XmlParser ();
        ArrayList<XMLData> extractedDataList = new ArrayList<XMLData> ();
        extractedDataList = thisParser.extractDataFromXML (pathToXMLFile, inputDataList);

        //Display the extracted data
        foreach (XMLData aExtractedData in extractedDataList) {
            debug ("********************* Showing Extracted Data for " +
                aExtractedData.containerTagName + "/" +
                aExtractedData.inputTagName + "/" +
                aExtractedData.inputAttributeName + " **********************");
            debug ("Items in List: " + aExtractedData.extractedTagValues.size.to_string ());
            foreach (string aTagValue in aExtractedData.extractedTagValues) {
                debug ("Tag Value: " + aTagValue);
            }
            debug ("Items in List: " + aExtractedData.extractedTagAttributes.size.to_string ());
            foreach (string aAttributeValue in aExtractedData.extractedTagAttributes) {
                debug ("Attribute Value: " + aAttributeValue);
            }
        }
        return 0;
    }*/

    public ArrayList<XMLData> extractDataFromXML (
        string path, owned ArrayList<XMLData> inputDataList,
        bool isXMLExtraction = false //optional: set to true only for extracting xml sub-content
    ) {
        info ("[START] [FUNCTION:extractDataFromXML] extracting xml from file=" + path);
        int count = 0;
        foreach (XMLData aXMLData in inputDataList) {
            debug ("Reading XML to extract data for input set #" + count.to_string () + ":" +
                aXMLData.containerTagName + "/" + aXMLData.inputTagName + "/" + aXMLData.inputAttributeName);
            thisXMLData = aXMLData;
            parseXML (path);
            inputDataList[count] = thisXMLData;
            count++;
        }
        info ("[END] [FUNCTION:extractDataFromXML] extracting xml from file=" + path);
        return inputDataList;
    }

    public void parseXML (string path) {
        Parser.init ();
        var handler = SAXHandler ();
        void* user_data = null;
        handler.startElement = start_element;
        handler.characters = get_text;
        handler.endElement = end_element;
        handler.user_parse_file (user_data, path);
        Parser.cleanup ();
    }

    public void start_element (string name, string[] attributeList) {
        //debug (">>>>>Start Tag: " + name);
        thisXMLData.currentTagName = process_tagname (name);
        //If Start element matches container tag - set container flag to true
        if (thisXMLData.currentTagName == thisXMLData.containerTagName) {
            thisXMLData.isContainerTagMatched = true;
        }

        //Check if the tag name matches the input tag name
        if (thisXMLData.currentTagName == thisXMLData.inputTagName) {
            //Check if a container tag is required and if it has been matched - set extraction flag to true
            if (thisXMLData.containerTagName.length != 0 && thisXMLData.isContainerTagMatched) {
                thisXMLData.shouldExtractionStart = true;
                thisXMLData.charBuffer.assign ("");
            } else {
                //Check if no container tag is required and since the tag name has been matched - set extraction flag to true
                if (thisXMLData.containerTagName.strip ().length == 0) {
                    thisXMLData.shouldExtractionStart = true;
                    thisXMLData.charBuffer.assign ("");
                }
            }
        }
        //If Extraction criteria is met and attribute extraction is required, extract required attribute
        if (thisXMLData.shouldExtractionStart && thisXMLData.inputAttributeName.length > 0) {
            //check whether attributes are expected
            int count = 0;
            bool wasAttributeExtracted = false;
            foreach (string attribute in attributeList) {
                if (attributeList.length >= count + 1) {
                    if (thisXMLData.inputAttributeName == attributeList[count]) {
                        //extract the odd attribute data as the even attribute is the name of the attribute
                        thisXMLData.extractedTagAttributes.add (attributeList[count + 1]);
                        wasAttributeExtracted = true;
                        break;
                    }
                    count = count + 2;
                }
            }
            //Check if attribute was not extracted and the attribute value is forced, add an empty value
            if (!wasAttributeExtracted && thisXMLData.enforceAttributeData) {
                thisXMLData.extractedTagAttributes.add ("");
            }
        }
        //If Extraction criteria is met and sub-xml exraction is required, add tag name to extracted data
        if (thisXMLData.shouldExtractionStart && thisXMLData.isXMLExtraction) {
            //to preserve the tag names, add the start tag to the collected data
            thisXMLData.charBuffer.append (" <").append (name).append (" ")
                .append (string.joinv (" ", attributeList)).append (">");
        }
    }

    public void end_element (string name) {
        //debug ("<<<<<End Tag: " + name);
        string processed_name = process_tagname (name);
        //If sub-xml exraction is required, add the end tag to extracted data
        if (thisXMLData.shouldExtractionStart && thisXMLData.isXMLExtraction) {
            //to preserve the tag names, add the start tag to the collected data
            thisXMLData.charBuffer.append (" </").append (name).append (">");
        }
        //If End element matches container tag - set container flag to false and extraction flag to false
        if (processed_name == thisXMLData.containerTagName) {
            thisXMLData.isContainerTagMatched = false;
            thisXMLData.shouldExtractionStart = false;
        }
        //Check if tag name has been matched - set extraction flag to false
        if (processed_name == thisXMLData.inputTagName) {
            //Add the collected tag value data into the extracted list
            if (thisXMLData.shouldExtractionStart) {
                if (!thisXMLData.isXMLExtraction) {
                    //this check ensures unrelated tags within a required tag are not picked up
                    if (thisXMLData.currentTagName == thisXMLData.inputTagName ) {
                        thisXMLData.extractedTagValues.add (thisXMLData.charBuffer.str);
                    }
                } else {
                    //this extracts contents of other tags within the required tag
                    thisXMLData.extractedTagValues.add (thisXMLData.charBuffer.str);
                }
            }
            thisXMLData.shouldExtractionStart = false;
            thisXMLData.currentTagName = "";
        }
    }

    public void get_text (string chars, int len) {
        //debug ("...Tag Data [len=" + len.to_string () + "] :" + chars.slice (0, len));
        //Extract tag value if extraction criteria is met
        if ( thisXMLData.shouldExtractionStart) {
            if (!thisXMLData.isXMLExtraction) {
                //this check ensures unrelated tags within a required tag are not picked up
                if (thisXMLData.currentTagName == thisXMLData.inputTagName ) {
                    thisXMLData.charBuffer.append (chars.slice (0, len));
                }
            } else {
                //this extracts contents of other tags within the required tag
                thisXMLData.charBuffer.append (chars.slice (0, len));
            }
        }
    }

    public string process_tagname (string tagname) {
        string local_tagname = tagname.strip ();
        if (thisXMLData.useLocalTagName) {
            if (local_tagname.index_of (":") != -1) {
                local_tagname = local_tagname.slice (
                    local_tagname.index_of (":") + 1,
                    local_tagname.length);
            }
        }
        return local_tagname;
    }
}

//valac --pkg libxml-2.0 --pkg gee-0.8 saxParser.vala
