/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.ruta.engine;

import java.awt.Color;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
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 org.apache.commons.lang3.StringUtils;
import org.apache.uima.cas.FSIterator;
import org.apache.uima.cas.FeatureStructure;
import org.apache.uima.cas.Type;
import org.apache.uima.internal.util.XMLUtils;
import org.apache.uima.jcas.JCas;
import org.apache.uima.ruta.type.RutaColoring;
import org.apache.uima.tools.stylemap.ColorParser;
import org.apache.uima.tools.stylemap.StyleMapEntry;
import org.apache.uima.tools.stylemap.StyleMapXmlParser;
import org.apache.uima.util.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

public class StyleMapFactory {

  private Map<String, String> colorNameMap;

  public StyleMapFactory() {
    initializeColorNameMap();
  }

  private void initializeColorNameMap() {
    colorNameMap = new HashMap<String, String>();
    colorNameMap.put("#000000", "black");
    colorNameMap.put("#c0c0c0", "silver");
    colorNameMap.put("#808080", "gray");
    colorNameMap.put("#ffffff", "white");
    colorNameMap.put("#800000", "maroon");
    colorNameMap.put("#ff0000", "red");
    colorNameMap.put("#800080", "purple");
    colorNameMap.put("#ff00ff", "fuchsia");
    colorNameMap.put("#008000", "green");
    colorNameMap.put("#00ff00", "lime");
    colorNameMap.put("#808000", "olive");
    colorNameMap.put("#ffff00", "yellow");
    colorNameMap.put("#000080", "navy");
    colorNameMap.put("#0000ff", "blue");
    colorNameMap.put("#00ffff", "aqua");
    colorNameMap.put("#000000", "black");
    colorNameMap.put("#add8e6", "lightblue");
    colorNameMap.put("#90ee90", "lightgreen");
    colorNameMap.put("#ffa500", "orange");
    colorNameMap.put("#ffc0cb", "pink");
    colorNameMap.put("#fa8072", "salmon");
    colorNameMap.put("#00ffff", "cyan");
    colorNameMap.put("#ee82ee", "violet");
    colorNameMap.put("#d2b48c", "tan");
    colorNameMap.put("#a52a2a", "brown");
    colorNameMap.put("#ffffff", "white");
    colorNameMap.put("#9370db", "mediumpurple");
    // in other order for lookup
    colorNameMap.put("black", "#000000");
    colorNameMap.put("silver", "#c0c0c0");
    colorNameMap.put("gray", "#808080");
    colorNameMap.put("white", "#ffffff");
    colorNameMap.put("maroon", "#800000");
    colorNameMap.put("red", "#ff0000");
    colorNameMap.put("purple", "#800080");
    colorNameMap.put("fuchsia", "#ff00ff");
    colorNameMap.put("green", "#008000");
    colorNameMap.put("lime", "#00ff00");
    colorNameMap.put("olive", "#808000");
    colorNameMap.put("yellow", "#ffff00");
    colorNameMap.put("navy", "#000080");
    colorNameMap.put("blue", "#0000ff");
    colorNameMap.put("aqua", "#00ffff");
    colorNameMap.put("black", "#000000");
    colorNameMap.put("lightblue", "#add8e6");
    colorNameMap.put("lightgreen", "#90ee90");
    colorNameMap.put("orange", "#ffa500");
    colorNameMap.put("pink", "#ffc0cb");
    colorNameMap.put("salmon", "#fa8072");
    colorNameMap.put("cyan", "#00ffff");
    colorNameMap.put("violet", "#ee82ee");
    colorNameMap.put("tan", "#d2b48c");
    colorNameMap.put("brown", "#a52a2a");
    colorNameMap.put("white", "#ffffff");
    colorNameMap.put("mediumpurple", "#9370db");
  }

  public void createStyleMap(String styleMapLocation, JCas jcas) throws IOException {
    List<StyleMapEntry> styleList = createStyleList(jcas);
    String styleXml = createStyleXml(styleList);
    writeStyleFile(styleXml, styleMapLocation);
  }

  private String createStyleXml(List<StyleMapEntry> styleList) {
    StringBuffer result = new StringBuffer();
    result.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n");
    result.append("<styleMap>\n");

    for (StyleMapEntry e : styleList) {
      result.append("<rule>\n");
      result.append("<pattern>");
      result.append(e.getPattern());
      result.append("</pattern>\n");
      result.append("<label>");
      String label = e.getLabel();
      if (label != null) {
        if (!StringUtils.isEmpty(label))
          result.append(label);
        else
          result.append(e.getAnnotationTypeName());
      } else
        result.append(e.getAnnotationTypeName());

      result.append("</label>\n");
      result.append("<style>");

      String foregroundColor = "#" + Integer.toHexString(e.getForeground().getRGB()).substring(2);
      String backgroundColor = "#" + Integer.toHexString(e.getBackground().getRGB()).substring(2);

      if (colorNameMap.containsKey(foregroundColor)) {
        result.append("color:" + colorNameMap.get(foregroundColor) + ";");
      } else {
        result.append("color:" + foregroundColor + ";");
      }

      if (colorNameMap.containsKey(backgroundColor)) {
        result.append("background:" + colorNameMap.get(backgroundColor) + ";");
      } else {
        result.append("background:" + backgroundColor + ";");
      }
      Boolean ck = e.getChecked();
      String ckString = ck.toString();

      Boolean hid = Boolean.FALSE;
      String hidString = hid.toString();
      // this prevents hidden from being checked,
      // becasue that is not a meaningful combination
      if (hidString.equals("true")) {
        ckString = "false";
      }
      result.append("checked:" + ckString + ";");
      result.append("hidden:" + hidString + ";");

      result.append("</style>\n");
      result.append("</rule>\n");
    }

    result.append("</styleMap>\n");

    return result.toString();
  }

  private void writeStyleFile(String output, String styleMapLocation) throws IOException {
    File file = new File(styleMapLocation);
    FileUtils.saveString2File(output, file);
  }

  private List<StyleMapEntry> createStyleList(JCas jcas) {
    List<StyleMapEntry> result = new ArrayList<StyleMapEntry>();
    Type type = jcas.getCasType(RutaColoring.type);
    FSIterator<FeatureStructure> iterator = jcas.getFSIndexRepository().getAllIndexedFS(type);
    while (iterator.hasNext()) {
      RutaColoring each = (RutaColoring) iterator.next();
      StyleMapEntry entry = new StyleMapEntry();
      entry.setAnnotationTypeName(each.getTargetType());
      String fgColor = each.getFgColor();
      entry.setForeground(parseColorBackground(fgColor));
      String bgColor = each.getBgColor();
      entry.setBackground(parseColorBackground(bgColor));
      entry.setChecked(each.getSelected());
      result.add(entry);
    }

    return result;
  }

  private Color parseColorBackground(String color) {
    if (color == null) {
      return Color.BLACK;
    }
    if (color.startsWith("#")) {
      return Color.decode(color);
    } else {
      String string = colorNameMap.get(color);
      if (string != null)
        return Color.decode(string);
      else
        return Color.LIGHT_GRAY;
    }
  }

  private Color parseColorForeground(String color) {
    if (color.startsWith("#")) {
      return Color.decode(color);
    } else {
      String string = colorNameMap.get(color);
      if (string != null)
        return Color.decode(string);
      else
        return Color.BLACK;
    }
  }

  public Map<String, StyleMapEntry> parseStyleMap(String styleFileString) {
    Map<String, StyleMapEntry> result = new HashMap<String, StyleMapEntry>();
    StyleMapXmlParser smxp = new StyleMapXmlParser(styleFileString);
    ColorParser cp = new ColorParser();
    for (int i = 0; i < smxp.annotType.size(); i++) {
      String typeName = ((String) smxp.annotType.elementAt(i));
      String labelString = ((String) smxp.styleLabel.elementAt(i));
      String featureValue = ((String) smxp.featureValue.elementAt(i));
      String styleColor = smxp.styleColor.elementAt(i).toString();
      StyleMapEntry e = cp.parseAndAssignColors(typeName, featureValue, labelString, styleColor);
      result.put(typeName, e);
    }
    return result;
  }

  public Map<String, StyleMapEntry> parseStyleMapDOM(String styleFileString) {
    Map<String, StyleMapEntry> result = new HashMap<String, StyleMapEntry>();
    if (styleFileString == null) {
      return result;
    }

    File styleMapFile = new File(styleFileString);
    Document parse = null;

    try (FileInputStream stream = new FileInputStream(styleMapFile)) {
      DocumentBuilderFactory documentBuilderFactory = XMLUtils.createDocumentBuilderFactory();
      DocumentBuilder db = documentBuilderFactory.newDocumentBuilder();
      parse = db.parse(stream);
    } catch (SAXException | IOException | ParserConfigurationException e) {
      throw new IllegalStateException(e);
    }
    final Node root = parse.getDocumentElement();
    final NodeList nodeList = root.getChildNodes();

    for (int i = 0; i < nodeList.getLength(); ++i) {
      final Node node = nodeList.item(i);
      final String nodeName = node.getNodeName();
      // "rule" node ?
      if (!nodeName.equals("rule")) { //$NON-NLS-1$
        continue;
      }

      // Collect type or pattern, label, and color text style
      NodeList childrenList = node.getChildNodes();
      String type = ""; //$NON-NLS-1$
      String label = ""; //$NON-NLS-1$
      String colorText = ""; //$NON-NLS-1$
      for (int j = 0; j < childrenList.getLength(); ++j) {
        final Node child = childrenList.item(j);
        final String childName = child.getNodeName();

        if (childName.equals("pattern")) { //$NON-NLS-1$
          type = getTextValue(child);
        } else if (childName.equals("label")) { //$NON-NLS-1$
          label = getTextValue(child);
        } else if (childName.equals("style")) { //$NON-NLS-1$
          colorText = getTextValue(child);
        }
      }

      StyleMapEntry styleMapEntry = getStyleMapEntry(type, label, colorText);
      result.put(styleMapEntry.getAnnotationTypeName(), styleMapEntry);
    }
    return result;
  }

  protected StyleMapEntry getStyleMapEntry(String type, String label, String styleColor) {
    StyleMapEntry result = new StyleMapEntry();
    result.setAnnotationTypeName(type);
    result.setLabel(label);
    StringTokenizer token = new StringTokenizer(styleColor, ":;");
    if (!token.hasMoreTokens()) {
      return null; // No token
    }

    token.nextToken();
    String fgString = token.nextToken().toLowerCase().trim();
    result.setForeground(parseColorForeground(fgString));

    token.nextToken();
    String bgString = token.nextToken().toLowerCase().trim();
    result.setBackground(parseColorBackground(bgString));

    boolean checked = false; // default to Checked
    if (token.hasMoreTokens()) {
      String ck = token.nextToken();
      String tf = token.nextToken();
      if (ck.equals("checked")) {
        if (tf.equals("false")) {
          checked = false;
        } else if (tf.equals("true")) {
          checked = true;
        }
      }
    }
    result.setChecked(checked);

    boolean hidden = false;
    if (token.hasMoreTokens()) {
      String ck = token.nextToken();
      String tf = token.nextToken();
      if (ck.equals("hidden")) {
        if (tf.equals("true")) {
          hidden = true;
        }
      }
    }
    result.setHidden(hidden);
    return result;
  }

  private String getTextValue(final Node node) {
    final Node first = node.getFirstChild();

    if (first != null) {
      return ((Text) first).getNodeValue().trim();

    } else {
      return null;
    }
  }
}
