package net.sf.jasperreports.engine.util;

import java.awt.Color;
import java.awt.GraphicsEnvironment;
import java.awt.font.TextAttribute;
import java.io.IOException;
import java.io.StringReader;
import java.lang.ref.SoftReference;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.xml.JRXmlConstants;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class JRStyledTextParser implements ErrorHandler {
  private static final String ROOT_START = "<st>";
  
  private static final String ROOT_END = "</st>";
  
  private static final String NODE_style = "style";
  
  private static final String NODE_bold = "b";
  
  private static final String NODE_italic = "i";
  
  private static final String NODE_underline = "u";
  
  private static final String NODE_sup = "sup";
  
  private static final String NODE_sub = "sub";
  
  private static final String NODE_font = "font";
  
  private static final String NODE_br = "br";
  
  private static final String NODE_li = "li";
  
  private static final String ATTRIBUTE_fontName = "fontName";
  
  private static final String ATTRIBUTE_fontFace = "face";
  
  private static final String ATTRIBUTE_color = "color";
  
  private static final String ATTRIBUTE_size = "size";
  
  private static final String ATTRIBUTE_isBold = "isBold";
  
  private static final String ATTRIBUTE_isItalic = "isItalic";
  
  private static final String ATTRIBUTE_isUnderline = "isUnderline";
  
  private static final String ATTRIBUTE_isStrikeThrough = "isStrikeThrough";
  
  private static final String ATTRIBUTE_forecolor = "forecolor";
  
  private static final String ATTRIBUTE_backcolor = "backcolor";
  
  private static final String ATTRIBUTE_pdfFontName = "pdfFontName";
  
  private static final String ATTRIBUTE_pdfEncoding = "pdfEncoding";
  
  private static final String ATTRIBUTE_isPdfEmbedded = "isPdfEmbedded";
  
  private static final String SPACE = " ";
  
  private static final String EQUAL_QUOTE = "=\"";
  
  private static final String QUOTE = "\"";
  
  private static final String SHARP = "#";
  
  private static final String LESS = "<";
  
  private static final String LESS_SLASH = "</";
  
  private static final String GREATER = ">";
  
  private static final ThreadLocal threadInstances = new ThreadLocal();
  
  public static JRStyledTextParser getInstance() {
    JRStyledTextParser instance = null;
    SoftReference instanceRef = threadInstances.get();
    if (instanceRef != null)
      instance = instanceRef.get(); 
    if (instance == null) {
      instance = new JRStyledTextParser();
      threadInstances.set(new SoftReference(instance));
    } 
    return instance;
  }
  
  private DocumentBuilder documentBuilder = null;
  
  public JRStyledTextParser() {
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      this.documentBuilder = factory.newDocumentBuilder();
      this.documentBuilder.setErrorHandler(this);
    } catch (ParserConfigurationException e) {
      throw new JRRuntimeException(e);
    } 
  }
  
  public JRStyledText parse(Map attributes, String text) throws SAXException {
    JRStyledText styledText = new JRStyledText();
    Document document = null;
    try {
      document = this.documentBuilder.parse(new InputSource(new StringReader("<st>" + text + "</st>")));
    } catch (IOException e) {
      throw new JRRuntimeException(e);
    } 
    parseStyle(styledText, document.getDocumentElement());
    styledText.setGlobalAttributes(attributes);
    return styledText;
  }
  
  public JRStyledText getStyledText(Map parentAttributes, String text, boolean isStyledText) {
    JRStyledText styledText = null;
    if (isStyledText)
      try {
        styledText = parse(parentAttributes, text);
      } catch (SAXException e) {} 
    if (styledText == null) {
      styledText = new JRStyledText();
      styledText.append(text);
      styledText.setGlobalAttributes(parentAttributes);
    } 
    return styledText;
  }
  
  public String write(JRStyledText styledText) {
    return write(styledText.getGlobalAttributes(), styledText.getAttributedString().getIterator(), styledText.getText());
  }
  
  public String write(Map parentAttrs, AttributedCharacterIterator iterator, String text) {
    StringBuffer sbuffer = new StringBuffer();
    int runLimit = 0;
    while (runLimit < iterator.getEndIndex() && (runLimit = iterator.getRunLimit()) <= iterator.getEndIndex()) {
      String chunk = text.substring(iterator.getIndex(), runLimit);
      Map attrs = iterator.getAttributes();
      StringBuffer styleBuffer = writeStyleAttributes(parentAttrs, attrs);
      if (styleBuffer.length() > 0) {
        sbuffer.append("<");
        sbuffer.append("style");
        sbuffer.append(styleBuffer.toString());
        sbuffer.append(">");
        writeChunk(sbuffer, parentAttrs, attrs, chunk);
        sbuffer.append("</");
        sbuffer.append("style");
        sbuffer.append(">");
      } else {
        writeChunk(sbuffer, parentAttrs, attrs, chunk);
      } 
      iterator.setIndex(runLimit);
    } 
    return sbuffer.toString();
  }
  
  public String write(JRStyledText styledText, int startIndex, int endIndex) {
    AttributedCharacterIterator subIterator = (new AttributedString(styledText.getAttributedString().getIterator(), startIndex, endIndex)).getIterator();
    String subText = styledText.getText().substring(startIndex, endIndex);
    return write(styledText.getGlobalAttributes(), subIterator, subText);
  }
  
  public void writeChunk(StringBuffer sbuffer, Map parentAttrs, Map attrs, String chunk) {
    Object value = attrs.get(TextAttribute.SUPERSCRIPT);
    Object oldValue = parentAttrs.get(TextAttribute.SUPERSCRIPT);
    boolean isSuper = false;
    boolean isSub = false;
    if (value != null && !value.equals(oldValue)) {
      isSuper = TextAttribute.SUPERSCRIPT_SUPER.equals(value);
      isSub = TextAttribute.SUPERSCRIPT_SUB.equals(value);
    } 
    if (isSuper || isSub) {
      String node = isSuper ? "sup" : "sub";
      sbuffer.append("<");
      sbuffer.append(node);
      sbuffer.append(">");
      sbuffer.append(JRStringUtil.xmlEncode(chunk));
      sbuffer.append("</");
      sbuffer.append(node);
      sbuffer.append(">");
    } else {
      sbuffer.append(JRStringUtil.xmlEncode(chunk));
    } 
  }
  
  private void parseStyle(JRStyledText styledText, Node parentNode) throws SAXException {
    NodeList nodeList = parentNode.getChildNodes();
    int i;
    label116: for (i = 0; i < nodeList.getLength(); i++) {
      Node node = nodeList.item(i);
      if (node.getNodeType() == 3) {
        styledText.append(node.getNodeValue());
      } else if (node.getNodeType() == 1 && "style".equals(node.getNodeName())) {
        NamedNodeMap nodeAttrs = node.getAttributes();
        Map styleAttrs = new HashMap();
        if (nodeAttrs.getNamedItem("fontName") != null)
          styleAttrs.put(TextAttribute.FAMILY, nodeAttrs.getNamedItem("fontName").getNodeValue()); 
        if (nodeAttrs.getNamedItem("isBold") != null)
          styleAttrs.put(TextAttribute.WEIGHT, Boolean.valueOf(nodeAttrs.getNamedItem("isBold").getNodeValue()).booleanValue() ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR); 
        if (nodeAttrs.getNamedItem("isItalic") != null)
          styleAttrs.put(TextAttribute.POSTURE, Boolean.valueOf(nodeAttrs.getNamedItem("isItalic").getNodeValue()).booleanValue() ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR); 
        if (nodeAttrs.getNamedItem("isUnderline") != null)
          styleAttrs.put(TextAttribute.UNDERLINE, Boolean.valueOf(nodeAttrs.getNamedItem("isUnderline").getNodeValue()).booleanValue() ? TextAttribute.UNDERLINE_ON : null); 
        if (nodeAttrs.getNamedItem("isStrikeThrough") != null)
          styleAttrs.put(TextAttribute.STRIKETHROUGH, Boolean.valueOf(nodeAttrs.getNamedItem("isStrikeThrough").getNodeValue()).booleanValue() ? TextAttribute.STRIKETHROUGH_ON : null); 
        if (nodeAttrs.getNamedItem("size") != null)
          styleAttrs.put(TextAttribute.SIZE, new Float(nodeAttrs.getNamedItem("size").getNodeValue())); 
        if (nodeAttrs.getNamedItem("pdfFontName") != null)
          styleAttrs.put(JRTextAttribute.PDF_FONT_NAME, nodeAttrs.getNamedItem("pdfFontName").getNodeValue()); 
        if (nodeAttrs.getNamedItem("pdfEncoding") != null)
          styleAttrs.put(JRTextAttribute.PDF_ENCODING, nodeAttrs.getNamedItem("pdfEncoding").getNodeValue()); 
        if (nodeAttrs.getNamedItem("isPdfEmbedded") != null)
          styleAttrs.put(JRTextAttribute.IS_PDF_EMBEDDED, Boolean.valueOf(nodeAttrs.getNamedItem("isPdfEmbedded").getNodeValue())); 
        if (nodeAttrs.getNamedItem("forecolor") != null) {
          Color color = JRXmlConstants.getColor(nodeAttrs.getNamedItem("forecolor").getNodeValue(), Color.black);
          styleAttrs.put(TextAttribute.FOREGROUND, color);
        } 
        if (nodeAttrs.getNamedItem("backcolor") != null) {
          Color color = JRXmlConstants.getColor(nodeAttrs.getNamedItem("backcolor").getNodeValue(), Color.black);
          styleAttrs.put(TextAttribute.BACKGROUND, color);
        } 
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "b".equalsIgnoreCase(node.getNodeName())) {
        Map styleAttrs = new HashMap();
        styleAttrs.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "i".equalsIgnoreCase(node.getNodeName())) {
        Map styleAttrs = new HashMap();
        styleAttrs.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "u".equalsIgnoreCase(node.getNodeName())) {
        Map styleAttrs = new HashMap();
        styleAttrs.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "sup".equalsIgnoreCase(node.getNodeName())) {
        Map styleAttrs = new HashMap();
        styleAttrs.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "sub".equalsIgnoreCase(node.getNodeName())) {
        Map styleAttrs = new HashMap();
        styleAttrs.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "font".equalsIgnoreCase(node.getNodeName())) {
        NamedNodeMap nodeAttrs = node.getAttributes();
        Map styleAttrs = new HashMap();
        if (nodeAttrs.getNamedItem("face") != null)
          styleAttrs.put(JRTextAttribute.HTML_FONT_FACE, nodeAttrs.getNamedItem("face").getNodeValue()); 
        if (nodeAttrs.getNamedItem("size") != null)
          styleAttrs.put(TextAttribute.SIZE, new Float(nodeAttrs.getNamedItem("size").getNodeValue())); 
        if (nodeAttrs.getNamedItem("color") != null) {
          Color color = JRXmlConstants.getColor(nodeAttrs.getNamedItem("color").getNodeValue(), Color.black);
          styleAttrs.put(TextAttribute.FOREGROUND, color);
        } 
        if (nodeAttrs.getNamedItem("face") != null) {
          String[] fontList = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
          String fontFaces = nodeAttrs.getNamedItem("face").getNodeValue();
          StringTokenizer t = new StringTokenizer(fontFaces, ",");
          while (t.hasMoreTokens()) {
            String face = t.nextToken().trim();
            for (int j = 0; j < fontList.length; j++) {
              if (fontList[j].equals(face)) {
                styleAttrs.put(TextAttribute.FAMILY, face);
                continue label116;
              } 
            } 
          } 
        } 
        int startIndex = styledText.length();
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(styleAttrs, startIndex, styledText.length()));
      } else if (node.getNodeType() == 1 && "br".equalsIgnoreCase(node.getNodeName())) {
        styledText.append("\n");
        int startIndex = styledText.length();
        resizeRuns(styledText.getRuns(), startIndex, 1);
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(new HashMap(), startIndex, styledText.length()));
        if (startIndex < styledText.length()) {
          styledText.append("\n");
          resizeRuns(styledText.getRuns(), startIndex, 1);
        } 
      } else if (node.getNodeType() == 1 && "li".equalsIgnoreCase(node.getNodeName())) {
        String tmpText = styledText.getText();
        if (tmpText.length() > 0 && !tmpText.endsWith("\n"))
          styledText.append("\n"); 
        styledText.append(" • ");
        int startIndex = styledText.length();
        resizeRuns(styledText.getRuns(), startIndex, 1);
        parseStyle(styledText, node);
        styledText.addRun(new JRStyledText.Run(new HashMap(), startIndex, styledText.length()));
        Node nextNode = node.getNextSibling();
        String textContent = getFirstTextOccurence(nextNode);
        if (nextNode != null && (nextNode.getNodeType() != 1 || !"li".equalsIgnoreCase(nextNode.getNodeName())) && (textContent == null || !textContent.startsWith("\n"))) {
          styledText.append("\n");
          resizeRuns(styledText.getRuns(), startIndex, 1);
        } 
      } else if (node.getNodeType() == 1) {
        String nodeName = "<" + node.getNodeName() + ">";
        throw new SAXException("Tag " + nodeName + " is not a valid styled text tag.");
      } 
    } 
  }
  
  private void resizeRuns(List runs, int startIndex, int count) {
    for (int j = 0; j < runs.size(); j++) {
      JRStyledText.Run run = runs.get(j);
      if (run.startIndex <= startIndex && run.endIndex > startIndex - count)
        run.endIndex += count; 
    } 
  }
  
  private StringBuffer writeStyleAttributes(Map parentAttrs, Map attrs) {
    StringBuffer sbuffer = new StringBuffer();
    Object value = attrs.get(TextAttribute.FAMILY);
    Object oldValue = parentAttrs.get(TextAttribute.FAMILY);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("fontName");
      sbuffer.append("=\"");
      sbuffer.append(value);
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.WEIGHT);
    oldValue = parentAttrs.get(TextAttribute.WEIGHT);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("isBold");
      sbuffer.append("=\"");
      sbuffer.append(value.equals(TextAttribute.WEIGHT_BOLD));
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.POSTURE);
    oldValue = parentAttrs.get(TextAttribute.POSTURE);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("isItalic");
      sbuffer.append("=\"");
      sbuffer.append(value.equals(TextAttribute.POSTURE_OBLIQUE));
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.UNDERLINE);
    oldValue = parentAttrs.get(TextAttribute.UNDERLINE);
    if ((value == null && oldValue != null) || (value != null && !value.equals(oldValue))) {
      sbuffer.append(" ");
      sbuffer.append("isUnderline");
      sbuffer.append("=\"");
      sbuffer.append((value != null));
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.STRIKETHROUGH);
    oldValue = parentAttrs.get(TextAttribute.STRIKETHROUGH);
    if ((value == null && oldValue != null) || (value != null && !value.equals(oldValue))) {
      sbuffer.append(" ");
      sbuffer.append("isStrikeThrough");
      sbuffer.append("=\"");
      sbuffer.append((value != null));
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.SIZE);
    oldValue = parentAttrs.get(TextAttribute.SIZE);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("size");
      sbuffer.append("=\"");
      sbuffer.append(((Float)value).intValue());
      sbuffer.append("\"");
    } 
    value = attrs.get(JRTextAttribute.PDF_FONT_NAME);
    oldValue = parentAttrs.get(JRTextAttribute.PDF_FONT_NAME);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("pdfFontName");
      sbuffer.append("=\"");
      sbuffer.append(value);
      sbuffer.append("\"");
    } 
    value = attrs.get(JRTextAttribute.PDF_ENCODING);
    oldValue = parentAttrs.get(JRTextAttribute.PDF_ENCODING);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("pdfEncoding");
      sbuffer.append("=\"");
      sbuffer.append(value);
      sbuffer.append("\"");
    } 
    value = attrs.get(JRTextAttribute.IS_PDF_EMBEDDED);
    oldValue = parentAttrs.get(JRTextAttribute.IS_PDF_EMBEDDED);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("isPdfEmbedded");
      sbuffer.append("=\"");
      sbuffer.append(value);
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.FOREGROUND);
    oldValue = parentAttrs.get(TextAttribute.FOREGROUND);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("forecolor");
      sbuffer.append("=\"");
      sbuffer.append("#");
      sbuffer.append(JRColorUtil.getColorHexa((Color)value));
      sbuffer.append("\"");
    } 
    value = attrs.get(TextAttribute.BACKGROUND);
    oldValue = parentAttrs.get(TextAttribute.BACKGROUND);
    if (value != null && !value.equals(oldValue)) {
      sbuffer.append(" ");
      sbuffer.append("backcolor");
      sbuffer.append("=\"");
      sbuffer.append("#");
      sbuffer.append(JRColorUtil.getColorHexa((Color)value));
      sbuffer.append("\"");
    } 
    return sbuffer;
  }
  
  private String getFirstTextOccurence(Node node) {
    if (node != null) {
      if (node.getNodeValue() != null)
        return node.getNodeValue(); 
      NodeList nodeList = node.getChildNodes();
      for (int i = 0; i < nodeList.getLength(); i++) {
        String firstOccurence = getFirstTextOccurence(nodeList.item(i));
        if (firstOccurence != null)
          return firstOccurence; 
      } 
    } 
    return null;
  }
  
  public void error(SAXParseException e) {}
  
  public void fatalError(SAXParseException e) {}
  
  public void warning(SAXParseException e) {}
}
