# Copyright (c) 2010 NORC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import division, unicode_literals, absolute_import
from itertools import izip
from pymongo import Connection

from . import Loader, Saver, register, SrcLoader, SrcSaver
from ..errors import *
from ..variables import VariableSpec, _c
from .options import option, loads, after
from ..util import memoize
from ..format import lookup_format
from ..convert import str_to_boolean
from decimal import Decimal
import datetime
from time import mktime

@option(default='localhost')
def host(options):
    """Name of host to connect to. Default: localhost."""
    pass

@option(default=27017, convert=int)
def port(options):
    """TCP port of MySQL server. Default: standard port (27017)."""
    pass

@option(required=True)
def source(options):
    """TCP port of MySQL server. Default: standard port (27017)."""
    pass

#===============================================================================
# @loads
# @option(default=False, convert=bool)
# def get_vars(options):
#    """Set this to True to iterate over all fields (variables) in each 
#    document within the collection (dataset) to populate the VariableSet 
#    for loading. If this option is not specified the first document will 
#    be used to create a variable set for loading if var_names has not 
#    been specified."""
#    pass
#===============================================================================

@loads
@option(default=False, convert=bool)
def exclude_id(options):
    """Set this to True to exclude the _id column generated by MongoDB."""
    pass

@loads
@option(required=True)
def var_names(options):
    """This option requires a VariableSet to be defined by the user to 
    be used during loading."""
    pass

@option(default=False)
def overwrite(options):
    """Set overwrite to True to drop and create new table if 
    the table currently exists."""
    pass

@option(default=False)
def update(options):
    """If **update** is set to True then existing values in a table,
    will be overwritten otherwise they will be inserted into the table. 
    Dataset must include _id variable to update existing values."""
    if options.get('overwrite') and options.get('update'):
        err = "Overwrite and update options can not both be set to True"
        raise OptionError(err)
    
@option(default=True, convert=bool)
def safe(options):
    """If safe is True than errors will be checked for. Otherwise Mongo 
    errors will be suppressed. Set to False for speed-ups"""
    pass
    #if options['safe']:
    #    print "Warning: Safe is on. Set Safe = False for speedups."
        
#create a connection to the MongoDB instance at host, port
def _get_cnxn(opt):
    return Connection(opt['host'], opt['port']) 

#https://jira.mongodb.org/browse/PYTHON-106?page=com.atlassian.jira.plugin.system.issuetabpanels%3Aall-tabpanel
#A datetime object with a date of 1940, 12, 31 gives a negative millisecond value. 
#>>> import datetime 
#>>> datetime.datetime(1940, 3, 3).timetuple() 
#(1940, 3, 3, 0, 0, 0, 6, 63, -1) 
#
#This ends in bson.py _get_date method with a ValueError: 
#"timestamp out of range for platform localtime()/gmtime() function" 
#
#because of the negative seconds given from the -1 in the datetime. 
#
#The solution whould be to change the datetime part in 
#bson.py _element_to_bson and make sure that the struct value is not negative. 
#
#btw, this carshes the hole python process if cbson is used. 
_sav_map = {
    'float' : lambda v: v,
    'date' : lambda v: str(v),
    'datetime' : lambda v: str(v),
    'time' : lambda v: str(v),
    'integer' : lambda v: v,
    'decimal' : lambda v: float(v),
    'boolean' : lambda v: v,
    'binary' : lambda v: v.encode('base64')
}


for f in _sav_map.keys():
    _sav_map[f] = memoize(_sav_map[f])

_sav_map['string'] = lambda v: v

def _conv_var(val, format):
    if str(format) == 'binary':
        return  v.encode('base64')
          
    return val

#TODO: add update class for updating the documents
class MongoHandler(SrcLoader, SrcSaver):
    """Handler for loading MongoDB collection into a csharp dataset or 
    saving a csharp dataset into a MongoDB collection. MongoDB is the 
    default database handler for cardsharp.
    
    Public Methods:
    
    list_datasets -- Returns a list of the collection names in a Mongo
    database where the database is specified by 'source'. 
    
    get_dataset_info -- Sets the dataset VariableSet, row count, and format. 
    
    can_load -- Returns a load score based on value of 'format' in options.
    
    can_save -- Returns a save score based on value of 'format' in options.
    
    Exported Classes:
    
    loader -- Class to handle loading a MongoDB collection into csharp dataset.
    
    saver -- Class to handle saving a csharp dataset into a MongoDB collection.
    
    """
    
    id = ('cardsharp.drivers.mongo', 'cardsharp.drivers.mongodb')
    #make mongo the default for handler for databases
    formats = ('mongo', 'db', 'database')
         
    def list_datasets(self, options):
        """Return a list of the collection names in the specified MongoDB 
        database.
        
        Arguments:
        
        options -- defines the MongoDB database (options['source']).
        
        """
        
        return _get_cnxn(options)[options['source']].collection_names()
    
    def get_dataset_info(self, options):
        """Set the dataset VariableSet, row count, and format.
        
        Arguments:
        
        options -- Specify the MongoDB database (options['source']),
        the collection (options['dataset']), and optional ability
        to specify the method of creation of the dataset VaraibleSet 
        (options['get_vars'] or options['var_names']).
        
        """
        cnxn = _get_cnxn(options)
        opt = options
        #access the collection (dataset) in the database (source) 
        collection = cnxn[opt['source']][opt['dataset']]
        
        vars = opt.get('var_names')
        
        #TODO: restructure mongodb to have the data stored in a list,
        #this will allow metadata like var names
        
        #if variable names are defined use them to define the dataset
        #if opt.get('var_names'):
        
        #otherwise try to use the metadata column
        #try:
            #collection.find_one()['var_names']
            
            
        #if get_vars option is True then iterate over all fields in all 
        #documents within the collection adding new variables to the 
        #VariableSet when they are found.
        
        #if None then type is incorrectly set, need to figure this out b4 dynamic var load
        #=======================================================================
        # elif opt.get('get_vars'):
        #    for docs in collection.find():
        #        for k,v in docs.iteritems():
        #            if opt.get('exclude_id'):
        #                if k not in vars and not isinstance(v, ObjectId):
        #                    vars.append((k, lookup_format(type(v))))
        #            else:
        #                if k not in vars:
        #                    vars.append((k, lookup_format(type(v))))
        #    
        # #otherwise look at first document in the collection to create 
        # #the VaraibleSet
        # else:            
        #    for k,v in collection.find_one().iteritems():
        #        if opt.get('exclude_id'):
        #            if k not in vars and not isinstance(v, ObjectId):
        #                vars.append((k, lookup_format(type(v))))
        #        else:
        #            if k not in vars: 
        #                vars.append((k, lookup_format(type(v))))
        #=======================================================================
        
        
        options['_variables'] = VariableSpec(vars)
        options['_cases'] = collection.count()
        
        options['format'] = 'mongo'
        cnxn.disconnect()
        
    def can_load(self, options):
        """Return a load score based on value of 'format' in options."""
        
        #Return high load score
        if options.get('format').lower() in ('mongo', 'mongodb'):
            return 5000
        
        #Return medium load score
        elif options.get('format').lower() in ('database', 'db'):
            return 2500
        
        return 0
                
    class loader(Loader):
        """Class to handle loading of MongoDB collection into csharp dataset."""

        #yield a list of values
        def rows(self):   
            opt = self.options
            
            cnxn = _get_cnxn(opt)
            collection = cnxn[opt['source']][opt['dataset']]
            
            #filter determines if row out should not be yielded 
            #based on sample, skip, or limit options 
            for doc in opt['_filter'].filter(collection.find()):
                #create list of (variable value, variable index)
                row = []
                for k,v in doc.iteritems():
                    if k in opt['_variables'] and opt['_variables'].keep(k):
                        row.append((v, opt['_variables'].index(k),
                                    opt['_variables'][k].format))
                
                #print [_conv_var(v[0], v[2]) for v in sorted(row, 
                #                                       key=lambda var: var[1])]
                yield [_conv_var(v[0], v[2]) for v in sorted(row, 
                                                       key=lambda var: var[1])]                        
    
            cnxn.disconnect()
                           
    def can_save(self, options):
        """Return a save score based on value of 'format' in options."""
        
        #Return high load score
        if options.get('format').lower() in ('mongo', 'mongodb'):
            return 5000
        
        #Return medium load score
        elif options.get('format').lower() in ('database', 'db'):
            return 2500
        
        return 0

    class saver(Saver):
        """Class to handle saving of csharp dataset to MongoDB collection."""
        def rows(self):
            opt = self.options
            has_data = False
            
            cnxn = _get_cnxn(opt)
            collection = cnxn[opt['source']][opt['dataset']]
            bulk_insert = []
            
            #check to see if the collection already contains data
            if collection.find_one():
                has_data = True
                if opt['overwrite']:
                    collection.drop()
                    collection = cnxn[opt['source']][opt['dataset']]
                
                elif opt['update']:
                    #make sure _id in VariableSet to make sure that the 
                    #correct document is overwritten in the collection
                    if '_id' not in [var.name for var in opt['_variables']]:
                        e = "Can not update when no _id in VariableSet"
                        raise OptionError(e)
                
            while True:
                row = (yield)
                d = dict((var.name, _sav_map[str(var.format)](val)) for var, val in 
                                            opt['_variables'].pair_filter(row))
                #if update set than use save method, this will update if 
                #_id already in table, otherwise will add
                if opt['update']:
                    #can you do bulk save
                    collection.save(d, safe=opt['safe']) 
                elif opt['overwrite'] or not has_data:
                    #TODO Add bulk insert
                    #bulk_insert.append(d)
                    collection.insert(d, safe=opt['safe'])
                else:
                    e = "Collection already exists and overwrite, "
                    e += "append, or update not set."
                    raise SaveError(e)
            
#            if opt['overwrite'] or not has_data:
#                print bulk_insert
#                collection.insert(bulk_insert, safe=opt['safe'])
                
            cnxn.disconnect()
                
register(MongoHandler())