/*
 *  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.table;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.FacesEvent;

import org.apache.myfaces.trinidad.component.UIXHierarchy;
import org.apache.myfaces.trinidad.component.UIXNavigationTree;
import org.apache.myfaces.trinidad.component.UIXPage;
import org.apache.myfaces.trinidad.component.UIXTree;
import org.apache.myfaces.trinidad.component.UIXTreeTable;
import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidad.context.RequestContext;
import org.apache.myfaces.trinidad.event.FocusEvent;
import org.apache.myfaces.trinidad.event.RowDisclosureEvent;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.model.RowKeySet;
import org.apache.myfaces.trinidad.util.IntegerUtils;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.HiddenLabelUtils;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.SkinSelectors;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.TableRenderer;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.XhtmlConstants;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.XhtmlRenderer;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.XhtmlUtils;

/**
 * Class that handles all tree related events
 */
public final class TreeUtils
{
  private TreeUtils()
  {
  }

  // Names of the parameters used by TreeUtils.  The names
  // need to be public so form value can be prepared;
  // the meaning of the parameters are not public
  public static final String EVENT_PARAM = "event";
  public static final String SOURCE_PARAM = "source";


  /**
   * This method is used for writing the depth of node from it's root in
   * the case tree and HGrid, thus making it more accessible.
   */
  public static void writeNodeLevel(
    FacesContext          context,
    RenderingContext   arc,
    int              depth,
    String           nodeLevelTextKey
    ) throws IOException
  {
    ResponseWriter writer = context.getResponseWriter();
    writer.startElement("label", null);
    String pattern = arc.getTranslatedString(nodeLevelTextKey);
    String levelInfo =
      XhtmlUtils.getFormattedString(pattern,
                                    new String[]
                                    {
                                      IntegerUtils.getString(depth)
                                    });

    if (HiddenLabelUtils.supportsHiddenLabels(arc))
      XhtmlRenderer.renderStyleClass(context, arc,
                         SkinSelectors.HIDDEN_LABEL_STYLE_CLASS);

    writer.writeText(levelInfo, null);
    writer.endElement("label");
  }

  // TODO: this method should be removed.
  public static Object getFocusRowKey(UIXTreeTable treeTable)
  {
    Object path = treeTable.getFocusRowKey();
    if (path == null)
    {
      int oldIndex = treeTable.getRowIndex();
      treeTable.setRowIndex(0);
      Object rowKey = treeTable.getRowKey();
      treeTable.setRowIndex(oldIndex);
      return rowKey;
    }
    else
      return path;
  }

  /**
   * Returns tree focus node client id if any node is focused. Otherwise it
   * returns <tt>null</tt>.
   * 
   * @param context faces context
   * @param tree tree component
   * @return focus node client id
   */
  public static String getFocusNodeClientId(FacesContext context, UIXHierarchy tree) 
  {
    String focusNodeClientId = null;
    Object oldRowKey = tree.getRowKey();
    
    try 
    {
      Object focusRowKey = tree.getFocusRowKey();
      
      if (focusRowKey != null) 
      {
        tree.setRowKey(focusRowKey);
        focusNodeClientId = tree.getClientId(context);
      }
    }
    finally
    {
      tree.setRowKey(oldRowKey);
    }
    
    return focusNodeClientId;
  }

  /**
   * Utility method to expand the focusRowKey during initial
   * rendering.
   * @param tree
   */
  public static void expandFocusRowKey(UIXTree tree)
  {
    if (!RequestContext.getCurrentInstance().isPostback())
    {
      Object focusRowKey = tree.getFocusRowKey();
      if ( focusRowKey != null)
      {
        List<Object> focusPath = 
          new ArrayList<Object>(tree.getAllAncestorContainerRowKeys(focusRowKey));
        focusPath.add(focusRowKey);
        int size = focusPath.size();
        RowKeySet disclosedRowKeys = tree.getDisclosedRowKeys();
        for ( int i = 0 ; i < size; i++)
        {
          Object subkey = focusPath.get(i);
          disclosedRowKeys.add(subkey);
        }     
      }
    }
  }

  /**
   * Utility method to default the focusRowKey to the root node, if
   * it is not set
   * @param tree
   */
  public static void setDefaultFocusRowKey(UIXTree tree)
  {
    Object path = tree.getFocusRowKey();
    if (path == null)
    {
      Object oldKey = tree.getRowKey();
      tree.setRowKey(null);
      tree.setRowIndex(0);
      if (tree.isRowAvailable())
      {
        tree.setFocusRowKey(tree.getRowKey());
      }
      tree.setRowKey(oldKey);
    }    
  }  

  /**
   * writes the JS function needed for generating tree events.
   * @param buff the function source is appended to this buffer
   * @return
   */
  public static StringBuffer setupTreeCollectionComponent(
    StringBuffer buff,
    boolean      validate)
  {
    String validateString = validate?"1":"0";
    buff.append
      ("CollectionComponent.defineTree('"+
        EVENT_PARAM+"','"+
        SOURCE_PARAM+"','"+
        _PATH_PARAM+"','"+
        _START_PARAM+"','"+
        _GOTO+"','"+
        _FOCUS+"'," +
        validateString + ");"
      );

    return buff;
  }

  public static String setupJSTreeCollectionComponent(boolean validate)
  {
    String validateString = validate?"1":"0";
    return
       "CollectionComponent.defineTree('"+
        EVENT_PARAM+"','"+
        SOURCE_PARAM+"','"+
        _PATH_PARAM+"','"+
        _START_PARAM+"','"+
        _GOTO+"','"+
        _FOCUS+ "'," +
        validateString + ")";
  }

  public static String setupJSMultiSelectCollectionComponent(
    String selectedKey,
    String selectedModeKey,
    boolean autoSubmit)
  {
    return
      "CollectionComponent.defineMultiSelect('"+
        selectedKey+"','"+
        selectedModeKey+"',"+(autoSubmit ? "true" : "false")+")";
  }

  public static String createNewJSCollectionComponentState(String formName, String treeClientId)
  {
    // must not end with a ";". This is because this might be passed as an
    // argument to some other function:
    return "new CollectionComponent('"+formName+"','"+treeClientId+"')";
  }

  public static String callJSExpandNode(UIXHierarchy tree, String jsVarName,
                                        boolean isExpand)
  {
    String path = _getPathParam(tree);
    return jsVarName+".action('"+(isExpand ? _SHOW : _HIDE)+
      "','"+path+"',this);return false;";
  }

  public static String callJSGotoNode(UIXHierarchy tree, String jsVarName,
                                      int rangeStart)
  {
    String path = _getPathParam(tree);
    return jsVarName+".range('"+path+"',"+rangeStart+");return false;";
  }

  public static String callJSFocusNode(UIXHierarchy tree, String jsVarName)
  {
    String path = _getPathParam(tree);
    return jsVarName+".focus('"+path+"',this);return false;";
  }

  public static String callJSExpandAll(UIXHierarchy tree, String jsVarName,
                                      boolean isExpand)
  {
    return jsVarName+".action('"+(isExpand ? _SHOW : _HIDE)+
      "','"+_ALL_VALUE+"',this);return false;";
  }

  public static String callJSSelectAll(String jsVarName, boolean isSelectAll)
  {
    return jsVarName+".multiSelect("+(isSelectAll ? "true" : "false")+");return false;";
  }

  public static void decodeGotoEvent(final Map<String, String> parameters,
                                     UIComponent tree)
  {
    Object event = parameters.get(EVENT_PARAM);
    if (_GOTO.equals(event))
    {
      PreserveState preserve = new PreserveState()
      {
        @Override
        protected void process(UIXHierarchy tree)
        {
          final int newStart;
          String startParam = parameters.get(_START_PARAM);
          if ((startParam == null) || ("".equals(startParam)))
          {
            // this must be a root level range change:
            startParam = parameters.get(XhtmlConstants.VALUE_PARAM);
            newStart = Integer.parseInt(startParam) - 1; // value is based at one.
            tree.setRowKey(tree.getFocusRowKey());
            tree.setRowIndex(newStart);
            // queue a focusChange event as well as range change event:
            new FocusEvent(tree).queue();
          }
          else // large record set navigation
          {
            // set the currency to be the container that was scrolled:
            _restorePathFromParam(parameters, tree);
            newStart = Integer.parseInt(startParam);
          }
          TableRenderer.createRangeChangeEvent(tree, newStart).queue();
        }
      };
      preserve.run((UIXHierarchy) tree);
    }
  }

  public static void decodeFocusEvent(final Map<String, String> parameters,
                                      UIComponent tree)
  {
    Object event = parameters.get(EVENT_PARAM);
    if (_FOCUS.equals(event))
    {
      PreserveState preserve = new PreserveState()
      {
        @Override
        protected void process(UIXHierarchy tree)
        {
          _restorePathFromParam(parameters, tree);
          new FocusEvent(tree).queue();
        }
      };
      preserve.run((UIXHierarchy) tree);
    }
  }

  public static void decodeExpandEvents(final Map<String, String> parameters,
                                        final UIComponent tree,
                                        final Object focusRowKey)
  {
    Object event = parameters.get(EVENT_PARAM);
    final Boolean expand;
    if (_HIDE.equals(event))
    {
      expand = Boolean.FALSE;
    }
    else if (_SHOW.equals(event))
    {
      expand = Boolean.TRUE;
    }
    else
    {
      return;
    }

    PreserveState preserve = new PreserveState()
    {
      @Override
      protected void process(UIXHierarchy tree)
      {
        final FacesEvent event;

        Object key = parameters.get(_PATH_PARAM);
        if (_ALL_VALUE.equals(key)) // expandAll event
        {
          if (focusRowKey == null)
          {
            _LOG.severe("UNEXPECTED_TREE_STATE");
            return;
          }
          else
          {
            tree.setRowKey(focusRowKey);
            RowKeySet old = _getExpandedRowKeys(tree);
            RowKeySet newset = old.clone();
            if (expand)
              newset.addAll();
            else
              newset.removeAll();
            event = new RowDisclosureEvent(old, newset, tree);
          }
        }
        else  // expand/collapse event
        {
          _restorePathFromParam(parameters, tree);
          RowKeySet old = _getExpandedRowKeys(tree);
          RowKeySet newset = old.clone();
          newset.setContained(expand);
          event = new RowDisclosureEvent(old, newset, tree);
        }
        event.queue();
      }
    };

    preserve.run((UIXHierarchy) tree);
  }
  
  /**
   * Returns <code>String</code> object containing encoded 
   * parameter name and value pair for Non-JavaScript browsers.
   */   
  public static String renderEncodedNameAttri(
                    FacesContext context,
                    RenderingContext rc,
                    UIXHierarchy tree,
                    String treeName,
                    boolean isExpand) 
    throws IOException
  {

    return XhtmlUtils.getEncodedNameAttribute ( 
                       // Array should be in the order of name
                       // and value pair
                          new String[]{ XhtmlConstants.SOURCE_PARAM,
                                        treeName,
                                        XhtmlConstants.EVENT_PARAM,
                                        isExpand ? _SHOW : _HIDE,
                                        _PATH_PARAM,
                                        _getPathParam(tree)});
                                        
  }

  private static RowKeySet _getExpandedRowKeys(UIXHierarchy tree)
  {
    if (tree instanceof UIXTree)
      return ((UIXTree) tree).getDisclosedRowKeys();
    if (tree instanceof UIXNavigationTree)
      return ((UIXNavigationTree) tree).getDisclosedRowKeys();
    if (tree instanceof UIXPage)
      return ((UIXPage) tree).getDisclosedRowKeys();
    throw new IllegalArgumentException("Don't know how to get disclosedRowKeys from:"+tree);
  }

  private static void _restorePathFromParam(
      Map<String, String> parameters, 
      UIXHierarchy tree)
  {
    String currencyString = parameters.get(_PATH_PARAM);
    tree.setClientRowKey(currencyString);
  }

  private static String _getPathParam(UIXHierarchy tree)
  {
    String currencyString = tree.getClientRowKey();
    return currencyString;
  }

  private abstract static class PreserveState
  {
    public void run(UIXHierarchy tree)
    {
      Object oldPath = tree.getRowKey();
      try
      {
        process(tree);
      }
      finally
      {
        tree.setRowKey(oldPath);
      }
    }

    protected abstract void process(UIXHierarchy tree);
  }

  private static final String _GOTO = "goto";
  private static final String _HIDE = "hide";
  private static final String _SHOW = "show";
  private static final String _FOCUS = "focus";
  private static final String _ALL_VALUE = "all";
  private static final String _PATH_PARAM = "path";
  private static final String _START_PARAM = "start";

  private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(TreeUtils.class);
}
