package org.jboss.seam.excel.css;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.component.UIComponent;

import org.apache.commons.beanutils.PropertyUtils;
import org.jboss.seam.core.Interpolator;
import org.jboss.seam.excel.ExcelWorkbookException;
import org.jboss.seam.excel.ui.UILink;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;

/**
 * CSS parser for the XLS-CSS
 * 
 * @author Nicklas Karlsson (nickarls@gmail.com)
 */
public class CSSParser
{
   // Where to look for the style
   private static final String STYLE_ATTRIBUTE = "style";

   // Where to look for the style class
   private static final String STYLE_CLASS_ATTRIBUTE = "styleClass";

   // What separates multiple style class references
   private static final String STYLE_SHORTHAND_SEPARATOR = " ";

   // What starts a rule block in a CSS file
   private static final String LEFT_BRACE = "{";

   // What ends a rule block in a CSS file
   private static final String RIGHT_BRACE = "}";

   // The style classes that have been read in from e:link referenced, mapped on
   // style class name
   private Map<String, StyleMap> definedStyleClasses = new HashMap<String, StyleMap>();

   // The registered property builders, mapped on attribute name
   private Map<String, PropertyBuilder> propertyBuilders = new HashMap<String, PropertyBuilder>();

   // A cache of previously parsed css, mapped on component
   private Map<UIComponent, StyleMap> cellStyleCache = new HashMap<UIComponent, StyleMap>();

   private Log log = Logging.getLog(CSSParser.class);

   /**
    * Constructor, initializes the property builders
    */
   public CSSParser()
   {
      initPropertyBuilders();
   }

   /**
    * Constructor with stylesheets
    * 
    * @param stylesheets The list of referenced stylesheets in UILink elements
    * @throws MalformedURLException If the URL was bad
    * @throws IOException If the URL could not be read
    */
   public CSSParser(List<UILink> stylesheets) throws MalformedURLException, IOException
   {
      initPropertyBuilders();
      loadStylesheets(stylesheets);
   }

   /**
    * Loads stylesheets (merging by class name)
    * 
    * @param stylesheets The stylesheets to read/merge
    * @throws MalformedURLException If the URL was bad
    * @throws IOException If the URL could not be read
    */
   private void loadStylesheets(List<UILink> stylesheets) throws MalformedURLException, IOException
   {
      for (UILink stylesheet : stylesheets)
      {
         definedStyleClasses.putAll(parseStylesheet(stylesheet.getURL()));
      }
   }

   /**
    * Registers the property builders
    */
   private void initPropertyBuilders()
   {
      propertyBuilders.put(CSSNames.FONT_FAMILY, new PropertyBuilders.FontFamily());
      propertyBuilders.put(CSSNames.FONT_SIZE, new PropertyBuilders.FontSize());
      propertyBuilders.put(CSSNames.FONT_COLOR, new PropertyBuilders.FontColor());
      propertyBuilders.put(CSSNames.FONT_ITALIC, new PropertyBuilders.FontItalic());
      propertyBuilders.put(CSSNames.FONT_SCRIPT_STYLE, new PropertyBuilders.FontScriptStyle());
      propertyBuilders.put(CSSNames.FONT_STRUCK_OUT, new PropertyBuilders.FontStruckOut());
      propertyBuilders.put(CSSNames.FONT_UNDERLINE_STYLE, new PropertyBuilders.FontUnderlineStyle());
      propertyBuilders.put(CSSNames.FONT_BOLD, new PropertyBuilders.FontBold());
      propertyBuilders.put(CSSNames.FONT, new PropertyBuilders.FontShorthand());
      propertyBuilders.put(CSSNames.BACKGROUND_PATTERN, new PropertyBuilders.BackgroundPattern());
      propertyBuilders.put(CSSNames.BACKGROUND_COLOR, new PropertyBuilders.BackgroundColor());
      propertyBuilders.put(CSSNames.BACKGROUND, new PropertyBuilders.BackgroundShorthand());
      propertyBuilders.put(CSSNames.BORDER_LEFT_COLOR, new PropertyBuilders.BorderLeftColor());
      propertyBuilders.put(CSSNames.BORDER_LEFT_LINE_STYLE, new PropertyBuilders.BorderLeftLineStyle());
      propertyBuilders.put(CSSNames.BORDER_LEFT, new PropertyBuilders.BorderLeftShorthand());
      propertyBuilders.put(CSSNames.BORDER_TOP_COLOR, new PropertyBuilders.BorderTopColor());
      propertyBuilders.put(CSSNames.BORDER_TOP_LINE_STYLE, new PropertyBuilders.BorderTopLineStyle());
      propertyBuilders.put(CSSNames.BORDER_TOP, new PropertyBuilders.BorderTopShorthand());
      propertyBuilders.put(CSSNames.BORDER_RIGHT_COLOR, new PropertyBuilders.BorderRightColor());
      propertyBuilders.put(CSSNames.BORDER_RIGHT_LINE_STYLE, new PropertyBuilders.BorderRightLineStyle());
      propertyBuilders.put(CSSNames.BORDER_RIGHT, new PropertyBuilders.BorderRightShorthand());
      propertyBuilders.put(CSSNames.BORDER_BOTTOM_COLOR, new PropertyBuilders.BorderBottomColor());
      propertyBuilders.put(CSSNames.BORDER_BOTTOM_LINE_STYLE, new PropertyBuilders.BorderBottomLineStyle());
      propertyBuilders.put(CSSNames.BORDER_BOTTOM, new PropertyBuilders.BorderBottomShorthand());
      propertyBuilders.put(CSSNames.BORDER, new PropertyBuilders.BorderShorthand());
      propertyBuilders.put(CSSNames.FORMAT_MASK, new PropertyBuilders.FormatMask());
      propertyBuilders.put(CSSNames.ALIGNMENT, new PropertyBuilders.Alignment());
      propertyBuilders.put(CSSNames.INDENTATION, new PropertyBuilders.Indentation());
      propertyBuilders.put(CSSNames.ORIENTATION, new PropertyBuilders.Orientation());
      propertyBuilders.put(CSSNames.LOCKED, new PropertyBuilders.Locked());
      propertyBuilders.put(CSSNames.SHRINK_TO_FIT, new PropertyBuilders.ShrinkToFit());
      propertyBuilders.put(CSSNames.WRAP, new PropertyBuilders.Wrap());
      propertyBuilders.put(CSSNames.VERICAL_ALIGNMENT, new PropertyBuilders.VericalAlignment());
      propertyBuilders.put(CSSNames.COLUMN_WIDTH, new PropertyBuilders.ColumnWidth());
      propertyBuilders.put(CSSNames.COLUMN_AUTO_SIZE, new PropertyBuilders.ColumnAutoSize());
      propertyBuilders.put(CSSNames.COLUMN_HIDDEN, new PropertyBuilders.ColumnHidden());
      propertyBuilders.put(CSSNames.COLUMN_EXPORT, new PropertyBuilders.ColumnExport());
      propertyBuilders.put(CSSNames.COLUMN_WIDTHS, new PropertyBuilders.ColumnWidths());
      propertyBuilders.put(CSSNames.FORCE_TYPE, new PropertyBuilders.ForceType());
   }

   /**
    * Reads data from an URL to a String
    * 
    * @param url The URL to read
    * @return The read data as a String
    * @throws IOException If the stream could not be read
    */
   private static String readCSS(InputStream in) throws IOException
   {
      BufferedReader reader = new BufferedReader(new InputStreamReader(in));
      StringBuffer buffer = new StringBuffer();
      String line;
      while ((line = reader.readLine()) != null)
      {
         buffer.append(line);
      }
      reader.close();
      return buffer.toString();
   }

   /**
    * Parses a style sheet. Really crude. Assumes data is nicely formatted on
    * one line per entry
    * 
    * @param urlString The URL to read
    * @return A map of style class names mapped to StyleMaps
    * @throws MalformedURLException
    * @throws IOException
    */
   private Map<String, StyleMap> parseStylesheet(String urlString) throws MalformedURLException, IOException
   {
      Map<String, StyleMap> styleClasses = new HashMap<String, StyleMap>();
      InputStream cssStream = null;
      if (urlString.indexOf("://") < 0) {
         cssStream = getClass().getResourceAsStream(urlString);
      } else {
         cssStream = new URL(urlString).openStream();
      }
      String css = readCSS(cssStream).toLowerCase();
      int firstBrace = -1;
      int secondBrace = -1;
      while (!"".equals(css))
      {
         firstBrace = css.indexOf(LEFT_BRACE);
         if (firstBrace >= 0)
         {
            secondBrace = css.indexOf(RIGHT_BRACE, firstBrace + 1);
         }
         if (firstBrace >= 0 && secondBrace >= 0 && firstBrace != secondBrace)
         {
            String styleName = css.substring(0, firstBrace).trim();
            if (styleName.startsWith(".")) {
                styleName = styleName.substring(1);
            }
            String styleString = css.substring(firstBrace + 1, secondBrace).trim();
            StyleMap styleMap = StyleStringParser.of(styleString, propertyBuilders).parse();
            styleClasses.put(styleName, styleMap);
            css = css.substring(secondBrace + 1);
         }
         else
         {
            css = "";
         }
      }
      return styleClasses;
   }

   /**
    * Gets style from a component
    * 
    * @param component The component to examine
    * @return null if not found, otherwise style string
    */
   public static String getStyle(UIComponent component)
   {
      return getStyleProperty(component, STYLE_ATTRIBUTE);
   }

   /**
    * Gets style class from a component
    * 
    * @param component The component to examine
    * @return null if not found, otherwise style class(es) string
    */
   public static String getStyleClass(UIComponent component)
   {
      return getStyleProperty(component, STYLE_CLASS_ATTRIBUTE);
   }

   /**
    * Reads a property from a component
    * 
    * @param component The component to examine
    * @param field The field to read
    * @return The value from the field
    */
   private static String getStyleProperty(UIComponent component, String field)
   {
      try
      {
         return (String) PropertyUtils.getProperty(component, field);
      }
      catch (NoSuchMethodException e)
      {
         // No panic, no property
         return null;
      }
      catch (Exception e)
      {
         String message = Interpolator.instance().interpolate("Could not read field #0 of bean #1", field, component.getId());
         throw new ExcelWorkbookException(message, e);
      }
   }

   /**
    * Cascades on parents, collecting them into list
    * 
    * @param component The component to examine
    * @param styleMaps The list of collected style maps
    * @return The list of style maps
    */
   private List<StyleMap> cascadeStyleMap(UIComponent component, List<StyleMap> styleMaps)
   {
      styleMaps.add(getStyleMap(component));
      if (component.getParent() != null)
      {
         cascadeStyleMap(component.getParent(), styleMaps);
      }
      return styleMaps;
   }

   /**
    * Gets the cascaded style map for a component. Recurses on parents,
    * collecting style maps. The reverses the list and merges the styles
    * 
    * @param component The component to examine
    * @return The merged style map
    */
   public StyleMap getCascadedStyleMap(UIComponent component)
   {
      List<StyleMap> styleMaps = cascadeStyleMap(component, new ArrayList<StyleMap>());
      Collections.reverse(styleMaps);
      StyleMap cascadedStyleMap = new StyleMap();
      for (StyleMap styleMap : styleMaps)
      {
         cascadedStyleMap.putAll(styleMap);
      }
      return cascadedStyleMap;
   }

   /**
    * Gets a style map for a component (from cache if available)
    * 
    * @param component The component to examine
    * @return The style map of the component
    */
   private StyleMap getStyleMap(UIComponent component)
   {
      if (cellStyleCache.containsKey(component))
      {
         return cellStyleCache.get(component);
      }

      StyleMap styleMap = new StyleMap();

      String componentStyleClass = getStyleProperty(component, STYLE_CLASS_ATTRIBUTE);
      if (componentStyleClass != null)
      {
         String[] styleClasses = StyleStringParser.trimArray(componentStyleClass.split(STYLE_SHORTHAND_SEPARATOR));
         for (String styleClass : styleClasses)
         {
            if (!definedStyleClasses.containsKey(styleClass))
            {
               log.warn("Uknown style class #0", styleClass);
               continue;
            }
            styleMap.putAll(definedStyleClasses.get(styleClass));
         }
      }

      String componentStyle = getStyleProperty(component, STYLE_ATTRIBUTE);
      if (componentStyle != null)
      {
         styleMap.putAll(StyleStringParser.of(componentStyle, propertyBuilders).parse());
      }

      cellStyleCache.put(component, styleMap);
      return styleMap;
   }

//   private StyleMap parseStyleString(String styleString)
//   {
//      StyleMap styleMap = new StyleMap();
//
//      String[] styles = trimArray(styleString.split(STYLES_SEPARATOR));
//      for (String style : styles)
//      {
//         int breakpoint = style.indexOf(STYLE_NAME_VALUE_SEPARATOR);
//         if (breakpoint < 0) {
//             log.warn("Style component #0 should be of form <key>#1<value>", style, STYLE_NAME_VALUE_SEPARATOR);
//             continue;
//         }
//         String styleName = style.substring(0, breakpoint).toLowerCase().trim();
//         if (!propertyBuilders.containsKey(styleName))
//         {
//            log.warn("No property builder (unknown style) for property #0", styleName);
//            continue;
//         }
//         PropertyBuilder propertyBuilder = propertyBuilders.get(styleName);
//         String styleValue = style.substring(breakpoint + 1);
//         String[] styleValues = trimArray(styleValue.trim().split(STYLE_SHORTHAND_SEPARATOR));
//         styleMap.putAll(propertyBuilder.parseProperty(styleName, styleValues));
//      }
//
//      return styleMap;
//   }

   /**
    * Setter for stylesheets. Loads them also.
    * 
    * @param stylesheets The stylesheets to load
    * @throws MalformedURLException If the URL is bad
    * @throws IOException If the URL cannot be read
    */
   public void setStylesheets(List<UILink> stylesheets) throws MalformedURLException, IOException
   {
      loadStylesheets(stylesheets);
   }
}
