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

import java.io.Serializable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.faces.context.FacesContext;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;

import org.apache.myfaces.trinidadinternal.util.SubKeyMap;
import org.apache.myfaces.trinidadinternal.util.TokenCache;

/**
 *
 */
class PageFlowScopeMap implements Map<String, Object>, Serializable
{
  /**
   * Return a PageFlowScopeMap stored with a token.
   */
  static public PageFlowScopeMap getPageFlowScopeMap(
    FacesContext context, String token, int lifetime)
  {
    TokenCache cache = _getRootTokenCache(context, lifetime);
    PageFlowScopeMap map = _getPageFlowScopeMap(context, cache, token);

    if (_LOG.isFine())
    {
      _LOG.fine("pageFlowScope: found map {0} at token {1}",
                new Object[]{(map == null) ? (Object) "null" : map, token});
    }

    if (map == null)
    {
      return null;
    }
    else
    {
      // Don't return the same instance of PageFlowScopeMap as was used
      // on the previous page;  otherwise, for instance, we'll overwrite
      // its token as we mutate.  Instead, create a new PageFlowScopeMap,
      // but reuse the _map;  we'll clone the _map itself if we mutate
      return new PageFlowScopeMap(map._map,
                                 token,
                                 map._sharedData);
    }
  }

  static private PageFlowScopeMap _getPageFlowScopeMap(
    FacesContext context, TokenCache cache, String token)
  {
    if (token == null)
      throw new NullPointerException();

    int lastSeparator = token.lastIndexOf(TokenCache.SEPARATOR_CHAR);
    String parentToken;
    String childToken;
    if (lastSeparator < 0)
    {
      parentToken = null;
      childToken = token;
    }
    else
    {
      parentToken = token.substring(0, lastSeparator);
      childToken = token.substring(lastSeparator + 1);
    }

    Map<String, Object> storeMap = _createMapToStore(context, parentToken);
    return (PageFlowScopeMap) storeMap.get(childToken);
  }

  /**
   * Only for serialization
   */
  public PageFlowScopeMap()
  {
  }


  public PageFlowScopeMap(int lifetime)
  {
    this(new HashMap<String, Object>(13), null, new SharedData(lifetime));
  }

  private PageFlowScopeMap(
    HashMap<String, Object> map,
    String token,
    SharedData sharedData)
  {
    _map = map;
    _sharedData = sharedData;
    _token = token;
  }

  //
  // Create a PageFlowScopeMap pointing at a parent
  //
  private PageFlowScopeMap(PageFlowScopeMap parent, boolean copyParent)
  {
    assert(parent != null);

    _sharedData = new SharedData(parent._sharedData._lifetime);
    _sharedData._parent = parent;

    _map = new HashMap<String, Object>();
    if (copyParent)
      _map.putAll(parent._map);
  }


  public PageFlowScopeMap getParent()
  {
    return _sharedData._parent;
  }

  synchronized public String getToken(FacesContext context)
  {
    if (_token != null)
      return _token;

    // Don't need a token when nothing's in the map, and we
    // don't have a parent
    if (isEmpty() &&
        (_sharedData._children == null) &&
        (_sharedData._parent == null))
      return null;

    String parentToken;
    TokenCache cache;
    if (_sharedData._parent != null)
    {
      parentToken = _sharedData._parent.getToken(context);
      cache = _sharedData._parent._getTokenCache();
    }
    else
    {
      parentToken = null;
      cache = _getRootTokenCache(context, _sharedData._lifetime);
    }

    Map<String, Object> store = _createMapToStore(context, parentToken);

    String token = cache.addNewEntry(this, store);

    if (parentToken != null)
      token = parentToken + TokenCache.SEPARATOR_CHAR + token;

    _token = token;

    // With a new token, there cannot be any shared children
    // with a prior request.
    if (_sharedData._children != null)
    {
      // =-=AEW NEED TO CLONE SHARED DATA
      _LOG.fine("Discarding child PageFlowScopes; new token is {0}", token);
      _sharedData._children = null;
    }

    return _token;
  }

  @SuppressWarnings("unchecked")
  static private Map<String, Object> _createMapToStore(
    FacesContext context,
    String       parentToken)
  {
    String fullToken;
    if (parentToken == null)
    {
      fullToken = _PAGE_FLOW_SCOPE_CACHE + TokenCache.SEPARATOR_CHAR;
    }
    else
    {
      fullToken = (_PAGE_FLOW_SCOPE_CACHE + TokenCache.SEPARATOR_CHAR +
                    parentToken + TokenCache.SEPARATOR_CHAR);

    }

    return new SubKeyMap(context.getExternalContext().getSessionMap(),
                         fullToken);
  }

  @Override
  public boolean equals(Object o)
  {
    if (o instanceof PageFlowScopeMap)
      o = ((PageFlowScopeMap) o)._map;

    return _map.equals(o);
  }

  @Override
  public int hashCode()
  {
    return _map.hashCode();
  }

  public int size()
  {
    return _map.size();
  }

  public boolean isEmpty()
  {
    return _map.isEmpty();
  }

  public boolean containsKey(Object key)
  {
    return _map.containsKey(key);
  }

  public boolean containsValue(Object value)
  {
    return _map.containsValue(value);
  }

  public Collection<Object> values()
  {
    // Use an unmodifiableCollection to save me the headache
    // of catching mutations
    return Collections.unmodifiableCollection(_map.values());
  }


  public Set<Map.Entry<String, Object>> entrySet()
  {
    // Use an unmodifiableSet to save me the headache
    // of catching mutations
    return Collections.unmodifiableSet(_map.entrySet());
  }

  public Set<String> keySet()
  {
    // Use an unmodifiableSet to save me the headache
    // of catching mutations
    return Collections.unmodifiableSet(_map.keySet());
  }

  public Object get(Object key)
  {
    return _map.get(key);
  }

  public Object put(String key, Object value)
  {
    _detachIfNeeded();
    if (_LOG.isFine())
    {
      _LOG.fine("pageFlowScope: put({0}, {1})", new Object[]{key, value});
    }

    return _map.put(key, value);
  }

  public Object remove(Object key)
  {
    _detachIfNeeded();
    if (_LOG.isFine())
    {
      _LOG.fine("pageFlowScope: remove({0})", key);
    }

    return _map.remove(key);
  }

  public void putAll(Map<? extends String, ? extends Object> t)
  {
    _detachIfNeeded();
    if (_LOG.isFine())
    {
      _LOG.fine("pageFlowScope: putAll({0})", t);
    }
    _map.putAll(t);
  }

  public void clear()
  {
    _detachIfNeeded();
    if (_LOG.isFine())
    {
      _LOG.fine("pageFlowScope: clear()");
    }
    _map.clear();
  }


  public PageFlowScopeMap createChild(boolean copyParent)
  {
    return new PageFlowScopeMap(this, copyParent);
  }

  public void discard()
  {
    FacesContext context = FacesContext.getCurrentInstance();

    String token = getToken(context);
    int lastSeparator = token.lastIndexOf(TokenCache.SEPARATOR_CHAR);

    String parentToken;
    String childToken;
    if (lastSeparator < 0)
    {
      parentToken = null;
      childToken = token;
    }
    else
    {
      parentToken = token.substring(0, lastSeparator);
      childToken = token.substring(lastSeparator + 1);
    }

    // Remove ourselves
    if (_sharedData._parent != null)
    {
      Map<String, Object> storeMap = _createMapToStore(context, parentToken);
      _sharedData._parent._sharedData._children.removeOldEntry(childToken,
                                                               storeMap);
    }

    // And clean up all of our children
    _removeAllChildren(context, token);
  }

  private void _removeAllChildren(FacesContext context, String token)
  {
    // Clear everything - note that because of naming conventions,
    // this will in fact automatically recurse through all children
    // grandchildren etc. - which is kind of a design flaw of SubKeyMap,
    // but one we're relying on
    Map<String, Object> store = _createMapToStore(context, token);
    store.clear();
    _sharedData._children = null;
  }

  @Override
  public String toString()
  {
    return "PageFlowScopeMap@" + System.identityHashCode(this) +
           "[_map=" + _map + ", _token=" + _token +
           ",_children=" + _sharedData._children + "]";
  }

  static private TokenCache _getRootTokenCache(FacesContext context,
                                               int lifetime)
  {
    return TokenCache.getTokenCacheFromSession(context,
                                               _PAGE_FLOW_SCOPE_CACHE,
                                               true,
                                               lifetime);
  }

  private TokenCache _getTokenCache()
  {
    if (_sharedData._children == null)
      _sharedData._children = new TokenCache(_sharedData._lifetime);

    return _sharedData._children;
  }

  // =-=AEW This strategy assumes that the PageFlowScopeMap
  // will be inherited from a prior request, have things
  // added and removed prior to Render Response *without
  // the token being requested*, then have the token used
  // repeatedly during Render Response *without further
  // mutations*.  Both of these assumptions seem very
  // dubious!
  @SuppressWarnings("unchecked")
  private void _detachIfNeeded()
  {
    if (_token != null)
    {
      _map = (HashMap<String, Object>) _map.clone();
      _token = null;

      // =-=AEW When do we discard children?
    }
  }

  static public class SharedData implements Serializable
  {
    public SharedData()
    {
    }

    public SharedData(int lifetime)
    {
      _lifetime = lifetime;
    }

    private int             _lifetime;
    // =-=AEW Make transient for efficiency
    private PageFlowScopeMap _parent;
    private TokenCache      _children;
  }

  // DELETE AFTER DIALOG SERVICE IS CLEANED UP
  boolean    __invalid;

  private SharedData _sharedData;
  private String     _token;
  private HashMap<String, Object> _map;

  static private final String _PAGE_FLOW_SCOPE_CACHE =
    "org.apache.myfaces.trinidadinternal.application.PageFlowScope";
  static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(PageFlowScopeMap.class);
}
