/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.util.document;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.model.Enclosure;
import net.sourceforge.rssowl.model.NewsItem;
import net.sourceforge.rssowl.util.DateParser;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.shop.BrowserShop;
import net.sourceforge.rssowl.util.shop.FontShop;
import net.sourceforge.rssowl.util.shop.StringShop;
import net.sourceforge.rssowl.util.shop.URLShop;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.FontData;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;

/**
 * <p>
 * The HTMLExporter is using some of the Methods of iText in order to export all
 * Items of a Channel into HTML.
 * </p>
 * Note: Some parts copied from iText are licensed under the Mozilla Public
 * License.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class HTMLExporter {

  /** Doctype to be used for the generated HTML */
  private static final String DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";

  /** This is some byte that is often used. */
  private static final byte NEWLINE = (byte) '\n';

  /** This is some byte that is often used. */
  private static final byte TAB = (byte) '\t';

  /** XML Declaration to be used for the generated HTML */
  private static final String XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";

  private Channel channel;
  private ArrayList newsOrder;
  private BufferedOutputStream out;

  /**
   * Instantiate a new HTMLExporter to generate a HTML Document out of the given
   * Channel into the given Stream.
   * 
   * @param channel The Channel containing NewsItems to export.
   * @param newsOrder The Order and Number of Items to export.
   * @param os A Stream to write the HTML into.
   */
  public HTMLExporter(Channel channel, ArrayList newsOrder, OutputStream os) {
    this.channel = channel;
    this.newsOrder = newsOrder;
    this.out = new BufferedOutputStream(os);
  }

  /**
   * Converts a String into a Byte array according to the ISO-8859-1 codepage.
   * 
   * @param text the text to be converted
   * @see com.lowagie.text.DocWriter#getISOBytes(java.lang.String)
   * @return the conversion result
   */
  private static byte[] getISOBytes(String text) {
    if (text == null)
      return null;
    int len = text.length();
    byte b[] = new byte[len];
    for (int k = 0; k < len; ++k)
      b[k] = (byte) text.charAt(k);
    return b;
  }

  /**
   * Writes a number of tabs.
   * 
   * @param indent the number of tabs to add
   * @throws IOException In case of an Error.
   */
  private void addTabs(int indent) throws IOException {
    out.write(NEWLINE);
    for (int i = 0; i < indent; i++)
      out.write(TAB);
  }

  /**
   * Closes the Outputstream.
   * 
   * @throws IOException In case of an Error.
   */
  private void close() throws IOException {
    out.close();
  }

  /**
   * Writes a String to the OutputStream.
   * 
   * @param string the String to write
   * @throws IOException In case of an Error.
   */
  private void write(String string) throws IOException {
    out.write(getISOBytes(string));
  }

  /**
   * Writes the BASE HTML Tag to resolve relative Links.
   * 
   * @throws IOException In case of an Error.
   */
  private void writeBase() throws IOException {
    write("<base href=\"");
    write(channel.getLink());
    write("\" />");
  }

  /**
   * Write the BODY of the HTML Document containing all Items.
   * 
   * @throws IOException In case of an Error.
   */
  private void writeBody() throws IOException {

    /** Channel Title */
    addTabs(2);
    write("<div class=\"channelTitle\">");
    addTabs(3);
    write("<h2>");
    addTabs(4);

    /** Link to the Channel's Homepage */
    if (StringShop.isset(channel.getHomepage()))
      writeLink(channel.getHomepage(), StringShop.unicodeToEntities(channel.getTitle()), null);

    /** Only show Title */
    else
      write(StringShop.unicodeToEntities(channel.getTitle()));

    addTabs(3);
    write("</h2>");
    addTabs(2);
    write("</div>");

    /** Channel News Items */
    Hashtable newsItems = channel.getItems();

    /** Write all newsitems in their sort order into document */
    for (int a = 0; a < newsOrder.size(); a++) {
      NewsItem rssNewsItem = (NewsItem) newsItems.get(newsOrder.get(a));

      /** Write this newsitem */
      writeNews(rssNewsItem);
    }
  }

  /**
   * Write the given FontData as CSS to the HTML Document.
   * 
   * @param fontData The FontData defining the Font.
   * @throws IOException In case of an Error.
   */
  private void writeFont(FontData fontData) throws IOException {
    StringBuffer fontBuf = new StringBuffer();
    fontBuf.append("font-family: ").append(fontData.getName()).append("; ");
    fontBuf.append("font-size: ").append(fontData.getHeight());
    fontBuf.append(GlobalSettings.isMac() ? "px" : "pt").append("; ");

    if ((fontData.getStyle() & SWT.BOLD) != 0)
      fontBuf.append("font-weight: bold; ");

    if ((fontData.getStyle() & SWT.ITALIC) != 0)
      fontBuf.append("font-style: italic; ");

    write(fontBuf.toString());
  }

  /**
   * Write the HEAD of the HTML Document containing Title and CSS Definition.
   * 
   * @throws IOException In case of an Error.
   */
  private void writeHead() throws IOException {

    /** Title */
    write("<title>");
    write(channel.getTitle());
    write("</title>");

    /** CSS Definition */
    addTabs(2);
    write("<style type=\"text/css\">");
    addTabs(3);

    /** General Links */
    write("a:link { text-decoration: none; color: rgb(0,0,153); }");
    addTabs(3);
    write("a:visited { text-decoration: none; color: rgb(0,0,153); }");
    addTabs(3);
    write("a:hover { text-decoration: underline; color: rgb(0,0,153); }");
    addTabs(3);
    write("a:active { text-decoration: none; color: rgb(0,0,153); }");
    addTabs(3);

    /** Date Links */
    write("a.homepageLink:link { text-decoration: none; color: rgb(127,127,127); } ");
    addTabs(3);
    write("a.homepageLink:visited { text-decoration: none; color: rgb(127,127,127); } ");
    addTabs(3);
    write("a.homepageLink:hover { text-decoration: none; color: rgb(127,127,127); } ");
    addTabs(3);
    write("a.homepageLink:active { text-decoration: none; color: rgb(127,127,127); } ");
    out.write(NEWLINE);
    addTabs(3);

    /** Body */
    write("body { margin: 25px; background: rgb(244,244,244); ");
    writeFont(FontShop.textFont.getFontData()[0]);
    write("} ");
    out.write(NEWLINE);
    addTabs(3);

    /** List showing Enclosures */
    write("ul.enclosureList { list-style-type: square; }");
    out.write(NEWLINE);
    addTabs(3);

    /** Div Containing Channel Title */
    write("div.channelTitle { text-align: center; }");
    out.write(NEWLINE);
    addTabs(3);

    /** Div containing Additional Info */
    write("div.addInfo { margin-top: 15px; }");
    out.write(NEWLINE);
    addTabs(3);

    /** Table Containing Item */
    write("table.item { width: 100%; margin-bottom: 20px; border-width: 1px; border-color: rgb(192,192,192); border-style: solid; background: rgb(255,255,255); }");
    out.write(NEWLINE);
    addTabs(3);

    /** TD Containing Item */
    write("td.item { padding: 10px; }");
    out.write(NEWLINE);
    addTabs(3);

    /** Div Containing Title */
    write("div.title { font-weight: bold; margin-bottom: 5px; ");
    writeFont(FontShop.textFont.getFontData()[0]);
    write("} ");
    out.write(NEWLINE);
    addTabs(3);

    /** Div Containing Body */
    write("div.body { margin-top: 15px; margin-bottom: ");
    write(channel.isAggregatedCat() ? "15px; " : "0px; ");
    writeFont(FontShop.textFont.getFontData()[0]);
    write("} ");
    out.write(NEWLINE);
    addTabs(3);

    /** Div Containing Date */
    write("div.date { ");
    writeFont(FontShop.textFont.getFontData()[0]);
    write(" text-decoration: none; color: rgb(127,127,127); }");
    out.write(NEWLINE);
    addTabs(3);

    /** Div Containing Feed Title */
    write("div.feedTitle { color: rgb(127,127,127); ");
    writeFont(FontShop.textFont.getFontData()[0]);
    write("} ");
    addTabs(2);

    /** Close */
    write("</style>");
  }

  /**
   * Write the Contents of an Item into the body Paragraph.
   * 
   * @param item The Item to add to the body Paragraph.
   * @throws IOException In case of an Error.
   */
  private void writeItemBody(NewsItem item) throws IOException {

    /** NewsItem content containing description, comments, source... */
    StringBuffer description = new StringBuffer();

    /** Description */
    if (StringShop.isset(item.getDescription()))
      description.append(item.getDescription());

    /** Enclosures of the news */
    if (item.getEnclosures() != null && item.getEnclosures().size() > 0) {
      description.append("\n<div class=\"addInfo\">");
      description.append("\n<strong>").append(GUI.i18n.getTranslation("NEWS_ITEM_INFO_ENCLOSURE")).append("</strong>: ");
      description.append("\n<ul class=\"enclosureList\">");

      /** Write all enclosures to document in a List */
      for (int c = 0; c < item.getEnclosures().size(); c++) {
        Enclosure enclosure = (Enclosure) item.getEnclosures().get(c);
        String url = enclosure.getUrl();

        /** Create Anchor */
        String anchor;
        try {
          URL enclosureUrl = new URL(url);
          anchor = StringShop.createAnchor(url, URLShop.getFile(enclosureUrl));
        } catch (MalformedURLException e) {
          anchor = url;
        }

        /** Create List Item */
        description.append("\n").append("<li>");
        description.append("\n").append(anchor);
        description.append(" (").append(enclosure.getType()).append(" / ").append(enclosure.getLength(true)).append(")");
        description.append("\n").append("</li>");
      }

      /** Clost List */
      description.append("\n</ul>");
      description.append("\n</div>");
    }

    /** Source of the news */
    if (StringShop.isset(item.getSource())) {
      description.append("\n<div class=\"addInfo\">");
      description.append("\n<strong>").append(GUI.i18n.getTranslation("NEWS_ITEM_INFO_SOURCE")).append("</strong>: ");

      /** Source is a URL, show as Link */
      if (URLShop.looksLikeURL(item.getSource()))
        description.append("\n<a href=\"").append(item.getSource()).append("\">").append(item.getSource()).append("</a>");

      /** Source is not a URL */
      else
        description.append(item.getSource());

      /** Close Source Div */
      description.append("\n</div>");
    }

    /** Comments of the news */
    if (item.getComments() != null && item.getComments().size() > 0) {
      description.append("\n<div class=\"addInfo\">");
      description.append("\n<strong>").append(GUI.i18n.getTranslation("NEWS_ITEM_INFO_COMMENTS")).append("</strong>: ");

      /** Write all comments to the document */
      int size = item.getComments().size();
      for (int b = 0; b < size; b++)
        if (StringShop.isset((String) item.getComments().get(b))) {
          String comment = (String) item.getComments().get(b);

          /** Comment is a URL, show as Link */
          if (URLShop.looksLikeURL(comment))
            description.append("\n<a href=\"").append(comment).append("\">").append(comment).append("</a>");

          /** Comment is not a URL */
          else
            description.append(comment);

          /** Add new-line for following Comments */
          if (size > 1)
            description.append("\n<br />");
        }

      /** Close Comments Div */
      description.append("\n</div>");
    }

    /** In case description is empty */
    if (description.length() == 0)
      description.append(GUI.i18n.getTranslation("NEWS_NO_DESCRIPTION"));

    /** Write News Body into HTML Document */
    write(StringShop.unicodeToEntities(description.toString()));
  }

  /**
   * Write a Link.
   * 
   * @param href The URL Target of the Link.
   * @param name The Name of the Link.
   * @param clazz The CSS Class of the Link.
   * @throws IOException In case of an Error.
   */
  private void writeLink(String href, String name, String clazz) throws IOException {

    /** Open */
    write("<a ");

    /** CSS Class if given */
    if (StringShop.isset(clazz)) {
      write("class=\"");
      write(clazz);
      write("\" ");
    }

    /** URL Target */
    write("href=\"");
    write(href);
    write("\">");

    /** Name */
    write(name);

    /** Close */
    write("</a>");
  }

  /**
   * Write the given NewsItem into the Body of the HTML Document.
   * 
   * @param item The NewsItem to write into the Body of the HTML Document.
   * @throws IOException In case of an Error.
   */
  private void writeNews(NewsItem item) throws IOException {
    String newsTitle = StringShop.isset(item.getTitle()) ? item.getTitle() : GUI.i18n.getTranslation("NO_TITLE");
    newsTitle = StringShop.unicodeToEntities(newsTitle);
    boolean hasLink = StringShop.isset(item.getLink());

    /** Table surrounding entire Item */
    addTabs(2);
    write("<table class=\"item\"><tr><td class=\"item\">");
    addTabs(3);

    /** Div surrounding Item Title */
    write("<div class=\"title\">");
    addTabs(4);
    if (hasLink)
      writeLink(item.getLink(), newsTitle, null);
    else
      write(newsTitle);
    addTabs(3);
    write("</div>");

    /** Check if a publish Date is available */
    StringBuffer subTitleBuf = new StringBuffer();
    if (item.getPubDateParsed() != null)
      subTitleBuf.append(DateParser.formatDate(item.getPubDateParsed(), true, true));
    else if (item.getPubDate() != null)
      subTitleBuf.append(item.getPubDate());

    /** Check if author is available */
    if (StringShop.isset(item.getAuthor())) {
      subTitleBuf.append(subTitleBuf.length() > 0 ? " - " : "");
      subTitleBuf.append(item.getAuthor());
    }

    /** Sub Title is available */
    if (subTitleBuf.length() > 0) {
      String subTitleStr = subTitleBuf.toString();
      subTitleStr = StringShop.unicodeToEntities(subTitleStr);
      addTabs(3);
      write("<div class=\"date\">");
      addTabs(4);
      write(subTitleStr);
      addTabs(3);
      write("</div>");
    }

    /** Write Item Body */
    addTabs(3);
    write("<div class=\"body\">");
    writeItemBody(item);
    write("</div>");

    /** Check if the Newsfeed Title is available in case of an Aggregation */
    if (item.isPartOfAggregation() && StringShop.isset(item.getNewsfeedTitle())) {
      addTabs(3);
      write("<div class=\"feedTitle\">");

      /** Show Feed Title as Link to the Newsfeed's Homepage */
      if (URLShop.looksLikeURL(item.getNewsfeedHomepage()))
        writeLink(item.getNewsfeedHomepage(), StringShop.unicodeToEntities(item.getNewsfeedTitle()), "homepageLink");

      /** Just show Feed Title */
      else
        write(StringShop.unicodeToEntities(item.getNewsfeedTitle()));

      addTabs(3);
      write("</div>");
    }

    /** Close Div */
    addTabs(2);
    write("</td></tr></table>");
  }

  /**
   * Create the HTML Document.
   * 
   * @throws IOException In case of an Error.
   */
  void create() throws IOException {

    /** Add XML Declaration */
    write(XML_DECLARATION);
    out.write(NEWLINE);

    /** Add DOCTYPE */
    write(DOCTYPE);
    out.write(NEWLINE);

    /** Mark of the Web (Windows only) */
    if (GlobalSettings.isWindows()) {
      write(BrowserShop.IE_MOTW);
      out.write(NEWLINE);
    }

    /** Open HTML */
    write("<html>");
    addTabs(1);

    /** Open HEAD */
    write("<head>");
    addTabs(2);

    /** Explicitly set the BASE Tag to resolve relative Links */
    if (!channel.isAggregatedCat() && (StringShop.isset(channel.getLink())))
      writeBase();

    /** Add HEAD Definition */
    addTabs(2);
    writeHead();

    /** Close HEAD */
    addTabs(1);
    write("</head>");

    /** Open BODY */
    addTabs(1);
    write("<body>");

    /** Add BODY Content */
    writeBody();

    /** Close BODY */
    addTabs(1);
    write("</body>");

    /** Close HTML */
    out.write(NEWLINE);
    write("</html>");

    /** Close Stream */
    close();
  }
}