############################################################################
#                                                                          #
# Copyright (c)2008, 2009 Digi International (Digi). All Rights Reserved.  #
#                                                                          #
# Permission to use, copy, modify, and distribute this software and its    #
# documentation, without fee and without a signed licensing agreement, is  #
# hereby granted, provided that the software is used on Digi products only #
# and that the software contain this copyright notice,  and the following  #
# two paragraphs appear in all copies, modifications, and distributions as #
# well. Contact Product Management, Digi International, Inc., 11001 Bren   #
# Road East, Minnetonka, MN, +1 952-912-3444, for commercial licensing     #
# opportunities for non-Digi products.                                     #
#                                                                          #
# DIGI SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED   #
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A          #
# PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, #
# PROVIDED HEREUNDER IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND. #
# DIGI HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,         #
# ENHANCEMENTS, OR MODIFICATIONS.                                          #
#                                                                          #
# IN NO EVENT SHALL DIGI BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,      #
# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,   #
# ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF   #
# DIGI HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.                #
#                                                                          #
############################################################################

"""
iDigi Database Synchronization Module
"""

# imports
from settings.settings_base import SettingsBase, Setting
from presentations.presentation_base import PresentationBase
from common.helpers.format_channels import iso_date
from common.digi_device_info import get_platform_name
from common.shutdown import SHUTDOWN_WAIT

import threading
import time
import cStringIO

# Because the idigi_data module can be external to Dia, we should try/except
# around it, just in case the user does not have the module for some reason.
try:
    import idigi_data
except:
    print "iDigi_db: WARNING!!! Unable TO import idigi_data!"
    raise


# constants
ENTITY_MAP = {
    "<": "&lt;",
    ">": "&gt;",
    "&": "&amp;",
}

# exception classes

# interface functions

# classes
class iDigi_DB(PresentationBase, threading.Thread):
    
    """
    This class extends one of our base classes and is intended as an
    example of a concrete, example implementation, but it is not itself
    meant to be included as part of our developer API. Please consult the
    base class documentation for the API and the source code for this file
    for an example implementation.
    """
      
    def __init__(self, name, core_services):

        self.__name = name
        self.__core = core_services
        self.__stopevent = core_services

         # Settings
         # initial_upload: is the number of seconds before the first initial
         #     upload.  If it is not specified, initial upload is disabled.
         # interval: is the maximum interval in seconds that this module waits 
         #     before sending data to the iDigi Manager Database.  If it is 
         #     equal to 0, the feature is disabled.
         # sample_threshold: is the mininum number of samples required before
         #     sending data to the iDigi Manager Database.  If it is equal to 
         #     0, the feature is disabled.
         # collection: is the collection on the database where the data will
         #     be stored.
         # file_count: the number of unique files we will keep on iDigi.
         # filename: the name of the xml file we will push to iDigi, with
         #     a number appended to the end (cycling from 1 to file_count)
         # channels: is the list of channels the module is subscribed to.
         #     If no channels are listed, all channels are subscribed to.
         # compact_xml: (when set to True) will produce output XML with the 
         #     information stored as attributes to the sample node instead of
         #     separately tagged, resulting in smaller XML output.

        settings_list = [
           Setting(
              name='initial_upload', type=int,required=False,
              default_value=None),
           Setting(
              name='interval', type=int, required=False,
              default_value=60),
           Setting(
              name='sample_threshold', type=int, required=False,
              default_value=10),
           Setting(
              name='collection', type=str, required=False,
              default_value=""),
           Setting(
              name="channels", type=list, required=False,
              default_value=[]),
           Setting(
              name='file_count', type=int, required=False,
              default_value=20),
           Setting(
              name='filename', type=str, required=False,
              default_value="sample"),
           Setting(
              name='secure', type=bool, required=False,
              default_value=True),           
           Setting(
              name='compact_xml', type=bool, required=False,
              default_value=False),
        ]

        PresentationBase.__init__(self, name=name,
                                   settings_list=settings_list)
        self.__stopevent = threading.Event()
        threading.Thread.__init__(self, name=name)
        threading.Thread.setDaemon(self, True)

    def start(self):

        # Verify that the user has a reasonable iDigi host set on their device.
        host, token, path, port, secureport = idigi_data.get_idigi_values()
        if host == None or host == "" or host == " ":
            print "idigi_db (%s): iDigi Host '%s' is not a valid value. " \
                  "Please check your device and set the Host value " \
                  "appropriately" % (self.__name, host)
            raise ValueError, "name must be a non-empty string"
        
        # Start by appending 1 to filename of pushed data
        self.__current_file_number = 1
        
        # Event to use for notification of meeting the sample threshold
        self.__threshold_event = threading.Event()

        # Count of samples since last data push
        self.__sample_count = 0

        # Here we grab the channel publisher
        channels = SettingsBase.get_setting(self, "channels")
        cm = self.__core.get_service("channel_manager")
        cp = cm.channel_publisher_get()

        # And subscribe to receive notification about new samples
        # as long as sample_threshold is not 0
        sample_threshold = SettingsBase.get_setting(self, "sample_threshold")
        if sample_threshold:
            if len(channels) > 0:
                for channel in channels:
                    cp.subscribe(channel, self.receive)
            else:
                cp.subscribe_to_all(self.receive)

        threading.Thread.start(self)
        self.apply_settings()
        return True


    def stop(self):
        self.__stopevent.set()
        self.join(SHUTDOWN_WAIT)
        if self.isAlive():
            raise RuntimeError("Could not stop %s in time" % self.__name)
        return True

    def apply_settings(self):
        
        SettingsBase.merge_settings(self)
        accepted, rejected, not_found = SettingsBase.verify_settings(self)

        if len(rejected) or len(not_found):
            # There were problems with settings, terminate early:
            print "idigi_db (%s): Settings rejected/not found: %s %s" % \
                (self.__name, rejected, not_found)
            return (accepted, rejected, not_found)
        
        SettingsBase.commit_settings(self, accepted)
        return (accepted, rejected, not_found)

    def receive(self, channel):
        # Check how many samples it takes to meet the sample threshold
        sample_threshold = SettingsBase.get_setting(self, "sample_threshold")
        self.__sample_count += 1
        # print "idigi_db (%s): Received sample %i" % \
        #       (self.__name, self.__sample_count)

        # If we have exceeded the sample threshold, notify the thread
        # responsible for pushing up data
        if self.__sample_count >= sample_threshold:
            print "idigi_db (%s): Reached threshold of %i, setting event flag" % \
                (self.__name, sample_threshold)
            self.__sample_count = 0
            self.__threshold_event.set()

    def run(self):
    
        interval = SettingsBase.get_setting(self, "initial_upload")
        if interval is None:
            interval = SettingsBase.get_setting(self, "interval")
        self.__last_upload_clock = 0
        self.__last_upload_time = 0
        while not self.__stopevent.isSet():
            try:
                # 32 bit modulo math to account for an NDS bug :-(
                now = int(time.clock()) & 0xffffffff
                time_passed = (now - self.__last_upload_clock) & 0xffffffff
                interval_met = (interval > 0 and
                                time_passed > interval)
                threshold_met = self.__threshold_event.isSet()
                if interval_met:
                    interval = SettingsBase.get_setting(self, "interval")
                    self.__sample_count = 0
                    self.__threshold_event.clear()
                    self.__upload_data()
                elif threshold_met:
                    interval = SettingsBase.get_setting(self, "interval")
                    self.__threshold_event.clear()
                    self.__upload_data()
                time.sleep(1)
            except Exception, e:
                print "idigi_db (%s): exception while uploading: %s" % \
                    (self.__name, str(e))


        print "idigi_db (%s): Out of run loop.  Shutting down..." % \
            (self.__name)

        # Clean up channel registration
        cm = self.__core.get_service("channel_manager")
        cp = cm.channel_publisher_get()
        cp.unsubscribe_from_all(self.receive)

    def __upload_data(self):
        
        xml = cStringIO.StringIO()
        
        xml.write("<?xml version=\"1.0\"?>")
        compact_xml = SettingsBase.get_setting(self, "compact_xml")    
        if compact_xml:
            xml.write("<idigi_data compact=\"True\">")
        else:
            xml.write("<idigi_data>")
        
        cm = self.__core.get_service("channel_manager")
        cdb = cm.channel_database_get()

        channel_list = SettingsBase.get_setting(self, "channels")
        if len(channel_list) == 0:
            channel_list = cdb.channel_list()

        new_sample_count = 0

        for channel_name in channel_list:
            try:
                channel = cdb.channel_get(channel_name)
                sample = channel.get()
                if sample.timestamp >= self.__last_upload_time:
                    print "idigi_db (%s): Channel %s was updated since last " \
                           "push" % (self.__name, channel_name)
                    new_sample_count += 1
                    compact_xml = SettingsBase.get_setting(self, "compact_xml")    
                    if compact_xml:
                        xml.write(self.__make_compact_xml(channel_name, sample))
                    else:
                        xml.write(self.__make_xml(channel_name, sample))
                else:
                    print "idigi_db (%s): Channel %s was not updated since " \
                          "last push" % (self.__name, channel_name)
            except Exception, e:
                # Failed to retrieve the data
                print "idigi_db (%s): Exception in getting sample data: %s" \
                       % (self.__name, str(e))

        xml.write("</idigi_data>")

        if new_sample_count > 0:
            print "idigi_db (%s): Starting upload to iDigi" % (self.__name)
            # Due to an NDS issue, clock may roll over, we'll just
            # keep track modulo 32-bit to allow for that.
            self.__last_upload_clock = int(time.clock()) & 0xffffffff
            self.__last_upload_time = time.time()
            
            success = self.__send_to_idigi(xml.getvalue())
            if success == True:
                print "idigi_db (%s): Finished upload to iDigi" % (self.__name)
            else:
                print "idigi_db (%s): Upload failed to iDigi" % (self.__name)
        else:
            print "idigi_db (%s): No new Sample data to send to iDigi" % \
                   (self.__name)

        xml.close()

    def __make_xml(self, channel_name, sample):

        data = "<sample>"
        data += "<name>%s</name>"
        data += "<value>%s</value>"
        data += "<unit>%s</unit>"
        data += "<timestamp>%s</timestamp>"
        data += "</sample>"

        return data % (channel_name, self.__escape_entities(sample.value),
                       sample.unit, iso_date(sample.timestamp))


    def __make_compact_xml(self, channel_name, sample):

        data = "<sample name=\"%s\" value=\"%s\" unit=\"%s\" timestamp=\"%s\" />"

        return data % (channel_name, self.__escape_entities(sample.value),
                       sample.unit, iso_date(sample.timestamp))


    def __send_to_idigi(self, data):

        success = False
        filename = SettingsBase.get_setting(self, "filename")    
        filename = filename + "%i.xml" % self.__current_file_number
        collection = SettingsBase.get_setting(self, "collection")
        secure = SettingsBase.get_setting(self, "secure")
        try:
            print "idigi_db (%s): Attempting to upload %s to iDigi" % \
                   (self.__name, filename)
            success, err, errmsg = idigi_data.send_idigi_data(data, filename,
                                                           collection, secure)
            if success == True:
                print "idigi_db (%s): Successfully uploaded %s to iDigi" % \
                       (self.__name, filename)
            else:
                print "idigi_db (%s): Unsuccessfully uploaded %s to iDigi." \
                      "(Err: %s Errmsg: %s)" % \
                       (self.__name, filename, str(err), str(errmsg))
        except Exception, e:
            print "idigi_db (%s): Took an Exception during upload of %s to " \
                  "iDigi: %s" % (self.__name, filename, str(e))

        self.__current_file_number += 1
    
        max_files = SettingsBase.get_setting(self, "file_count")
    
        if self.__current_file_number >= max_files + 1:
            self.__current_file_number = 1

        return success

    def __escape_entities(self, sample_value):

        if not isinstance(sample_value, str):
            return sample_value
        for ch in ENTITY_MAP:
            sample_value = sample_value.replace(ch, ENTITY_MAP[ch])

        return sample_value
