/* Copyright 2017 Siddhartha Das (bablu.boy@gmail.com)
*
* This file is part of Bookworm and is used for parsing EPUB file formats
*
* 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 Gee;
public class BookwormApp.ePubReader {
    public static string NCXRefInSpineData = "";
    public static BookwormApp.Book parseEPubBook (owned BookwormApp.Book aBook) {
        info ("[START] [FUNCTION:parseEPubBook] book.location=" + aBook.getBookLocation ());
        //Only parse the eBook if it has not been parsed already
        if (!aBook.getIsBookParsed ()) {
            debug ("Starting to parse EPub Book located at:" + aBook.getBookLocation ());
            //Extract the content of the EPub
            string extractionLocation = extractEBook (aBook.getBookLocation ());
            if ("false" == extractionLocation) { //handle error condition
                aBook.setIsBookParsed (false);
                aBook.setParsingIssue (BookwormApp.Constants.TEXT_FOR_EXTRACTION_ISSUE);
                return aBook;
            } else {
                aBook.setBookExtractionLocation (extractionLocation);
            }
            //Check if the EPUB mime type is correct
            bool isEPubFormat = isEPubFormat (extractionLocation);
            if (!isEPubFormat) { //handle error condition
                aBook.setIsBookParsed (false);
                aBook.setParsingIssue (BookwormApp.Constants.TEXT_FOR_MIMETYPE_ISSUE);
                return aBook;
            }
            //Determine the location of OPF File
            string locationOfOPFFile = getOPFFileLocation (extractionLocation);
            if ("false" == locationOfOPFFile) { //handle error condition
                aBook.setIsBookParsed (false);
                aBook.setParsingIssue (BookwormApp.Constants.TEXT_FOR_CONTENT_ISSUE);
                return aBook;
            }
            string baseLocationOfContents = locationOfOPFFile.replace (File.new_for_path (locationOfOPFFile).get_basename (), "");
            aBook.setBaseLocationOfContents (baseLocationOfContents);
            //Populate content list for EPub Book
            aBook = determineToC (aBook, locationOfOPFFile);
            if (aBook.getBookContentList ().size < 1) {
                aBook.setIsBookParsed (false);
                aBook.setParsingIssue (BookwormApp.Constants.TEXT_FOR_CONTENT_ISSUE);
                return aBook;
            }
            //Try to determine Book Cover Image if it is not already available
            if (!aBook.getIsBookCoverImagePresent ()) {
                aBook = setCoverImage (aBook, locationOfOPFFile);
            }
            //Determine Book Meta Data like Title, Author, etc
            aBook = setBookMetaData (aBook, locationOfOPFFile);
            aBook.setIsBookParsed (true);
        }
        info ("[END] [FUNCTION:parseEPubBook]");
        return aBook;
    }

    public static string extractEBook (string eBookLocation) {
        info ("[START] [FUNCTION:extractEBook] eBookLocation=" + eBookLocation);
        string extractionLocation = "false";
        debug ("Initiated process for content extraction of ePub Book located at:" + eBookLocation);
        //create a location for extraction of eBook based on local storage prefference
        if (BookwormApp.Bookworm.settings == null) {
            BookwormApp.Bookworm.settings = BookwormApp.Settings.get_instance ();
        }
        if (BookwormApp.Bookworm.settings.is_local_storage_enabled) {
            extractionLocation = BookwormApp.Bookworm.bookworm_config_path + "/books/" + File.new_for_path (eBookLocation).get_basename ();
        } else {
            extractionLocation = BookwormApp.Constants.EBOOK_EXTRACTION_LOCATION + File.new_for_path (eBookLocation).get_basename ();
        }
        //check and create directory for extracting contents of ebook
        BookwormApp.Utils.fileOperations ("CREATEDIR", extractionLocation, "", "");
        //unzip eBook contents into extraction location
        string status = BookwormApp.Utils.execute_sync_command ("unzip -o \"" + eBookLocation + "\" -d \"" + extractionLocation + "\"");
        if ("false" == status) {
            extractionLocation = "false";
        }
        info ("[END] [FUNCTION:extractEBook] extractionLocation=" + extractionLocation);
        return extractionLocation;
    }

    public static bool isEPubFormat (string extractionLocation) {
        info ("[START] [FUNCTION:isEPubFormat] extractionLocation=" + extractionLocation);
        bool ePubFormat = false;
        debug ("Checking if mime type is valid ePub for contents at:" + extractionLocation);
        string ePubMimeContents = BookwormApp.Utils.fileOperations (
            "READ", extractionLocation, BookwormApp.Constants.EPUB_MIME_SPECIFICATION_FILENAME, "");
        if ("false" == ePubMimeContents) {
            //Mime Content File was not found at expected location
            warning ("Mime Content file could not be located at expected location:" +
                extractionLocation + "/" + BookwormApp.Constants.EPUB_MIME_SPECIFICATION_FILENAME);
            return false;
        }
        debug ("Mime Contents found in file :" + extractionLocation + "/" +
            BookwormApp.Constants.EPUB_MIME_SPECIFICATION_FILENAME + " is:" + ePubMimeContents);
        if (ePubMimeContents.strip () != BookwormApp.Constants.EPUB_MIME_SPECIFICATION_CONTENT) {
            debug ("Mime Contents in file :" + extractionLocation + "/" + 
                BookwormApp.Constants.EPUB_MIME_SPECIFICATION_FILENAME + " is not :" + 
                BookwormApp.Constants.EPUB_MIME_SPECIFICATION_CONTENT + ". No further parsing will be done.");
            return false;
        } else {
            //mime content is as expected
            ePubFormat = true;
        }
        info ("[END] [FUNCTION:isEPubFormat] ePubFormat=" + ePubFormat.to_string ());
        return ePubFormat;
    }

    public static string getOPFFileLocation (string extractionLocation) {
        info ("[START] [FUNCTION:getOPFFileLocation] extractionLocation=" + extractionLocation);
        string locationOfOPFFile = "false";
        //Form the path to the META-INF/container.xml file
        string pathToXMLFile = extractionLocation + "/" + BookwormApp.Constants.EPUB_META_INF_FILENAME;
        //Parse META-INF/container.xml file to locate the path to the OPF file
        ArrayList<XMLData> inputDataList = new ArrayList<XMLData> ();
        inputDataList.add (new XMLData () {
            containerTagName = "rootfiles",
            inputTagName = "rootfile",
            inputAttributeName = "full-path"
        });
        XmlParser thisParser = new XmlParser ();
        ArrayList<XMLData> extractedDataList = new ArrayList<XMLData> ();
        extractedDataList = thisParser.extractDataFromXML (pathToXMLFile, inputDataList);

        foreach (XMLData aExtractedData in extractedDataList) {
            foreach (string aAttributeValue in aExtractedData.extractedTagAttributes) {
                string OPFFilePath = aAttributeValue;
                locationOfOPFFile = extractionLocation + "/" + OPFFilePath;
            }
        }
        info ("[END] [FUNCTION:getOPFFileLocation] locationOfOPFFile=" + locationOfOPFFile);
        return locationOfOPFFile;
    }

    public static BookwormApp.Book determineToC (owned BookwormApp.Book aBook, string locationOfOPFFile) {
        info ("[START] [FUNCTION:determineToC] book.location=" + aBook.getBookLocation () + ", locationOfOPFFile=" + locationOfOPFFile);
        //Parse OPF xml file to read the MANIFEST data (id, href, media-type)
        ArrayList<XMLData> inputDataList = new ArrayList<XMLData> ();
        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 = "manifest",
            inputTagName = "item",
            inputAttributeName = "media-type"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "spine",
            inputTagName = "itemref",
            inputAttributeName = "idref"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "spine",
            inputAttributeName = "toc"
        });
        XmlParser thisParser = new XmlParser ();
        ArrayList<XMLData> opfItemsList = new ArrayList<XMLData> ();
        opfItemsList = thisParser.extractDataFromXML (locationOfOPFFile, inputDataList);

        if (opfItemsList.size > 3 && opfItemsList.get (4).extractedTagAttributes.size>0) {
            debug ("Successfully extracted SPINE data..");
            //Get the reference of the NCX file in the SPINE data
            string spineNCXReference = opfItemsList.get (4).extractedTagAttributes.get (0);
            debug ("Successfully determined NCX File Reference as:" + spineNCXReference);
            //Get the position of NCX Reference in MANIFEST data
            if (opfItemsList.size>0 && opfItemsList.get (0).extractedTagAttributes.contains (spineNCXReference)) {
                debug ("Successfully extracted MANIFEST data");
                int spineNCXPosition = opfItemsList.get (0).extractedTagAttributes.index_of (spineNCXReference);
                debug ("Successfully matched NCX File path information on MANIFEST data at position:" + spineNCXPosition.to_string ());
                //Get the location of the NCX file from the MANIFEST href attribute
                string NCXFileRelativePath = opfItemsList.get (1).extractedTagAttributes.get (spineNCXPosition);
                debug ("Extracted relative NCX file path from MANIFEST data as:" + NCXFileRelativePath);
                string ncxFilePath = (
                    BookwormApp.Utils.getFullPathFromFilename (
                        aBook.getBaseLocationOfContents (), NCXFileRelativePath.strip ())).strip ();
                if ("true" == BookwormApp.Utils.fileOperations ("EXISTS", "", ncxFilePath, "")) {
                    debug ("Successfully determined NCX File Path as:" + ncxFilePath);
                    //Parse NCX xml file to read the ToC data (id, href, media-type)
                    ArrayList<XMLData> inputDataListForToC = new ArrayList<XMLData> ();
                    inputDataListForToC.add (new XMLData () {
                        containerTagName = "navLabel",
                        inputTagName = "text",
                        inputAttributeName = ""
                    });
                    inputDataListForToC.add (new XMLData () {
                        containerTagName = "",
                        inputTagName = "content",
                        inputAttributeName = "src"
                    });
                    XmlParser ncxParser = new XmlParser ();
                    ArrayList<XMLData> ncxDataExtractedList = new ArrayList<XMLData> ();
                    ncxDataExtractedList = ncxParser.extractDataFromXML (ncxFilePath, inputDataListForToC);
                    if (ncxDataExtractedList.get (0).extractedTagValues.size > 0 &&
                        ncxDataExtractedList.get (1).extractedTagAttributes.size > 0 &&
                        ncxDataExtractedList.get (0).extractedTagValues.size == ncxDataExtractedList.get (1).extractedTagAttributes.size)
                    {
                        for (int count=0; count<ncxDataExtractedList.get (0).extractedTagValues.size; count++) {
                            HashMap<string,string> TOCMapItem = new HashMap<string,string> ();
                            string tocLocation = ncxDataExtractedList.get (1).extractedTagAttributes.get (count);
                            //Handle the links with anchor elements
                            string anchorValue = "";
                            if (tocLocation.index_of ("#") != -1 ) {
                                anchorValue = tocLocation.slice (tocLocation.index_of ("#"), tocLocation.length);
                                tocLocation = tocLocation.slice (0, tocLocation.index_of ("#"));
                            }
                            tocLocation = BookwormApp.Utils.getFullPathFromFilename (aBook.getBaseLocationOfContents (), tocLocation);
                            TOCMapItem.set (tocLocation + anchorValue, ncxDataExtractedList.get (0).extractedTagValues.get (count));
                            aBook.setTOC (TOCMapItem);
                            debug ("Extracted ToC Chapter Name:" + ncxDataExtractedList.get (0)
                                .extractedTagValues.get (count) + " at location:" + tocLocation + anchorValue);
                        }
                    }
                }
            }
        }

        // Create the content list - clear the content list of any previous items
        aBook.clearBookContentList ();
        //loop over all idref attributes in spine data
        foreach (string spineIDREF in opfItemsList[3].extractedTagAttributes) {
            //check if the SPINE IDREF exists in the MANIFEST Attributes
            if (opfItemsList[0].extractedTagAttributes.contains (spineIDREF)) {
                int positionOfIDREF = opfItemsList[0].extractedTagAttributes.index_of (spineIDREF);
                //extract the HREF from MANIFEST corresponding to the SPINE IDREF
                string locationOfContentData = opfItemsList[1].extractedTagAttributes.get (positionOfIDREF);
                aBook.setBookContentList (aBook.getBaseLocationOfContents () + locationOfContentData);
                debug ("Book content data :" + aBook.getBaseLocationOfContents () + locationOfContentData);
            }
        }
        info ("[END] [FUNCTION:determineToC]");
        return aBook;
    }

    public static BookwormApp.Book setCoverImage (owned BookwormApp.Book aBook, string locationOfOPFFile) {
        info ("[START] [FUNCTION:setCoverImage] book.location=" + aBook.getBookLocation () + ", locationOfOPFFile=" + locationOfOPFFile);
        string bookCoverLocation = "";
        //Parse OPF xml file to read the MANIFEST data
        ArrayList<XMLData> inputDataList = new ArrayList<XMLData> ();
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            inputAttributeName = "id"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            enforceAttributeData = true,
            inputAttributeName = "media-type"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            inputAttributeName = "href"
        });
        inputDataList.add (new XMLData () {
            containerTagName = "manifest",
            inputTagName = "item",
            enforceAttributeData = true,
            inputAttributeName = "properties"
        });
        XmlParser thisParser = new XmlParser ();
        ArrayList<XMLData> opfItemsList = new ArrayList<XMLData> ();
        opfItemsList = thisParser.extractDataFromXML (locationOfOPFFile, inputDataList);
        int count = 0;
        //epub3.1 : Check for a MANIFEST item with "properties" attribute contaning the word "cover-image"
        foreach (string properties in opfItemsList[3].extractedTagAttributes) {
            if (properties.contains ("cover-image")) {
                //Get media type for the cover items
                string coverMediaType = opfItemsList[1].extractedTagAttributes.get (count);
                //get cover location if media type matches "image"
                if (coverMediaType.index_of ("image") != -1) {
                    bookCoverLocation = opfItemsList[2].extractedTagAttributes.get (count);
                    bookCoverLocation = aBook.getBaseLocationOfContents () + bookCoverLocation;
                    break;
                }
            }
            count++;
        }
        //If cover could not be located in properties="cover-image" : 
        //Check for a MANIFEST item with "id" attribute contaning the word "cover"
        if (bookCoverLocation.length < 1 &&
            "true" == BookwormApp.Utils.fileOperations ("EXISTS", "", bookCoverLocation, "") )
        {
            count = 0;
            foreach (string id in opfItemsList[0].extractedTagAttributes) {
                if (id.contains ("cover") ) {
                    //Get media type for the cover items
                    string coverMediaType = opfItemsList[1].extractedTagAttributes.get (count);
                    //get cover location if media type matches "image"
                    if (coverMediaType.index_of ("image") != -1) {
                        bookCoverLocation = opfItemsList[2].extractedTagAttributes.get (count);
                        bookCoverLocation = aBook.getBaseLocationOfContents () + bookCoverLocation;
                        break;
                    }
                }
                count++;
            }
        }
        //check if cover was still not found and assign flag for default cover to be used
        if (bookCoverLocation.length < 1 &&
            "true" == BookwormApp.Utils.fileOperations ("EXISTS", "", bookCoverLocation, ""))
        {
            aBook.setIsBookCoverImagePresent (false);
            debug ("Cover image not found for book located at:" + aBook.getBookExtractionLocation ());
        } else {
            //copy cover image to bookworm cover image cache
            aBook = BookwormApp.Utils.setBookCoverImage (aBook, bookCoverLocation);
        }
        info ("[END] [FUNCTION:setCoverImage] book.location=" + aBook.getBookLocation () + ", bookCoverLocation=" + bookCoverLocation);
        return aBook;
    }

    public static BookwormApp.Book setBookMetaData (owned BookwormApp.Book aBook, string locationOfOPFFile) {
        info ("[START] [FUNCTION:setBookMetaData] book.location=" +
            aBook.getBookLocation () + ", locationOfOPFFile=" + locationOfOPFFile);
        //Parse OPF xml file to read the book meta data
        ArrayList<XMLData> inputDataList = new ArrayList<XMLData> ();
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "title",
            inputAttributeName = ""
        });
        inputDataList.add (new XMLData () {
            containerTagName = "",
            inputTagName = "creator",
            inputAttributeName = ""
        });
        XmlParser thisParser = new XmlParser ();
        ArrayList<XMLData> opfItemsList = new ArrayList<XMLData> ();
        opfItemsList = thisParser.extractDataFromXML (locationOfOPFFile, inputDataList);
        if (opfItemsList[0].extractedTagValues.size > 0) {
            string bookTitle = opfItemsList[0].extractedTagValues.get (0);
            if (bookTitle.length > 0) {
                aBook.setBookTitle (BookwormApp.Utils.decodeHTMLChars (bookTitle));
                debug ("Determined eBook Title as:" + bookTitle);
            } else {
                //If the book title has not been determined, use the file name as book title
                if (aBook.getBookTitle () != null && (
                                    aBook.getBookTitle () == BookwormApp.Constants.TEXT_FOR_UNKNOWN_TITLE ||
                                    aBook.getBookTitle ().length < 1
                                )
                ) {
                    bookTitle = File.new_for_path (aBook.getBookExtractionLocation ()).get_basename ();
                    if (bookTitle.last_index_of (".") != -1) {
                        bookTitle = bookTitle.slice (0, bookTitle.last_index_of ("."));
                    }
                    aBook.setBookTitle (bookTitle);
                    debug ("File name set as Title:" + bookTitle);
                }
            }
        }
        //determine the author of the book
        if (opfItemsList[1].extractedTagValues.size > 0) {
            string bookAuthor = opfItemsList[1].extractedTagValues.get (0);
            if (bookAuthor.length > 0) {
                aBook.setBookAuthor (BookwormApp.Utils.decodeHTMLChars (bookAuthor));
                debug ("Determined eBook Author as:" + bookAuthor);
            } else {
                //If the book author has not been determined, use a default text for author
                aBook.setBookAuthor (BookwormApp.Constants.TEXT_FOR_UNKNOWN_TITLE);
                debug ("Could not determine eBook Author, default Author set");
            }
        }
        info ("[END] [FUNCTION:setBookMetaData]");
        return aBook;
    }
}
