package org.jboss.cache.loader;

import net.jcip.annotations.ThreadSafe;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;
import org.jboss.cache.marshall.NodeData;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JDBC implementation of <tt>AdjListJDBCCacheLoader</tt>.
 * Represents a faster alternative than JDBCCacheLoaderOld and relies on the same database structrure.
 * It is backward compatible with data created by existing <tt>JDBCCacheLoaderOld</tt> implemetation.
 * All configuration elements described there {@link org.jboss.cache.loader.JDBCCacheLoaderOld} also apply for this
 * implementation.
 * <p/>
 * <p/>
 * Additional configuration info: <br>
 * cache.jdbc.sql-concat : DBMS specific function for concat strings. Most likely this will be concat(1,2), but might
 * be different for proprietary systems.
 *
 * @author Mircea.Markus@iquestint.com
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 * @version 1.0
 */
@ThreadSafe
public class JDBCCacheLoader extends AdjListJDBCCacheLoader
{
   private JDBCCacheLoaderConfig config;

   /**
    * Builds a AdjListJDBCCacheLoaderConfig based on the supplied base config.
    */
   @Override
   protected AdjListJDBCCacheLoaderConfig processConfig(CacheLoaderConfig.IndividualCacheLoaderConfig base)
   {
      if (base instanceof JDBCCacheLoaderConfig)
      {
         config = (JDBCCacheLoaderConfig) base;
      }
      else
      {
         config = new JDBCCacheLoaderConfig(base);
      }
      return config;
   }

   /**
    * As per interface's contract.
    * Performance Note: Optimised O(nodeDepth) db calls.
    */
   public Object put(Fqn name, Object key, Object value) throws Exception
   {
      Map<Object, Object> m = new HashMap<Object, Object>(1);
      m.put(key, value);
      Map existing = _put(name, m);
      return existing == null ? null : existing.get(key);
   }

   /**
    * As per interface's contract.
    * Performance Note: Optimised O(nodeDepth) db calls.
    */
   public void put(Fqn name, Map attributes) throws Exception
   {
      _put(name, attributes);
   }

   @Override
   protected void storeStateHelper(Fqn subtree, List nodeData, boolean moveToBuddy) throws Exception
   {
      for (Object aNodeData : nodeData)
      {
         NodeData nd = (NodeData) aNodeData;
         if (nd.isMarker()) break;
         Fqn fqn;
         if (moveToBuddy)
         {
            fqn = buddyFqnTransformer.getBackupFqn(subtree, nd.getFqn());
         }
         else
         {
            fqn = nd.getFqn();
         }

         if (nd.getAttributes() != null)
         {
            this.put(fqn, nd.getAttributes(), true);// creates a node with 0 or more attributes
         }
         else
         {
            this.put(fqn, null);// creates a node with null attributes
         }
      }
   }

   /**
    * As per interface's contrect.
    * Performance Note: O(1) db calls.
    */
   public void remove(Fqn fqn) throws Exception
   {
      Connection conn = null;
      PreparedStatement ps = null;
      try
      {
         conn = cf.getConnection();
         ps = conn.prepareStatement(config.getDeleteNodeSql());
         //apend / at the end avoids this issue: 'a/b/cd' is not a child of 'a/b/c'
         ps.setString(1, fqn.isRoot() ? fqn.toString() : fqn + Fqn.SEPARATOR);
         lock.acquireLock(fqn, true);
         ps.executeUpdate();
         if (log.isTraceEnabled())
         {
            log.trace("Deleting all the children of " + fqn + ". Used sql is'" + config.getDeleteNodeSql() + '\'');
         }
      }
      catch (SQLException e)
      {
         log.error("Failed to remove the node : " + fqn, e);
         throw new IllegalStateException("Failure while removing sub-tree (" + fqn + ")" + e.getMessage());
      }
      finally
      {
         safeClose(ps);
         cf.close(conn);
         lock.releaseLock(fqn);
      }
   }


   /**
    * Subscribes to contract.
    * Performance Note: O(2) db calls.
    */
   @Override
   protected void getNodeDataList(Fqn fqn, List<NodeData> list) throws Exception
   {
      Map nodeAttributes = loadNode(fqn);
      if (nodeAttributes == null)
      {
         return;
      }
      Connection connection = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      try
      {
         connection = cf.getConnection();
         ps = connection.prepareStatement(config.getRecursiveChildrenSql());
         ps.setString(1, fqn.isRoot() ? fqn.toString() : fqn.toString() + Fqn.SEPARATOR);
         rs = ps.executeQuery();
         while (rs.next())
         {
            Map<Object, Object> attributes = readAttributes(rs, 2);
            Fqn path = Fqn.fromString(rs.getString(1));
            NodeData nodeData = (attributes == null || attributes.isEmpty()) ? new NodeData(path) : new NodeData(path, attributes);
            list.add(nodeData);
         }
      }
      catch (SQLException e)
      {
         log.error("Failed to load state for node(" + fqn + ") :" + e.getMessage(), e);
         throw new IllegalStateException("Failed to load state for node(" + fqn + ") :" + e.getMessage());
      }
      finally
      {
         safeClose(rs);
         safeClose(ps);
         cf.close(connection);
      }
   }

   private Map<Object, Object> readAttributes(ResultSet rs, int index) throws SQLException
   {
      Map<Object, Object> result;
      InputStream is = rs.getBinaryStream(index);
      if (is != null && !rs.wasNull())
      {
         try
         {
            Object marshalledNode = unmarshall(is);
            result = (Map<Object, Object>) marshalledNode;
         }
         catch (Exception e)
         {
            log.error("Failure while reading attribute set from db", e);
            throw new SQLException("Failure while reading attribute set from db " + e);
         }
      }
      else
      {
         result = null;
      }
      return result;
   }

   private Map _put(Fqn name, Map attributes) throws Exception
   {
      lock.acquireLock(name, true);
      try
      {
         Map result = null;
         Map treeNode = loadNode(name);
         if (treeNode == null)
         {
            addNewSubtree(name, attributes);
         }
         else if (treeNode == NULL_NODE_IN_ROW)
         {
            updateNode(name, attributes);
         }
         else if (attributes != null && !attributes.isEmpty())
         {
            //the node exists and the attribute map is NOT null
            Map<Object, Object> newAttributes = new HashMap<Object, Object>(treeNode);
            newAttributes.putAll(attributes);//creation sequnce important - we need to overwrite old values
            updateNode(name, newAttributes);
            result = treeNode;
         }
         return result;
      }
      finally
      {
         lock.releaseLock(name);
      }
   }

   private void addNewSubtree(Fqn name, Map attributes) throws Exception
   {
      Fqn currentNode = name;
      do
      {
         if (currentNode.equals(name))
         {
            insertNode(currentNode, attributes, false);
         }
         else
         {
            insertNode(currentNode, null, true);
         }
         if (currentNode.isRoot()) break;
         currentNode = currentNode.getParent();
      }
      while (!exists(currentNode));
   }

   /**
    * Start is overwritten for the sake of backward compatibility only.
    * Here is the issue: old implementation does not create a Fqn.ROOT if not specifically told so.
    * As per put's contract, when calling put('/a/b/c', 'key', 'value') all parent nodes should be created up to root.
    * Root is not created, though. The compatibility problem comes in the case of loade ENTIRE state.
    * The implementation checks node's existence firstly, and based on that continues or not. As root is not
    * persisted nothing is loaded etc.
    */
   @Override
   public void start() throws Exception
   {
      log = LogFactory.getLog(getClass());
      super.start();
      if (!exists(Fqn.ROOT) && getNodeCount() > 0)
      {
         put(Fqn.ROOT, null);
      }
   }

   /**
    * Returns a number representing the count of persisted children.
    */
   public int getNodeCount() throws Exception
   {
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      try
      {
         if (log.isDebugEnabled())
         {
            log.debug("executing sql: " + config.getNodeCountSql());
         }

         conn = cf.getConnection();
         ps = conn.prepareStatement(config.getNodeCountSql());
         rs = ps.executeQuery();
         rs.next();//count(*) will always return one row
         return rs.getInt(1);
      }
      catch (Exception e)
      {
         log.error("Failure while trying to get the count of persisted nodes: " + e.getMessage(), e);
         throw new IllegalStateException("Failure while trying to get the count of persisted nodes: " + e.getMessage());
      }
      finally
      {
         safeClose(rs);
         safeClose(ps);
         cf.close(conn);
      }
   }

   @Override
   public void storeState(Fqn subtree, ObjectInputStream in) throws Exception
   {
      super.storeState(subtree, in);
   }

   public IndividualCacheLoaderConfig getConfig()
   {
      return config;
   }
}
