/*
* JBoss, a division of Red Hat
* Copyright 2006, Red Hat Middleware, LLC, and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.picketlink.idm.impl.cache;

import org.jboss.cache.*;
import org.jboss.cache.eviction.ExpirationAlgorithmConfig;

import org.picketlink.idm.spi.configuration.IdentityConfigurationContext;
import org.picketlink.idm.spi.model.IdentityObject;
import org.picketlink.idm.spi.model.IdentityObjectAttribute;
import org.picketlink.idm.spi.model.IdentityObjectRelationship;
import org.picketlink.idm.spi.cache.IdentityStoreCacheProvider;
import org.picketlink.idm.spi.cache.IdentityObjectSearch;
import org.picketlink.idm.spi.cache.IdentityObjectRelationshipSearch;
import org.picketlink.idm.spi.cache.IdentityObjectRelationshipNameSearch;
import org.picketlink.idm.spi.configuration.IdentityRepositoryConfigurationContext;
import org.picketlink.idm.impl.types.SimpleIdentityObject;
import org.picketlink.idm.impl.types.SimpleIdentityObjectType;
import org.picketlink.idm.impl.types.SimpleIdentityObjectRelationship;
import org.picketlink.idm.impl.types.SimpleIdentityObjectRelationshipType;
import org.picketlink.idm.impl.api.SimpleAttribute;

import java.io.InputStream;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.*;

/**
 * Helper class providing caching support for IdentityStore.
 *
 * @author <a href="mailto:boleslaw.dawidowicz at redhat.com">Boleslaw Dawidowicz</a>
 * @version : 0.1 $
 */
public class JBossCacheIdentityStoreCacheProviderImpl implements IdentityStoreCacheProvider
{
   private static Logger log = Logger.getLogger(JBossCacheIdentityStoreCacheProviderImpl.class.getName());


   private Cache cache;

   public static final String CONFIG_FILE_OPTION = "cache.configFile";

   public static final String CONFIG_CACHE_REGISTRY_OPTION = "cache.cacheRegistryName";

   public static final String NODE_OBJECT_KEY = "object";

   public static final String NODE_SEARCH_KEY = "search";

   public static final String NODE_SEARCH_UNIQUE_KEY = "query_unique";

   public static final String NODE_MAIN_ROOT = "IDM_ROOT";

   public static final String NODE_COMMON_ROOT = "COMMON_ROOT";

   public static final String NODE_IO_COUNT = "NODE_IO_COUNT";

   public static final String NODE_IO_ATTRIBUTES = "NODE_IO_ATTRIBUTES";

   public static final String NODE_OBJECTS = "NODE_OBJECTS";

   public static final String NODE_REL_PROPS = "NODE_REL_PROPS";

   public static final String NODE_REL_NAME_PROPS = "NODE_REL_NAME_PROPS";

   public static final String NODE_IO_SEARCH = "NODE_IO_SEARCH";

   public static final String NODE_IO_REL_SEARCH = "NODE_IO_REL_SEARCH";

   public static final String NODE_IO_REL_NAME_SEARCH = "NODE_IO_REL_NAME_SEARCH";

   public static final String NULL_NS_NODE = "PL_COMMON_NS";

   public static final String MAIN_ROOT = "NODE_MAIN_ROOT";

   private int expiration = -1;

   private Fqn getRootNode()
   {
      return Fqn.fromString("/" + MAIN_ROOT);
   }

   private Fqn getNamespacedFqn(String ns)
   {
      String namespace = ns != null ? ns : NULL_NS_NODE;
      namespace = namespace.replaceAll("/", "_");
      return Fqn.fromString(getRootNode() + "/" + ns);
   }

   private Fqn getFqn(String ns, String node, Object o)
   {
      return Fqn.fromString(getNamespacedFqn(ns) + "/" + node + "/" + o);
   }

   private Fqn getFqn(String ns, String node, int hash)
   {
      return Fqn.fromString(getNamespacedFqn(ns) + "/" + node + "/" + hash);
   }

   private Fqn getFqn(String ns, String node)
   {
      return Fqn.fromString(getNamespacedFqn(ns) + "/" + node);
   }

   public void initialize(Map<String, String> properties, IdentityConfigurationContext configurationContext)
   {
      CacheFactory factory = new DefaultCacheFactory();

      String registryName = properties.get(CONFIG_CACHE_REGISTRY_OPTION);

      // Get cache from registry
      if (registryName != null)
      {
         try
         {
            this.cache = (Cache)configurationContext.getConfigurationRegistry().getObject(registryName);
         }
         catch (Exception e)
         {
            throw new IllegalArgumentException("Cannot find JBoss Cache 'Cache' object in configuration registry with provided" +
               "name: " + registryName);
         }

         return;
      }

      String config = properties.get(CONFIG_FILE_OPTION);

      if (config == null)
      {
         throw new IllegalArgumentException("Cannot find '" + CONFIG_FILE_OPTION + "' in passed properties. Failed to initialize" +
            "cache provider.");
      }

      this.cache = factory.createCache(config);

      this.cache.create();
      this.cache.start();

   }

   public void initialize(InputStream jbossCacheConfiguration)
   {
      CacheFactory factory = new DefaultCacheFactory();

      if (jbossCacheConfiguration == null)
      {
         throw new IllegalArgumentException("JBoss Cache configuration InputStream is null");
      }

      this.cache = factory.createCache(jbossCacheConfiguration);

      this.cache.create();
      this.cache.start();

   }

    public void initialize(Cache cache)
   {
      this.cache = cache;

      CacheStatus status = cache.getCacheStatus();

      if (status.createAllowed())
      {
         this.cache.create();
      }
      if (status.startAllowed())
      {
         this.cache.start();
      }

   }

   Cache getCache()
   {
      return cache;
   }


   public void invalidate(String ns)
   {
      cache.getRoot().removeChild(getNamespacedFqn(ns));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating namespace:" + ns);
      }
   }

   public void invalidateAll()
   {
      boolean success = cache.getRoot().removeChild(getRootNode());

      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating whole cache - success=" + success);
      }
   }

   public String getNamespace(String storeId)
   {
      if (storeId == null)
      {
         return NODE_COMMON_ROOT;
      }
      return storeId;
   }

   public String getNamespace(String storeId, String sessionId)
   {
      if (sessionId == null)
      {
         return getNamespace(storeId);
      }
      return storeId + "/" + sessionId;
   }

   public String getNamespace(String storeId, String sessionId, String realmId)
   {
      if (realmId == null)
      {
         return getNamespace(sessionId);
      }
      return storeId + "/" + sessionId + "/" + realmId;
   }

   public void putIdentityObjectCount(String ns, String type, int count)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_COUNT, type);

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, count);

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObject count stored in cache: " + count + "; type=" + type
               + ";namespace=" + ns);
         }
      }
   }

   public int getIdentityObjectCount(String ns, String type)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_COUNT, type);

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         int count = -1;
         Integer i = (Integer)node.get(NODE_OBJECT_KEY);
         if (i != null)
         {
            count = i;
         }

         if (log.isLoggable(Level.FINER) && count != -1)
         {
            log.finer(this.toString() + "IdentityObject count found in cache: " + count + "; type=" + type
             + ";namespace=" + ns);
         }

         return count;
      }

      return -1;
   }

   public void invalidateIdentityObjectCount(String ns, String type)
   {
      getCache().getRoot().removeChild(Fqn.fromString(getNamespacedFqn(ns) + "/" + NODE_IO_COUNT + "/" + type));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObject count. Namespace:" + ns + "; type=" + type
          + ";namespace=" + ns);
      }
   }

   public void putIdentityObjectSearch(String ns, IdentityObjectSearch search, Collection<IdentityObject> results)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_SEARCH, search.hashCode());

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, safeCopyIO(results));

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObject search stored in cache: results.size()=" + results.size()
               + ";namespace=" + ns);
         }
      }
   }

   public Collection<IdentityObject> getIdentityObjectSearch(String ns, IdentityObjectSearch search)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_SEARCH, search.hashCode());

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Collection<IdentityObject> results = (Collection<IdentityObject>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && results != null)
         {
            log.finer(this.toString() + "IdentityObject search found in cache: results.size()=" + results.size()
             + ";namespace=" + ns);
         }

         return results;
      }

      return null;
   }

   public void invalidateIdentityObjectSearches(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_IO_SEARCH));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObject searches. Namespace:" + ns);
      }
   }

   public void putIdentityObjectRelationshipSearch(String ns, IdentityObjectRelationshipSearch search, Set<IdentityObjectRelationship> results)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_REL_SEARCH, search.hashCode());

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, safeCopyIOR(results));

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObjectRelationship search stored in cache: results.size()=" + results.size()
               + ";namespace=" + ns);
         }
      }
   }

   public Set<IdentityObjectRelationship> getIdentityObjectRelationshipSearch(String ns, IdentityObjectRelationshipSearch search)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_REL_SEARCH, search.hashCode());

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Set<IdentityObjectRelationship> results = (Set<IdentityObjectRelationship>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && results != null)
         {
            log.finer(this.toString() + "IdentityObjectRelationship search found in cache: results.size()=" + results.size()
             + ";namespace=" + ns);
         }

         return results;
      }

      return null;
   }

   public void invalidateIdentityObjectRelationshipSearches(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_IO_REL_SEARCH));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationship searches. Namespace:" + ns);
      }
   }

   public void putIdentityObjectRelationshipNameSearch(String ns, IdentityObjectRelationshipNameSearch search, Set<String> results)
   {
     Fqn nodeFqn = getFqn(ns, NODE_IO_REL_NAME_SEARCH, search.hashCode());

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, results);

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObjectRelationshipName search stored in cache: results.size()=" + results.size()
               + ";namespace=" + ns);
         }
      }
   }

   public Set<String> getIdentityObjectRelationshipNameSearch(String ns, IdentityObjectRelationshipNameSearch search)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_REL_NAME_SEARCH, search.hashCode());

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Set<String> results = (Set<String>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && results != null)
         {
            log.finer(this.toString() + "IdentityObjectRelationshipName search found in cache: results.size()=" + results.size()
             + ";namespace=" + ns);
         }

         return results;
      }

      return null;
   }

   public void invalidateIdentityObjectRelationshipNameSearches(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_IO_REL_NAME_SEARCH));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationshipName searches. Namespace:" + ns);
      }
   }

   public void putProperties(String ns, IdentityObjectRelationship relationship, Map<String, String> properties)
   {
      Fqn nodeFqn = getFqn(ns, NODE_REL_PROPS, decode(relationship));

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, properties);

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObjectRelationship properties stored in cache: relationship="
               + relationship + "; properties.size()=" + properties.size() + ";namespace=" + ns);
         }
      }
   }

   private String decode(IdentityObjectRelationship r)
   {
      return r.getFromIdentityObject().getIdentityType().getName() +
         r.getFromIdentityObject().getName() +
         r.getToIdentityObject().getIdentityType().getName() +
         r.getToIdentityObject().getName() +
         r.getType().getName();
   }

   public Map<String, String> getProperties(String ns, IdentityObjectRelationship relationship)
   {
      Fqn nodeFqn = getFqn(ns, NODE_REL_PROPS, decode(relationship));

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Map<String, String> props = (Map<String, String>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && props != null)
         {
            log.finer(this.toString() + "IdentityObjectRelationship properties found in cache: properties.size()=" + props.size() +
         "; relationship=" + relationship + ";namespace=" + ns);
         }

         return props;
      }

      return null;
   }

   public void invalidateRelationshipProperties(String ns, IdentityObjectRelationship relationship)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_REL_PROPS, decode(relationship)));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationship properties. Namespace:" + ns
            + "; relationship=" + relationship + ";namespace=" + ns);
      }
   }

   public void invalidateRelationshipProperties(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_REL_PROPS));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationship properties. Namespace:" + ns);
      }
   }

   public void putProperties(String ns, String name, Map<String, String> properties)
   {
      Fqn nodeFqn = getFqn(ns, NODE_REL_NAME_PROPS, name);

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, properties);

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObjectRelationshipName properties stored in cache: name="
               + name + "; properties.size()=" + properties.size() + ";namespace=" + ns);
         }
      }
   }

   public Map<String, String> getProperties(String ns, String name)
   {
      Fqn nodeFqn = getFqn(ns, NODE_REL_NAME_PROPS, name);

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Map<String, String> props = (Map<String, String>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && props != null)
         {
            log.finer(this.toString() + "IdentityObjectRelationshipName properties found in cache: properties.size()=" + props.size() +
         "; name=" + name + ";namespace=" + ns);
         }

         return props;
      }

      return null;
   }

   public void invalidateRelationshipNameProperties(String ns, String relationship)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_REL_NAME_PROPS, relationship));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationshipName properties." +
            " Namespace:" + ns + "; name=" + relationship);
      }
   }

   public void invalidateRelationshipNameProperties(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_REL_NAME_PROPS));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObjectRelationshipName properties. " +
            "Namespace:" + ns);
      }

   }

   public void putIdentityObjectAttributes(String ns, IdentityObject io, Map<String, IdentityObjectAttribute> attributes)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_ATTRIBUTES, io.getIdentityType().getName() + io.getName());

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, safeCopyAttr(attributes));

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "IdentityObject attributes stored in cache: io=" + io
               + "; attributes.size()=" + attributes.size() + ";namespace=" + ns);
         }
      }
   }

   public Map<String, IdentityObjectAttribute> getIdentityObjectAttributes(String ns, IdentityObject io)
   {
      Fqn nodeFqn = getFqn(ns, NODE_IO_ATTRIBUTES, io.getIdentityType().getName() + io.getName());

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Map<String, IdentityObjectAttribute> props = (Map<String, IdentityObjectAttribute>)node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && props != null)
         {
            log.finer(this.toString() + "IIdentityObject attributes found in cache: attributes.size()=" + props.size() +
         "; io=" + io + ";namespace=" + ns);
         }

         return props;
      }

      return null;
   }

   public void invalidateIdentityObjectAttriubtes(String ns, IdentityObject io)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_IO_ATTRIBUTES, io.getIdentityType().getName() + io.getName()));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObject attributes. Namespace:" + ns + "; io=" + io);
      }
   }

   public void invalidateIdentityObjectAttriubtes(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_IO_ATTRIBUTES));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating IdentityObject attributes. Namespace:" + ns);
      }
   }

   public void putObject(String ns, int hash, Object value)
   {
      Fqn nodeFqn = getFqn(ns, NODE_OBJECTS, hash);

      Node ioNode = getCache().getRoot().addChild(nodeFqn);

      if (ioNode != null)
      {
         setExpiration(ioNode);
         ioNode.put(NODE_OBJECT_KEY, value);

         if (log.isLoggable(Level.FINER))
         {
            log.finer(this.toString() + "Object stored in cache: hash=" + hash
               + "; value=" + value + ";namespace=" + ns);
         }
      }
   }

   public Object getObject(String ns, int hash)
   {
      Fqn nodeFqn = getFqn(ns, NODE_OBJECTS, hash);

      Node node = getCache().getRoot().getChild(nodeFqn);

      if (node != null)
      {
         Object value = node.get(NODE_OBJECT_KEY);

         if (log.isLoggable(Level.FINER) && value != null)
         {
            log.finer(this.toString() + "Object found in cache: hash" + hash +
          ";namespace=" + ns);
         }

         return value;
      }

      return null;
   }

   public void invalidateObject(String ns, int hash)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_OBJECTS, hash));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating object. Namespace:" + ns + "; hash=" + hash);
      }
   }

   public void invalidateObjects(String ns)
   {
      getCache().getRoot().removeChild(getFqn(ns, NODE_OBJECTS));
      if (log.isLoggable(Level.FINER))
      {
         log.finer(this.toString() + "Invalidating objects. Namespace:" + ns);
      }
   }

   private List<IdentityObject> safeCopyIO(Collection<IdentityObject> res)
   {
      List<IdentityObject> nr = new LinkedList<IdentityObject>();

      for (IdentityObject io : res)
      {
         nr.add(new SimpleIdentityObject(io.getName(),
            new SimpleIdentityObjectType(io.getIdentityType().getName())));
      }

      return nr; 
   }

   private Set<IdentityObjectRelationship> safeCopyIOR(Set<IdentityObjectRelationship> res)
   {
      Set<IdentityObjectRelationship> nr = new HashSet<IdentityObjectRelationship>();

      for (IdentityObjectRelationship ior : res)
      {
         IdentityObject from = new SimpleIdentityObject(ior.getFromIdentityObject().getName(),
            new SimpleIdentityObjectType(ior.getFromIdentityObject().getIdentityType().getName()));
         IdentityObject to = new SimpleIdentityObject(ior.getToIdentityObject().getName(),
            new SimpleIdentityObjectType(ior.getToIdentityObject().getIdentityType().getName()));

         nr.add(new SimpleIdentityObjectRelationship(from, to, ior.getName(), new SimpleIdentityObjectRelationshipType(ior.getType().getName())));
      }

      return nr;
   }

   private Map<String, IdentityObjectAttribute> safeCopyAttr(Map<String, IdentityObjectAttribute> res)
   {
      Map<String, IdentityObjectAttribute> nr = new HashMap<String, IdentityObjectAttribute>();

      for (IdentityObjectAttribute attr : res.values())
      {
         nr.put(attr.getName(), new SimpleAttribute(attr));
      }

      return nr;
   }

   public void setExpiration(Node node)
   {
      if (expiration != -1 && expiration > 0)
      {
         Long future = new Long(System.currentTimeMillis() + expiration);
         node.put(ExpirationAlgorithmConfig.EXPIRATION_KEY, future);
      }
   }

   public int getExpiration()
   {
      return expiration;
   }

   public void setExpiration(int expiration)
   {
      this.expiration = expiration;
   }

}
