/**
 * ===========================================
 * LibLayout : a free Java layouting library
 * ===========================================
 *
 * Project Info:  http://reporting.pentaho.org/liblayout/
 *
 * (C) Copyright 2006-2007, by Pentaho Corporation and Contributors.
 *
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation;
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
 * in the United States and other countries.]
 *
 * ------------
 * $Id: CSSValueFactory.java 6653 2008-12-02 14:53:40Z tmorgner $
 * ------------
 * (C) Copyright 2006-2007, by Pentaho Corporation.
 */
package org.jfree.layouting.input.style.parser;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.jfree.layouting.LibLayoutBoot;
import org.jfree.layouting.input.style.CSSDeclarationRule;
import org.jfree.layouting.input.style.StyleKey;
import org.jfree.layouting.input.style.StyleKeyRegistry;
import org.jfree.layouting.input.style.values.CSSAttrFunction;
import org.jfree.layouting.input.style.values.CSSCompoundAttrFunction;
import org.jfree.layouting.input.style.values.CSSConstant;
import org.jfree.layouting.input.style.values.CSSFunctionValue;
import org.jfree.layouting.input.style.values.CSSInheritValue;
import org.jfree.layouting.input.style.values.CSSNumericType;
import org.jfree.layouting.input.style.values.CSSNumericValue;
import org.jfree.layouting.input.style.values.CSSStringType;
import org.jfree.layouting.input.style.values.CSSStringValue;
import org.jfree.layouting.input.style.values.CSSValue;
import org.w3c.css.sac.LexicalUnit;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;

/**
 * Creation-Date: 25.11.2005, 17:43:38
 *
 * @author Thomas Morgner
 */
public class CSSValueFactory
{
  private static final Log logger = LogFactory.getLog(CSSValueFactory.class);
  public static final String SIMPLE_PREFIX = "org.jfree.layouting.parser.handlers.";
  public static final String COMPOUND_PREFIX = "org.jfree.layouting.parser.compoundhandlers.";

  private HashMap handlers;
  private HashMap compoundHandlers;
  private StyleKeyRegistry registry;

  public CSSValueFactory(final StyleKeyRegistry registry)
  {
    if (registry == null)
    {
      throw new NullPointerException();
    }
    this.registry = registry;
    this.handlers = new HashMap();
    this.compoundHandlers = new HashMap();
    this.registerDefaults();
  }


  public void registerDefaults()
  {
    final Configuration config = LibLayoutBoot.getInstance().getGlobalConfig();
    final Iterator sit = config.findPropertyKeys(SIMPLE_PREFIX);
    while (sit.hasNext())
    {
      final String key = (String) sit.next();
      final String name = key.substring(SIMPLE_PREFIX.length()).toLowerCase();
      final String c = config.getConfigProperty(key);
      final Object module =
              ObjectUtilities.loadAndInstantiate(c, CSSValueFactory.class, CSSValueReadHandler.class);
      if (module instanceof CSSValueReadHandler)
      {
        handlers.put(name, module);
      }
      else
      {
        logger.warn("Invalid module implementation: " + c);
      }
    }

    final Iterator cit = config.findPropertyKeys(COMPOUND_PREFIX);
    while (cit.hasNext())
    {
      final String key = (String) cit.next();
      final String name = key.substring(COMPOUND_PREFIX.length()).toLowerCase();
      final String c = config.getConfigProperty(key);
      final Object module =
              ObjectUtilities.loadAndInstantiate(c, CSSValueFactory.class, CSSCompoundValueReadHandler.class);
      if (module instanceof CSSCompoundValueReadHandler)
      {
        compoundHandlers.put(name, module);
      }
    }
  }


  private CSSValue createValue(final StyleKey key, final LexicalUnit value)
  {
    final CSSValueReadHandler module =
            (CSSValueReadHandler) handlers.get(key.getName());
    if (module == null)
    {
      //  || module instanceof CSSCompoundValueReadHandler
      // Compund handler are more important than simple handlers ..
      return null;
    }

    return module.createValue(key, value);
  }

  public static CSSAttrFunction parseAttrFunction(final LexicalUnit unit)
  {
    if (unit.getLexicalUnitType() != LexicalUnit.SAC_ATTR)
    {
      return null;
    }

    final String attrName = unit.getStringValue().trim();
    final String[] name = StyleSheetParserUtil.parseNamespaceIdent(attrName);
    return new CSSAttrFunction(name[0], name[1]);
  }

  public static boolean isFunctionValue(final LexicalUnit unit)
  {
    if (unit.getLexicalUnitType() != LexicalUnit.SAC_FUNCTION &&
            unit.getLexicalUnitType() != LexicalUnit.SAC_COUNTER_FUNCTION &&
            unit.getLexicalUnitType() != LexicalUnit.SAC_COUNTERS_FUNCTION &&
            unit.getLexicalUnitType() != LexicalUnit.SAC_RGBCOLOR &&
            unit.getLexicalUnitType() != LexicalUnit.SAC_RECT_FUNCTION &&
            unit.getLexicalUnitType() != LexicalUnit.SAC_FUNCTION)
    {
      return false;
    }
    return true;
  }

  private static CSSAttrFunction parseComplexAttrFn(final LexicalUnit parameters)
  {
    if (parameters == null)
    {
      return null;
    }

    final String attrName = parameters.getStringValue().trim();
    final String[] name = StyleSheetParserUtil.parseNamespaceIdent(attrName);

    final LexicalUnit afterComma = parseComma(parameters);
    if (afterComma == null)
    {
      return new CSSAttrFunction(name[0], name[1]);
    }

    final String attrType = parseAttributeType(afterComma);
    if (attrType == null)
    {
      return new CSSAttrFunction(name[0], name[1]);
    }
    else
    {
      return new CSSAttrFunction(name[0], name[1], attrType);
    }
  }

  public static CSSFunctionValue parseFunction(final LexicalUnit unit)
  {
    if (isFunctionValue(unit) == false)
    {
      return null;
    }
    LexicalUnit parameters = unit.getParameters();
    final String functionName = unit.getFunctionName();
    if (parameters == null)
    {
      // no-parameter function include the date() function...
      return new CSSFunctionValue(functionName, new CSSValue[0]);
    }
    if ("attr".equalsIgnoreCase(functionName))
    {
      return parseComplexAttrFn(unit.getParameters());
    }


    final ArrayList contentList = new ArrayList();
    while (parameters != null)
    {
      if (parameters.getLexicalUnitType() == LexicalUnit.SAC_IDENT)
      {
        contentList.add(new CSSConstant(parameters.getStringValue()));
      }
      else if (parameters.getLexicalUnitType() == LexicalUnit.SAC_STRING_VALUE)
      {
        contentList.add(new CSSStringValue(CSSStringType.STRING,
                parameters.getStringValue()));
      }
      else if (CSSValueFactory.isNumericValue(parameters))
      {
        final CSSNumericValue numericValue =
                CSSValueFactory.createNumericValue(parameters);
        if (numericValue == null)
        {
          return null;
        }
        contentList.add(numericValue);
      }
      else if (CSSValueFactory.isLengthValue(parameters))
      {
        final CSSNumericValue lengthValue =
                CSSValueFactory.createLengthValue(parameters);
        if (lengthValue == null)
        {
          return null;
        }
        contentList.add(lengthValue);
      }
      else if (parameters.getLexicalUnitType() == LexicalUnit.SAC_ATTR)
      {
        final CSSAttrFunction attrFn =
                CSSValueFactory.parseAttrFunction(parameters);
        if (attrFn == null)
        {
          return null;
        }
        contentList.add(attrFn);
      }
      else if (parameters.getLexicalUnitType() == LexicalUnit.SAC_URI)
      {
        final CSSStringValue uriValue = CSSValueFactory.createUriValue(
                parameters);
        if (uriValue == null)
        {
          return null;
        }
        contentList.add(uriValue);
      }
      else if (isFunctionValue(parameters))
      {
        final CSSFunctionValue functionValue = parseFunction(parameters);
        if (functionValue == null)
        {
          return null;
        }
        contentList.add(functionValue);
      }
      else
      {
        // parse error: Something we do not understand ...
        return null;
      }
      parameters = CSSValueFactory.parseComma(parameters);
    }
    final CSSValue[] paramVals = (CSSValue[])
            contentList.toArray(new CSSValue[contentList.size()]);
    return new CSSFunctionValue(functionName, paramVals);
  }


  private static String parseAttributeType(final LexicalUnit unit)
  {
    if (unit == null)
    {
      return null;
    }
    if (unit.getLexicalUnitType() == LexicalUnit.SAC_IDENT)
    {
      return unit.getStringValue();
    }
    return null;
  }

  private void setCompundInheritValue(final String name,
                                      final CSSDeclarationRule rule,
                                      final boolean important)
  {
    final CSSCompoundValueReadHandler handler =
            (CSSCompoundValueReadHandler) compoundHandlers.get(name);
    if (handler == null)
    {
      logger.warn("Got no key for inherited value: " + name);
      return;
    }

    final StyleKey[] keys = handler.getAffectedKeys();
    for (int i = 0; i < keys.length; i++)
    {
      final StyleKey key = keys[i];
      rule.setPropertyValue(key, CSSInheritValue.getInstance());
      rule.setImportant(key, important);
    }
  }

  private void setCompundAttrValue(final String name,
                                   final CSSAttrFunction attr,
                                   final CSSDeclarationRule rule,
                                   final boolean important)
  {


    final CSSCompoundValueReadHandler handler =
            (CSSCompoundValueReadHandler) compoundHandlers.get(name);
    if (handler == null)
    {
      logger.warn("Got no key for compound attr function: " + name);
      return;
    }

    final StyleKey[] keys = handler.getAffectedKeys();
    for (int i = 0; i < keys.length; i++)
    {
      final StyleKey key = keys[i];
      final CSSCompoundAttrFunction cattr = new CSSCompoundAttrFunction
              (name, attr.getNamespace(), attr.getName(), attr.getType());
      rule.setPropertyValue(key, cattr);
      rule.setImportant(key, important);
    }
  }


  public void parseValue(final CSSDeclarationRule rule,
                         final String name,
                         final LexicalUnit value,
                         final boolean important)
          throws CSSParserFactoryException
  {
    if (rule == null)
    {
      throw new NullPointerException("Rule given is null.");
    }

    final String normalizedName = name.toLowerCase();
    final StyleKey key = registry.findKeyByName(normalizedName);
    if (value.getLexicalUnitType() == LexicalUnit.SAC_INHERIT)
    {
      if (key == null)
      {
        setCompundInheritValue(normalizedName, rule, important);
        return;
      }
      rule.setPropertyValue(key, CSSInheritValue.getInstance());
      rule.setImportant(key, important);
      return;
    }

    if (value.getLexicalUnitType() == LexicalUnit.SAC_ATTR)
    {
      final CSSAttrFunction attrFn = parseAttrFunction(value);
      // ATTR function.
      if (attrFn != null)
      {
        if (key == null)
        {
          // Log.warn("Got no key for attribute-function " + normalizedName);
          setCompundAttrValue(normalizedName, attrFn, rule, important);
          return;
        }
        rule.setPropertyValue(key, attrFn);
        rule.setImportant(key, important);
      }
      return;
    }
    else if (isFunctionValue(value) && "attr".equals(value.getFunctionName()))
    {
      // ATTR function (extended version).
      if (key == null)
      {
        logger.warn("Got no key for attribute-function " + normalizedName);
        return;
      }
      final CSSAttrFunction attrFn = parseComplexAttrFn(value.getParameters());
      if (attrFn != null)
      {
        rule.setPropertyValue(key, attrFn);
        rule.setImportant(key, important);
      }
      return;
    }

    if (key != null)
    {
      final CSSValue cssValue = createValue(key, value);
      if (cssValue != null)
      {
        rule.setPropertyValue(key, cssValue);
        rule.setImportant(key, important);
        //Log.debug ("Got value " + key.getName() + " = " + cssValue + "(" + cssValue.getClass() + ") - (important = " + important + ")");
        return;
      }
    }

    final CSSCompoundValueReadHandler module =
            (CSSCompoundValueReadHandler) compoundHandlers.get(normalizedName);
    if (module == null)
    {
      if (key == null)
      {
        logger.info("Unknown style-key: Neither compound handlers nor single-value handers are registered for " + normalizedName);
        return;
      }

      logger.warn("Unparsable value: Got no valid result for " + normalizedName + " (" + value + ')');
      return; // ignore this rule ..
    }
    final Map map = module.createValues(value);
    if (map == null)
    {
      return;
    }
    final Iterator iterator = map.entrySet().iterator();
    while (iterator.hasNext())
    {
      final Map.Entry entry = (Map.Entry) iterator.next();
      final StyleKey entryKey = (StyleKey) entry.getKey();
      final CSSValue mapCssValue = (CSSValue) entry.getValue();

      rule.setPropertyValue(entryKey, mapCssValue);
      rule.setImportant(entryKey, important);
      //Log.debug ("Got value " + entryKey.getName() + " = " + mapCssValue + "(" + mapCssValue.getClass() + ") - (important = " + important + ")");
    }
  }

  public static CSSStringValue createUriValue(final LexicalUnit value)
  {
    if (value.getLexicalUnitType() != LexicalUnit.SAC_URI)
    {
      return null;
    }

    final String uri = value.getStringValue();
    return new CSSStringValue(CSSStringType.URI, uri);
  }

  public static boolean isNumericValue(final LexicalUnit value)
  {
    if (value.getLexicalUnitType() == LexicalUnit.SAC_INTEGER)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_REAL)
    {
      return true;
    }
    return false;
  }


  public static CSSNumericValue createNumericValue(final LexicalUnit value)
  {
    if (value.getLexicalUnitType() == LexicalUnit.SAC_INTEGER)
    {
      return CSSNumericValue.createValue(CSSNumericType.NUMBER,
              value.getIntegerValue());
    }
    if (value.getLexicalUnitType() == LexicalUnit.SAC_REAL)
    {
      return CSSNumericValue.createValue(CSSNumericType.NUMBER,
              value.getFloatValue());
    }
    return null;
  }

  public static boolean isLengthValue(final LexicalUnit value)
  {
    if (value.getLexicalUnitType() == LexicalUnit.SAC_EM)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_EX)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_PIXEL)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_INCH)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_CENTIMETER)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_MILLIMETER)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_PICA)
    {
      return true;
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_POINT)
    {
      return true;
    }
    return false;
  }


  public static CSSNumericValue createLengthValue(final LexicalUnit value)
  {
    if (value.getLexicalUnitType() == LexicalUnit.SAC_INTEGER)
    {
      if (value.getFloatValue() != 0)
      {
        return null;
      }
      return CSSNumericValue.createValue(CSSNumericType.PT, 0);
    }
    if (value.getLexicalUnitType() == LexicalUnit.SAC_EM)
    {
      return CSSNumericValue.createValue(CSSNumericType.EM, value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_EX)
    {
      return CSSNumericValue.createValue(CSSNumericType.EX,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_PIXEL)
    {
      return CSSNumericValue.createValue(CSSNumericType.PX,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_INCH)
    {
      return CSSNumericValue.createValue(CSSNumericType.INCH,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_CENTIMETER)
    {
      return CSSNumericValue.createValue(CSSNumericType.CM,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_MILLIMETER)
    {
      return CSSNumericValue.createValue(CSSNumericType.MM,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_PICA)
    {
      return CSSNumericValue.createValue(CSSNumericType.PC,
              value.getFloatValue());
    }
    else if (value.getLexicalUnitType() == LexicalUnit.SAC_POINT)
    {
      return CSSNumericValue.createValue(CSSNumericType.PT,
              value.getFloatValue());
    }
    return null;
  }

  public static LexicalUnit parseComma(final LexicalUnit value)
  {
    if (value == null)
    {
      return null;
    }

    final LexicalUnit maybeComma = value.getNextLexicalUnit();
    if (maybeComma == null)
    {
      return null;
    }
    if (maybeComma.getLexicalUnitType() == LexicalUnit.SAC_OPERATOR_COMMA)
    {
      return maybeComma.getNextLexicalUnit();
    }
    return null;
  }

}
