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

import org.apache.myfaces.tobago.exception.TobagoException;

import javax.el.ValueExpression;
import javax.faces.component.StateHelper;
import javax.faces.component.StateHolder;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

// todo: clean up (is a copy of MyFaces, but not all stuff is refactored)

/**
 * A delta enabled state holder implementing the StateHolder Interface.
 * <p>
 * Components implementing the PartalStateHolder interface have an initial state
 * and delta states, the initial state is the one holding all root values
 * and deltas store differences to the initial states
 * </p>
 * <p>
 * For components not implementing partial state saving only the initial states are
 * of importance, everything is stored and restored continously there
 * </p>
 * <p>
 * The state helper seems to have three internal storage mechanisms:
 * one being a list which stores plain values,
 * one being a key value pair which stores key values in maps
 * add serves the plain list type while put serves the
 * key value type,
 * the third is the value which has to be stored plainly as is!
 * </p>
 * In other words, this map can be seen as a composite map. It has two maps:
 * initial state map and delta map.
 * <p>
 * If delta map is used (method component.initialStateMarked() ),
 * base or initial state map cannot be changed, since all changes
 * should be tracked on delta map.
 * </p>
 * <p>
 * The intention of this class is just hold property values
 * and do a clean separation between initial state and delta.
 * </p>
 * <p>
 * The code from this class comes from a refactor of
 * org.apache.myfaces.trinidad.bean.util.PropertyHashMap
 * </p>
 * <p>
 * The context from this class comes and that should be taken into account
 * is this:
 * </p>
 * <p>
 * First request:
 * </p>
 * <ul>
 * <li> A new template is created (using
 * javax.faces.view.ViewDeclarationLanguage.buildView method)
 * and component.markInitialState is called from its related TagHandler classes
 * (see javax.faces.view.facelets.ComponentHandler ).
 * When this method is executed, the component tree was populated from the values
 * set in the facelet abstract syntax tree (or in other words composition of
 * facelets templates). </li>
 * <li> From this point all updates on the variables are considered "delta". </li>
 * <li> SaveState, if initialStateMarked is true, only delta is saved. </li>
 * </ul>
 * <p>
 * Second request (and next ones)
 * </p>
 * <ul>
 * <li> A new template is created and component.markInitialState is called from
 * its related TagHandler classes again. In this way, components like c:forEach
 * or c:if, that add or remove components could notify about this and handle
 * them properly (see javax.faces.view.StateManagementStrategy). Note that a
 * component restored using this method is no different as the same component
 * at the first request at the same time. </li>
 * <li> A call for restoreState is done, passing the delta as object value. If no
 * delta, the state is complete and no call is triggered. </li>
 * <li> Lifecycle occur, changing the necessary stuff. </li>
 * <li> SaveState, if initialStateMarked is true, only delta is saved. </li>
 * </ul>
 * <p>
 * From the previous analysis, the following conclusions arise:
 * <ul>
 * <li>This class only needs to keep track of delta changes, so when
 * restoreState/saveState is called, the right objects are passed.</li>
 * <li>UIComponent.clearInitialState is used to reset the partial
 * state holder to a non delta state, so the state to be saved by
 * saveState is no longer a delta instead is a full state. If a call
 * to clearInitialState occur it is not expected a call for
 * UIComponent.markInitialState occur on the current request.</li>
 * <li>The state is handled in the same way on UIData, so components
 * inside UIData share its state on all rows. There is no way to save
 * delta per row.</li>
 * <li>The map backed by method put(Serializable,String,Object) is
 * a replacement of UIComponentBase.attributesMap and UIComponent.bindings map.
 * Note that on jsf 1.2, instances saved on attributesMap should not be
 * StateHolder, but on jsf 2.0 it is possible to have it. PartialStateHolder
 * instances are not handled in this map, or in other words delta state is not
 * handled in this classes (markInitialState and clearInitialState is not propagated).</li>
 * <li>The list backed by method add(Serializable,Object) should be (is not) a
 * replacement of UIComponentBase.facesListeners, but note that StateHelper
 * does not implement PartialStateHolder, and facesListener could have instances
 * of that class that needs to be notified when UIComponent.markInitialState or
 * UIComponent.clearInitialState is called, or in other words facesListeners
 * should deal with PartialStateHolder instances.</li>
 * <li>The list backed by method add(Serializable,Object) is
 * a replacement of UIViewRoot.phaseListeners list. Note that instances of
 * PhaseListener are not expected to implement StateHolder or PartialStateHolder.</li>
 * </ul>
 * </p>
 * <p>
 * NOTE: The current implementation of StateHelper on RI does not handle
 * stateHolder values internally. To prevent problems when developers create
 * custom components we should do this too. But anyway, the code that
 * handle this case should be let here as comment, if some day this feature
 * is provided. Note than stateHolder aware properties like converter,
 * validator or listeners should deal with StateHolder or PartialStateHolder
 * on component classes.
 * <p>
 * </p>
 *
 * @since 3.0.0
 */
class DeltaStateHelper<A extends EventBehavior> implements StateHelper {

  /**
   * We need to hold a component instance because:
   * <p>
   * - The component is the one who knows if we are on initial or delta mode
   * - eval assume calls to component.ValueExpression
   */
  private A target;

  /**
   * This map holds the full current state
   */
  private Map<Serializable, Object> fullState;

  /**
   * This map only keep track of delta changes to be saved
   */
  private Map<Serializable, Object> deltas;

  /**
   * This map keep track of StateHolder keys, to be saved when
   * saveState is called.
   */
  //private Set<Serializable> _stateHolderKeys;

  private boolean transientBoolean = false;

  DeltaStateHelper(final A target) {
    super();
    this.target = target;
    fullState = new HashMap<>();
    deltas = null;
    //_stateHolderKeys = new HashSet<Serializable>();
  }

  /**
   * Used to create delta map on demand
   *
   * @return
   */
  private boolean createDeltas() {
    if (isInitialStateMarked()) {
      if (deltas == null) {
        deltas = new HashMap<>(2);
      }
      return true;
    }

    return false;
  }

  protected boolean isInitialStateMarked() {
    return target.initialStateMarked();
  }

  @Override
  public void add(final Serializable key, final Object value) {
    if (createDeltas()) {
      //Track delta case
      Map<Object, Boolean> deltaListMapValues = (Map<Object, Boolean>) deltas
          .get(key);
      if (deltaListMapValues == null) {
        deltaListMapValues = new DeltaStateHelper.InternalDeltaListMap<>(
            3);
        deltas.put(key, deltaListMapValues);
      }
      deltaListMapValues.put(value, Boolean.TRUE);
    }

    //Handle change on full map
    List<Object> fullListValues = (List<Object>) fullState.get(key);
    if (fullListValues == null) {
      fullListValues = new DeltaStateHelper.InternalList<>(3);
      fullState.put(key, fullListValues);
    }
    fullListValues.add(value);
  }

  @Override
  public Object eval(final Serializable key) {
    final Object returnValue = fullState.get(key);
    if (returnValue != null) {
      return returnValue;
    }
    final ValueExpression expression = target.getValueExpression(key
        .toString());
    if (expression != null) {
      return expression.getValue(FacesContext.getCurrentInstance()
          .getELContext());
    }
    return null;
  }

  @Override
  public Object eval(final Serializable key, final Object defaultValue) {
    final Object returnValue = fullState.get(key);
    if (returnValue != null) {
      return returnValue;
    }
    final ValueExpression expression = target.getValueExpression(key
        .toString());
    if (expression != null) {
      return expression.getValue(FacesContext.getCurrentInstance()
          .getELContext());
    }
    return defaultValue;
  }

  @Override
  public Object get(final Serializable key) {
    return fullState.get(key);
  }

  @Override
  public Object put(final Serializable key, final Object value) {
    Object returnValue = null;
    if (createDeltas()) {
      if (deltas.containsKey(key)) {
        returnValue = deltas.put(key, value);
        fullState.put(key, value);
      } else if (value == null && !fullState.containsKey(key)) {
        returnValue = null;
      } else {
        deltas.put(key, value);
        returnValue = fullState.put(key, value);
      }
    } else {
            /*
            if (value instanceof StateHolder)
            {
                _stateHolderKeys.add(key);
            }
            */
      returnValue = fullState.put(key, value);
    }
    return returnValue;
  }

  @Override
  public Object put(final Serializable key, final String mapKey, final Object value) {
    boolean returnSet = false;
    Object returnValue = null;
    if (createDeltas()) {
      //Track delta case
      Map<String, Object> mapValues = (Map<String, Object>) deltas
          .get(key);
      if (mapValues == null) {
        mapValues = new DeltaStateHelper.InternalMap<>();
        deltas.put(key, mapValues);
      }
      if (mapValues.containsKey(mapKey)) {
        returnValue = mapValues.put(mapKey, value);
        returnSet = true;
      } else {
        mapValues.put(mapKey, value);
      }
    }

    //Handle change on full map
    Map<String, Object> mapValues = (Map<String, Object>) fullState
        .get(key);
    if (mapValues == null) {
      mapValues = new DeltaStateHelper.InternalMap<>();
      fullState.put(key, mapValues);
    }
    if (returnSet) {
      mapValues.put(mapKey, value);
    } else {
      returnValue = mapValues.put(mapKey, value);
    }
    return returnValue;
  }

  @Override
  public Object remove(final Serializable key) {
    Object returnValue = null;
    if (createDeltas()) {
      if (deltas.containsKey(key)) {
        // Keep track of the removed values using key/null pair on the delta map
        returnValue = deltas.put(key, null);
        fullState.remove(key);
      } else {
        // Keep track of the removed values using key/null pair on the delta map
        deltas.put(key, null);
        returnValue = fullState.remove(key);
      }
    } else {
      returnValue = fullState.remove(key);
    }
    return returnValue;
  }

  @Override
  public Object remove(final Serializable key, final Object valueOrKey) {
    // Comment by lu4242 : The spec javadoc says if it is a Collection
    // or Map deal with it. But the intention of this method is work
    // with add(?,?) and put(?,?,?), this ones return instances of
    // InternalMap and InternalList to prevent mixing, so to be
    // consistent we'll cast to those classes here.

    final Object collectionOrMap = fullState.get(key);
    Object returnValue = null;
    if (collectionOrMap instanceof DeltaStateHelper.InternalMap) {
      if (createDeltas()) {
        returnValue = removeValueOrKeyFromMap(deltas, key,
            valueOrKey, true);
        removeValueOrKeyFromMap(fullState, key, valueOrKey, false);
      } else {
        returnValue = removeValueOrKeyFromMap(fullState, key,
            valueOrKey, false);
      }
    } else if (collectionOrMap instanceof DeltaStateHelper.InternalList) {
      if (createDeltas()) {
        returnValue = removeValueOrKeyFromCollectionDelta(deltas,
            key, valueOrKey);
        removeValueOrKeyFromCollection(fullState, key, valueOrKey);
      } else {
        returnValue = removeValueOrKeyFromCollection(fullState, key,
            valueOrKey);
      }
    }
    return returnValue;
  }

  private static Object removeValueOrKeyFromCollectionDelta(
      final Map<Serializable, Object> stateMap, final Serializable key,
      final Object valueOrKey) {
    Object returnValue = null;
    final Map<Object, Boolean> c = (Map<Object, Boolean>) stateMap.get(key);
    if (c != null) {
      if (c.containsKey(valueOrKey)) {
        returnValue = valueOrKey;
      }
      c.put(valueOrKey, Boolean.FALSE);
    }
    return returnValue;
  }

  private static Object removeValueOrKeyFromCollection(
      final Map<Serializable, Object> stateMap, final Serializable key,
      final Object valueOrKey) {
    Object returnValue = null;
    final Collection c = (Collection) stateMap.get(key);
    if (c != null) {
      if (c.remove(valueOrKey)) {
        returnValue = valueOrKey;
      }
      if (c.isEmpty()) {
        stateMap.remove(key);
      }
    }
    return returnValue;
  }

  private static Object removeValueOrKeyFromMap(
      final Map<Serializable, Object> stateMap, final Serializable key,
      final Object valueOrKey, final boolean delta) {
    if (valueOrKey == null) {
      return null;
    }

    Object returnValue = null;
    final Map<String, Object> map = (Map<String, Object>) stateMap.get(key);
    if (map != null) {
      if (delta) {
        // Keep track of the removed values using key/null pair on the delta map
        returnValue = map.put((String) valueOrKey, null);
      } else {
        returnValue = map.remove(valueOrKey);
      }

      if (map.isEmpty()) {
        //stateMap.remove(key);
        stateMap.put(key, null);
      }
    }
    return returnValue;
  }

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

  /**
   * Serializing cod
   * the serialized data structure consists of key value pairs unless the value itself is an internal array
   * or a map in case of an internal array or map the value itself is another array with its initial value
   * myfaces.InternalArray, myfaces.internalMap
   * <p>
   * the internal Array is then mapped to another array
   * <p>
   * the internal Map again is then mapped to a map with key value pairs
   */
  @Override
  public Object saveState(final FacesContext context) {
    final Map serializableMap = (isInitialStateMarked()) ? deltas : fullState;

    if (serializableMap == null || serializableMap.size() == 0) {
      return null;
    }

        /*
        int stateHolderKeyCount = 0;
        if (isInitalStateMarked())
        {
            for (Iterator<Serializable> it = _stateHolderKeys.iterator(); it.hasNext();)
            {
                Serializable key = it.next();
                if (!deltas.containsKey(key))
                {
                    stateHolderKeyCount++;
                }
            }
        }*/

    Map.Entry<Serializable, Object> entry;
    //entry == key, value, key, value
    final Object[] retArr = new Object[serializableMap.entrySet().size() * 2];
    //Object[] retArr = new Object[serializableMap.entrySet().size() * 2 + stateHolderKeyCount];

    final Iterator<Map.Entry<Serializable, Object>> it = serializableMap.entrySet().iterator();
    int cnt = 0;
    while (it.hasNext()) {
      entry = it.next();
      retArr[cnt] = entry.getKey();

      final Object value = entry.getValue();

      // The condition in which the call to saveAttachedState
      // is to handle List, StateHolder or non Serializable instances.
      // we check it here, to prevent unnecessary calls.
      if (value instanceof StateHolder || value instanceof List || !(value instanceof Serializable)) {
        final Object savedValue = saveAttachedState(context, value);
        retArr[cnt + 1] = savedValue;
      } else {
        retArr[cnt + 1] = value;
      }
      cnt += 2;
    }

        /*
        if (isInitalStateMarked())
        {
            for (Iterator<Serializable> it2 = _stateHolderKeys.iterator(); it.hasNext();)
            {
                Serializable key = it2.next();
                if (!deltas.containsKey(key))
                {
                    retArr[cnt] = key;
                    Object value = fullState.get(key);
                    if (value instanceof PartialStateHolder)
                    {
                        //Could contain delta, save it as _AttachedDeltaState
                        PartialStateHolder holder = (PartialStateHolder) value;
                        if (holder.isTransient())
                        {
                            retArr[cnt + 1] = null;
                        }
                        else
                        {
                            retArr[cnt + 1] = new _AttachedDeltaWrapper(value.getClass(), holder.saveState(context));
                        }
                    }
                    else
                    {
                        //Save everything
                        retArr[cnt + 1] = saveAttachedState(context, fullState.get(key));
                    }
                    cnt += 2;
                }
            }
        }
        */
    return retArr;
  }

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

    final Object[] serializedState = (Object[]) state;

    if (!isInitialStateMarked() && !fullState.isEmpty()) {
      fullState.clear();
      if (deltas != null) {
        deltas.clear();
      }
    }

    for (int cnt = 0; cnt < serializedState.length; cnt += 2) {
      final Serializable key = (Serializable) serializedState[cnt];
      final Object savedValue = restoreAttachedState(context,
          serializedState[cnt + 1]);

      if (isInitialStateMarked()) {
        if (savedValue instanceof DeltaStateHelper.InternalDeltaListMap) {
          for (final Map.Entry<Object, Boolean> mapEntry : ((Map<Object, Boolean>) savedValue)
              .entrySet()) {
            final boolean addOrRemove = mapEntry.getValue();
            if (addOrRemove) {
              //add
              this.add(key, mapEntry.getKey());
            } else {
              //remove
              this.remove(key, mapEntry.getKey());
            }
          }
        } else if (savedValue instanceof DeltaStateHelper.InternalMap) {
          for (final Map.Entry<String, Object> mapEntry : ((Map<String, Object>) savedValue)
              .entrySet()) {
            this.put(key, mapEntry.getKey(), mapEntry.getValue());
          }
                /*
                else if (savedValue instanceof _AttachedDeltaWrapper)
                {
                    AttachedStateWrapper wrapper = (AttachedStateWrapper) savedValue;
                    //Restore delta state
                    ((PartialStateHolder)fullState.get(key)).restoreState(context, wrapper.getWrappedStateObject());
                    //Add this key as StateHolder key
                    _stateHolderKeys.add(key);
                }
                */
        } else {
          put(key, savedValue);
        }
      } else {
        put(key, savedValue);
      }
    }
  }

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

  //We use our own data structures just to make sure
  //nothing gets mixed up internally
  static class InternalMap<K, V> extends HashMap<K, V> implements StateHolder {
    InternalMap() {
      super();
    }

    InternalMap(final int initialCapacity, final float loadFactor) {
      super(initialCapacity, loadFactor);
    }

    InternalMap(final Map<? extends K, ? extends V> m) {
      super(m);
    }

    InternalMap(final int initialSize) {
      super(initialSize);
    }

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

    @Override
    public void setTransient(final boolean newTransientValue) {
      // No op
    }

    @Override
    public void restoreState(final FacesContext context, final Object state) {
      final Object[] listAsMap = (Object[]) state;
      for (int cnt = 0; cnt < listAsMap.length; cnt += 2) {
        this.put((K) listAsMap[cnt], (V) UIComponentBase.restoreAttachedState(context, listAsMap[cnt + 1]));
      }
    }

    @Override
    public Object saveState(final FacesContext context) {
      int cnt = 0;
      final Object[] mapArr = new Object[this.size() * 2];
      for (final Map.Entry<K, V> entry : this.entrySet()) {
        mapArr[cnt] = entry.getKey();
        final Object value = entry.getValue();

        if (value instanceof StateHolder || value instanceof List || !(value instanceof Serializable)) {
          mapArr[cnt + 1] = saveAttachedState(context, value);
        } else {
          mapArr[cnt + 1] = value;
        }
        cnt += 2;
      }
      return mapArr;
    }
  }

  /**
   * Map used to keep track of list changes
   */
  static class InternalDeltaListMap<K, V> extends DeltaStateHelper.InternalMap<K, V> {

    InternalDeltaListMap() {
      super();
    }

    InternalDeltaListMap(final int initialCapacity, final float loadFactor) {
      super(initialCapacity, loadFactor);
    }

    InternalDeltaListMap(final int initialSize) {
      super(initialSize);
    }

    InternalDeltaListMap(final Map<? extends K, ? extends V> m) {
      super(m);
    }
  }

  static class InternalList<T> extends ArrayList<T> implements StateHolder {
    InternalList() {
      super();
    }

    InternalList(final Collection<? extends T> c) {
      super(c);
    }

    InternalList(final int initialSize) {
      super(initialSize);
    }

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

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

    @Override
    public void restoreState(final FacesContext context, final Object state) {
      final Object[] listAsArr = (Object[]) state;
      //since all other options would mean dual iteration
      //we have to do it the hard way
      for (final Object elem : listAsArr) {
        add((T) restoreAttachedState(context, elem));
      }
    }

    @Override
    public Object saveState(final FacesContext context) {
      final Object[] values = new Object[size()];
      for (int i = 0; i < size(); i++) {
        final Object value = get(i);

        if (value instanceof StateHolder || value instanceof List || !(value instanceof Serializable)) {
          values[i] = saveAttachedState(context, value);
        } else {
          values[i] = value;
        }
      }
      return values;
    }
  }

  private static Object saveAttachedState(final FacesContext context, final Object attachedObject) {
    if (context == null) {
      throw new NullPointerException("context");
    }

    if (attachedObject == null) {
      return null;
    }
    // StateHolder interface should take precedence over
    // List children
    if (attachedObject instanceof StateHolder) {
      final StateHolder holder = (StateHolder) attachedObject;
      if (holder.isTransient()) {
        return null;
      }

      return new AttachedStateWrapper(attachedObject.getClass(), holder.saveState(context));
    } else if (attachedObject instanceof List) {
      final List<Object> lst = new ArrayList<>(((List<?>) attachedObject).size());
      for (final Object item : (List<?>) attachedObject) {
        if (item != null) {
          lst.add(saveAttachedState(context, item));
        }
      }

      return new AttachedListStateWrapper(lst);
    } else if (attachedObject instanceof Serializable) {
      return attachedObject;
    } else {
      return new AttachedStateWrapper(attachedObject.getClass(), null);
    }
  }

  private static Object restoreAttachedState(final FacesContext context, final Object stateObj)
      throws IllegalStateException {
    if (context == null) {
      throw new NullPointerException("context");
    }
    if (stateObj == null) {
      return null;
    }
    if (stateObj instanceof AttachedListStateWrapper) {
      final List<Object> lst = ((AttachedListStateWrapper) stateObj).getWrappedStateList();
      final List<Object> restoredList = new ArrayList<>(lst.size());
      for (final Object item : lst) {
        restoredList.add(restoreAttachedState(context, item));
      }
      return restoredList;
    } else if (stateObj instanceof AttachedStateWrapper) {
      final Class<?> clazz = ((AttachedStateWrapper) stateObj).getClazz();
      final Object restoredObject;
      try {
        restoredObject = clazz.newInstance();
      } catch (final InstantiationException e) {
        throw new TobagoException("Could not restore StateHolder of type " + clazz.getName()
            + " (missing no-args constructor?)", e);
      } catch (final IllegalAccessException e) {
        throw new TobagoException(e);
      }
      if (restoredObject instanceof StateHolder) {
        final AttachedStateWrapper wrapper = (AttachedStateWrapper) stateObj;
        final Object wrappedState = wrapper.getWrappedStateObject();

        final StateHolder holder = (StateHolder) restoredObject;
        holder.restoreState(context, wrappedState);
      }
      return restoredObject;
    } else {
      return stateObj;
    }
  }
}
