/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., 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.jboss.ejb.plugins.cmp.jdbc2.schema;

import org.jboss.ejb.plugins.cmp.jdbc2.bridge.JDBCCMRFieldBridge2;
import org.jboss.ejb.plugins.cmp.jdbc2.bridge.JDBCCMPFieldBridge2;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCRelationMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.SQLUtil;
import org.jboss.ejb.plugins.cmp.jdbc.JDBCUtil;
import org.jboss.deployment.DeploymentException;
import org.jboss.logging.Logger;

import javax.sql.DataSource;
import javax.transaction.Transaction;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.PreparedStatement;

/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision$</tt>
 */
public class RelationTable
   implements Table
{
   private static final byte CREATED = 1;
   private static final byte DELETED = 2;

   private final Schema schema;
   private final int tableId;
   private final DataSource ds;
   private final String tableName;
   private final JDBCCMRFieldBridge2 leftField;
   private final JDBCCMRFieldBridge2 rightField;
   private final Logger log;

   private String insertSql;
   private String deleteSql;

   public RelationTable(JDBCCMRFieldBridge2 leftField, JDBCCMRFieldBridge2 rightField, Schema schema, int tableId)
      throws DeploymentException
   {
      this.schema = schema;
      this.tableId = tableId;
      this.leftField = leftField;
      this.rightField = rightField;

      JDBCRelationMetaData metadata = leftField.getMetaData().getRelationMetaData();
      ds = metadata.getDataSource();
      tableName = SQLUtil.fixTableName(metadata.getDefaultTableName(), ds);

      log = Logger.getLogger(getClass().getName() + "." + tableName);

      // generate sql

      insertSql = "insert into " + tableName + " (";

      JDBCCMPFieldBridge2[] keyFields = (JDBCCMPFieldBridge2[])this.leftField.getTableKeyFields();
      insertSql += keyFields[0].getColumnName();
      for(int i = 1; i < keyFields.length; ++i)
      {
         insertSql += ", " + keyFields[i].getColumnName();
      }

      keyFields = (JDBCCMPFieldBridge2[])this.rightField.getTableKeyFields();
      insertSql += ", " + keyFields[0].getColumnName();
      for(int i = 1; i < keyFields.length; ++i)
      {
         insertSql += ", " + keyFields[i].getColumnName();
      }

      insertSql += ") values (?";
      for(int i = 1; i < this.leftField.getTableKeyFields().length + this.rightField.getTableKeyFields().length; ++i)
      {
         insertSql += ", ?";
      }

      insertSql += ")";

      log.debug("insert sql: " + insertSql);

      deleteSql = "delete from " + tableName + " where ";
      keyFields = (JDBCCMPFieldBridge2[])this.leftField.getTableKeyFields();
      deleteSql += keyFields[0].getColumnName() + "=?";
      for(int i = 1; i < keyFields.length; ++i)
      {
         deleteSql += " and " + keyFields[i].getColumnName() + "=?";
      }

      keyFields = (JDBCCMPFieldBridge2[])this.rightField.getTableKeyFields();
      deleteSql += " and " + keyFields[0].getColumnName() + "=?";
      for(int i = 1; i < keyFields.length; ++i)
      {
         deleteSql += " and " + keyFields[i].getColumnName() + "=?";
      }

      log.debug("delete sql: " + deleteSql);
   }

   // Public

   public void addRelation(JDBCCMRFieldBridge2 field1, Object key1, JDBCCMRFieldBridge2 field2, Object key2)
   {
      View view = getView();
      if(field1 == leftField)
      {
         view.addKeys(key1, key2);
      }
      else
      {
         view.addKeys(key2, key1);
      }
   }

   public void removeRelation(JDBCCMRFieldBridge2 field1, Object key1, JDBCCMRFieldBridge2 field2, Object key2)
   {
      View view = getView();
      if(field1 == leftField)
      {
         view.removeKeys(key1, key2);
      }
      else
      {
         view.removeKeys(key2, key1);
      }
   }

   // Table implementation

   public int getTableId()
   {
      return tableId;
   }

   public String getTableName()
   {
      return tableName;
   }
   
   public Table.View createView(Transaction tx)
   {
      return new View();
   }

   // Private

   private void delete(View view) throws SQLException
   {
      if(view.deleted == null)
      {
         if(log.isTraceEnabled())
         {
            log.trace("no rows to delete");
         }
         return;
      }

      Connection con = null;
      PreparedStatement ps = null;
      try
      {
         if(log.isDebugEnabled())
         {
            log.debug("executing : " + deleteSql);
         }

         con = ds.getConnection();
         ps = con.prepareStatement(deleteSql);

         int batchCount = 0;
         while(view.deleted != null)
         {
            RelationKeys keys = view.deleted;

            int paramInd = 1;
            JDBCCMPFieldBridge2[] keyFields = (JDBCCMPFieldBridge2[])leftField.getTableKeyFields();
            for(int pkInd = 0; pkInd < keyFields.length; ++pkInd)
            {
               JDBCCMPFieldBridge2 pkField = keyFields[pkInd];
               Object fieldValue = pkField.getPrimaryKeyValue(keys.leftKey);
               paramInd = pkField.setArgumentParameters(ps, paramInd, fieldValue);
            }

            keyFields = (JDBCCMPFieldBridge2[])rightField.getTableKeyFields();
            for(int pkInd = 0; pkInd < keyFields.length; ++pkInd)
            {
               JDBCCMPFieldBridge2 pkField = keyFields[pkInd];
               Object fieldValue = pkField.getPrimaryKeyValue(keys.rightKey);
               paramInd = pkField.setArgumentParameters(ps, paramInd, fieldValue);
            }

            ps.addBatch();
            ++batchCount;

            keys.dereference();
         }

         ps.executeBatch();

         if(view.deleted != null)
         {
            throw new IllegalStateException("There are still rows to delete!");
         }

         if(log.isTraceEnabled())
         {
            log.trace("deleted rows: " + batchCount);
         }
      }
      catch(SQLException e)
      {
         log.error("Failed to delete view: " + e.getMessage(), e);
         throw e;
      }
      finally
      {
         JDBCUtil.safeClose(ps);
         JDBCUtil.safeClose(con);
      }
   }

   private void insert(View view) throws SQLException
   {
      if(view.created == null)
      {
         if(log.isTraceEnabled())
         {
            log.trace("no rows to insert");
         }
         return;
      }

      Connection con = null;
      PreparedStatement ps = null;
      try
      {
         if(log.isDebugEnabled())
         {
            log.debug("executing : " + insertSql);
         }

         con = ds.getConnection();
         ps = con.prepareStatement(insertSql);

         int batchCount = 0;
         while(view.created != null)
         {
            RelationKeys keys = view.created;

            JDBCCMPFieldBridge2[] keyFields = (JDBCCMPFieldBridge2[])leftField.getTableKeyFields();
            int paramInd = 1;
            for(int fInd = 0; fInd < keyFields.length; ++fInd)
            {
               JDBCCMPFieldBridge2 field = keyFields[fInd];
               Object fieldValue = field.getPrimaryKeyValue(keys.leftKey);
               paramInd = field.setArgumentParameters(ps, paramInd, fieldValue);
            }

            keyFields = (JDBCCMPFieldBridge2[])rightField.getTableKeyFields();
            for(int fInd = 0; fInd < keyFields.length; ++fInd)
            {
               JDBCCMPFieldBridge2 field = keyFields[fInd];
               Object fieldValue = field.getPrimaryKeyValue(keys.rightKey);
               paramInd = field.setArgumentParameters(ps, paramInd, fieldValue);
            }

            ps.addBatch();
            ++batchCount;

            keys.dereference();
         }

         ps.executeBatch();

         if(log.isTraceEnabled())
         {
            log.trace("inserted rows: " + batchCount);
         }
      }
      catch(SQLException e)
      {
         log.error("Failed to insert new rows: " + e.getMessage(), e);
         throw e;
      }
      finally
      {
         JDBCUtil.safeClose(ps);
         JDBCUtil.safeClose(con);
      }
   }

   private View getView()
   {
      return (View) schema.getView(this);
   }

   // Inner

   private class View implements Table.View
   {
      private RelationKeys created;
      private RelationKeys deleted;

      // Public

      public void addKeys(Object leftKey, Object rightKey)
      {
         // if it was deleted then dereference
         RelationKeys keys = deleted;
         while(keys != null)
         {
            if(keys.equals(leftKey, rightKey))
            {
               keys.dereference();
               return;
            }
            keys = keys.next;
         }

         // add to created
         keys = new RelationKeys(this, leftKey, rightKey);

         if(created != null)
         {
            keys.next = created;
            created.prev = keys;
         }
         created = keys;
         keys.state = CREATED;
      }

      public void removeKeys(Object leftKey, Object rightKey)
      {
         // if it was created then dereference
         RelationKeys keys = created;
         while(keys != null)
         {
            if(keys.equals(leftKey, rightKey))
            {
               keys.dereference();
               return;
            }
            keys = keys.next;
         }

         // add to deleted
         keys = new RelationKeys(this, leftKey, rightKey);

         if(deleted != null)
         {
            keys.next = deleted;
            deleted.prev = keys;
         }
         deleted = keys;
         keys.state = DELETED;
      }

      // Table.View implementation

      public void flushDeleted(Schema.Views views) throws SQLException
      {
         delete(this);
      }

      public void flushCreated(Schema.Views views) throws SQLException
      {
         insert(this);
      }

      public void flushUpdated() throws SQLException
      {
      }

      public void beforeCompletion()
      {
      }

      public void committed()
      {
      }

      public void rolledback()
      {
      }
   }

   private class RelationKeys
   {
      private final View view;
      private final Object leftKey;
      private final Object rightKey;

      private byte state;
      private RelationKeys next;
      private RelationKeys prev;

      public RelationKeys(View view, Object leftKey, Object rightKey)
      {
         this.view = view;
         this.leftKey = leftKey;
         this.rightKey = rightKey;
      }

      // Public

      public boolean equals(Object leftKey, Object rightKey)
      {
         return this.leftKey.equals(leftKey) && this.rightKey.equals(rightKey);
      }

      public void dereference()
      {
         if(state == CREATED && this == view.created)
         {
            view.created = next;
         }
         else if(state == DELETED && this == view.deleted)
         {
            view.deleted = next;
         }

         if(next != null)
         {
            next.prev = prev;
         }

         if(prev != null)
         {
            prev.next = next;
         }

         next = null;
         prev = null;
      }
   }
}
