import os.path
from model.Persistent import Persistent


class Schema_upgrader:
  def __init__( self, database, glob = None, read_file = None ):
    """
    Create a new schema upgrader and return it.

    @type database: Database
    @param database: the database to use for all schema upgrades
    @type glob: function or NoneType
    @param glob: a custom function to use for globbing files as per glob.glob() (optional)
    @type read_file: function or NoneType
    @param read_file: a custom function to use for reading schema files (optional)
    @rtype: Schema_upgrader
    @return: newly constructed Schema_upgrader
    """
    from glob import glob as real_glob

    self.__database = database
    self.__glob = glob or real_glob
    self.__read_file = read_file or Schema_upgrader.__read_file

  @staticmethod
  def __read_file( filename ):
    """
    Read a file and return all of its contents.

    @type filename: unicode
    @param filename: full path of the file to read
    @rtype: unicode
    @return: full contents of the file
    """
    return file( filename ).read()

  def upgrade_schema( self, to_version ):
    """
    Upgrade the database from its current version to a given version, applying all intervening
    schema delta files necessary to get there, and apply them in version order. If the given
    version is unknown, this method will upgrade to the latest known schema version that is
    before the given version.

    @type to_version: unicode
    @param to_version: the desired version to upgrade to, as a string
    """
    to_version = self.version_string_to_tuple( to_version )
    from_version = self.schema_version( self.__database )
    self.__database.commit()

    # if the database schema version is already equal to to_version, there's nothing to do
    if to_version == from_version:
      return

    if self.__database.backend == Persistent.SQLITE_BACKEND:
      extension = u"sqlite"
    else:
      extension = u"sql"

    filenames = self.__glob( u"model/delta/*.%s" % extension )
    versions = []

    # make a list of all available schema delta files
    for filename in filenames:
      base_filename = os.path.basename( filename )

      try:
        version = self.version_string_to_tuple( base_filename )
      except ValueError:
        continue

      # skip those versions that won't help us upgrade
      if version <= from_version:
        continue
      if version > to_version:
        continue

      versions.append( ( version, filename ) )

    if len( versions ) == 0:
      print "no new database schemas available"
      return

    # sort the schema delta files by version
    versions.sort( lambda a, b: cmp( a[ 0 ], b[ 0 ] ) )

    # apply the schema delta files in sorted order
    for ( version, filename ) in versions:
      self.apply_schema_delta( version, filename )

    self.__database.commit()
    print "successfully upgraded database schema"

  @staticmethod
  def schema_version( database, default_version = None ):
    try:
      schema_version = database.select_one( tuple, "select * from schema_version;" );
      schema_version = tuple( map( int, schema_version ) )
    # if there's no schema version table, then use the default version given. if there's no default
    # version, then assume the from_version is 1.5.4, which was the last version not to include a
    # schema_version table
    except:
      database.rollback()
      schema_version = default_version or ( 1, 5, 4 )

      # "release" is a reserved keyword in newer versions of sqlite, so put it in quotes
      database.execute( "create table schema_version ( major numeric, minor numeric, \"release\" numeric );", commit = False );
      database.execute( "insert into schema_version values ( %s, %s, %s );" % schema_version, commit = False );

    return schema_version

  def apply_schema_delta( self, version, filename ):
    """
    Upgrade the database from its current version to a given version, applying only the named
    schema delta file to do so. The changes are commited once complete, and the schema_version
    within the database is updated accordingly. This method assumes that the schema_version
    table exists and has one row.

    @type version: tuple
    @param version: ( major, minor, release ) with each version part as an integer
    @type filename: unicode
    @param filename: full path to the schema delta file to apply
    """
    print "upgrading database schema to version %s.%s.%s" % version

    # note: SQLite will auto-commit before certain statements (such as "create table"), which sort
    # of defeats the point of transactions. doing an explicit "begin transaction" first gives an
    # error later
    # http://oss.itsystementwicklung.de/download/pysqlite/doc/sqlite3.html#sqlite3-controlling-transactions

    self.__database.execute_script( self.__read_file( filename ), commit = False )
    self.__database.execute( "update schema_version set major = %s, minor = %s, \"release\" = %s;" % version, commit = False );
    self.__database.commit()

  @staticmethod
  def version_string_to_tuple( version ):
    """
    Given a version string with an optional file extension tacked on, convert the version to a
    tuple of integers and return it.

    @type version: unicode
    @param version: a version string of the form "major.minor.release"
    @rtype: tuple
    @return: ( major, minor, release ) with each version part as an integer
    @raises ValueError: invalid version or version parts cannot be converted to integers
    """
    VERSION_PARTS_COUNT = 3

    parts = version.split( "." )
    length = len( parts )

    if length == VERSION_PARTS_COUNT + 1:
      ( major, minor, release, extension ) = parts
    elif length == VERSION_PARTS_COUNT:
      ( major, minor, release ) = parts
    else:
      raise ValueError()

    try:
      major = int( major )
      minor = int( minor )
      release = int( release )
    except ( TypeError, ValueError ):
      raise ValueError()

    return ( major, minor, release )
