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

import org.apache.myfaces.tobago.component.Attributes;
import org.apache.myfaces.tobago.component.ClientBehaviors;
import org.apache.myfaces.tobago.internal.behavior.EventBehavior;
import org.apache.myfaces.tobago.internal.component.AbstractUIEvent;

import javax.el.MethodExpression;
import javax.faces.component.PartialStateHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.AjaxBehaviorListener;
import javax.faces.view.BehaviorHolderAttachedObjectHandler;
import javax.faces.view.facelets.ComponentConfig;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributeException;
import javax.faces.view.facelets.TagException;
import java.io.IOException;

/**
 * This tag creates an instance of AjaxBehavior, and associates it with the nearest
 * parent UIComponent that implements ClientBehaviorHolder interface. This tag can
 * be used on single or composite components.
 * <p>
 * Unless otherwise specified, all attributes accept static values or EL expressions.
 * </p>
 * <p>
 * According to the documentation, the tag handler implementing this tag should meet
 * the following conditions:
 * </p>
 * <ul>
 * <li>Since this tag attach objects to UIComponent instances, and those instances
 * implements Behavior interface, this component should implement
 * BehaviorHolderAttachedObjectHandler interface.</li>
 * <li>f:ajax does not support binding property. In theory we should do something similar
 * to f:convertDateTime tag does: extends from ConverterHandler and override setAttributes
 * method, but in this case BehaviorTagHandlerDelegate has binding property defined, so
 * if we extend from BehaviorHandler we add binding support to f:ajax.</li>
 * <li>This tag works as a attached object handler, but note on the api there is no component
 * to define a target for a behavior. See comment inside apply() method.</li>
 * </ul>
 *
 * @since 3.0.0
 */
public class EventHandler extends TobagoComponentHandler implements BehaviorHolderAttachedObjectHandler {

  public static final Class<?>[] AJAX_BEHAVIOR_LISTENER_SIG = new Class<?>[]{AjaxBehaviorEvent.class};

  private final TagAttribute event;

// todo (see original AjaxHandler impl)  private final boolean _wrapMode;

  public EventHandler(final ComponentConfig config) {
    super(config);
    event = getAttribute(Attributes.event.getName());
  }

  @Override
  public void apply(final FaceletContext ctx, final UIComponent parent)
      throws IOException {

    super.apply(ctx, parent);

    //Apply only if we are creating a new component
    if (!ComponentHandler.isNew(parent)) {
      return;
    }
    if (parent instanceof ClientBehaviorHolder) {
      //Apply this handler directly over the parent
      applyAttachedObject(ctx.getFacesContext(), parent);
//todo      } else if (UIComponent.isCompositeComponent(parent)) {
//todo        FaceletCompositionContext mctx = FaceletCompositionContext.getCurrentInstance(ctx);
      // It is supposed that for composite components, this tag should
      // add itself as a target, but note that on whole api does not exists
      // some tag that expose client behaviors as targets for composite
      // components. In RI, there exists a tag called composite:clientBehavior,
      // but does not appear on spec or javadoc, maybe because this could be
      // understand as an implementation detail, after all there exists a key
      // called AttachedObjectTarget.ATTACHED_OBJECT_TARGETS_KEY that could be
      // used to create a tag outside jsf implementation to attach targets.
//todo        mctx.addAttachedObjectHandler(parent, this);
    } else {
      throw new TagException(this.tag,
          "Parent is not composite component or of type ClientBehaviorHolder, type is: "
              + parent);
    }
  }

  /**
   * ViewDeclarationLanguage.retargetAttachedObjects uses it to check
   * if the the target to be processed is applicable for this handler
   */
  @Override
  public String getEventName() {
    if (event == null) {
      return null;
    } else {
      return event.getValue();
    }
  }

  /**
   * This method should create an AjaxBehavior object and attach it to the
   * parent component.
   * <p>
   * Also, it should check if the parent can apply the selected AjaxBehavior
   * to the selected component through ClientBehaviorHolder.getEventNames() or
   * ClientBehaviorHolder.getDefaultEventName()
   */
  @Override
  public void applyAttachedObject(final FacesContext context, final UIComponent parent) {
    // Retrieve the current FaceletContext from FacesContext object
    final FaceletContext faceletContext = (FaceletContext) context
        .getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);

    final ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) parent;
    final UIComponent lastChild = parent.getChildren().stream().skip(parent.getChildCount() - 1)
        .findFirst().orElse(null);
    final AbstractUIEvent abstractUIEvent = lastChild instanceof AbstractUIEvent ? (AbstractUIEvent) lastChild : null;

    if (abstractUIEvent != null) {
      String eventName = getEventName();
      if (eventName == null) {
        eventName = clientBehaviorHolder.getDefaultEventName();
        if (eventName == null) {
          throw new TagAttributeException(event, "eventName could not be defined for f:ajax tag with no wrap mode.");
        }
      } else if (!clientBehaviorHolder.getEventNames().contains(eventName)) {
        throw new TagAttributeException(event, "event it is not a valid eventName defined for this component");
      }

      final EventBehavior eventBehavior = createBehavior(context);
      eventBehavior.setFor(abstractUIEvent.getId());

      clientBehaviorHolder.addClientBehavior(eventName, eventBehavior);
    }
  }

  protected EventBehavior createBehavior(final FacesContext context) {
    return (EventBehavior) context.getApplication().createBehavior(EventBehavior.BEHAVIOR_ID);
  }

  @Override
  public void onComponentCreated(
      final FaceletContext faceletContext, final UIComponent component, final UIComponent parent) {
    super.onComponentCreated(faceletContext, component, parent);

    final AbstractUIEvent uiEvent = (AbstractUIEvent) component;
    if (uiEvent.getEvent() == null) {
      final ClientBehaviorHolder holder = (ClientBehaviorHolder) parent;
      uiEvent.setEvent(ClientBehaviors.getEnum(holder.getDefaultEventName()));
    }
  }

  /**
   * The documentation says this attribute should not be used since it is not
   * taken into account. Instead, getEventName is used on
   * ViewDeclarationLanguage.retargetAttachedObjects.
   */
  @Override
  public String getFor() {
    return null;
  }

  /**
   * Wraps a method expression in a AjaxBehaviorListener
   */
  public static final class AjaxBehaviorListenerImpl implements
      AjaxBehaviorListener, PartialStateHolder {
    private MethodExpression expression;
    private boolean transientBoolean;
    private boolean initialStateMarked;

    public AjaxBehaviorListenerImpl() {
    }

    public AjaxBehaviorListenerImpl(final MethodExpression expr) {
      expression = expr;
    }

    @Override
    public void processAjaxBehavior(final AjaxBehaviorEvent event)
        throws AbortProcessingException {
      expression.invoke(FacesContext.getCurrentInstance().getELContext(),
          new Object[]{event});
    }

    @Override
    public boolean isTransient() {
      return transientBoolean;
    }

    @Override
    public void restoreState(final FacesContext context, final Object state) {
      if (state == null) {
        return;
      }
      expression = (MethodExpression) state;
    }

    @Override
    public Object saveState(final FacesContext context) {
      if (initialStateMarked()) {
        return null;
      }
      return expression;
    }

    @Override
    public void setTransient(final boolean newTransientValue) {
      transientBoolean = newTransientValue;
    }

    @Override
    public void clearInitialState() {
      initialStateMarked = false;
    }

    @Override
    public boolean initialStateMarked() {
      return initialStateMarked;
    }

    @Override
    public void markInitialState() {
      initialStateMarked = true;
    }
  }
}
