/*
 *  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.renderkit.core.xhtml;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.apache.myfaces.trinidad.bean.FacesBean;
import org.apache.myfaces.trinidad.bean.PropertyKey;
import org.apache.myfaces.trinidad.component.core.output.CoreMessages;

import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidad.context.RequestContext;
import org.apache.myfaces.trinidad.util.LabeledFacesMessage;
import org.apache.myfaces.trinidadinternal.util.MessageUtils;

/**
 * Renderer for org.apache.myfaces.trinidad.Messages, family org.apache.myfaces.trinidad.Messages.
 *
 */
public class MessageBoxRenderer extends XhtmlRenderer
{
  public MessageBoxRenderer()
  {
    this(CoreMessages.TYPE);
  }

  protected MessageBoxRenderer(FacesBean.Type type)
  {
    super(type);
  }

  @Override
  protected void findTypeConstants(FacesBean.Type type)
  {
    super.findTypeConstants(type);
    _textKey     = type.findKey("text");
    _messageKey     = type.findKey("message");
    _globalOnlyKey  = type.findKey("globalOnly");

    _headerRenderer = new HeaderRenderer(type);
    _boxRenderer = new BoxRenderer(type);
  }

  @Override
  public boolean getRendersChildren()
  {
    return true;
  }

  @SuppressWarnings("unchecked")
  @Override
  protected void encodeAll(FacesContext context, RenderingContext arc,
      UIComponent component, FacesBean bean) throws IOException
  {
    // Force MessageBox to be re-rendered via PPR, since the set
    // of messages may have changed.
    RequestContext afContext = RequestContext.getCurrentInstance();
    if (afContext != null)
      afContext.addPartialTarget(component);

    ResponseWriter writer = context.getResponseWriter();

    Map<String, String> origSkinResourceMap = arc.getSkinResourceKeyMap();

    // Setup the rendering context, so that default skin selectors of
    // delegate renderers are mapped to those of this renderer
    arc.setSkinResourceKeyMap(_RESOURCE_KEY_MAP);

    // Check if INLINE validation mode is enabled
    boolean inlineValidation =
        RequestContext.ClientValidation.INLINE.equals(
            RequestContext.getCurrentInstance().getClientValidation());
    
    boolean isGlobalOnly = isGlobalOnly(bean);
    
    // Determine if we should render the MessageBox
    boolean renderMsgBox = (isGlobalOnly && context.getMessages(null).hasNext()) ||
      (!isGlobalOnly && inlineValidation) || (!isGlobalOnly && context.getMessages().hasNext());

    if (renderMsgBox)
    {

      if (!isGlobalOnly && inlineValidation && supportsScripting(arc))
      {
        writer.startElement(XhtmlConstants.SCRIPT_ELEMENT, null);
        renderScriptDeferAttribute(context, arc);
        renderScriptTypeAttribute(context, arc);

        // Output the styles required for client-side manipulation of the MessageBox

        // Output style for list of messages
        writer.writeText("TrPage.getInstance().addStyleClassMap( {'", null);
        writer.writeText(SkinSelectors.AF_MESSAGES_LIST_STYLE_CLASS + "':'", null);
        writer.writeText(arc.getStyleClass(SkinSelectors.AF_MESSAGES_LIST_STYLE_CLASS), null);

        // Single entry list uses two styles
        writer.writeText("','" + SkinSelectors.AF_MESSAGES_LIST_SINGLE_STYLE_CLASS + "':'", null);
        writer.writeText(arc.getStyleClass(SkinSelectors.AF_MESSAGES_LIST_STYLE_CLASS), null);
        writer.writeText(" " + arc.getStyleClass(SkinSelectors.AF_MESSAGES_LIST_SINGLE_STYLE_CLASS), null);

        // Output Style for MessageBox Anchors
        writer.writeText("','" + SkinSelectors.LINK_STYLE_CLASS + "':'", null);
        writer.writeText(arc.getStyleClass(SkinSelectors.LINK_STYLE_CLASS), null);
        writer.writeText("'} ); ", null);

        // Output the script that will register the MessageBox with
        // the TrMessageBox javascript class that handles client-side
        // add/remove of messages.
        writer.writeText("TrMessageBox._registerMessageBox(\"", null);
        writer.writeText(getClientId(context, component), null);
        writer.writeText("\");", null);
        writer.endElement("script");
      }

      // Delegate rendering of the outer shell to the BoxRenderer class
      // which will call back to this renderer to output the messages
      _boxRenderer.encodeAll(context, arc, component, bean);
    }
    else
    {
      // Always render an element, for update at PPR-time
      writer.startElement(XhtmlConstants.SPAN_ELEMENT, component);
      renderId(context, component);
      writer.endElement(XhtmlConstants.SPAN_ELEMENT);
    }
    
    // Reset the skin resource map
    arc.setSkinResourceKeyMap(origSkinResourceMap);
  }

  protected void _renderContent(
      FacesContext context,
      RenderingContext arc,
      UIComponent component,
      FacesBean bean)
      throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();

    boolean globalOnly = isGlobalOnly(bean);

    // TODO - Merge styles into AF_MESSAGES_STYLE_CLASS
    writer.startElement(XhtmlConstants.DIV_ELEMENT, component);
    renderStyleClass(context, arc, SkinSelectors.AF_MESSAGES_BODY_STYLE_CLASS);

    _renderHeader(context, arc, component, bean);

    // Render the 'message' attribute if specified
    String message = getMessage(bean);
    if (message != null)
    {
      writer.startElement(XhtmlConstants.DIV_ELEMENT, null);
      renderStyleClass(context, arc, SkinSelectors.AF_MESSAGES_MESSAGE_TEXT_STYLE_CLASS);
      writer.write(message);
      writer.endElement(XhtmlConstants.DIV_ELEMENT);
    }

    // Render messages as a list
    writer.startElement("ol", null);

    // Output an id for the list so client-side validation can
    // easily access the element
    String listId = getClientId(context, component) + "__LIST__";
    writer.writeAttribute(XhtmlConstants.ID_ATTRIBUTE, listId, null);

    // Switch list style depending if no. of messages is 1 or >1
    String[] styleClasses = null;
    if (MessageUtils.multipleMessagesQueued(context, globalOnly))
      styleClasses = new String[] {SkinSelectors.AF_MESSAGES_LIST_STYLE_CLASS};
    else
      styleClasses = new String[] {SkinSelectors.AF_MESSAGES_LIST_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_LIST_SINGLE_STYLE_CLASS};

    renderStyleClasses(context, arc, styleClasses);

    _renderGlobalMessages(context, arc, component, bean);

    if (!globalOnly)
      _renderComponentMessages(context, arc, component, bean);

    // End of list
    writer.endElement("ol");

    writer.endElement(XhtmlConstants.DIV_ELEMENT);
  }

  @SuppressWarnings("unchecked")
  protected void _renderGlobalMessages(
      FacesContext context,
      RenderingContext arc,
      UIComponent component,
      FacesBean bean)
      throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();

    // Get all messages without and id
    Iterator<FacesMessage> msgIter = context.getMessages(null);
    while (msgIter.hasNext())
    {
      FacesMessage msg = msgIter.next();

      writer.startElement("li", null);

      String text = MessageUtils.getGlobalMessage(arc, msg.getSummary(), msg.getDetail());
      renderPossiblyFormattedText(context, text);

      writer.endElement("li");
    }
  }

  @SuppressWarnings("unchecked")
  protected void _renderComponentMessages(
      FacesContext context,
      RenderingContext arc,
      UIComponent component,
      FacesBean bean)
      throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();

    Iterator<String> idIter = context.getClientIdsWithMessages();
    while (idIter.hasNext())
    {
      String id = idIter.next();

      // Skip global messages
      if (id == null)
        continue;

      Iterator<FacesMessage> msgIter = context.getMessages(id);
      while (msgIter.hasNext())
      {
        FacesMessage msg = msgIter.next();

        writer.startElement("li", null);

        _renderMessageAnchor(context, arc, msg, id);

        String text = MessageUtils.getClientMessage(arc, msg.getSummary(), msg.getDetail());
        
        // If the first two characters are "- ", assume it's due to af_messages.LIST_FORMAT_private;
        // alternatively, we could change the value of this key in CoreBundle.xrts, located in:
        // trinidad-impl\src\main\xrts\org\apache\myfaces\trinidadinternal\renderkit\core\resource
        // If the label is null, then we don't want to render the "- ".
        boolean isNullLabel = false;
        if (msg instanceof LabeledFacesMessage)
        {
          LabeledFacesMessage labeledMsg = (LabeledFacesMessage) msg;
          String labelString = labeledMsg.getLabelAsString(context);
          if (labelString == null || labelString.length() == 0)
            isNullLabel = true;
        }
        else
        {
          isNullLabel = true;
        }

        if (isNullLabel && text.charAt(0) == '-' && text.charAt(1) == ' ')
          text = text.substring(2);

        renderPossiblyFormattedText(context, text);

        writer.endElement("li");
      }
    }
  }

  protected void _renderHeader(
      FacesContext        context,
      RenderingContext arc,
      UIComponent         component,
      FacesBean           bean) throws IOException
  {
    delegateRenderer(context, arc, component, bean, _headerRenderer);
  }

  protected void _renderMessageAnchor(
      FacesContext        context,
      RenderingContext arc,
      FacesMessage msg,
      String componentId) throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();

    if (componentId == null)
      return;

    // Anchor rendering currently only possible for messages that
    // contain a label, but we could use summary text in future
    if (msg instanceof LabeledFacesMessage)
    {
      LabeledFacesMessage labeledMsg = (LabeledFacesMessage)msg;
      String labelString = labeledMsg.getLabelAsString(context);
      if (labelString != null && labelString.length() > 0) // check for empty string
      {
        String anchor = MessageUtils.getAnchor(componentId);
        writer.startElement(XhtmlConstants.LINK_ELEMENT, null);
        renderStyleClass(context, arc, SkinSelectors.LINK_STYLE_CLASS);
        writer.writeAttribute(XhtmlConstants.HREF_ATTRIBUTE, "#" + anchor, null);
        writer.write(labelString);
        writer.endElement(XhtmlConstants.LINK_ELEMENT);
        writer.write(' ');
      }
    }
  }

  // Rendering delegate to handle output of the header for the message box
  private class HeaderRenderer extends PanelHeaderRenderer
  {
    public HeaderRenderer(FacesBean.Type type)
    {
      super(type);
    }

    @Override
    protected boolean shouldRenderId(FacesContext context, UIComponent component)
    {
      // Header will always be refreshed as sub-element of parent
      return false;
    }

    @Override
    protected void renderEventHandlers(FacesContext context, FacesBean bean)
        throws IOException
    {
      // Prevent HeaderRenderer from re-rendering event handlers
    }

    @Override
    protected String getMessageType(FacesBean bean)
    {
      String messageType = null;

      FacesMessage.Severity maxSeverity =
        FacesContext.getCurrentInstance().getMaximumSeverity();

      // Map FacesMessage severity to levels expected by panelHeaderRenderer
      if (maxSeverity == null)
        messageType = XhtmlConstants.MESSAGE_TYPE_ERROR;
      else if (FacesMessage.SEVERITY_FATAL.equals(maxSeverity))
        messageType = XhtmlConstants.MESSAGE_TYPE_ERROR;
      else if (FacesMessage.SEVERITY_ERROR.equals(maxSeverity))
        messageType = XhtmlConstants.MESSAGE_TYPE_ERROR;
      else if (FacesMessage.SEVERITY_WARN.equals(maxSeverity))
        messageType = XhtmlConstants.MESSAGE_TYPE_WARNING;
      else if (FacesMessage.SEVERITY_INFO.equals(maxSeverity))
        messageType = XhtmlConstants.MESSAGE_TYPE_INFO;

      return messageType;
    }

    @Override
    protected String getText(RenderingContext arc, FacesBean bean,
        String messageType)
    {
      String text = MessageBoxRenderer.this.getText(bean);
      if (text != null)
        // Use Text attribute of this component for header text
        return text;

      // Otherwise parent will decide text & style based on messageType
      return super.getText(arc, bean, messageType);
    }

    @Override
    protected String getMessageIconName(String messageType)
    {
      String iconName = null;

      // Use the af|messages skin selectors instead of those
      // used by panelHeader
      if (XhtmlConstants.MESSAGE_TYPE_ERROR.equals(messageType))
        iconName = SkinSelectors.AF_MESSAGES_ERROR_ICON_NAME;
      else if (XhtmlConstants.MESSAGE_TYPE_WARNING.equals(messageType))
        iconName = SkinSelectors.AF_MESSAGES_WARNING_ICON_NAME;
      else if (XhtmlConstants.MESSAGE_TYPE_INFO.equals(messageType))
        iconName = SkinSelectors.AF_MESSAGES_INFO_ICON_NAME;
      else if (XhtmlConstants.MESSAGE_TYPE_CONFIRMATION.equals(messageType))
        iconName = SkinSelectors.AF_MESSAGES_CONFIRMATION_ICON_NAME;

      assert ((iconName != null) ||
              XhtmlConstants.MESSAGE_TYPE_NONE.equals(messageType));

      return iconName;
    }

  }

  // Delegate renderer, handles the outer element rendering and
  // provides option to wrap message box using rounded borders etc.
  private class BoxRenderer extends PanelBoxRenderer
  {

    public BoxRenderer(FacesBean.Type type)
    {
      super(type);
    }

    @Override
    protected boolean shouldRenderId(FacesContext context, UIComponent component)
    {
      // As panelBox is handling the outer rendering, then it should render
      // the id of the MessageBox component
      return true;
    }

    @Override
    protected String getBackground(FacesBean bean)
    {
      // Force use of 'light' style, so we know which style
      // to re-map in _RESOURCE_KEY_MAP
      return "light";
    }

    @Override
    protected String getInlineStyle(FacesBean bean)
    {
      String inlineStyle = super.getInlineStyle(bean);

      boolean inlineValidation =
        RequestContext.ClientValidation.INLINE.equals(
            RequestContext.getCurrentInstance().getClientValidation());

      if (!inlineValidation)
        return inlineStyle;

      boolean hasMessages = FacesContext.getCurrentInstance().getMessages().hasNext();

      if (hasMessages)
        return inlineStyle;

      // Ensure the MessageBox is hidden for inline mode when there are no messages
      if (inlineStyle != null)
        return inlineStyle + ";display:none;";

      return "display:none;";
    }

    @Override
    protected boolean hasChildren(UIComponent component)
    {
      // Required to force panelBox to call render properly
      return true;
    }

    @Override
    protected void renderBody(FacesContext context, RenderingContext arc,
        UIComponent component, FacesBean bean, Object icon, Object text) throws IOException
    {
      // Pass control back to MessageBoxRenderer to continue rendering
      // the content of the message box.
      MessageBoxRenderer.this._renderContent(context, arc, component, bean);
    }
  }

  @Override
  protected boolean shouldRenderId(FacesContext context, UIComponent component)
  {
    // Normally BoxRenderer will output the id for this component, but
    // if we're just outputting an empty element for PPR purposes, then
    // this renderer should output the id.
    return true;
  }

  protected String getText(FacesBean bean)
  {
    return (String)this.resolveProperty(bean, _textKey);
  }

  protected String getMessage(FacesBean bean)
  {
    return (String)this.resolveProperty(bean, _messageKey);
  }

  protected boolean isGlobalOnly(FacesBean bean)
  {
    return (Boolean)this.resolveProperty(bean, _globalOnlyKey, true);
  }

  private PropertyKey _textKey;
  private PropertyKey _messageKey;
  private PropertyKey _globalOnlyKey;
  private XhtmlRenderer _headerRenderer;
  private PanelBoxRenderer _boxRenderer;

  // Map panelHeader & panelBox Styles/Icons etc. to this renderer's selectors.
  private static final Map<String, String> _RESOURCE_KEY_MAP;

  static
  {
    _RESOURCE_KEY_MAP = new HashMap<String, String>();

    // translation keys
    _RESOURCE_KEY_MAP.put("af_panelHeader.INFORMATION",
                              "af_messages.INFORMATION");
    _RESOURCE_KEY_MAP.put("af_panelHeader.WARNING",
                              "af_messages.WARNING");
    _RESOURCE_KEY_MAP.put("af_panelHeader.ERROR",
                              "af_messages.ERROR");
    _RESOURCE_KEY_MAP.put("af_panelHeader.CONFIRMATION",
                              "af_messages.CONFIRMATION");
    // icons
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_ERROR_ICON_NAME,
        SkinSelectors.AF_MESSAGES_ERROR_ICON_NAME);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_WARNING_ICON_NAME,
        SkinSelectors.AF_MESSAGES_WARNING_ICON_NAME);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_INFO_ICON_NAME,
        SkinSelectors.AF_MESSAGES_INFO_ICON_NAME);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_CONFIRMATION_ICON_NAME,
        SkinSelectors.AF_MESSAGES_CONFIRMATION_ICON_NAME);

    // styles
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_ERROR_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_ERROR_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_HEADER_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_HEADER_STYLE_CLASS);

    // We forced the use of 'light' style above, so now map it
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_LIGHT_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_STYLE_CLASS);

    // frame styles
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_TOP_START_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_TOP_START_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_TOP_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_TOP_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_TOP_END_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_TOP_END_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_START_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_START_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_END_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_END_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_BOTTOM_START_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_BOTTOM_START_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_BOTTOM_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_BOTTOM_STYLE_CLASS);
    _RESOURCE_KEY_MAP.put(SkinSelectors.AF_PANEL_BOX_BOTTOM_END_STYLE_CLASS,
        SkinSelectors.AF_MESSAGES_BOTTOM_END_STYLE_CLASS);
  }

}
