package com.gmail.dpierron.calibre.opds;

import com.gmail.dpierron.calibre.configuration.ConfigurationManager;
import com.gmail.dpierron.calibre.datamodel.DataModel;
import com.gmail.dpierron.tools.i18n.Localization;
import com.gmail.dpierron.tools.Helper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.*;
import org.jdom2.input.JDOMParseException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.input.sax.SAXHandlerFactory;
import org.jdom2.input.sax.XMLReaders;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
import org.w3c.tidy.Tidy;

import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


public class JDOMManager {
  private final static Logger logger = LogManager.getLogger(JDOMManager.class);

  private static JDOMFactory factory;
  private static XMLOutputter outputter;
  private static XMLOutputter serializer;
  private static TransformerFactory transformerFactory;
  private static Transformer bookFullEntryTransformer;
  private static Transformer catalogTransformer;
  private static Transformer mainTransformer;
  private static SAXBuilder sb;

  public static void reset() {
    factory = null;
    outputter = null;
    serializer = null;
    transformerFactory = null;
    bookFullEntryTransformer = null;
    catalogTransformer = null;
    mainTransformer = null;
    sb = null;
  }

  /**
   * Set the paremeters for creating the header.html file
   *
   * @return
   */
  public static Transformer getIncludeTransformer(String xslname) {
    Transformer headerTransformer;
    try {
      headerTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(xslname)));
      setParametersOnCatalog(headerTransformer);
      setIntroParameters(headerTransformer);
    } catch (TransformerConfigurationException e) {
      logger.error("getIncludeTransformer(): Error while configuring header transformer", e); Helper.statsErrors++;
      headerTransformer = null;
    }
    return headerTransformer;
  }

  public enum Namespace {
    Atom("", "http://www.w3.org/2005/Atom"),
    Opds("opds", "http://opds-spec.org/2010/catalog"),
    Opf("opf", "http://www.idpf.org/2007/opf"),
    Dc("dc", "http://purl.org/dc/elements/1.1/"),
    DcTerms("dcterms", "http://purl.org/dc/terms"),
    Calibre("calibre", "http://calibre.kovidgoyal.net/2009/metadata"),
    Xhtml("xhtml", "http://www.w3.org/1999/xhtml");

    private org.jdom2.Namespace jdomNamespace;

    private Namespace(String prefix, String uri) {
      jdomNamespace = org.jdom2.Namespace.getNamespace(prefix, uri);
    }

    public org.jdom2.Namespace getJdomNamespace() {
      return this.jdomNamespace;
    }
  }

  /**
   * Set parameters that are common to many the transformers we use.
   * @param catalogTransformer
   */
  private static void setParametersOnCatalog(Transformer catalogTransformer) {
    double dh = ConfigurationManager.getCurrentProfile().getCoverHeight();
    double dw = 2f / 3f * dh;
    long lh = (long) Math.floor(dh);
    long lw = (long) Math.floor(dw);
    catalogTransformer.setParameter("coverWidth", lw);
    catalogTransformer.setParameter("coverHeight", lh);
    dh = ConfigurationManager.getCurrentProfile().getThumbnailHeight();
    dw = 2f / 3f * dh;
    lh = (long) Math.floor(dh);
    lw = (long) Math.floor(dw);
    catalogTransformer.setParameter("thumbWidth", lw);
    catalogTransformer.setParameter("thumbHeight", lh);
    catalogTransformer.setParameter("generateDownloads", Boolean.toString(ConfigurationManager.getCurrentProfile().getGenerateHtmlDownloads()).toLowerCase());
    catalogTransformer.setParameter("libraryTitle", ConfigurationManager.getCurrentProfile().getCatalogTitle());
    catalogTransformer.setParameter("i18n.and", Localization.Main.getText("i18n.and"));
    catalogTransformer.setParameter("i18n.backToMain", Localization.Main.getText("i18n.backToMain"));
    catalogTransformer.setParameter("i18n.downloads", Localization.Main.getText("i18n.downloads"));
    catalogTransformer.setParameter("i18n.links", Localization.Main.getText("i18n.links"));
    catalogTransformer.setParameter("i18n.downloadfile", Localization.Main.getText("i18n.downloadfile"));
    catalogTransformer.setParameter("i18n.coversection", Localization.Main.getText("i18n.coversection"));
    catalogTransformer.setParameter("i18n.summarysection", Localization.Main.getText("i18n.summarysection"));
    catalogTransformer.setParameter("i18n.downloadsection", Localization.Main.getText("i18n.downloadsection"));
    catalogTransformer.setParameter("i18n.relatedsection", Localization.Main.getText("i18n.relatedsection"));
    catalogTransformer.setParameter("i18n.linksection", Localization.Main.getText("i18n.linksection"));
    catalogTransformer.setParameter("browseByCover", Boolean.toString(ConfigurationManager.getCurrentProfile().getBrowseByCover()).toLowerCase());
    catalogTransformer.setParameter("generateIndex", Boolean.toString(ConfigurationManager.getCurrentProfile().getGenerateIndex()).toLowerCase());
    // We only want to add the Date Generated to the bottom of each catalog page if
    // we have not elected to try and minimise the number of files changed each run
    // (it will still be added to the top page)
    // TODO:  decide if we never want this on each page?
    // TODO:  Now that minimizechangedfiles removed need to rethink
    // TODO:  Probably best to acti as if minimizechangedfiles had been set
//    if (ConfigurationManager.getCurrentProfile().getMinimizeChangedFiles()) {
      catalogTransformer.setParameter("i18n.dateGenerated","");
//    } else {
//      String dateGenerated =
//          DateFormat.getDateInstance(DateFormat.DEFAULT, ConfigurationManager.getCurrentProfile().getLanguage()).format(new Date());
//      catalogTransformer.setParameter("i18n.dateGenerated", Localization.Main.getText("i18n.dateGenerated", dateGenerated));
//    }
    // Add book count if not generating ALl Books (which gives count if present)
    // headerTransformer.setParameter("programName", Constants.PROGNAME);
    // headerTransformer.setParameter("programVersion", Constants.PROGVERSION + Constants.BZR_VERSION);
    //        headerTransformer.setParameter("bookCount", Localization.Main.getText("bookword.many", DataModel.getListOfBooks().size()));
    String dateGenerated =
        DateFormat.getDateInstance(DateFormat.DEFAULT, ConfigurationManager.getCurrentProfile().getLanguage()).format(new Date());
    String sizeText = "("+ Localization.Main.getText("bookword.many", DataModel.getListOfBooks().size()) +")";
    catalogTransformer.setParameter("i18n.dateGenerated",
        Localization.Main.getText("i18n.dateGenerated",dateGenerated)
        + " " + sizeText);
  }
  /**
   * set the transformer that is used for the top level page.
   *
   * @return
   */
  public static Transformer getMainCatalogTransformer() {
    if (mainTransformer == null) {
      try {
        mainTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.CATALOG_XSL)));
        setParametersOnCatalog(mainTransformer);
        setIntroParameters(mainTransformer);
      } catch (TransformerConfigurationException e) {
        logger.error("getMainCatalogTransformer(): Error while configuring catalog transformer", e); Helper.statsErrors++;
        mainTransformer = null;
      }
    }
    return mainTransformer;
  }

  public static Transformer getCatalogTransformer() {
    if (catalogTransformer == null) {
      try {
        catalogTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.CATALOG_XSL)));
        setParametersOnCatalog(catalogTransformer);
        catalogTransformer.setParameter("programName", "");  // Set to empty for all pages except top level
      } catch (TransformerConfigurationException e) {
        logger.error("getCatalogTransformer(): Error while configuring catalog transformer", e); Helper.statsErrors++;
        catalogTransformer = null;
     }
    }
    return catalogTransformer;
  }

  /**
   * Transformer used on a Book fulle entry.
   *
   * @return
   */
  public static Transformer getBookFullEntryTransformer() {
    if (bookFullEntryTransformer == null) {
      try {
        bookFullEntryTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.FULLENTRY_XSL)));
        setParametersOnCatalog(bookFullEntryTransformer);
      } catch (TransformerConfigurationException e) {
        logger.error("getCatalogTransformer(): Error while configuring book full entry transformer", e); Helper.statsErrors++;
        bookFullEntryTransformer = null;
      }
    }
    return bookFullEntryTransformer;
  }

  /**
   * Set parameters that reate to the 'about' information for calibre2opds
   *
   * @param transformer
   * @return
   */
  private static Transformer setIntroParameters (Transformer transformer) {
    if (transformer != null) {
      boolean includeAbout = ConfigurationManager.getCurrentProfile().getIncludeAboutLink();
      transformer.setParameter("programName", Constants.PROGNAME);
      transformer.setParameter("programVersion", Constants.PROGVERSION + Constants.BZR_VERSION);

      //  Set up options to display t bottom of top pge in catalog

      // Always include these options on top page
      transformer.setParameter("intro.goal", Localization.Main.getText("intro.goal"));
      transformer.setParameter("intro.wiki.title", Localization.Main.getText("intro.wiki.title"));
      transformer.setParameter("intro.wiki.url", Localization.Main.getText("intro.wiki.url"));
      transformer.setParameter("intro.userguide", Localization.Main.getText("gui.menu.help.userGuide"));
      transformer.setParameter("intro.userguide.url", Constants.USERGUIDE_URL);
      transformer.setParameter("intro.developerguide", Localization.Main.getText("gui.menu.help.developerGuide"));
      transformer.setParameter("intro.developerguide.url", Constants.DEVELOPERGUIDE_URL );
      // Only include these options on top page if the about section not suppresssed
      transformer.setParameter("intro.team.title", includeAbout ? Localization.Main.getText("intro.team.title") : "");
      transformer.setParameter("intro.team.list1", includeAbout ? Localization.Main.getText("intro.team.list1") : "");
      transformer.setParameter("intro.team.title2", includeAbout ? Localization.Main.getText("intro.team.title2") : "");
      transformer.setParameter("intro.team.list2", includeAbout ? Localization.Main.getText("intro.team.list2") : "");
      transformer.setParameter("intro.team.list3", includeAbout ? Localization.Main.getText("intro.team.list3") : "");
      transformer.setParameter("intro.team.list4", includeAbout ? Localization.Main.getText("intro.team.list4") : "");
      transformer.setParameter("intro.thanks.1",   includeAbout ? Localization.Main.getText("intro.thanks.1") : "");
      transformer.setParameter("intro.thanks.2",   includeAbout ? Localization.Main.getText("intro.thanks.2") : "");
    }
    return transformer;
  }

  /**
   * Code common to setting up all transformer factories
   *
   * @return
   */
  public static TransformerFactory getTransformerFactory() {
    if (transformerFactory == null) {
      transformerFactory = TransformerFactory.newInstance();
    }
    return transformerFactory;
  }

  public static void setTransformerFactory(TransformerFactory transformerFactory) {
    transformerFactory = transformerFactory;
  }

  public static JDOMFactory getFactory() {
    if (factory == null) {
      factory = new DefaultJDOMFactory();
    }
    return factory;
  }

  public static SAXBuilder getSaxBuilder() {
    if (sb == null) {
      sb = new SAXBuilder();
    }
    return sb;
  }

  public static XMLOutputter getPrettyXML() {
    if (outputter == null) {
      outputter = new XMLOutputter(Format.getPrettyFormat());
    }
    return outputter;
  }

  public static XMLOutputter getRawXml() {
    if (serializer == null) {
      serializer = new XMLOutputter(Format.getRawFormat());
    }
    return serializer;
  }

  public static XMLOutputter getCompactXML() {
    if (serializer == null) {
      serializer = new XMLOutputter(Format.getCompactFormat());
    }
    return serializer;
  }

  public static Element rootElement(String name, Namespace namespace, Namespace... declaredNamespaces) {
    Element result = element(name, namespace);
    for (Namespace declaredNamespace : declaredNamespaces) {
      result.addNamespaceDeclaration(declaredNamespace.getJdomNamespace());
    }
    return result;
  }

  public static Element element(String name, Namespace namespace) {
    Element result = getFactory().element(name);
    if (namespace != null) {
      result.setNamespace(namespace.getJdomNamespace());
    }
    return result;
  }

  public static Element element(String name) {
    return element(name, Namespace.Atom);
  }

  public static Element newParagraph() {
    return element("p", Namespace.Atom);
  }

  private static Tidy tidyForTidyInputStream = null;

  /**
   * Routine to tidy up the HTML in the Summary field
   *
   * @param in
   * @return
   * @throws JDOMException
   * @throws IOException
   */
  public static List<Element> tidyInputStream(InputStream in) throws JDOMException, IOException {
    List<Element> result = null;
    // initializing tidy
    if (logger.isTraceEnabled()) logger.trace("tidyInputStream: initializing tidy");
    if (tidyForTidyInputStream == null) {
      tidyForTidyInputStream = new Tidy();
      tidyForTidyInputStream.setShowWarnings(false);
      tidyForTidyInputStream.setXmlOut(true);
      tidyForTidyInputStream.setInputEncoding("utf-8");
      tidyForTidyInputStream.setQuiet(true);
      tidyForTidyInputStream.setDropEmptyParas(false);
    }
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    if (logger.isTraceEnabled()) logger.trace("tidyInputStream: parsing with Tidy");
    try {
      tidyForTidyInputStream.parseDOM(in, out);
    } finally {
      out.close();
    }
    String text2 = new String(out.toByteArray());
    SAXBuilder sb = new SAXBuilder(XMLReaders.NONVALIDATING, (SAXHandlerFactory)null, (JDOMFactory)null);
    if (logger.isTraceEnabled()) logger.trace("tidyInputStream: building doc");
    Document doc = sb.build(new StringReader(text2));
    Element html = doc.getRootElement();
    if (! html.getName().equalsIgnoreCase("html")) {
      if (logger.isTraceEnabled()) logger.trace("tidyInputStream: no html tag found");
    } else {
      if (logger.isTraceEnabled()) logger.trace("tidyInputStream: found html tag");
      for (Object o : html.getChildren()) {
        if (o instanceof Element) {
          Element child = (Element) o;
          if (! child.getName().equalsIgnoreCase("body")) {
            if (logger.isTraceEnabled()) logger.trace("tidyInputStream: no body tag found");
          } else {
            if (logger.isTraceEnabled()) logger.trace("tidyInputStream: found body tag");
            (result = new ArrayList<Element>()).addAll(child.getChildren());
          }
        }
      }
    }
    if (logger.isTraceEnabled()) logger.trace("tidyInputStream: completed tidy");
    return result;
  }

  /**
   *
   * @param text      text to convert to HTML
   * @return
   */
  public static List<Element> convertHtmlTextToXhtml(String text) {
    List<Element> result = null;

    // if (logger.isTraceEnabled())
    //   logger.trace("Comment to convert: " + Database.stringToHex(text));
    if (Helper.isNotNullOrEmpty(text)) {
      if (!text.startsWith("<")) {
        // plain text
        if (logger.isTraceEnabled())logger.trace("convertHtmlTextToXhtml: plain text");
        StringBuffer sb = new StringBuffer();
        if (Helper.isNotNullOrEmpty(text)) {
          List<String> strings = Helper.tokenize(text, "\n", true);
          for (String string : strings) {
            if (Helper.isNullOrEmpty(string)) {
               sb.append("<p />");
            } else {
              sb.append("<p>");
              sb.append(string);
              sb.append("</p>\n");
            }
          }
        }
        text = sb.toString();
      }

      // tidy the text
      if (logger.isTraceEnabled()) logger.trace("convertHtmlTextToXhtml: tidy the text");
      try {
        String noNL = text.replaceAll("\\n","");
        result = tidyInputStream(new ByteArrayInputStream(noNL.getBytes("utf-8")));
        // result = tidyInputStream(new ByteArrayInputStream(text.getBytes("utf-8")));
        String afterTIDY = result.toString();
        int dummy = 1;
      } catch (JDOMParseException j) {
        if (logger.isDebugEnabled()) logger.trace("convertHtmlTextToXhtml: caught JDOMParseException in the tidy process");
        if (logger.isTraceEnabled()) logger.trace( "" + j);
        tidyForTidyInputStream = null;    // Force a new clean object to be gebnerated for next time around
      } catch (Exception ee) {
        if (logger.isDebugEnabled()) logger.debug("convertHtmlTextToXhtml: caught exception in the tidy process", ee);
        tidyForTidyInputStream = null;    // Force a new clean object to be gebnerated for next time around
      } catch (Throwable t) {
        logger.error("convertHtmlTextToXhtml: caught throwable in the tidy process", t); Helper.statsErrors++;
        tidyForTidyInputStream = null;    // Force a new clean object to be gebnerated for next time around
      }

      if (result != null) {
        if (logger.isTraceEnabled()) logger.trace("convertHtmlTextToXhtml: returning XHTML");
      } else {
        if (Helper.isNotNullOrEmpty(text)) logger.debug("convertHtmlTextToXhtml: Cannot convert comment text\n" + text);
      }
    }
    return result;
  }
}
