/**
 * Database-independent synchronization strategy that does full record transfer between two
 * databases. This strategy is best used when there are <em>many</em> differences between the active
 * database and the inactive database (i.e. very much out of sync). The following algorithm is used:
 *
 * <ol>
 *   <li>Drop the foreign keys on the inactive database (to avoid integrity constraint violations)
 *   <li>For each database table:
 *       <ol>
 *         <li>Delete all rows in the inactive database table
 *         <li>Query all rows on the active database table
 *         <li>For each row in active database table:
 *             <ol>
 *               <li>Insert new row into inactive database table
 *             </ol>
 *       </ol>
 *   <li>Re-create the foreign keys on the inactive database
 *   <li>Synchronize sequences
 * </ol>
 *
 * @author Paul Ferraro
 */
public class FullSynchronizationStrategy
    implements SynchronizationStrategy, TableSynchronizationStrategy, Serializable {
  private static final long serialVersionUID = 9190347092842178162L;

  private static Logger logger = LoggerFactory.getLogger(FullSynchronizationStrategy.class);

  private SynchronizationStrategy strategy = new PerTableSynchronizationStrategy(this);
  private int maxBatchSize = 100;
  private int fetchSize = 0;

  @Override
  public String getId() {
    return "full";
  }

  @Override
  public <Z, D extends Database<Z>> void init(DatabaseCluster<Z, D> cluster) {
    this.strategy.init(cluster);
  }

  @Override
  public <Z, D extends Database<Z>> void synchronize(SynchronizationContext<Z, D> context)
      throws SQLException {
    this.strategy.synchronize(context);
  }

  @Override
  public <Z, D extends Database<Z>> void destroy(DatabaseCluster<Z, D> cluster) {
    this.strategy.destroy(cluster);
  }

  @Override
  public <Z, D extends Database<Z>> void synchronize(
      SynchronizationContext<Z, D> context, TableProperties table) throws SQLException {
    final String tableName = table.getName().getDMLName();
    final Collection<String> columns = table.getColumns();

    final String commaDelimitedColumns = Strings.join(columns, Strings.PADDED_COMMA);

    final String selectSQL = String.format("SELECT %s FROM %s", commaDelimitedColumns, tableName);
    final String deleteSQL = context.getDialect().getTruncateTableSQL(table);
    final String insertSQL =
        String.format(
            "INSERT INTO %s (%s) VALUES (%s)",
            tableName,
            commaDelimitedColumns,
            Strings.join(
                Collections.nCopies(columns.size(), Strings.QUESTION), Strings.PADDED_COMMA));

    Connection sourceConnection = context.getConnection(context.getSourceDatabase());
    final Statement selectStatement = sourceConnection.createStatement();
    try {
      selectStatement.setFetchSize(this.fetchSize);

      Callable<ResultSet> callable =
          new Callable<ResultSet>() {
            @Override
            public ResultSet call() throws SQLException {
              logger.log(Level.DEBUG, selectSQL);
              return selectStatement.executeQuery(selectSQL);
            }
          };

      Future<ResultSet> future = context.getExecutor().submit(callable);

      Connection targetConnection = context.getConnection(context.getTargetDatabase());
      Statement deleteStatement = targetConnection.createStatement();

      try {
        logger.log(Level.DEBUG, deleteSQL);
        int deletedRows = deleteStatement.executeUpdate(deleteSQL);

        logger.log(Level.INFO, Messages.DELETE_COUNT.getMessage(), deletedRows, tableName);
      } finally {
        Resources.close(deleteStatement);
      }

      logger.log(Level.DEBUG, insertSQL);
      PreparedStatement insertStatement = targetConnection.prepareStatement(insertSQL);

      try {
        int statementCount = 0;

        ResultSet resultSet = future.get();

        while (resultSet.next()) {
          int index = 0;

          for (String column : table.getColumns()) {
            index += 1;

            int type = context.getDialect().getColumnType(table.getColumnProperties(column));

            Object object = context.getSynchronizationSupport().getObject(resultSet, index, type);

            if (resultSet.wasNull()) {
              insertStatement.setNull(index, type);
            } else {
              insertStatement.setObject(index, object, type);
            }
          }

          insertStatement.addBatch();
          statementCount += 1;

          if ((statementCount % this.maxBatchSize) == 0) {
            insertStatement.executeBatch();
            insertStatement.clearBatch();
          }

          insertStatement.clearParameters();
        }

        if ((statementCount % this.maxBatchSize) > 0) {
          insertStatement.executeBatch();
        }

        logger.log(Level.INFO, Messages.INSERT_COUNT.getMessage(), statementCount, table);
      } catch (ExecutionException e) {
        throw ExceptionType.getExceptionFactory(SQLException.class).createException(e.getCause());
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new SQLException(e);
      } finally {
        Resources.close(insertStatement);
      }
    } finally {
      Resources.close(selectStatement);
    }
  }

  @Override
  public <Z, D extends Database<Z>> void dropConstraints(SynchronizationContext<Z, D> context)
      throws SQLException {
    context.getSynchronizationSupport().dropForeignKeys();
  }

  @Override
  public <Z, D extends Database<Z>> void restoreConstraints(SynchronizationContext<Z, D> context)
      throws SQLException {
    context.getSynchronizationSupport().restoreForeignKeys();
  }

  /** @return the fetchSize. */
  public int getFetchSize() {
    return this.fetchSize;
  }

  /** @param fetchSize the fetchSize to set. */
  public void setFetchSize(int fetchSize) {
    this.fetchSize = fetchSize;
  }

  /** @return the maxBatchSize. */
  public int getMaxBatchSize() {
    return this.maxBatchSize;
  }

  /** @param maxBatchSize the maxBatchSize to set. */
  public void setMaxBatchSize(int maxBatchSize) {
    this.maxBatchSize = maxBatchSize;
  }
}
/** @author Paul Ferraro */
public class SQLStateManagerFactory extends GenericObjectPoolConfiguration
    implements StateManagerFactory {
  private static final long serialVersionUID = -544548607415128414L;

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  enum EmbeddedVendor {
    H2("jdbc:h2:{1}/{0}"),
    HSQLDB("jdbc:hsqldb:{1}/{0}"),
    DERBY("jdbc:derby:{1}/{0};create=true");

    final String pattern;

    EmbeddedVendor(String pattern) {
      this.pattern = pattern;
    }
  }

  private String urlPattern = this.defaultUrlPattern();
  private String user;
  private String password;

  private String defaultUrlPattern() {
    for (EmbeddedVendor vendor : EmbeddedVendor.values()) {
      String url = MessageFormat.format(vendor.pattern, "test", Strings.HA_JDBC_HOME);

      try {
        for (Driver driver : Collections.list(DriverManager.getDrivers())) {
          if (driver.acceptsURL(url)) {
            return vendor.pattern;
          }
        }
      } catch (SQLException e) {
        // Skip vendor
      }
    }

    return null;
  }

  @Override
  public String getId() {
    return "sql";
  }

  /**
   * {@inheritDoc}
   *
   * @see net.sf.hajdbc.state.StateManagerFactory#createStateManager(net.sf.hajdbc.DatabaseCluster)
   */
  @Override
  public <Z, D extends Database<Z>> StateManager createStateManager(DatabaseCluster<Z, D> cluster) {
    if (this.urlPattern == null) {
      throw new IllegalArgumentException(
          "No embedded database driver was detected on the classpath.");
    }

    String url = MessageFormat.format(this.urlPattern, cluster.getId(), Strings.HA_JDBC_HOME);
    DriverDatabase database = new DriverDatabase();
    database.setLocation(url);
    database.setUser(this.user);
    database.setPassword(this.password);

    this.logger.log(
        Level.INFO, "State for database cluster {0} will be persisted to {1}", cluster, url);

    return new SQLStateManager<Z, D>(cluster, database, new GenericObjectPoolFactory(this));
  }

  public String getUrlPattern() {
    return this.urlPattern;
  }

  public void setUrlPattern(String urlPattern) {
    this.urlPattern = urlPattern;
  }

  public String getUser() {
    return this.user;
  }

  public void setUser(String user) {
    this.user = user;
  }

  public String getPassword() {
    return this.password;
  }

  public void setPassword(String password) {
    this.password = password;
  }
}
/** @author Paul Ferraro */
public class LifecycleRegistry<K, V extends Lifecycle, C, E extends Exception>
    implements Registry<K, V, C, E> {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  private final LifecycleRegistry.Store<K, RegistryEntry> store;

  final Factory<K, V, C, E> factory;
  final ExceptionFactory<E> exceptionFactory;

  public LifecycleRegistry(
      Factory<K, V, C, E> factory,
      RegistryStoreFactory<K> storeFactory,
      ExceptionFactory<E> exceptionFactory) {
    this.store = storeFactory.createStore();
    this.factory = factory;
    this.exceptionFactory = exceptionFactory;
  }

  /**
   * {@inheritDoc}
   *
   * @see net.sf.hajdbc.util.concurrent.Registry#get(java.lang.Object, java.lang.Object)
   */
  @Override
  public V get(K key, C context) throws E {
    RegistryEntry entry = this.store.get(key);

    if (entry != null) {
      return entry.getValue();
    }

    V value = this.factory.create(key, context);

    entry = new RegistryEntry(value);

    RegistryEntry existing = this.store.setIfAbsent(key, entry);

    if (existing != null) {
      return existing.getValue();
    }

    try {
      value.start();

      entry.started();

      return value;
    } catch (Exception e) {
      this.store.clear(key);

      try {
        value.stop();
      } catch (Exception re) {
        this.logger.log(Level.INFO, re);
      }

      throw this.exceptionFactory.createException(e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see net.sf.hajdbc.util.concurrent.Registry#remove(java.lang.Object)
   */
  @Override
  public void remove(K key) throws E {
    RegistryEntry entry = this.store.clear(key);

    if (entry != null) {
      entry.getValue().stop();
    }
  }

  private class RegistryEntry {
    private final V value;
    private final AtomicReference<CountDownLatch> latchRef =
        new AtomicReference<CountDownLatch>(new CountDownLatch(1));

    RegistryEntry(V value) {
      this.value = value;
    }

    V getValue() throws E {
      CountDownLatch latch = this.latchRef.get();

      if (latch != null) {
        try {
          if (!latch.await(
              LifecycleRegistry.this.factory.getTimeout(),
              LifecycleRegistry.this.factory.getTimeoutUnit())) {
            throw LifecycleRegistry.this.exceptionFactory.createException(new TimeoutException());
          }
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw LifecycleRegistry.this.exceptionFactory.createException(e);
        }
      }

      return this.value;
    }

    void started() {
      CountDownLatch latch = this.latchRef.getAndSet(null);

      if (latch != null) {
        latch.countDown();
      }
    }
  }
}