/*
 *  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.myfaces.trinidadinternal.skin;

import java.io.IOException;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Stack;

import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.context.LocaleContext;
import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidad.skin.Skin;
import org.apache.myfaces.trinidad.skin.Icon;

import org.apache.myfaces.trinidadinternal.skin.icon.ReferenceIcon;

import org.apache.myfaces.trinidadinternal.style.StyleContext;
import org.apache.myfaces.trinidadinternal.style.xml.StyleSheetDocumentUtils;
import org.apache.myfaces.trinidadinternal.style.xml.parse.StyleSheetDocument;

/**
 * A Skin which extends another Skin, possibly adding
 * customizations.
 * @version $Name:  $ ($Revision: adfrt/faces/adf-faces-impl/src/main/java/oracle/adfinternal/view/faces/skin/SkinExtension.java#0 $) $Date: 10-nov-2005.18:58:55 $
 */
public class SkinExtension extends SkinImpl
{
  /**
   * Constructs a SkinExtension of id and family and renderKitId. It also
   * sets the styleSheetname and a resourceBundle name.
   * 
   * @param baseSkin The base Skin that this custom
   *        Skin "extends". If it is a Skin designed for "org.apache.myfaces.trinidad.desktop"
   *        render-kit-id, then its base skin should be SimpleDesktopSkin.
   *        If it is a Skin designed for "org.apache.myfaces.trinidad.pda" render-kit-id,
   *        then its base skin should be SimplePdaSkin.
   *        Must be non-null.
   * @param id A string which can be used to uniquely identify the
   *           Skin .
   *           Must be non-null.
   * @param family The Skin family name that this
   *               SkinExtension belongs to. For example, you might have
   *               a Skin that makes your pages look purple for the
   *               desktop renderkit and a Skin that makes your pages
   *               look purple for the pda renderkit.
   *               You can set the skin-family to "purple" in
   *               trinidad-config.xml, and the Skin with skin-family
   *               and render-kit-id match will be chosen.
   *               Must be non-null.
   * @param renderKitId The render-kit-id that this Skin is designed for.
   * @param styleSheetName The name of the stylesheet for this Skin.
   * @param resourceBundleName The name of the resource bundle for this Skin
   *    to be used to translate strings that a renderer renders.

   * @throws NullPointerException if baseSkin, id, or family is null.
   * 
   */
  public SkinExtension(
    Skin baseSkin,
    String id,
    String family,
    String renderKitId,
    String styleSheetName,
    String resourceBundleName
    )
  {
    if (baseSkin == null)
      throw new NullPointerException("Null baseSkin");
    if (id == null)
      throw new NullPointerException(_LOG.getMessage(
        "NULL_SKIN_ID"));
    if (family == null)
      throw new NullPointerException("Null family");
    if (renderKitId == null)
      renderKitId = _DEFAULT_RENDERKIT;


    _baseSkin = (SkinImpl)baseSkin;
    _id = id;
    _family = family;
    _renderKitId = renderKitId;
    _styleSheetName = styleSheetName;
    _bundleName = resourceBundleName;
    _translationSourceVB = null;
  }
  
  /**
   * Constructs a SkinExtension of id and family and renderKitId. It also
   * sets the styleSheetname and a translation-source (instead of a resource
   * bundle name).
   * 
   * @param baseSkin The base Skin that this custom
   *        Skin "extends". If it is a Skin designed for "org.apache.myfaces.trinidad.desktop"
   *        render-kit-id, then its base skin should be SimpleDesktopSkin.
   *        If it is a Skin designed for "org.apache.myfaces.trinidad.pda" render-kit-id,
   *        then its base skin should be SimplePdaSkin.
   *        Must be non-null.
   * @param id A string which can be used to uniquely identify the
   *           Skin .
   *           Must be non-null.
   * @param family The Skin family name that this
   *               SkinExtension belongs to. For example, you might have
   *               a Skin that makes your pages look purple for the
   *               desktop renderkit and a Skin that makes your pages
   *               look purple for the pda renderkit.
   *               You can set the skin-family to "purple" in
   *               trinidad-config.xml, and the Skin with skin-family
   *               and render-kit-id match will be chosen.
   *               Must be non-null.
   * @param renderKitId The render-kit-id that this Skin is designed for.
   * @param styleSheetName The name of the stylesheet for this Skin.
   * @param translationSourceValueBinding 
   *          A ValueBinding that points to a translation source of type
   *          Map or ResourceBundle. This can be used instead of a 
   *          resource bundle name.
   * @throws NullPointerException if baseSkin, id, or family is null.
   * 
   */
  public SkinExtension(
    Skin   baseSkin,
    String id,
    String family,
    String renderKitId,
    String styleSheetName,
    ValueBinding translationSourceValueBinding
    )
  {
    if (baseSkin == null)
      throw new NullPointerException("Null baseSkin");
    if (id == null)
      throw new NullPointerException(_LOG.getMessage(
        "NULL_SKIN_ID"));
    if (family == null)
      throw new NullPointerException("Null family");
    if (renderKitId == null)
      renderKitId = _DEFAULT_RENDERKIT;

    _baseSkin = (SkinImpl)baseSkin;
    _id = id;
    _family = family;
    _renderKitId = renderKitId;
    _styleSheetName = styleSheetName;
    _bundleName = null;
    _translationSourceVB = translationSourceValueBinding;
  }

  
  /**
   * Constructs a SkinExtension of id and family and renderKitId. It also
   * sets the styleSheetname, but not the resource bundle name or 
   * translation source. This is the most frequently-used constructor.
   *
   * @param baseSkin The base Skin that this custom
   *        Skin "extends". If it is a Skin designed for "org.apache.myfaces.trinidad.desktop"
   *        render-kit-id, then its base skin should be SimpleDesktopSkin.
   *        If it is a Skin designed for "org.apache.myfaces.trinidad.pda" render-kit-id,
   *        then its base skin should be SimplePdaSkin.
   *        Must be non-null.
   * @param id A string which can be used to uniquely identify the
   *           Skin .
   *           Must be non-null.
   * @param family The Skin family name that this
   *               SkinExtension belongs to. For example, you might have
   *               a Skin that makes your pages look purple for the
   *               desktop renderkit and a Skin that makes your pages
   *               look purple for the pda renderkit.
   *               You can set the skin-family to "purple" in
   *               trinidad-config.xml, and the Skin with skin-family
   *               and render-kit-id match will be chosen.
   *               Must be non-null.
   * @param renderKitId The render-kit-id that this Skin is designed for.
   * @param styleSheetName The name of the stylesheet for this Skin.
   *
   * @throws NullPointerException if baseSkin, id, or family is null.
   * 
   */  
  public SkinExtension(
    Skin baseSkin,
    String id,
    String family,
    String renderKitId,
    String styleSheetName
    )
  {
    if (baseSkin == null)
      throw new NullPointerException("Null baseSkin");
    if (id == null)
      throw new NullPointerException(_LOG.getMessage(
        "NULL_SKIN_ID"));
    if (family == null)
      throw new NullPointerException("Null family");
    if (renderKitId == null)
      renderKitId = _DEFAULT_RENDERKIT;

    _baseSkin = (SkinImpl)baseSkin;
    _id = id;
    _family = family;
    _renderKitId = renderKitId;
    _styleSheetName = styleSheetName;
    _bundleName = null;
    _translationSourceVB = null;
  }
  /**
   * Creates a Skin which extends the specified base
   * Skin.
   *
   * @param baseSkin The base Skin that this custom
   *        Skin "extends". If it is a Skin designed for "org.apache.myfaces.trinidad.desktop"
   *        render-kit-id, then its base skin should be SimpleDesktopSkin.
   *        If it is a Skin designed for "org.apache.myfaces.trinidad.pda" render-kit-id,
   *        then its base skin should be SimplePdaSkin.
   *        Must be non-null.
   * @param id A string which can be used to uniquely identify the
   *           Skin .
   *           Must be non-null.
   * @param family The Skin family name that this
   *               SkinExtension belongs to. For example, you might have
   *               a Skin that makes your pages look purple for the
   *               desktop renderkit and a Skin that makes your pages
   *               look purple for the pda renderkit.
   *               You can set the skin-family to "purple" in
   *               trinidad-config.xml, and the Skin with skin-family
   *               and render-kit-id match will be chosen.
   *               Must be non-null.
   * @param renderKitId The render-kit-id that this Skin is designed for.
   * @throws NullPointerException if baseSkin, id, or family is null.
   * @deprecated Use the constructor that also contains styleSheetName
   *
   */
  public SkinExtension(
    Skin baseSkin,
    String id,
    String family,
    String renderKitId
    )
  {
    this(baseSkin, id, family, renderKitId, null);
  }



  /**
   * Returns the base Skin which this custom Skin
   * "extends".
   */
  public Skin getBaseSkin()
  {
    return _baseSkin;
  }

  /**
   * Returns the id of this custom Skin.
   */
  @Override
  public String getId()
  {
    return _id;
  }

  /**
   * Returns the name of the Skin family that this
   * SkinExtension belongs to.
   */
  @Override
  public String getFamily()
  {
    return _family;
  }

  /**
   * Returns the name of the style sheet for this Skin if
   * one has been set
   * @see #setStyleSheetName(String)
   */
  @Override
  public String getStyleSheetName()
  {
    return _styleSheetName;
  }

  /**
   * Returns the name of the render-kit-id for this Skin.
   */
  @Override
  public String getRenderKitId()
  {
    return _renderKitId;
  }


  /**
   * Returns the name of the bundle for the SkinExtension.
   * Note: A skin cannot have both a bundleName and a translation source
   * value expression. If they do, then the bundlename takes precedence.
   */
  @Override
  public String getBundleName()
  {
    return _bundleName;
  }

  /**
   * Returns the name of the bundle for this SkinExtension.
   * @deprecated Use the constructor that takes a resourceBundleName instead
   */
  public void setBundleName(String bundleName)
  {
     // TODO take out method once sufficient time has past since deprecation
    // in July, 2007
    _bundleName = bundleName;
  }

  /**
   * Returns the translation source ValueBinding for the SkinExtension.
   * Note: A skin cannot have both a bundleName and a translation source
   * value binding. If they do, then the bundlename takes precedence.
   * A translation source can be a map of keys/values or a ResourceBundle.
   */
  @Override
  public ValueBinding getTranslationSourceValueBinding()
  {
    return _translationSourceVB;
  }
  
  /**
   * Override of Skin.registerIcon().
   */
  @Override
  public void registerIcon(
    String  iconName,
    Icon    icon
    )
  {
    // If a null icon is being registered, use a NullIcon
    // as a placeholder so that we don't look for a non-null
    // Icon in the base LAF.
    if (icon == null)
      icon = _NULL_ICON;

    super.registerIcon(iconName, icon);
  }

  /**
   * Returns the styleClassMap for this extension
   */
    @Override
  public Map<String, String> getStyleClassMap(
       RenderingContext arc
     )
  {
    return _baseSkin.getStyleClassMap(arc);
  }



  /**
    * Override of Skin.getTranslatedValue() which
    * supports pulling translations from Skin and if not found from the base Skin.
  */
  @Override
  public Object getTranslatedValue(
    LocaleContext lContext,
    String        key
    ) throws MissingResourceException
  {
    // Look for the skin's translated value 
    // -first Skin's translation source, then SkinAddition translation sources.
    // A translation source is either a bundleName or 
    // a translationSourceValueBinding
    // If that's not found, then look in the base skin's translated value.
    // getCachedTranslatedValue will protect against MissingResourceExceptions

    Object translatedValue = getCachedTranslatedValue(lContext, key);

    if (translatedValue == null)
    {
      translatedValue = getBaseSkin().getTranslatedValue(lContext, key);
      // Cache the non-null translated value with the SkinExtension to avoid looking
      // at the base skin's map.
      if (translatedValue != null)
        putTranslatedValueInLocaleCache(lContext, key, translatedValue);

    }

    return translatedValue;
  }
   
  /**
   * Try to pull a locally set property, if null
   * pull a property from the base skin.
   * This means you cannot set a local property to null and expect it to
   * "null out" a property on the base skin.
   */
  @Override
  public Object getProperty(Object key)
  {
    Object value = super.getProperty(key);

    if ( value == null)
    {
      Skin baseSkin = getBaseSkin();
      value =  baseSkin.getProperty(key);
    }

    return value;

  }

  /**
   * Override of Skin.getIcon() to look in the base skin for the icon
   * if it isn't registered yet with this skin.
   */
  @Override
  public Icon getIcon(
    String  iconName,
    boolean resolve
    )
  {
    // First check to see if we already have an Icon registered.
    // it might be a ReferenceIcon. If so, resolve the reference and
    // register the true icon (if resolve is true)

    Icon icon = super.getIcon(iconName, false);

    if (icon != null)
    {
      if (icon == _NULL_ICON)
        return null;

      if (resolve)
      {
        // if icon is a ReferenceIcon, I need to get the actual icon, and
        // re-register it.
        if (icon instanceof ReferenceIcon)
        {
          // go find the actual icon
          Icon resolvedIcon = _resolveReferenceIcon((ReferenceIcon)icon,
                                                     null);

          // register icon so we don't have to resolve reference next time.
          registerIcon(iconName, resolvedIcon);

          return resolvedIcon;
        }
      }
      return icon;
    }

    // If we don't have the icon locally, check the base skin.

    Skin baseSkin = getBaseSkin();
    // get baseSkin's icon. Don't resolve to a real icon. If it is a
    // ReferenceIcon, return the ReferenceIcon.
    icon = baseSkin.getIcon(iconName, false);

    // we found the icon on the base Skin, but it is a ReferenceIcon.
    // find the actual icon
    if (resolve)
    {
      if (icon instanceof ReferenceIcon)
      {
        Icon resolvedIcon = _resolveReferenceIcon((ReferenceIcon)icon,
                                                   null);
        // register icon so we don't have to resolve reference next time.
        registerIcon(iconName, resolvedIcon);

        return resolvedIcon;
      }
    }

    // If we found the icon in the base Skin, register it on ourselves
    // so that we don't have to repeatedly retrieve the icon from
    // the base Skin.
    registerIcon(iconName, icon);


    return icon;
  }

  /**
   * Sets the name of the style sheet for this Skin.
   * @deprecated Use the SkinExtension constructor that takes a styleSheetName instead.
   */
  public void setStyleSheetName(String styleSheetName)
  {
    _styleSheetName = styleSheetName;
  }

  /**
   * Override of Skin.getStyleSheetDocument() which merges
   * styles from the base Skin's style sheet and the
   * SkinExtension's style sheet.
   */
  @Override
  public StyleSheetDocument getStyleSheetDocument(StyleContext context)
  {
    // The SkinExtension's StyleSheetDocument is produced
    // by merging the styles provided by the base Skin with
    // the styles provided by the SkinExtension's own style
    // sheet.

    // Get the StyleSheetDocument from the base Skin
    SkinImpl baseSkin = (SkinImpl) getBaseSkin();
    StyleSheetDocument baseDocument = baseSkin.getStyleSheetDocument(context);

    // Get the StyleSheetDocument for the SkinExtension - we
    // just use super.getStyleSheetDocument() to get our own styles
    StyleSheetDocument extensionDocument = super.getStyleSheetDocument(
                                                   context);

    // Now, check to see if either of the StyleSheetDocuments have
    // changed. We synchronize here to prevent multiple threads coming
    // through and mucking with our StyleSheetDocument instance variables.
    synchronized (this)
    {
      if ((baseDocument != _baseStyleSheetDocument) ||
          (extensionDocument != _extensionStyleSheetDocument))
      {
        _baseStyleSheetDocument = baseDocument;
        _extensionStyleSheetDocument = extensionDocument;

        // One of the StyleSheetDocuments has changed - rebuild
        // the "full" StyleSheetDocument
        _fullStyleSheetDocument =
          StyleSheetDocumentUtils.mergeStyleSheetDocuments(baseDocument,
                                                           extensionDocument);
      }

      return _fullStyleSheetDocument;
    }
  }

  /**
   * Find the actual icon
   * @param refIcon a ReferenceIcon instance
   * @param referencedIconStack  The stack of reference icon names which have
   *          already been visited.  Used to detect circular dependencies.
   * @return icon which is resolved. i.e., it is not a ReferenceIcon.
   */
  private Icon _resolveReferenceIcon(
    ReferenceIcon refIcon,
    Stack<String> referencedIconStack)
  {

    String refName = refIcon.getName();

    // make sure we don't have a circular dependency
    if ( _stackContains(referencedIconStack, refName))
    {
      if (_LOG.isWarning())
        _LOG.warning(_CIRCULAR_INCLUDE_ERROR + refName);
      return null;
    }

    if (referencedIconStack == null)
    {
      // -= Simon Lessard =-
      // TODO: Check if something better than Stack can be used
      referencedIconStack = new Stack<String>();
    }

    referencedIconStack.push(refName);
    //

    // see if the referenced icon is registered already registered in this skin.
    Icon icon = super.getIcon(refName, false);

    if (icon != null)
    {
      if (icon instanceof ReferenceIcon)
      {
          Icon resolvedIcon = _resolveReferenceIcon((ReferenceIcon)icon,
                                                     referencedIconStack);
          return resolvedIcon;
      }
      else
      {
        return icon;
      }
    }
    else
    {

      Skin baseSkin = getBaseSkin();
      icon = baseSkin.getIcon(refName, false);

      if (icon instanceof ReferenceIcon)
      {
        Icon resolvedIcon = _resolveReferenceIcon((ReferenceIcon)icon,
                                                   referencedIconStack);
        return resolvedIcon;
      }
      else
      {
        return icon;
      }
    }
  }

    // Tests whether the value is present in the (possibly null) stack.
  private static boolean _stackContains(
      Stack<String> stack,
      Object value)
  {
    if (stack == null)
      return false;

    return stack.contains(value);
  }

  private SkinExtension() {}


  // Icon class that we use as a placeholder for null icons
  private static class NullIcon extends Icon
  {
    @Override
    public void renderIcon(
      FacesContext        context,
      RenderingContext    arc,
      Map<String, ? extends Object> attrs
      ) throws IOException
    {
      // null icons don't render anything
    }
  }
  
  private String          _id;
  private String          _family;
  private String          _renderKitId;
  private SkinImpl        _baseSkin;
  private String          _styleSheetName;
  private String          _bundleName;
  private ValueBinding    _translationSourceVB;



  // The StyleSheetDocument for the base LookAndFeel's style sheet
  private StyleSheetDocument _baseStyleSheetDocument;

  // The StyleSheetDocument for the LookAndFeelExtension's style sheet
  private StyleSheetDocument _extensionStyleSheetDocument;

  // The complete StyleSheetDocument which merges the styles
  // provided by the base LookAndFeel with the styles added by
  // the SkinExtension.
  private StyleSheetDocument _fullStyleSheetDocument;

  // Placeholder for null icons
  private static final Icon _NULL_ICON = new NullIcon();
  
  private static final String _DEFAULT_RENDERKIT = 
    "org.apache.myfaces.trinidad.desktop";

  // Error messages
  private static final String _CIRCULAR_INCLUDE_ERROR =
    "Circular dependency detected in skin reference icon ";
  private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(SkinExtension.class);
}
