# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file extends the basic query types, allowing them to represent
#  queries and operations executed against a MySQL data source.

from Library.DataSource.DataSources import ADataSource
from Library.DataSource.Queries import ADataRow
from Plugins.DataSource.SQLQueries import AnSQLQuery
from Library.String.Utilities import String
from DictionaryCursors import AMySQLDictionaryCursor, \
  AMySQLBufferedDictionaryCursor
import Plugins.DataSource.MySQL.connector as connector
import sys, time

## The status code that means success (OK)
MYSQL_OK = 0

## A SELECT statement
myqSelectStatement = "SELECT "
myqSelectTablesClause = " FROM "
myqCriteriaClause = " WHERE "
myqGroupingClause = " GROUP BY "
myqSortClause = " ORDER BY "
myqLimitClause = " LIMIT "
myqLimitMaximum = "18446744073709551615"
## The default sort order
myqDefaultSortOrder = 'asc'
## Sort order operators
myqSortOrderOperators = {'+': 'asc', '-': 'desc'}

## A full-text SELECT statement
myqMatchClause = " MATCH"
myqMatchCriteriaClause = " AGAINST"
myqMatchModeClause = " IN BOOLEAN MODE"
myqMatchRelevanceClause = "*100 AS "
myqMatchRelevanceFieldName = "$relevance"

## A SELECT COUNT(*) statement
myqSelectCountStatement = "SELECT COUNT"

## An INSERT statement
myqInsertStatement = "INSERT INTO "
myqValuesClause = " VALUES"
## The function to use when obtaining the last insert ID
msdfLastPrimaryKey = "LAST_INSERT_ID"

## An UPDATE statement
myqUpdateStatement = "UPDATE "
myqSetClause = " SET "

## A DELETE statement
myqDeleteStatement = "DELETE "
myqDeletePriority = " LOW_PRIORITY "
myqDeleteAll = "TRUNCATE TABLE"

## A SELECT FOUND_ROWS() statement
myqSelectFoundRowsStatement = "SELECT FOUND_ROWS()"

## A USE statement
myqUseStatement = "USE"

## A SHOW TABLES statement
myqShowTablesStatement = "SELECT `table_name` AS `TableName`"
myqShowFullTablesClause = ", `table_type` AS `TableType`"
myqShowTablesSourceClause = " FROM INFORMATION_SCHEMA.TABLES"
myqShowTablesCriteria = "table_schema = "

## Special characters that must be escaped in MySQL query strings
myqSpecialCharacters = {
    "\x00": "\\0",
    "'": "\\'",
    "\"": "\\\"",
    "\b": "\\b",
    "\n": "\\n",
    "\r": "\\r",
    "\t": "\\t",
    "\x1A": "\\Z",
    "\\": "\\\\"
}
## The quote character to use when quoting string literals in MySQL queries
myqQuoteCharacter = "\""
## The type of quote to use when marking identifiers in an SQL query string
myqIdentifierQuoteCharacter = "`"
## The type of "quotes" to use when marking lists in an SQL query string
myqListBeginCharacter = "("
myqListEndCharacter = ")"
myqListDelimiter = ","

## The default status message (Okay)
myqDefaultMessage = "Okay"

## A query executed against a MySQL data source.
class AMySQLQuery(AnSQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AnSQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AnSQLQuery.init(Self)

        if result:
            # Establish private attributes
            # The last message returned by the data source
            Self._message = myqDefaultMessage
            # The cursor type to use 
            Self._CursorType = AMySQLDictionaryCursor

        return result

    ## Destructor
    def __del__(Self):
        if Self._handle is not None:
            Self._handle.close()
            del Self._handle
            Self._handle = None

        AnSQLQuery.__del__(Self)

    ## Execute the query
    def execute(Self):
        result = -1

        Event = Self.generate("execute")
        
        if Event.handled():
            result = Event.result()
            del Event
            return result
        
        del Event
        
        # Prepare the query string
        Self.prepareQueryString()
        #print "Query:\n\t" + Self._queryString + "<br/>\n"        
        if len(Self._queryString) > 0:
            try:
                Self._handle = \
                  Self._Source.handle().cursor(None, None, Self._CursorType)
                # Time the query
                qStartTime = time.clock()
                # Execute the query
                Self._recordCount = Self._handle.execute(Self._queryString)
                # Stop timing the query
                qStopTime = time.clock()
                Self._runtime = qStopTime - qStartTime
                #sys.stderr.write("\ttook " + str(Self("runtime")) + " seconds\n")
                Self._result = 0
                result = Self._result

            except connector.errors.Error, Err:
                Self._result = Err.errno
                Self._message = Err.msg
                result = Self._result
                Self.error(Err)                
         
        return result

    ## Retrieve the next row from the query.
    def nextRecord(Self, RowClass=None, **params):
        Row = None

        Event = Self.generate("nextRecord", RowClass=RowClass, 
            params=params)
            
        if Event.handled():
            Row = Event.result()
            del Event
            return Row
            
        del Event
        
        if Self._handle is not None:
            rowValues = Self._handle.fetchone()
            if rowValues is not None:
                if RowClass is None:
                    Row = ADataRow(rowValues, **params)

                else:
                    Row = RowClass(rowValues, **params)

        return Row

    # Utility functions --------------------------------------------------------
    ## Escape special characters in the specified string so that it is
    #  safe for use with an SQL query.
    #
    #  This routine calls String.escape with #myqSpecialCharacters.
    def escapeString(Self, thisString):
        if len(thisString) > 0:
            return String.escape(thisString, myqSpecialCharacters)

        return thisString

    ## Return the quoted form of the specified string, such that it is
    #  suitable for use with an SQL query.
    #
    #  This routine calls String.quote() with #myqQuoteCharacter.
    def quoteString(Self, thisString):
        return String.quote(thisString, myqQuoteCharacter)

    ## Return the specified identifier, delimited with the special quotes
    #  used by the data source for identifiers.
    #
    #  If the data source for which the query is meant to be use does not use
    #  special quote characters, this routine should simply return the
    #  identifier string, unchanged.
    def quoteIdentifier(Self, thisIdentifier):
        if len(thisIdentifier) > 0:
            return String.quote(thisIdentifier, myqIdentifierQuoteCharacter)

        return thisIdentifier
        
    ## Implode identifiers in sort criteria, expanding sort order specifiers
    #  where given, and ensuring that identifiers are quoted properly.
    def implodeIdentifiersInSortCriteria(Self, thisList, itemsDelimiter=myqListDelimiter):
        result = ""

        if len(thisList) > 0:
            index = 0
            sortOrder = ""

            for identifier in thisList:
                if len(identifier) == 0:
                    continue
                    
                if index > 0:
                    result += itemsDelimiter
                    
                if identifier[0] in myqSortOrderOperators.keys():
                    sortOrder = myqSortOrderOperators[identifier[0]]
                    identifier = identifier[1:]

                result += Self.quoteIdentifier(identifier) + " " + sortOrder
                index += 1

        return result        

## A SELECT query executed against a MySQL data source.
class AMySQLSelectQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)

        if result:
            # Establish private attributes of the instance
            # The cursor type to use
            Self._CursorType = AMySQLBufferedDictionaryCursor
            
            # Establish public attributes of the instance
            # The fields that will be searched for values
            Self.fields = []
            # The tables to be searched
            Self.tables = []
            # The criteria used to match values
            Self.criteria = []
            # The grouping used when returning matching values
            Self.grouping = []
            # The sort order used when returning matching values
            Self.sort = []
            # The limit on the number of values returned
            Self.limit = -1
            # The number of records to skip before returning values
            Self.offset = 0

        return result

    ## Prepare the SELECT query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        ## SELECT ...
        Self._queryString += myqSelectStatement

        ## ... <field names> ...
        identifiers = Self.implodeIdentifiersIn(Self.fields)
        if len(identifiers) == 0:
            identifiers = "*"
        Self._queryString += identifiers

        ## ... FROM <table names> ...
        if len(Self.tables) > 0:
            Self._queryString += myqSelectTablesClause
            identifiers = Self.implodeIdentifiersIn(Self.tables)
            Self._queryString += identifiers      

        ## ... WHERE <criteria> ...
        if len(Self.criteria) > 0:
            Self._queryString += myqCriteriaClause
            criteria = Self.implodeCriteriaIn(Self.criteria)
            Self._queryString += criteria    
            
        ## ... GROUP BY <list> ...
        if len(Self.grouping) > 0:
            Self._queryString += myqGroupingClause
            identifiers = Self.implodeIdentifiersIn(Self.grouping)
            Self._queryString += identifiers

        ## ... ORDER BY <list> ...
        if len(Self.sort) > 0:                    
            Self._queryString += myqSortClause
            identifiers = Self.implodeIdentifiersInSortCriteria(Self.sort)
            Self._queryString += identifiers

        ## ... LIMIT <offset>, <limit>
        if Self.limit > -1: 
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + str(Self.limit)                

        elif Self.offset > 0:
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + myqLimitMaximum

## A full-text SELECT query.
class AMySQLFullTextQuery(AMySQLSelectQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLSelectQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        return AMySQLSelectQuery.init(Self)

    ## Prepare the SELECT query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        ## SELECT ...
        Self._queryString += myqSelectStatement
        
        ## ... <field names> ...
        identifiers = Self.implodeIdentifiersIn(Self.fields)
        if len(identifiers) == 0:
            identifiers = "*"
            
        Self._queryString += identifiers

        matchClause = ""
        ## ... criteria ...
        if len(Self.criteria) > 0:
            matchClause += Self.implodeCriteriaIn(Self.criteria)

        ## ... , <match clause> AS <field name> ...
        ## This way, we get an extra field that contains a value indicating
        ## how relevant each returned record is to the specified search terms.
        Self._queryString += myqListDelimiter + matchClause + \
          myqMatchRelevanceClause + \
          Self.quoteIdentifier(myqMatchRelevanceFieldName)

        ## ... FROM <table names> ...
        if len(Self.tables) > 0:
            Self._queryString += myqSelectTablesClause
            identifiers = Self.implodeIdentifiersIn(Self.tables)
            Self._queryString += identifiers

        ## ... WHERE <criteria> ...
        Self._queryString += myqCriteriaClause + matchClause

        ## ... ORDER BY <list> ...
        if len(Self.sort) > 0:
            Self._queryString += myqSortClause
            identifiers = Self.implodeIdentifiersInSortCriteria(Self.sort)
            Self._queryString += identifiers

        ## ... LIMIT <offset>, <limit>                  
        if Self.limit > -1:                         
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + str(Self.limit)

        elif Self.offset > 0:
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + myqLimitMaximum

## A MySQL SELECT COUNT(*) query
class AMySQLSelectCountQuery(AMySQLSelectQuery):
    ## Prepare the SELECT query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        ## SELECT COUNT...
        Self._queryString += myqSelectCountStatement

        ## ... <field names> ...
        identifiers = Self.implodeIdentifiersIn(Self.fields)
        if len(identifiers) == 0:
            identifiers = "*"
        Self._queryString += myqListBeginCharacter + identifiers + \
            myqListEndCharacter

        ## ... FROM <table names> ...
        if len(Self.tables) > 0:
            Self._queryString += myqSelectTablesClause
            identifiers = Self.implodeIdentifiersIn(Self.tables)
            Self._queryString += identifiers      

        ## ... WHERE <criteria> ...
        if len(Self.criteria) > 0:
            Self._queryString += myqCriteriaClause
            criteria = Self.implodeCriteriaIn(Self.criteria)
            Self._queryString += criteria    
            
        ## ... GROUP BY <list> ...
        if len(Self.grouping) > 0:
            Self._queryString += myqGroupingClause
            identifiers = Self.implodeIdentifiersIn(Self.grouping)
            Self._queryString += identifiers

        ## ... ORDER BY <list> ...
        if len(Self.sort) > 0:                    
            Self._queryString += myqSortClause
            identifiers = Self.implodeIdentifiersInSortCriteria(Self.sort)
            Self._queryString += identifiers

        ## ... LIMIT <offset>, <limit>
        if Self.limit > -1: 
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + str(Self.limit)                

        elif Self.offset > 0:
            Self._queryString += myqLimitClause + str(Self.offset) + \
              myqListDelimiter + myqLimitMaximum


## A MySQL INSERT query.
class AMySQLInsertQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)

        if result == True:
            # Establish private attributes
            # The unique identifier for the new row (after a successful 
            # insertion)
            Self._identifier = None
            
            # Establish public attributes of the instance
            # The location into which the new record will be placed
            Self.location = ""
            # The fields and values to be inserted
            Self.fields = {}

        return result

    ## Prepare the INSERT query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        # INSERT INTO <table> ...
        Self._queryString += myqInsertStatement
        Self._queryString += Self.quoteIdentifier(Self.location)

        # ... (<fields list>) ...
        identifiers = Self.implodeIdentifiersIn(Self.fields.keys())
        identifiers = myqListBeginCharacter + identifiers + myqListEndCharacter
        Self._queryString += identifiers

        # ... VALUES (<values list>) ...
        values = Self.implodeValuesIn(Self.fields.values())
        values = myqListBeginCharacter + values + myqListEndCharacter
        Self._queryString += myqValuesClause + values
        
    ## Execute the query
    def execute(Self):
        # If the query successfully executes, get the identifier of the new
        # record
        if AMySQLQuery.execute(Self) == 0:
            Query = AMySQLSelectFunctionQuery(DataSource = Self._Source)
            Query.function = msdfLastPrimaryKey
            if Query.execute() == MYSQL_OK:
                # Fetch the resulting row
                Row = Query.nextRecord()
                if Row is not None:
                    Self._identifier = long(Row[0])

                # Free the query and the row
                del Row
                del Query
        
    ## Properties --------------------------------------------------------------
    # Retrieve the identifier for the new record
    def identifier(Self):
        return Self._identifier

## A MySQL UPDATE query.
class AMySQLUpdateQuery(AMySQLInsertQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLInsertQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLInsertQuery.init(Self)

        if result == True:
            # Establish public attributes of the instance
            # The criteria used to match the records that will be updated
            Self.criteria = []

        return result

    ## Prepare the UPDATE query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        # UPDATE <table> ...
        Self._queryString += myqUpdateStatement
        Self._queryString += Self.quoteIdentifier(Self.location)

        # SET (<fields/values list>)
        values = Self.implodeFieldsIn(Self.fields)
        Self._queryString += myqSetClause + values

        # WHERE ...
        if len(Self.criteria) > 0:
            Self._queryString += myqCriteriaClause
            criteria = Self.implodeCriteriaIn(Self.criteria)
            Self._queryString += criteria
            
    ## Execute the query
    def execute(Self):
        AMySQLQuery.execute(Self)            

## A MySQL DELETE query.
class AMySQLDeleteQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)

        if result == True:
            # Establish public attributes of the instance
            # The locations from which records will be deleted
            Self.locations = []
            # The criteria used to determine which records are deleted
            Self.criteria = []
            # Whether or not the statement is to be executed immediately
            Self.immediate = False

        return result

    ## Prepare the DELETE query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        if len(Self.criteria) > 0:
            # DELETE ...
            Self._queryString += myqDeleteStatement

            # ... LOW_PRIORITY ...
            if Self.immediate == False:
                Self._queryString += myqDeletePriority

            # ... FROM <table name> ...
            Self._queryString += myqSelectTablesClause
            identifiers = Self.implodeIdentifiersIn(Self.locations)
            Self._queryString += identifiers

            # ... WHERE <criteria list> ...
            Self._queryString += myqCriteriaClause
            criteria = Self.implodeCriteriaIn(Self.criteria)
            Self._queryString += criteria

        else:
            # TRUNCATE TABLE <table names>
            Self._queryString = myqDeleteAll
            identifiers = Self.implodeIdentifiersIn(Self.locations)
            Self._queryString += identifiers

## A SELECT statement that returns the result of a function.
class AMySQLSelectFunctionQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)

        if result == True:
            # Establish public properties
            # The name of the function to call
            Self.function = ""
            # The function parameters
            Self.parameters = []

        return result

    ## Prepare the SELECT query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        if len(Self.function) > 0:
            # SELECT <function>
            Self._queryString += myqSelectStatement
            Self._queryString += Self.function
            Self._queryString += myqListBeginCharacter

            # ...(parameters)
            if len(Self.parameters) > 0:
                parameters = Self.implodeValuesIn(Self.parameters)
                Self._queryString += parameters

            Self._queryString += myqListEndCharacter
            
## A USE query executed against a MySQL database
class AMySQLUseDatabaseQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)
        
    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)
        
        if result:
            # Establish public attributes
            # The name of the database to use
            Self.database = ""
            
        return result
        
    ## Prepare the USE query string
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
        
        ## USE <database>
        Self._queryString += myqUseStatement
        Self._queryString += " " + Self.quoteIdentifier(Self.database)    

## A SHOW TABLES query executed against a MySQL database.
class AMySQLShowTablesQuery(AMySQLQuery):
    ## Constructor
    def __init__(Self, **params):
        AMySQLQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AMySQLQuery.init(Self)

        if result == True:
            # Establish public attributes
            # The database for which table names will be retrieved
            Self.location = ""
            # Criteria used to match table names
            Self.criteria = []
            # Whether or not to display full output (BASE TABLE or VIEW)
            Self.full = False

        return result

    ## Prepare the SHOW TABLE query string.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        if Event.handled():
            del Event
            return
            
        del Event
                
        # SELECT `table_name` AS `TableName`
        Self._queryString += myqShowTablesStatement

        # ... `table_type` AS `TableType` ...
        if Self.full == True:
            Self._queryString += myqShowFullTablesClause

        # ... FROM INFORMATION_SCHEMA.TABLES ...
        Self._queryString += myqShowTablesSourceClause

        # ... WHERE `table_schema`=<database name> ...
        Self.criteria.insert(0, myqShowTablesCriteria + Self.location)
        # ... AND ... <criteria>
        # If there are other criteria in the list, be sure to include them
        if len(Self.criteria) > 1:
            Self.criteria.insert(1, "AND")
        criteria = Self.implodeCriteriaIn(Self.criteria)
        Self._queryString += myqCriteriaClause + criteria
