package jp.sf.amateras.mirage.scala

import collection.JavaConversions._
import jp.sf.amateras.mirage.parser.{SqlContext, Node, SqlParserImpl}
import jp.sf.amateras.mirage.{IterationCallback, SqlManagerImpl, SqlExecutor}

/**
 * SqlManager wrapper for Scala.
 */
class SqlManager private (sqlManager: jp.sf.amateras.mirage.SqlManagerImpl) {

  private lazy val sqlExecutor: SqlExecutor = Utilities.getField(sqlManager, "sqlExecutor")

  /**
   * Returns a single result.
   *
   * @param clazz the type of result
   * @param sql the sql
   * @param param the parameter object
   * @return Some(result) or None
   */
  def getSingleResult[T](clazz: Class[T], sql: SqlProvider, param: AnyRef = null): Option[T] = {
    val node: Node = new SqlParserImpl(sql.getSql()).parse()
    val context: SqlContext = prepareSqlContext(convertParam(param))
    node.accept(context)

    Option(sqlExecutor.getSingleResult(clazz, context.getSql, context.getBindVariables))
  }

  /**
   * Returns a list of results.
   *
   * @param clazz the type of result
   * @param sql the sql
   * @param param the parameter object
   * @return Some(result) or None
   */
  def getResultList[T](clazz: Class[T], sql: SqlProvider, param: AnyRef = null): List[T] = {
    val node: Node = new SqlParserImpl(sql.getSql()).parse()
    val context: SqlContext = prepareSqlContext(convertParam(param))
    node.accept(context)

    if(clazz == classOf[Map[String, _]]){
      // convert java.util.Map to scala.Map
      sqlExecutor.getResultList(classOf[java.util.Map[String, _]], context.getSql,
        context.getBindVariables).toList.map { entry => entry.toMap }.asInstanceOf[List[T]]
    } else {
      sqlExecutor.getResultList(clazz, context.getSql, context.getBindVariables).toList
    }
  }

  def iterate[T, R](clazz: Class[T], sql: SqlProvider, param: AnyRef = null) (callback: (T) => R): R = {
    val node: Node = new SqlParserImpl(sql.getSql()).parse()
    val context: SqlContext = prepareSqlContext(convertParam(param))
    node.accept(context)

    sqlExecutor.iterate(clazz, new IterationCallbackAdapter(callback), context.getSql, context.getBindVariables)
  }

  def iterate[T, R](clazz: Class[T], sql: SqlProvider, context: R) (callback: (T, R) => R): R = iterate(clazz, sql, null, context) (callback)

  def iterate[T, R](clazz: Class[T], sql: SqlProvider, param: AnyRef, context: R) (callback: (T, R) => R): R = {
    var result = context
    iterate(clazz, sql, param){t =>
      result = callback(t, result)
      result
    }
  }

  /**
   * Executes the updating SQL.
   *
   * @param sql the sql
   * @param param the parameter object
   * @return updated row count
   */
  def executeUpdate(sql: SqlProvider, param: AnyRef = null): Int = {
    val node: Node = new SqlParserImpl(sql.getSql()).parse()
    val context: SqlContext = prepareSqlContext(convertParam(param))
    node.accept(context)

    sqlExecutor.executeUpdateSql(context.getSql, context.getBindVariables, null)
  }

  /**
   * Returns the row count of the query.
   *
   * @param sql the sql
   * @param param the parameter object
   * @return the row count of the executed SQL
   */
  def getCount(sql: SqlProvider, param: AnyRef = null): Int = {
    val node: Node = new SqlParserImpl(sql.getSql()).parse()
    val context: SqlContext = prepareSqlContext(convertParam(param))
    node.accept(context)

    val countSql: String = sqlManager.getDialect.getCountSql(context.getSql);
    sqlExecutor.getSingleResult(classOf[java.lang.Integer], countSql, context.getBindVariables).intValue
  }

  /**
   * Finds the entity by the given primary key.
   *
   * @param clazz the type of entity
   * @param id primary keys
   * @return the entity. If the entity which corresponds to the given primary key is not found, this method returns None.
   */
  def findEntity[T](clazz: Class[T], id: Any*): Option[T] = {
    Option(sqlManager.findEntity(clazz, id.map {_.asInstanceOf[AnyRef]}: _*))
  }

  /**
   * Inserts the given entity.
   *
   * @param entity the entity to insert
   * @return updated row count
   */
  def insertEntity(entity: AnyRef): Int = sqlManager.insertEntity(entity)

  /**
   * Inserts given entities with batch mode.
   *
   * @param entities entities to insert
   * @return updated row count
   */
  def insertBatch(entities: AnyRef*): Int = sqlManager.insertBatch(entities: _*)

  /**
   * Updates the given entity.
   *
   * @param entity the entity to update
   * @return updated row count
   */
  def updateEntity(entity: AnyRef): Int = sqlManager.updateEntity(entity)

  /**
   * Updates given entities with batch mode.
   *
   * @param entities entities to update
   * @return updated row count
   */
  def updateBatch(entities: AnyRef*): Int = sqlManager.updateBatch(entities: _*)

  /**
   * Deletes the given entity.
   *
   * @param entity the entity to delete
   * @return updated row count
   */
  def deleteEntity(entity: AnyRef): Int = sqlManager.deleteEntity(entity)

  /**
   * Deletes given entities with batch mode.
   *
   * @param entities entities to insert
   * @return updated row count
   */
  def deleteBatch(entities: AnyRef*): Int = sqlManager.deleteBatch(entities: _*)

  /**
   * Converts to java.util.Map if param is scala.Map.
   */
  private def convertParam(param: AnyRef): AnyRef = param match {
    case map: Map[_, _] => (map:java.util.Map[_, _])
    case params => params
  }

  private def prepareSqlContext(param: AnyRef): SqlContext =
    Utilities.invokeMethod(sqlManager, "prepareSqlContext", Array(classOf[Object]), Array(param))

  /**
   * Adapter for that callback function that would be given to iterate().
   */
  private class IterationCallbackAdapter[T, R](val callback: (T) => R) extends IterationCallback[T, R] {
    def iterate(entity: T): R = callback(entity)
  }

}

object SqlManager {
  BeanDescFactoryInitializer.initialize() //It is called only once.
  def apply(sqlManager: SqlManagerImpl): SqlManager = new SqlManager(sqlManager)
}
