/*
 * Aipo is a groupware program developed by TOWN, Inc.
 * Copyright (C) 2004-2015 TOWN, Inc.
 * http://www.aipo.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aimluck.eip.orm;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.sql.DataSource;

import org.apache.cayenne.CayenneDataObject;
import org.apache.cayenne.CayenneException;
import org.apache.cayenne.DataObject;
import org.apache.cayenne.DataObjectUtils;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.access.DataContext;
import org.apache.cayenne.access.DataDomain;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.Transaction;
import org.apache.cayenne.conf.Configuration;
import org.apache.cayenne.conf.CustomDBCPDataSourceFactory;
import org.apache.cayenne.conf.DBCPDataSourceFactory;
import org.apache.cayenne.conf.DataSourceFactoryDelegate;
import org.apache.cayenne.dba.AutoAdapter;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.resources.JetspeedResources;

import com.aimluck.eip.orm.access.CustomTransaction;
import com.aimluck.eip.orm.query.SQLTemplate;
import com.aimluck.eip.orm.query.SelectQuery;

/**
 * データベース操作ユーティリティ
 * 
 */
public class Database {

  private static final JetspeedLogger logger = JetspeedLogFactoryService
    .getLogger(Database.class.getName());

  public static final String DEFAULT_ORG = "org001";

  protected static final String SHARED_DOMAIN = "SharedDomain";

  /**
   * 検索用クエリを作成します。
   * 
   * @param <M>
   * @param modelClass
   * @return
   */
  public static <M> SelectQuery<M> query(Class<M> modelClass) {
    return new SelectQuery<M>(modelClass);
  }

  /**
   * 検索用クエリを作成します。
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @return
   */
  public static <M> SelectQuery<M> query(DataContext dataContext,
      Class<M> modelClass) {
    return new SelectQuery<M>(dataContext, modelClass);
  }

  /**
   * 検索用クエリを作成します。
   * 
   * @param <M>
   * @param modelClass
   * @param exp
   * @return
   */
  public static <M> SelectQuery<M> query(Class<M> modelClass, Expression exp) {
    return new SelectQuery<M>(modelClass, exp);
  }

  /**
   * 検索用クエリを作成します。
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @param exp
   * @return
   */
  public static <M> SelectQuery<M> query(DataContext dataContext,
      Class<M> modelClass, Expression exp) {
    return new SelectQuery<M>(dataContext, modelClass, exp);
  }

  /**
   * SQL検索クエリを作成します。
   * 
   * @param <M>
   * @param modelClass
   * @param sql
   * @return
   */
  public static <M> SQLTemplate<M> sql(Class<M> modelClass, String sql) {
    return new SQLTemplate<M>(modelClass, sql);
  }

  /**
   * SQL検索クエリを作成します。
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @param sql
   * @return
   */
  public static <M> SQLTemplate<M> sql(DataContext dataContext,
      Class<M> modelClass, String sql) {
    return new SQLTemplate<M>(dataContext, modelClass, sql);
  }

  /**
   * プライマリキーで指定されたオブジェクトモデルを取得します。
   * 
   * @param <M>
   * @param modelClass
   * @param primaryKey
   * @return
   */
  public static <M> M get(Class<M> modelClass, Object primaryKey) {
    return get(DataContext.getThreadDataContext(), modelClass, primaryKey);
  }

  /**
   * 指定されたオブジェクトモデルを取得します。
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @param primaryKey
   * @return
   */
  @SuppressWarnings("unchecked")
  public static <M> M get(DataContext dataContext, Class<M> modelClass,
      Object primaryKey) {
    beginTransaction(dataContext);
    return (M) DataObjectUtils.objectForPK(dataContext, modelClass, primaryKey);
  }

  /**
   * 
   * @param <M>
   * @param modelClass
   * @param key
   * @param value
   * @return
   */
  public static <M> M get(Class<M> modelClass, String key, Object value) {
    return get(DataContext.getThreadDataContext(), modelClass, key, value);
  }

  /**
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @param key
   * @param value
   * @return
   */
  @SuppressWarnings("unchecked")
  public static <M> M get(DataContext dataContext, Class<M> modelClass,
      String key, Object value) {
    beginTransaction(dataContext);
    return (M) dataContext.refetchObject(new ObjectId(modelClass
      .getSimpleName(), key, value));
  }

  /**
   * オブジェクトモデルを新規作成します。
   * 
   * @param <M>
   * @param modelClass
   * @return
   */
  public static <M> M create(Class<M> modelClass) {
    return create(DataContext.getThreadDataContext(), modelClass);
  }

  /**
   * オブジェクトモデルを新規作成します。
   * 
   * @param <M>
   * @param dataContext
   * @param modelClass
   * @return
   */
  @SuppressWarnings("unchecked")
  public static <M> M create(DataContext dataContext, Class<M> modelClass) {
    return (M) dataContext.createAndRegisterNewObject(modelClass);

  }

  /**
   * オブジェクトモデルを削除します。
   * 
   * @param target
   */
  public static void delete(Persistent target) {
    delete(DataContext.getThreadDataContext(), target);
  }

  /**
   * オブジェクトモデルを削除します。
   * 
   * @param dataContext
   * @param target
   */
  public static void delete(DataContext dataContext, Persistent target) {
    dataContext.deleteObject(target);
  }

  /**
   * オブジェクトモデルをすべて削除します。
   * 
   * @param target
   */
  public static void deleteAll(List<?> target) {
    deleteAll(DataContext.getThreadDataContext(), target);
  }

  /**
   * オブジェクトモデルをすべて削除します。
   * 
   * @param dataContext
   * @param target
   */
  public static void deleteAll(DataContext dataContext, List<?> target) {
    dataContext.deleteObjects(target);

  }

  /**
   * オブジェクトモデルをすべて削除します。
   * 
   * @param target
   */
  public static void deleteAll(DataObject... target) {
    deleteAll(DataContext.getThreadDataContext(), target);
  }

  /**
   * オブジェクトモデルをすべて削除します。
   * 
   * @param dataContext
   * @param target
   */
  public static void deleteAll(DataContext dataContext, DataObject... target) {
    dataContext.deleteObjects(Arrays.asList(target));
  }

  /**
   * 現在までの更新をコミットします。
   * 
   */
  public static void commit() {
    commit(DataContext.getThreadDataContext());
  }

  /**
   * 現在までの更新をコミットします。
   * 
   * @param dataContext
   */
  public static void commit(DataContext dataContext) {
    beginTransaction(dataContext);
    dataContext.commitChanges();
    Transaction threadTransaction = Transaction.getThreadTransaction();
    if (threadTransaction != null) {
      try {
        threadTransaction.commit();
      } catch (IllegalStateException e) {
        logger.error(e.getMessage(), e);
      } catch (SQLException e) {
        logger.error(e.getMessage(), e);
      } catch (CayenneException e) {
        logger.error(e.getMessage(), e);
      } finally {
        Transaction.bindThreadTransaction(null);
      }
    }
  }

  /**
   * 現在までの更新をロールバックします。
   * 
   */
  public static void rollback() {
    rollback(DataContext.getThreadDataContext());
  }

  /**
   * 現在までの更新をロールバックします。
   * 
   * @param dataContext
   */
  public static void rollback(DataContext dataContext) {
    try {
      beginTransaction(dataContext);
      dataContext.rollbackChanges();
    } catch (Throwable t) {
      logger.warn("[Database] rollback", t);
    }
  }

  /**
   * DataRow から指定したキーの値を取得します。
   * 
   * @param dataRow
   * @param key
   * @return
   */
  public static Object getFromDataRow(DataRow dataRow, Object key) {
    String lowerKey = ((String) key).toLowerCase();
    if (dataRow.containsKey(lowerKey)) {
      return dataRow.get(lowerKey);
    } else {
      return dataRow.get(((String) key).toUpperCase());
    }
  }

  public static <M> M objectFromRowData(DataRow dataRow, Class<M> rootClass) {
    try {
      M model = rootClass.newInstance();
      CayenneDataObject obj = (CayenneDataObject) model;

      ObjEntity objEntity =
        DataContext.getThreadDataContext().getEntityResolver().lookupObjEntity(
          obj);

      ObjectId objId =
        createObjectId(objEntity.getName(), dataRow, objEntity.getDbEntity());
      if (objId != null) {
        obj.setObjectId(objId);
      }

      @SuppressWarnings("unchecked")
      Iterator<ObjAttribute> iterator = objEntity.getAttributes().iterator();
      while (iterator.hasNext()) {
        ObjAttribute objAttribute = iterator.next();
        DbAttribute dbAttribute = objAttribute.getDbAttribute();
        Object value = getFromDataRow(dataRow, dbAttribute.getName());
        if (value != null) {
          obj.writeProperty(objAttribute.getName(), value);
        }
      }
      return model;
    } catch (InstantiationException ignore) {
      // ignore
    } catch (IllegalAccessException ignore) {
      // ignore
    }
    return null;
  }

  public static ObjectId createObjectId(String entityName, DataRow dataRow,
      DbEntity entity) {

    @SuppressWarnings("unchecked")
    List<DbAttribute> pk = entity.getPrimaryKey();
    if (pk.size() == 1) {
      DbAttribute attribute = pk.get(0);

      String key = attribute.getName();

      Object val = getFromDataRow(dataRow, key);
      if (val == null) {
        return null;
      } else {
        return new ObjectId(entityName, attribute.getName(), val);
      }
    }

    Map<String, Object> idMap = new HashMap<String, Object>(pk.size() * 2);
    DbAttribute attribute;
    Object val;
    for (Iterator<DbAttribute> it = pk.iterator(); it.hasNext(); idMap.put(
      attribute.getName(),
      val)) {
      attribute = it.next();
      String key = attribute.getName();

      val = getFromDataRow(dataRow, key);
      if (val == null) {
        return null;
      }
    }

    return new ObjectId(entityName, idMap);
  }

  public static String getDomainName() {
    try {
      return DataContext.getThreadDataContext().getParentDataDomain().getName();
    } catch (Throwable ignore) {
      return null;
    }
  }

  public static DataContext createDataContext(String orgId) throws Exception {

    DataDomain dataDomain =
      Configuration.getSharedConfiguration().getDomain(SHARED_DOMAIN);

    DataDomain destDataDomain =
      new DataDomain(orgId, dataDomain.getProperties());
    destDataDomain.setEntityResolver(dataDomain.getEntityResolver());
    destDataDomain.setEventManager(dataDomain.getEventManager());
    destDataDomain.setTransactionDelegate(dataDomain.getTransactionDelegate());
    DataNode dataNode = new DataNode(orgId + "domainNode");
    dataNode.setDataMaps(dataDomain.getDataMaps());
    dataSourceFactory.initializeWithParentConfiguration(Configuration
      .getSharedConfiguration());
    DataSource dataSource =
      dataSourceFactory.getDataSource("datasource/dbcp-"
        + orgId
        + ".properties");

    dataNode.setDataSource(dataSource);
    dataNode.setAdapter(new AutoAdapter(dataSource));
    destDataDomain.addNode(dataNode);

    return destDataDomain.createDataContext();
  }

  public static void tearDown() {
    Transaction threadTransaction = Transaction.getThreadTransaction();
    if (threadTransaction != null) {
      try {
        threadTransaction.rollback();
      } catch (IllegalStateException e) {
        logger.error(e.getMessage(), e);
      } catch (SQLException e) {
        logger.error(e.getMessage(), e);
      } catch (CayenneException e) {
        logger.error(e.getMessage(), e);
      } catch (Throwable t) {
        logger.warn(t.getMessage(), t);
      } finally {
        Transaction.bindThreadTransaction(null);
      }
    }
    if (dataSourceFactory instanceof DataSourceFactoryDelegate) {
      ((DataSourceFactoryDelegate) dataSourceFactory).tearDown();
    }
    DataContext.bindThreadDataContext(null);
  }

  public static boolean isJdbcPostgreSQL() {

    DataContext dataContext = DataContext.getThreadDataContext();
    String adapterName = null;
    try {
      adapterName =
        ((AutoAdapter) dataContext.getParentDataDomain().getNode(
          Database.getDomainName() + "domainNode").getAdapter())
          .getAdapter()
          .getClass()
          .getName();
    } catch (Exception e) {
      logger.warn(e.getMessage(), e);
    }

    return adapterName != null && adapterName.endsWith("PostgresAdapter");
  }

  public static boolean isJdbcMySQL() {

    DataContext dataContext = DataContext.getThreadDataContext();
    String adapterName = null;
    try {
      adapterName =
        ((AutoAdapter) dataContext.getParentDataDomain().getNode(
          Database.getDomainName() + "domainNode").getAdapter())
          .getAdapter()
          .getClass()
          .getName();
    } catch (Exception e) {
      logger.warn(e.getMessage(), e);
    }

    return adapterName != null && adapterName.endsWith("MySQLAdapter");
  }

  protected static DBCPDataSourceFactory createDataSourceFactory() {
    String property =
      System.getProperty("com.aimluck.eip.orm.DataSourceFactory");
    if (property == null || property.isEmpty()) {
      return new CustomDBCPDataSourceFactory();
    } else {
      try {
        Class<?> forName = Class.forName(property);
        DBCPDataSourceFactory instance =
          (DBCPDataSourceFactory) forName.newInstance();
        return instance;
      } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
      } catch (InstantiationException e) {
        throw new RuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private static DBCPDataSourceFactory dataSourceFactory = null;

  public static void initialize(ServletContext servletContext) {
    String property =
      servletContext.getInitParameter("com.aimluck.eip.orm.DataSourceFactory");
    if (property == null || property.isEmpty()) {
      dataSourceFactory = new CustomDBCPDataSourceFactory();
    } else {
      try {
        Class<?> forName = Class.forName(property);
        dataSourceFactory = (DBCPDataSourceFactory) forName.newInstance();
      } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
      } catch (InstantiationException e) {
        throw new RuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public static void beginTransaction(DataContext dataContext) {
    boolean res =
      JetspeedResources.getBoolean("aipo.jdbc.aggregateTransaction");
    if (!res) {
      return;
    }
    Transaction tx = Transaction.getThreadTransaction();
    if (tx == null) {
      CustomTransaction ctx = new CustomTransaction();
      ctx.begin();
      Transaction.bindThreadTransaction(ctx);
    }
  }

  public static String castToIntRawColumn(String column) {
    if (isJdbcMySQL()) {
      return "CAST(" + column + " AS UNSIGNED)";
    } else if (isJdbcPostgreSQL()) {
      return "CAST(" + column + " AS INT)";
    }
    return column;
  }

  private Database() {
  }

}
