#!/usr/bin/python
# -*- coding: utf-8 -*-

import urllib
import urlparse
import psycopg2, psycopg2.extras
import cStringIO as StringIO
import sys
import time


argparse_help = "Path to the PostgreSQL PiAnoS 4 database. This path has the form 'pgsql://username:password@host:port/dbname'. You can use the short syntax '/dbname', or omit some fields (e.g. pgsql://example.com/pianos), to use the defaults (username=pianos, password=pianos, host=localhost, port=5432)"

def parseConnectionString( string ):
	name = None
	user = 'pianos'
	password = 'pianos'
	hostname = 'localhost'
	port = 5432
	
	expectedScheme = 'pgsql'
	expectedFullScheme = expectedScheme + '://'
	
	if string[:len( expectedFullScheme )] != expectedFullScheme:
		url = expectedFullScheme + string
	else:
		url = string
		
	res = urlparse.urlsplit( url )
	if res.scheme != expectedScheme:
		raise ValueError( "Invalid PostgreSQL url scheme in '%s'" % string )
		
	# Since urlparse does not recognize pgsql:// as a valid scheme, it will not parse correctly
	# the hostname:port/path?foo=bar#baz part. We thus replace pgsql:// by http:// to get the
	# correct parsed values.
	url = 'http' + url[len( res.scheme ):]
	res = urlparse.urlsplit( url )
	if len( res.path ):
		name = urllib.unquote( res.path.strip().strip( '/' ) )
	if res.username: 
		user = res.username
	if res.password: 
		password = res.password
	if res.hostname: 
		hostname = res.hostname
	if res.port:
		port = int( res.port )
	
	if name is None or user is None or password is None or hostname is None or port is None:
		raise ValueError( "Invalid PostgreSQL url: %s" % string )
		
	return hostname, port, name, user, password, 'pgsql://' + user + '@' + hostname + ':' + str( port ) + '/' + name
	
	
	
class Database:

	def __init__( self, url, verbose = True, dont_register_admin = False, use_dict_cursors = True ):
		hostname, port, name, user, password, self.url = parseConnectionString( url )
		self.verbose = verbose
		if verbose:
			print "Connecting to '%s'" % self.url
		self.conn = psycopg2.connect( host = hostname, port = port, database = name, user = user, password = password, cursor_factory = psycopg2.extras.DictCursor if use_dict_cursors else None )
		c = self.conn.cursor()
		try:
			counts = []
			for x in ['object', 'image', 'exercise', 'person']:
				c.execute( "SELECT count(*) FROM " + x + ";" )
				counts.append( c.fetchone()[0] )
			if not dont_register_admin:
				c.execute( "SELECT set_config('unil.user',(SELECT person.id FROM person,membership,pgroup WHERE person.id = membership.person AND membership.pgroup = pgroup.id AND pgroup.tutors IS true ORDER BY person.id ASC LIMIT 1)::text,false);" )
		except psycopg2.ProgrammingError, err:
			raise ValueError( "'%s' is not a valid PiAnoS database (error reported: %s)" % ( self.url, str( err ) ) )
		if verbose:
			print "  Database OK (%d objects, %d images, %d exercises, %d users)" % tuple( counts )
	
	def cursor( self ):
		return self.conn.cusor()
		
	def query( self, sql, *args, **kwargs ):
		c = self.conn.cursor()
		c.execute( sql, *args, **kwargs )
		return c
		
	def create_folder( self, name ):
		c = self.conn.cursor()
		c.execute( "INSERT INTO folder(name) VALUES (%s) RETURNING id;", ( name, ) )
		return c.fetchone()[0]
	
	# Image is a PIL.Image instance, 'type' is the short text ID from the imagetype table (e.g. 'MARK' or 'PRINT')
	def create_image( self, type, image, resolution ):
		c = self.conn.cursor()
		buf = StringIO.StringIO()
		image.save( buf, 'PNG' )
		c.execute( "INSERT INTO image(type,name,mimetype,resolution,width,height,data) VALUES ((SELECT id FROM imagetype WHERE short=%s),%s,%s,%s,%s,%s,%s) RETURNING id;", ( type, image.filename, 'image/png', resolution, image.size[0], image.size[1], memoryview( buf.getvalue() ) ) )
		return c.fetchone()[0]
	
	# mark and _print must be PIL Images
	def create_exercise( self, folder_id, name, description, mark, mark_resolution, _print, _print_resolution ):
		mark_id = self.create_image( 'MARK', mark, mark_resolution )
		print_id = self.create_image( 'PRINT', _print, _print_resolution )
		c = self.conn.cursor()
		c.execute( "INSERT INTO exercise(name, description, folder, mark, print) VALUES (%s,%s,%s,%s,%s) RETURNING id;", ( name, description, folder_id, mark_id, print_id ) )
		return c.fetchone()[0]
		
	# return all assignments, optionally filtered by exercise name, user name or limited in number
	def collect_assignments( self, exname = None, username = None, limit = None ):
		sql = '''SELECT assignment.id,
			object.createdon,
			getAssignmentStatus(assignment.id) AS status,
			person.id AS userid,person.username AS username,
			exercise.id AS exid, exercise.name AS exname,
			exercise.mark AS mark_image, exercise.print AS print_image
		FROM assignment
		JOIN object ON assignment.id = object.id 
		JOIN person ON object.creator = person.id
		JOIN exercise ON exercise.id = assignment.exercise
		WHERE assignment.ispreview IS false
		'''
		replacements = []
		
		filters = []
		if username:
			sql += " AND person.username SIMILAR TO %s"
			replacements.append( username )
			filters.append( ( 'user', username ) )
		if exname:
			sql += " AND exercise.name SIMILAR TO %s"
			replacements.append( exname )
			filters.append( ( 'exercise', exname ) )
		
		sql += " ORDER BY exid,userid,createdon DESC"

		if self.verbose:
			if len( filters ):
				print "Searching in results filtered by", ', '.join( [x + '=' + y for x, y in filters] )
			else:
				print "Searching in all results"
		
		if limit:
			if self.verbose:
				print "Limiting the result set to " + limit + " users"
			sql += " LIMIT " + str( int( limit ) ) + ";"
			
		c = self.conn.cursor()
		res = c.execute( sql, tuple( replacements ) )
		if self.verbose:
			print "Found", c.rowcount, "assignments"
		
		return c

		
class Timer( object ):
	
	def __init__( self, total, max_itr = sys.maxint ):
		self.total = total
		self.ftotal = float( self.total )
		self.current = 0
		self.steps_before_next_estimation = 1
		self.estimation_steps = 0
		self.last_estimation_time = 0
		self.max_itr = max_itr
		
	def start( self ):
		self.start_time = time.time()
		
	def next( self, force_estimation = False ):
		self.current += 1
		self.estimation_steps += 1
		progress = float( self.current ) / self.ftotal * 100.0
		eta = None
		if force_estimation or self.estimation_steps >= self.steps_before_next_estimation:
			now = time.time()
			elapsed = now - self.start_time
			if elapsed > 0.0:
				speed = float( self.current ) / elapsed
				elapsed_last_check = now - self.last_estimation_time
				# print "Averaged last %d iterations (%f secs total, %f last check) to %fitr/sec" % (self.estimation_steps, elapsed, elapsed_last_check, speed)
				if elapsed_last_check < 0.5:
					self.steps_before_next_estimation = min( self.max_itr, 2 * self.steps_before_next_estimation )
				elif elapsed_last_check > 2.0 and self.steps_before_next_estimation >= 2:
					self.steps_before_next_estimation /= 2
				self.estimation_steps = 0
				self.last_estimation_time = now
				remaining = self.total - self.current
				if remaining < 0:	
					eta = 0.0
				else:
					eta = remaining / speed
		return progress, eta
			
	def end( self ):
		self.end_time = time.time()
		return self.end_time - self.start_time
		
