# -*- 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 defines a class that extends ADataSource to allow connections to
#  be made to MySQL databases.

from Library.DataSource.DataSources import ADataSourceError
from Library.DataSource.Databases import ADatabase, ADataOperation
from Plugins.DataSource.MySQL.Queries import AMySQLSelectQuery, \
  AMySQLSelectCountQuery, AMySQLFullTextQuery, AMySQLInsertQuery, \
  AMySQLUpdateQuery, AMySQLDeleteQuery, AMySQLShowTablesQuery, \
  AMySQLUseDatabaseQuery, MYSQL_OK
import Plugins.DataSource.MySQL.connector as connector

## The default message (OK) generated by the data source
mdsStatusOkay = "Okay"
## The default port to use when connecting to the data source
mdsDefaultPort = 3306

## The error message used when failing to open the specified database
mdsOpenDatabaseError = "failed to open the database \"{}\" in {} because: {}"
## The error message used when a data operation fails
mdsDataOperationError = "failed to {} data from {} because: {}; {}"

## This class extends ADatabase to allow connections to be made to MySQL
#  databases.
class AMySQLDatabase(ADatabase):
    ## Constructor
    def __init__(Self, **params):
        ADatabase.__init__(Self, **params)

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

        if result == True:
            # Establish private attributes
            # The handle assigned to the database connection
            Self._handle = None
            # The last message generated by the data source
            Self._lastMessage = mdsStatusOkay
            # The last query string generated by the data source
            Self._queryString = ""

        return result

    ## Connect to the data source.
    #
    #  This routine expects that \p params will contain the following items:
    #  - \p host -- The host name or IP address of the MySQL database server
    #  - \p user -- The username to use when connecting to the database server
    #
    #  If either parameter is missing, the call will fail.
    #
    #  The following optional parameters may also be specified in \p params:
    #  - \p port -- The port on the server to which to connect.  If this 
    #               parameter is omitted, the default port (3306) is used.
    #  - \p database -- The name of the database on the server to open after
    #                   the connection is made.  If this parameter is omitted,
    #                   NO database is opened and only a basic connection is
    #                   made.  
    #  - \p password -- The password to use when connecting.  If this parameter
    #                   is omitted, an empty password string is sent to the
    #                   server.
    def connect(Self, **params):
        result = -1
        
        Event = Self.generate("connect", params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        # Establish parameter defaults
        port = mdsDefaultPort
        database = ""
        password = ""

        if "password" in params:
            password = params["password"]
            del params["password"]

        Self._params.update(params)        

        if "host" in Self._params:
            host = Self._params["host"]
            
        else:
            return result
            
        if "user" in Self._params:
            user = Self._params["user"]
            
        else:
            return result
            
        if "port" in Self._params:
            port = Self._params["port"]
            
        if "database" in Self._params:
            database = Self._params["database"]
        
        # Try to connect
        try:
            Self._handle=connector.Connect(
              host=host, port=port, db=database, user=user, 
              passwd=password)
            
            result = 0

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

        return result

    ## Disconnect from the data source
    def disconnect(Self):
        Event = Self.generate("disconnect")
        if Event.handled():
            del Event
            return
            
        del Event
        
        if Self._handle is not None:
            # Commit any changes
            Self._handle.commit()
            # Close the connection
            Self._handle.close()
            Self._handle = None
            
    ## Open the specified database.
    #
    #  This method can be used when connected to a database provider to specify
    #  which database to use.  For certain database providers, such as sqlite, 
    #  this method may do nothing at all.
    def open(Self, databaseName, **params):
        result = -1
        
        Event = Self.generate("open", databaseName=databaseName, params=params)
        if Event.handled():
            result = Event.result()
            
        del Event
        
        Query = AMySQLUseDatabaseQuery(DataSource=Self)
        Query.database = databaseName
        Self._lastMessage = Query.message()
        Self._queryString = Query.queryString()
        
        if Query.execute() != MYSQL_OK:
            Self.error(ADataSourceError(mdsOpenDatabaseError,
                Self.displayName(), Self._lastMessage))
        
        return result  
        
    ## Count the number of records that would be returned by the specified
    #  fetch operation.
    #
    #  The parameters supplied to this function have the same meaning as those
    #  supplied to AMySQLDatabase.fetch()
    #
    #  This method generates the \p count event.
    def count(Self, categoryName, **params):
        result = 0
        
        Event = Self.generate("count", categoryName=categoryName, params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result   
            
        Query = AMySQLSelectCountQuery(DataSource=Self)
            
        Query.tables.append(categoryName)
            
        if "fieldNames" in params:
            Query.fields = params["fieldNames"]
            
        if "criteria" in params:
            Query.criteria = params["criteria"]
            
        if "sort" in params:
            Query.sort = params["sort"]
            
        if "limit" in params:
            Query.limit = params["limit"]
            
        if "offset" in params:
            Query.offset = params["offset"]
            
        # Execute the query
        Query.execute()
        Self._lastMessage = Query.message()
        Self.queryString = Query.queryString()
        
        if Query.result() != MYSQL_OK:
            Self.error(ADataSourceError(mdsDataOperationError, 
                "count", Self.displayName(), Self._lastMessage, 
                Self._queryString)
            )    
        
        for Row in Query:
            result += Row[0]        
        
        return result        
            
    ## Fetch data from the data source.
    #
    #  Note that only the \p categoryName parameter is required: this is the
    #  name of the category (or table) within the data source that will be
    #  searched.
    #
    #  The \p params passed to this method may include:
    #  - \p fieldNames -- A list() of which fields are to be returned
    #  - \p criteria -- A list() of filter criteria to apply to the search; only
    #                   those records that meet the criteria should be returned
    #  - \p sort -- A list() of fields and sort order indicators; the data 
    #               returned should be sorted based on this criteria
    #  - \p limit -- The maximum number of records to return
    #  - \p offset -- How many records to skip before returning results.
    #
    #  The following optional parameters may also be passed in \p params:
    #  - \p fullText -- Whether or not to perform a full-text search of the
    #                   MySQL database.  If this parameter is omitted, a 
    #                   standard search of the database is performed.
    #
    #  This method should generate an error event if the data retrieval fails.
    def fetch(Self, categoryName, **params):
        result = None
        
        Event = Self.generate("fetch", categoryName=categoryName, params=params)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        Query = None
        
        if "fullText" in params:
            if params["fullText"]:
                Query = AMySQLFullTextQuery(DataSource=Self)
                
            else:
                Query = AMySQLSelectQuery(DataSource=Self)
        
        else:
            Query = AMySQLSelectQuery(DataSource=Self)
            
        Query.tables.append(categoryName)
            
        if "fieldNames" in params:
            Query.fields = params["fieldNames"]
            
        if "criteria" in params:
            Query.criteria = params["criteria"]
            
        if "sort" in params:
            Query.sort = params["sort"]
            
        if "limit" in params:
            Query.limit = params["limit"]
            
        if "offset" in params:
            Query.offset = params["offset"]
            
        # Execute the query
        Query.execute()
        Self._lastMessage = Query.message()
        Self.queryString = Query.queryString()
        
        if Query.result() != MYSQL_OK:
            Self.error(ADataSourceError(mdsDataOperationError, 
                "fetch", Self.displayName(), Self._lastMessage, Self._queryString))    
                
        result = Query
        
        return result  
        
    ## Insert data into the data source.
    #
    #  The \p categoryName parameter is required; it specifies the category
    #  (or table) within the data source where the new data is to be stored.
    #
    #  Within \p params, the following parameters are expected:
    #  - \p fieldsValues -- A dictionary object that contains the names of 
    #                       fields and their associated values.
    #
    #  Other parameters may also be supplied within \p params, but these will
    #  have meaning only to descendant classes or event handlers attached to
    #  this method.
    def insert(Self, categoryName, **params):
        result = -1
        
        Event = Self.generate("insert", categoryName=categoryName, 
            params=params)
            
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        Query = AMySQLInsertQuery(DataSource=Self)
        
        Query.location = categoryName
        
        if "fieldsValues" in params:
            Query.fields = params["fieldsValues"]
            
        Query.execute()
        Self._lastMessage = Query.message()
        Self._queryString = Query.queryString()
        
        if Query.result() != MYSQL_OK:
            Self.error(ADataSourceError(mdsDataOperationError,
                "insert", Self.displayName(), Self._lastMessage))
                
        result = Query.identifier()
        del Query
        
        return result       
        
    ## Update data in the data source.
    #
    #  The \p categoryName parameter is required; it specifies the category
    #  (or table) within the data source where the data to update may be found.
    #
    #  Within \p params, the following parameters are expected:
    #  - \p fieldsValues -- A dictionary object that contains the names of 
    #                       fields and their associated values.
    #  - \p criteria -- A list() of criteria that is used to determine which
    #                   record will be updated.
    #
    #  Other parameters may also be supplied within \p params, but these will
    #  have meaning only to descendant classes or event handlers attached to
    #  this method.
    def update(Self, categoryName, **params):
        result = -1
        
        Event = Self.generate("update", categoryName=categoryName,
            params=params)
            
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        Query = AMySQLUpdateQuery(DataSource=Self)
        Query.location = categoryName
        
        if "fieldsValues" in params:
            Query.fields = params["fieldsValues"]
            
        if "criteria" in params:
            Query.criteria = params["criteria"]
            
        Query.execute()
        Self._lastMessage = Query.message()
        Self._queryString = Query.queryString()
        
        if Query.result() != MYSQL_OK:
            Self.error(ADataSourceError(mdsDataOperationError,
                "update", Self.displayName(), Self._lastMessage))
                
        result = Query.recordCount()
        del Query
        
        return result
        
    ## Delete data from the data source.
    #
    #  The \p categoryName parameter is required; it specifies the category
    #  (or table) within the data source where the data to be deleted may be
    #  found.
    #
    #  Within \p params, the following parameters are expected:
    #  - \p criteria -- A list() of criteria that is used to determine which
    #                   record will be updated.
    #
    #  The following optional parameters may also be specified in \p params:
    #  - \p immediate -- Whether or not the deletion is to take place 
    #                    immediately.  If this parameter is \p False, or if it
    #                    is omitted, the MySQL database may defer deleting the
    #                    data requested until a suitable time (for instance,
    #                    until the number of transactions requested of it drop
    #                    below a certain level).  This will potentially improve
    #                    the responsiveness of the database server, but may
    #                    cause minor issues related to the fact that the data to
    #                    be deleted will still exist in the database for a time.
    #
    #  Other parameters may also be supplied within \p params, but these will
    #  have meaning only to descendant classes or event handlers attached to
    #  this method.
    def delete(Self, categoryName, **params):
        result = -1
        
        Event = Self.generate("delete", categoryName=categoryName,
            params=params)
            
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        immediate = False
        
        Query = AMySQLDeleteQuery(DataSource=Self)
        Query.locations.append(categoryName)
        
        if "criteria" in params:
            Query.criteria = params["criteria"]
            
        if "immediate" in params:
            immediate = params["immediate"]
            
        Query.immediate = immediate
        Query.execute()
        Self._lastMessage = Query.message()
        Self._queryString = Query.queryString()
        
        if Query.result() != MYSQL_OK:
            Self.error(ADataSourceError(mdsDataOperationError,
                "delete", Self.displayName(), Self._lastMessage))
                
        result = Query.recordCount()
        del Query
        
        return result
            
    # Properties ---------------------------------------------------------------
    ## Retrieve a list of all tables in the data source.
    def categories(Self):
        result = None

        Event = Self.generate("categories")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event

        Query = AMySQLShowTablesQuery(Self)
        Query.location = Self._source
        if Query.execute() == MYSQL_OK:
            result = []
            # Fetch the resulting rows
            for Row in Query:
                result.append(Row[0])

            # Free the query
            Self._lastMessage = Query.message
            del Query

        return result

    ## Retrieve the handle assigned to the data source connection.
    def handle(Self):
        return Self._handle
        
    ## Retrieve the last status message generated by the host.
    def lastMessage(Self):
        return Self._lastMessage
        
    ## Retrieve the last query string generated by the host.
    def queryString(Self):
        return Self._queryString

