package gitbucket.core.servlet

import java.io.File
import java.sql.{DriverManager, Connection}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util._
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
import gitbucket.core.plugin._

object AutoUpdate {

  /**
   * The history of versions. A head of this sequence is the current BitBucket version.
   */
  val versions = Seq(
    new Version(3, 0),
    new Version(2, 8),
    new Version(2, 7) {
      override def update(conn: Connection, cl: ClassLoader): Unit = {
        super.update(conn, cl)
        conn.select("SELECT * FROM REPOSITORY"){ rs =>
          // Rename attached files directory from /issues to /comments
          val userName = rs.getString("USER_NAME")
          val repoName = rs.getString("REPOSITORY_NAME")
          defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
            val oldDir = new File(newDir.getParentFile, "issues")
            if(oldDir.exists && oldDir.isDirectory){
              oldDir.renameTo(newDir)
            }
          }
          // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
          val originalUserName = rs.getString("ORIGIN_USER_NAME")
          val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
          if(originalUserName != null && originalRepoName != null){
            if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
                originalUserName, originalRepoName) == 0){
              conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
                  "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
            }
          }
          // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
          val parentUserName = rs.getString("PARENT_USER_NAME")
          val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
          if(parentUserName != null && parentRepoName != null){
            if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
                parentUserName, parentRepoName) == 0){
              conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
                  "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
            }
          }
        }
      }
    },
    new Version(2, 6),
    new Version(2, 5),
    new Version(2, 4),
    new Version(2, 3) {
      override def update(conn: Connection, cl: ClassLoader): Unit = {
        super.update(conn, cl)
        conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
          val curInfo = rs.getString("ADDITIONAL_INFO")
          val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
          if (curInfo != newInfo) {
            conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
          }
        }
        ignore {
          FileUtils.deleteDirectory(Directory.getPluginCacheDir())
          //FileUtils.deleteDirectory(new File(Directory.PluginHome))
        }
      }
    },
    new Version(2, 2),
    new Version(2, 1),
    new Version(2, 0){
      override def update(conn: Connection, cl: ClassLoader): Unit = {
        import eu.medsea.mimeutil.{MimeUtil2, MimeType}

        val mimeUtil = new MimeUtil2()
        mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")

        super.update(conn, cl)
        conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
          defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
            if(dir.exists && dir.isDirectory){
              dir.listFiles.foreach { file =>
                if(file.getName.indexOf('.') < 0){
                  val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
                  if(mimeType.startsWith("image/")){
                    file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
                  }
                }
              }
            }
          }
        }
      }
    },
    Version(1, 13),
    Version(1, 12),
    Version(1, 11),
    Version(1, 10),
    Version(1, 9),
    Version(1, 8),
    Version(1, 7),
    Version(1, 6),
    Version(1, 5),
    Version(1, 4),
    new Version(1, 3){
      override def update(conn: Connection, cl: ClassLoader): Unit = {
        super.update(conn, cl)
        // Fix wiki repository configuration
        conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
          using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
            defining(git.getRepository.getConfig){ config =>
              if(!config.getBoolean("http", "receivepack", false)){
                config.setBoolean("http", null, "receivepack", true)
                config.save
              }
            }
          }
        }
      }
    },
    Version(1, 2),
    Version(1, 1),
    Version(1, 0),
    Version(0, 0)
  )
  
  /**
   * The head version of BitBucket.
   */
  val headVersion = versions.head
  
  /**
   * The version file (GITBUCKET_HOME/version).
   */
  lazy val versionFile = new File(GitBucketHome, "version")
  
  /**
   * Returns the current version from the version file.
   */
  def getCurrentVersion(): Version = {
    if(versionFile.exists){
      FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
        case Array(majorVersion, minorVersion) => {
          versions.find { v => 
            v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
          }.getOrElse(Version(0, 0))
        }
        case _ => Version(0, 0)
      }
    } else Version(0, 0)
  }
  
}

/**
 * Initialize GitBucket system.
 * Update database schema and load plug-ins automatically in the context initializing.
 */
class InitializeListener extends ServletContextListener {
  import AutoUpdate._

  private val logger = LoggerFactory.getLogger(classOf[InitializeListener])

  override def contextInitialized(event: ServletContextEvent): Unit = {
    val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
    if(dataDir != null){
      System.setProperty("gitbucket.home", dataDir)
    }
    org.h2.Driver.load()

    defining(getConnection()){ conn =>
      // Migration
      logger.debug("Start schema update")
      Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
        FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
      }
      // Load plugins
      logger.debug("Initialize plugins")
      PluginRegistry.initialize(event.getServletContext, conn)
    }

  }

  def contextDestroyed(event: ServletContextEvent): Unit = {
    // Shutdown plugins
    PluginRegistry.shutdown(event.getServletContext)
  }

  private def getConnection(): Connection =
    DriverManager.getConnection(
      DatabaseConfig.url,
      DatabaseConfig.user,
      DatabaseConfig.password)

}
