/*
 * Copyright (C) 2010 Pavel Stastny
 * 
 * This program 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.
 * 
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 */
package cz.incad.kramerius.pdf.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.logging.Level;

import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import org.antlr.stringtemplate.StringTemplate;
import org.apache.commons.lang3.StringEscapeUtils;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.PdfWriter;

import cz.incad.kramerius.FedoraAccess;
import cz.incad.kramerius.ObjectPidsPath;
import cz.incad.kramerius.SolrAccess;
import cz.incad.kramerius.document.model.AbstractPage;
import cz.incad.kramerius.document.model.PreparedDocument;
import cz.incad.kramerius.pdf.FirstPagePDFService;
import cz.incad.kramerius.pdf.commands.ITextCommands;
import cz.incad.kramerius.pdf.commands.render.RenderPDF;
import cz.incad.kramerius.pdf.utils.pdf.DocumentUtils;
import cz.incad.kramerius.pdf.utils.pdf.FontMap;
import cz.incad.kramerius.service.ResourceBundleService;
import cz.incad.kramerius.service.TextsService;
import cz.incad.kramerius.utils.IOUtils;
import cz.incad.kramerius.utils.XMLUtils;
import cz.incad.kramerius.utils.mods.ArticleTitleBuilder;
import cz.incad.kramerius.utils.mods.AuthorBuilder;
import cz.incad.kramerius.utils.mods.BuilderFilter;
import cz.incad.kramerius.utils.mods.IdentifiersBuilder;
import cz.incad.kramerius.utils.mods.ModsBuildersDirector;
import cz.incad.kramerius.utils.mods.PeriodicalIssueNumberBuilder;
import cz.incad.kramerius.utils.mods.PeriodicalVolumeNumberBuilder;
import cz.incad.kramerius.utils.mods.PublisherBuilder;
import cz.incad.kramerius.utils.mods.TitleBuilder;

public class FirstPagePDFServiceImpl implements FirstPagePDFService {

    static java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(FirstPagePDFServiceImpl.class.getName());

    @Inject
    @Named("securedFedoraAccess")
    FedoraAccess fedoraAccess;

    @Inject
    TextsService textsService;

    @Inject
    ResourceBundleService resourceBundleService;

    @Inject
    SolrAccess solrAccess;

    @Inject
    Provider<Locale> localesProvider;

    Map<String, Map<String, List<String>>> processMods(String... pids) throws IOException, JAXBException, XPathExpressionException {
        Map<String, Map<String, List<String>>> maps = new HashMap<String, Map<String, List<String>>>();
        for (String pid : pids) {
            ObjectPidsPath selectedPath = selectOnePath(pid);
            Map<String, Map<String, List<String>>> nmaps = processModsFromPath(selectedPath, null);
            maps.putAll(nmaps);
        }
        return maps;
    }

    Map<String, Map<String, List<String>>> processModsFromPath(ObjectPidsPath selectedPath, BuilderFilter filter) throws IOException, XPathExpressionException {
        Map<String, Map<String, List<String>>> maps = new HashMap<String, Map<String, List<String>>>();
        if (selectedPath != null) {
            String[] pathFromLeaf = selectedPath.getPathFromLeafToRoot();
            for (int i = 0; i < pathFromLeaf.length; i++) {
                String pidFromPath = pathFromLeaf[i];
                if (!maps.containsKey(pidFromPath)) {
                    org.w3c.dom.Document modsCol = this.fedoraAccess.getBiblioMods(pidFromPath);
                    String modelName = this.fedoraAccess.getKrameriusModelName(pidFromPath);
                    ModsBuildersDirector director = new ModsBuildersDirector();
                    director.setBuilderFilter(filter);
                    Map<String, List<String>> map = new HashMap<String, List<String>>();
                    director.build(modsCol, map, modelName);
                    maps.put(pidFromPath, map);
                }
            }
        }
        return maps;
    }

    ObjectPidsPath selectOnePath(String pid) throws IOException {
        ObjectPidsPath[] paths = this.solrAccess.getPath(pid);
        ObjectPidsPath selectedPath = paths.length > 0 ? paths[0] : null;
        return selectedPath;
    }

    @Override
    public void selection(PreparedDocument rdoc, OutputStream os, String[] pids,  FontMap fontMap) {
        try {

            Document doc = DocumentUtils.createDocument(rdoc);
            PdfWriter writer = PdfWriter.getInstance(doc, os);
            doc.open();
            String itextCommands = templateSelection(rdoc, pids);
            renderFromTemplate(rdoc, doc, writer, fontMap, new StringReader(itextCommands));

            doc.close();
            os.flush();
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (InstantiationException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (IllegalAccessException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (ParserConfigurationException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (SAXException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (DocumentException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (XPathExpressionException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }
    }

    void renderFromTemplate(PreparedDocument rdoc,Document doc, PdfWriter pdfWriter, FontMap fontMap, StringReader reader) throws IOException, InstantiationException, IllegalAccessException, ParserConfigurationException, SAXException {
        ITextCommands cmnds = new ITextCommands();
        cmnds.load(XMLUtils.parseDocument(reader).getDocumentElement(), cmnds);

        
        RenderPDF render = new RenderPDF(fontMap, this.fedoraAccess);
        render.render(doc, pdfWriter, cmnds);
    }

    String templateSelection(PreparedDocument rdoc, String ... pids) throws XPathExpressionException, IOException, ParserConfigurationException, SAXException {
        ResourceBundle resourceBundle = resourceBundleService.getResourceBundle("base", localesProvider.get());

        StringTemplate template = new StringTemplate(IOUtils.readAsString(this.getClass().getResourceAsStream("templates/_first_page.st"), Charset.forName("UTF-8"), true));
        FirstPageViewObject fpvo = prepareViewObject(resourceBundle);

        // tistena polozka
        GeneratedItem itm = new GeneratedItem();
        
  
        Map<String, LinkedHashSet<String>> detailItemValues = new HashMap<String, LinkedHashSet<String>>();
        Map<String, ObjectPidsPath> pathsMapping = new HashMap<String, ObjectPidsPath>();
        LinkedHashSet<String> roots = new LinkedHashSet<String>();
        for (String pid : pids) {
            ObjectPidsPath sPath = selectOnePath(pid);
            pathsMapping.put(pid, sPath);
            roots.add(sPath.getRoot());
        }
        
        for (String pid : pids) {
            ObjectPidsPath path = pathsMapping.get(pid);
            Map<String, Map<String, List<String>>> mods = processModsFromPath(path, null);
            String rootPid = path.getRoot();
            if (mods.get(rootPid).containsKey(TitleBuilder.MODS_TITLE)) {
                List<String> list = mods.get(rootPid).get(TitleBuilder.MODS_TITLE);
                if (!list.isEmpty()) {
                    String key = TitleBuilder.MODS_TITLE;
                    itemVals(detailItemValues, list, key);
                }
            }

            
            String[] rProps = renderedProperties(roots.size() == 1);
            String[] fromRootToLeaf = path.getPathFromRootToLeaf();
            for (int i = 0; i < fromRootToLeaf.length; i++) {
                String pidPath = fromRootToLeaf[i];
                for (String prop : rProps) {

                    if (mods.get(pidPath).containsKey(prop)) {
                        List<String> list = mods.get(pidPath).get(prop);
                        itemVals(detailItemValues, list, prop);
                    }
                }
            }
        }
        

        // hlavni nazev
        List<DetailItem> details = new ArrayList<FirstPagePDFServiceImpl.DetailItem>();
        LinkedHashSet<String> maintitles = detailItemValues.get(TitleBuilder.MODS_TITLE);
        String key =  maintitles != null && maintitles.size() > 1 ? resourceBundle.getString("pdf.fp.titles") :  resourceBundle.getString("pdf.fp.title");
        if (maintitles != null && (!maintitles.isEmpty())) {
            details.add(new DetailItem(key, vals(maintitles).toString()));
        }

        for(String prop: renderedProperties(roots.size() == 1)) {
            LinkedHashSet<String> vals = detailItemValues.get(prop);
            key = vals != null && vals.size() > 1 ? resourceBundle.getString(i18nKey(prop)+"s") :  resourceBundle.getString(i18nKey(prop));
            if (vals != null && (!vals.isEmpty())) {
                details.add(new DetailItem(key, vals(vals).toString()));
            }
        }

        // stranky v pdfku
        pagesInSelectiontPdf(rdoc, resourceBundle, details);
        
        itm.setDetailItems((DetailItem[]) details.toArray(new DetailItem[details.size()]));
        fpvo.setGeneratedItems(new GeneratedItem[] {itm});

        template.setAttribute("viewinfo", fpvo);
        
        String templateText = template.toString();
        
        return templateText;
    }

    public String[] renderedProperties(boolean oneRoot) {
        String[] rProps;
        // v pripade, ze je jeden rodic, ma smysl mit vice informaci
        if (oneRoot) {
            rProps = new String[] {
                    AuthorBuilder.MODS_AUTHOR,
                    ArticleTitleBuilder.MODS_TITLE,
                    PublisherBuilder.MODS_PUBLISHER, 
                    PublisherBuilder.MODS_DATE, 
                    PeriodicalVolumeNumberBuilder.MODS_VOLUMENUMBER, 
                    PeriodicalIssueNumberBuilder.MODS_ISSUESNUMBER, 
                    PeriodicalIssueNumberBuilder.MODS_DATE,
                    IdentifiersBuilder.MODS_ISBN,
                    IdentifiersBuilder.MODS_ISSN,
                    IdentifiersBuilder.MODS_SICI,
                    IdentifiersBuilder.MODS_CODEN

            };
        } else {
            rProps = new String[] {
                    TitleBuilder.MODS_TITLE,
                    ArticleTitleBuilder.MODS_TITLE,
                    PeriodicalVolumeNumberBuilder.MODS_VOLUMENUMBER, 
                    PeriodicalIssueNumberBuilder.MODS_ISSUESNUMBER, 
                    IdentifiersBuilder.MODS_ISBN,
                    IdentifiersBuilder.MODS_ISSN,
                    IdentifiersBuilder.MODS_SICI,
                    IdentifiersBuilder.MODS_CODEN

            };
        }
        return rProps;
    }

    void itemVals(Map<String, LinkedHashSet<String>> detailItemValues, List<String> list, String key) {
        LinkedHashSet<String> vals = detailItemValues.get(key);
        if (vals == null) { 
            vals = new LinkedHashSet<String>();
            detailItemValues.put(key, vals);
        }
        vals.addAll(list);
    }

    String templateParent(PreparedDocument rdoc, ObjectPidsPath path) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException, JAXBException {
        ResourceBundle resourceBundle = resourceBundleService.getResourceBundle("base", localesProvider.get());

        StringTemplate template = new StringTemplate(IOUtils.readAsString(this.getClass().getResourceAsStream("templates/_first_page.st"), Charset.forName("UTF-8"), true));
        FirstPageViewObject fpvo = prepareViewObject(resourceBundle);

        // tistena polozka
        GeneratedItem itm = new GeneratedItem();
        
        // detaily
        Map<String, LinkedHashSet<String>> detailItemValues = new HashMap<String, LinkedHashSet<String>>();
        Map<String, Map<String, List<String>>> mods = processModsFromPath(path, null);

        // Hlavni nazev
        String rootPid = path.getRoot();
        if (mods.get(rootPid).containsKey(TitleBuilder.MODS_TITLE)) {
            List<String> list = mods.get(rootPid).get(TitleBuilder.MODS_TITLE);
            if (!list.isEmpty()) {
                String key = TitleBuilder.MODS_TITLE;
                itemVals(detailItemValues, list, key);
            }
        }

        // pouze jeden root
        String[] rProps = renderedProperties(true);
        String[] fromRootToLeaf = path.getPathFromRootToLeaf();
        for (int i = 0; i < fromRootToLeaf.length; i++) {
            String pidPath = fromRootToLeaf[i];
            for (String prop : rProps) {

                if (mods.get(pidPath).containsKey(prop)) {
                    List<String> list = mods.get(pidPath).get(prop);
                    itemVals(detailItemValues, list, prop);
                }
            }
        }

        // hlavni nazev
        List<DetailItem> details = new ArrayList<FirstPagePDFServiceImpl.DetailItem>();
        LinkedHashSet<String> maintitles = detailItemValues.get(TitleBuilder.MODS_TITLE);
        String key =  maintitles != null && maintitles.size() > 1 ? resourceBundle.getString("pdf.fp.titles") :  resourceBundle.getString("pdf.fp.title");
        if (maintitles != null && (!maintitles.isEmpty())) {
            details.add(new DetailItem(key, vals(maintitles).toString()));
        }

        String[] props = renderedProperties(true);
        for(String prop: props) {
            LinkedHashSet<String> vals = detailItemValues.get(prop);
            key = vals != null && vals.size() > 1 ? resourceBundle.getString(i18nKey(prop)+"s") :  resourceBundle.getString(i18nKey(prop));
            if (vals != null && (!vals.isEmpty())) {
                details.add(new DetailItem(key, vals(vals).toString()));
            }
        }

        
        pagesInParentPdf(rdoc, resourceBundle, details);

        itm.setDetailItems((DetailItem[]) details.toArray(new DetailItem[details.size()]));
        

        fpvo.setGeneratedItems( new GeneratedItem[] { itm });
        template.setAttribute("viewinfo", fpvo);
        String templateText = template.toString();
        return templateText;
    }

    void pagesInParentPdf(PreparedDocument rdoc, ResourceBundle resourceBundle, List<DetailItem> details) {
        // tistene stranky
        List<AbstractPage> pages = rdoc.getPages();
        if (pages.size() == 1) {
            details.add(new DetailItem(resourceBundle.getString("pdf.fp.page"),pages.get(0).getPageNumber()));
        } else if (pages.size() > 1) {
            details.add(new DetailItem(resourceBundle.getString("pdf.fp.pages"),""+pages.get(0).getPageNumber() +" - "+pages.get(pages.size() - 1).getPageNumber() ));
        }
    }
    
    
    void pagesInSelectiontPdf(PreparedDocument rdoc, ResourceBundle resourceBundle, List<DetailItem> details) {
        // tistene stranky
        List<AbstractPage> pages = rdoc.getPages();
        if (pages.size() == 1) {
            details.add(new DetailItem(resourceBundle.getString("pdf.fp.page"),pages.get(0).getPageNumber()));
        } else if (pages.size() > 1) {
            StringTemplate template = new StringTemplate("$data:{page|$page.pageNumber$};separator=\", \"$");
            template.setAttribute("data", pages);
            details.add(new DetailItem(resourceBundle.getString("pdf.fp.pages"),
                    template.toString()));
        }
    }


    StringTemplate vals(Collection<String> list) {
        StringTemplate dataTemplate = new StringTemplate("$data;separator=\", \"$");
        dataTemplate.setAttribute("data", list);
        return dataTemplate;
    }

    FirstPageViewObject prepareViewObject(ResourceBundle resourceBundle) throws IOException, ParserConfigurationException, SAXException, UnsupportedEncodingException {
        FirstPageViewObject fpvo = new FirstPageViewObject();

        String xml = this.textsService.getText("first_page_nolines_xml", this.localesProvider.get());
        org.w3c.dom.Document doc = XMLUtils.parseDocument(new ByteArrayInputStream(xml.getBytes("UTF-8")), false);
        Element head = XMLUtils.findElement(doc.getDocumentElement(), "head");
        Element desc = XMLUtils.findElement(doc.getDocumentElement(), "desc");

        fpvo.setConditionUsage(head.getTextContent());
        fpvo.setConditionUsageText(desc.getTextContent());

        fpvo.setDitigalLibrary(resourceBundle.getString("pdf.digitallibrary"));
        fpvo.setHyphLang(this.localesProvider.get().getLanguage());
        fpvo.setHyphCountry(this.localesProvider.get().getCountry());
        fpvo.setPdfContainsTitle(resourceBundle.getString("pdf.pdfcontainstitle"));
        return fpvo;
    }

    
    public String i18nValue(ResourceBundle bundle, String modsKey) {
        String key = i18nKey(modsKey);
        if (key != null) {
            if (bundle.containsKey(key)) {
                return bundle.getString(key);
            } else {
                LOGGER.log(Level.WARNING, "cannot find key '"+ key+"'");
                return modsKey;
            }
        } else
            return modsKey;
    }

    public String i18nKey(String modsKey) {
        String[] splitted = modsKey.split(":");
        if (splitted.length == 2 && splitted[0].equals("mods")) {
            return "pdf.fp." + splitted[1];
        } else return modsKey;
    }

    public String localizedModel(ResourceBundle resourceBundle, String pid) throws IOException {
        String modelName = this.fedoraAccess.getKrameriusModelName(pid);
        String localizedModelName = resourceBundle.getString("fedora.model." + modelName);
        return localizedModelName;
    }

    @Override
    public void parent(PreparedDocument rdoc, OutputStream os, ObjectPidsPath path,   FontMap fontMap) {
        try {

            Document doc = DocumentUtils.createDocument(rdoc);

            PdfWriter writer = PdfWriter.getInstance(doc, os);
            doc.open();

            String itextCommands = templateParent(rdoc, path);

            renderFromTemplate(rdoc, doc, writer, fontMap, new StringReader(itextCommands));

            doc.close();
            os.flush();
        } catch (DocumentException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (InstantiationException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (IllegalAccessException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (ParserConfigurationException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (SAXException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (XPathExpressionException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        } catch (JAXBException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }

    }

    // Reprezentuje objekt do sablony pro zobrazeni
    static class FirstPageViewObject {

        private String ditigalLibrary;
        private String conditionUsage;
        private String conditionUsageText;
        private String pdfContainsTitle;

        private String hyphCountry;
        private String hyphLang;

        private GeneratedItem[] generatedItems = new GeneratedItem[0];

        
        public String getDitigalLibrary() {
            return StringEscapeUtils.escapeXml(this.ditigalLibrary);
        }

        public void setDitigalLibrary(String ditigalLibrary) {
            this.ditigalLibrary = ditigalLibrary;
        }

        public String getConditionUsage() {
            return StringEscapeUtils.escapeXml(conditionUsage);
        }

        public void setConditionUsage(String conditionUsage) {
            this.conditionUsage = conditionUsage;
        }

        public String getConditionUsageText() {
            return StringEscapeUtils.escapeXml(conditionUsageText);
        }

        public void setConditionUsageText(String conditionUsageText) {
            this.conditionUsageText = conditionUsageText;
        }

        public String getPdfContainsTitle() {
            return StringEscapeUtils.escapeXml(pdfContainsTitle);
        }

        public void setPdfContainsTitle(String pdfContainsTitle) {
            this.pdfContainsTitle = pdfContainsTitle;
        }

        public String getHyphCountry() {
            return hyphCountry;
        }

        public void setHyphCountry(String hyphCountry) {
            this.hyphCountry = hyphCountry;
        }

        public String getHyphLang() {
            return hyphLang;
        }

        public void setHyphLang(String hyphLang) {
            this.hyphLang = hyphLang;
        }

        public GeneratedItem[] getGeneratedItems() {
            return generatedItems;
        }

        public void setGeneratedItems(GeneratedItem[] generatedItems) {
            this.generatedItems = generatedItems;
        }

        public boolean isMoreGeneratedItems() {
            return this.generatedItems != null && this.generatedItems.length > 0;
        }

    }

    // reprezentuje generovanou polozku
    static class GeneratedItem {

        private DetailItem[] detailItems = new DetailItem[0];

        public GeneratedItem() {
        }

        public DetailItem[] getDetailItems() {
            return detailItems;
        }

        public void setDetailItems(DetailItem[] dCItems) {
            this.detailItems = dCItems;
        }

        public boolean isDetailItemsDefined() {
            return this.detailItems != null && this.detailItems.length > 0;
        }

    }


    static class DetailItem {

        private String key;
        private String value;

        public DetailItem(String key, String value) {
            super();
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return StringEscapeUtils.escapeXml(key);
        }

        public String getValue() {
            return StringEscapeUtils.escapeXml(value);
        }
    }
}
