#!/usr/bin/env python
# Copyright 2012 George Hunt -- georgejhunt@gmail.com
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# reminder to myself:
# Try to keep the transitory state in a tmpfs, so as to
# minimize the writes to a SD card or flash. Notice if the tmpfs file is missing, in the
# presence of the summary file, if the power was last seen as out, it will be
# recorded as the end of the power outage
# This implimentation requires that DATA_FILE be a temporary file system -- check fstab

import time
import datetime
from subprocess import Popen, PIPE
import datetime
import os, sys
import gconf
import logging
import json
import glob
from gettext import gettext as _

DATA_FILE = "/tmp/mains_power"
# data_dict is global config file initialized in is_exist_data_file - used throughout
data_dict = {}

SUMMARY_PREFIX = "/home/admin"
SUMMARY_SUFFIX = "_ac_power_summary"
SUMMARY_CURRENT = "current_ac_summary"
summary_dict = {}

VERSION = "0.1"
ENABLED = 'ENABLED'

SYS_AC = "  /sys/class/power_supply/olpc-ac/online"
WORK_DIR = "/home/admin"

logger = logging.getLogger('serversleep')
hdlr = logging.FileHandler(os.path.join(WORK_DIR,'getsource.log'))
formatter = logging.Formatter('%(asctime)s %(levelname)s %(messages)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.WARNING)
class AcException():
    def __init__(self, msg):
        print(msg)
        sys.exit(1)

class Tools:
    def __init__(self):
        pass

    def cli(self, cmd):
        """send cmd line to shell, rtn (text,error code)"""
        logger.debug('command_line cmd:%s'%cmd)
        p1 = Popen(cmd,stdout=PIPE, shell=True)
        output = p1.communicate()
        if p1.returncode != 0 :
            logger.debug('error returned from shell command: %s was %s'%(cmd,output[0]))
        return output[0],p1.returncode

    def get_ac_status(self):
        """ get the "1" or "0" strings indicating power state from kernel """
        line,err = self.cli('cat %s' % (SYS_AC,))
        return line.strip()

    def get_datetime(self, datestr):
        """ translate ymdhms string into datetime """
        #return datetime.datetime.strptime(datestr.strip(), "%a %b %d %H:%M:%S %Y")
        return datetime.datetime.strptime(datestr.strip(), "%Y/%m/%d-%H:%M:%S")

    def tstamp(self, dtime):
        '''return a UNIX style seconds since 1970 for datetime input'''
        epoch = datetime.datetime(1970, 1, 1)
        since_epoch_delta = dtime - epoch
        return since_epoch_delta.total_seconds()

    def tstamp_now(self):
        """ return seconds since 1970 """
        return self.tstamp(datetime.datetime.now())

    def format_datetime(self, dt):
        """ return ymdhms string """
        return datetime.datetime.strftime(dt, "%Y/%m/%d-%H:%M:%S")

    def is_exist_data_file(self):
        #get the tmp data file
        global data_dict
        if (len(data_dict)> 0):
            return True
        try:
            fd = file(DATA_FILE,'r')
            data_str = fd.read()
            data_dict = json.loads(data_str)
            fd.close()
            return True
        except IOError:
            return False

    def put_data_file(self):
        """ writes the data_dict to tmp file system """
        try:
            fd = file(DATA_FILE,'w')
            data_str = json.dumps(data_dict)
            fd.write(data_str)
            fd.close()
        except IOError,e:
            logging.exception("failed to write data file. error:%s"% (e,))
            raise AcException("Datafile write error")

    def get_summary_filename(self):
        """ returns the filename of current summary file or "" if it doesn't exist """
        fn = os.path.join(SUMMARY_PREFIX,SUMMARY_CURRENT)
        if (os.path.isfile(fn)):
            try:
                fd = open(fn,"r")
                fname = fd.read()
            except :
                cmd = "rm -f %s"%fn
                result,status = self.cli(cmd)
                return ""
            return fname
        return ""

    def put_summary_filename(self, fname):
        """ returns the filename of current summary file or "" if it doesn't exist """
        fn = os.path.join(SUMMARY_PREFIX,SUMMARY_CURRENT)
        try:
            fd = open(fn,"w")
            fd.write(fname)
            fd.close()
        except IOError,e:
            logging.exception("failed to write summary file name. error:%s"% (e,))
            raise AcException("Summary file write name error. path:%s"%fname)

    def is_exist_summary_file(self):
        """does the permanent  record exist? """
        fn = self.get_summary_filename()
        if (fn  == ""):
            return False
        try:
            fd = file(fn,'r')
            fd.close()
            return True
        except IOError,e:
            logging.exception("failed to read summary file. error:%s"% (e,))
            raise AcException("Summary file write key value error")

    def write_current_ac_status(self):
        """ write ac status to tmp file (catches reboot or loss of battery) """
        global data_dict
        """
        try:
            fd = file(DATA_FILE,'r')
            data_str = fd.read()
            data_dict = json.loads(data_str)
        except:
            pass
        """
        # if the status of power has changed, record the new state
        #last_summary_ac = self.last_summary_ac_state()
        #if last_summary_ac != self.get_ac_status():
        if self.last_summary_ac_state() != self.get_ac_status():
            self.write_summary()

        data_dict[self.format_datetime(datetime.datetime.now())] = \
                                    self.get_ac_status()
        self.put_data_file()

    def write_summary(self):
        """ record the change in ac power to permanent record """
        self.write_summary_key_value(self.format_datetime(datetime.datetime.now()),
                                    self.get_ac_status())

    def write_summary_key_value(self, key="", value=""):
        """ write summary file as key, value pair """
        global summary_dict
        name = self.get_summary_filename()
        if (len(name)>0):
            try:
                fsummary = file(name,'r')
                data_str = fsummary.read()
                summary_dict = json.loads(data_str)
            except:
                print("write_summary_error. name:%s"%name)
                raise AcException("Summary file write key value error")
        else:
            #we need to generate a new file name and file
            name = self.format_datetime(datetime.datetime.now())
            name = name.replace("/","_")
            name = name.replace(":","-")
            name = os.path.join(SUMMARY_PREFIX,name) + SUMMARY_SUFFIX
            self.put_summary_filename(name)
            summary_dict = {}
        summary_dict[key] = value
        data_str = json.dumps(summary_dict)
        try:
            #print data_str
            fsummary = file(name,'w')
            fsummary.write(data_str)
            fsummary.close()
        except IOError,e:
            logging.exception("failed to write summary file. error:%s"% (e,))
            raise AcException("Summary file write key value error")

    def last_summary_ac_state(self):
        """ return the key (datestring) of the most recently recorded change """
        global summary_dict
        try:
            fsummary = file(SUMMARY_FILE,'r')
            data_str = fsummary.read()
            summary_dict = json.loads(data_str)
        except:
            return ''
        keylist = sorted(summary_dict.keys())
        #print(keylist)
        last_key = keylist[-1]
        return summary_dict[last_key]

    def dhm_from_seconds(self,s):
        """ translate seconds into days, hour, minutes """
        #print s
        days, remainder = divmod(s, 86400)
        hours, remainder = divmod(remainder, 3600)
        minutes, remainder = divmod(remainder, 60)
        return (days, hours, minutes)

    def enable(self):
        result, flag = self.cli("grep -e xs-acpowergaps-cron /etc/crontab")
        print("result:%s  flag:%s"%(result,flag,))
        if flag != 0:
            result, flag = self.cli('echo "*/6 * * * * root /usr/bin/xs-acpowergaps-cron" >> /etc/crontab')
        self.write_summary_key_value("enabled","1")

    def isenabled(self):
            #check that the cron pointer is in place
            result, flag = self.cli("grep -e xs-acpowergaps-cron /etc/crontab")
            if flag == 0:
                return True
            return False

    def disable(self):
        """ remove the entry in crontab that samples the AC power state """
        cmd = "sed -i -e /xs-acpowergaps/d /etc/crontab"
        result, flag = self.cli(cmd)

        #record the current status -- And mark the end of the series
        cd = CollectData()

        # by removing the file that points to the current power series, the next
        #    call to make an entry will generate a new file
        fn = os.path.join(SUMMARY_PREFIX,SUMMARY_CURRENT)
        cmd = "rm -f %s" % fn
        result, status = self.cli(cmd)


class ShowPowerHistory(Tools):
    def __init__(self, action=""):
        global summary_dict, data_dict
        number_of_gaps = 0
        if not self.is_exist_summary_file() or not self.is_exist_data_file():
            # this is the first invocation of the AC logger
            self.write_summary()
        if action == "enable":
            self.enable()
            self.write_summary_key_value(ENABLED, "1")
        #record the current status -- primarily useful for debugging
        #cd = CollectData()
        self.output_state()
        self.output_all_summaries(SUMMARY_PREFIX)

    def output_all_summaries(self, summary_path):
        dirlist = glob.glob(os.path.join(SUMMARY_PREFIX,"*" + SUMMARY_SUFFIX))
        for fname in sorted(dirlist,reverse=True):
            self.output_summary(fname)


    def output_summary(self,fname):
        #get the summary file so we can operate upon the values
        first = None
        try:
            fsummary = file(fname,'r')
            data_str = fsummary.read()
        except IOError:
            raise AcException("Summaary file read error in init of ShowPowerHistory")
        fsummary.close()
        summary_dict = json.loads(data_str)
        keylist = sorted(summary_dict.keys())
        if self.is_exist_data_file():
            last = self.format_datetime(datetime.datetime.now())
        else:
            last = keylist[-1]
        # assume that the test starts with power on
        current_state = '1'
        # gaps is power outages in seconds
        gaps = {}
        gap_start = None
        print("\n\nGAPS IN AC POWER DURING %s to %s:" % (keylist[0], keylist[-1],))
        print("File Name: %s\n"%(fname,))
        for key in keylist:
            if not first:
                first = key
            if summary_dict[key] != "1":
                gap_start = key
            if gap_start and summary_dict[key] == "1":
                gaps[gap_start] = self.tstamp(self.get_datetime(key)) - \
                                    self.tstamp(self.get_datetime(gap_start))

        total_seconds = self.tstamp(self.get_datetime(last)) - \
                                self.tstamp(self.get_datetime(first))
        (days, hours, minutes) = self.dhm_from_seconds(total_seconds)
        print "length of log %s days, %s hours, %s minutes" % (days, hours, minutes)
        number_of_gaps = len(gaps)
        print "number of power outages: %s" % number_of_gaps
        mysum = 0L
        if number_of_gaps > 0:
            for k,v in gaps.iteritems():
                mysum += v
            average_seconds = mysum / float(number_of_gaps)
            (days, hours, minutes) = self.dhm_from_seconds(average_seconds)
            print "average length of outage: %s days %s hours %s minutes" % \
                                (days, hours,minutes)
            gap_length_list = []
            for k,v in gaps.iteritems():
                gap_length_list.append((k,v))
            gap_list = sorted(gap_length_list, key=lambda x:x[1])
            #for item, value in gap_list:
                #print item, value
            shortest_gap = gap_list[0][1]
            if shortest_gap < 60:
                print "shortest outage: %s seconds " % (shortest_gap)
            else:
                (days, hours, minutes) = self.dhm_from_seconds(shortest_gap)
                print "shortest outage: %s days %s hours %s minutes " % \
                            (days, hours, minutes,)
            longest_gap = gap_list[-1][1]
            (days, hours, minutes) = self.dhm_from_seconds(longest_gap)
            print "longest outage: %s days %s hours %s minutes " % \
                                            (days,hours, minutes,)
            print("\nINDIVIDUAL POWER DISRUPTIONS:")
            for item, value in gap_list:
                (days, hours, minutes) = self.dhm_from_seconds(value)
                print "%s %s days %s hours and %s minutes" % \
                            (item, days, hours, minutes, )

    def output_state(self):
        if self.isenabled():
            state = "ENABLED"
        else:
            state = "DISABLED"
        print("")
        print("AC Power Monitor is currently %s"%state)

class CollectData(Tools):
    def __init__(self):
        global summary_dict
        if not self.is_exist_summary_file():
            # this is the first invocation of the AC logger
            self.write_summary()
        if not self.is_exist_data_file():
            # this is a startup after a reboot, or battery run down
            self.write_summary()
        # if the status of power has changed, record the new state
        last_summary_ac = self.last_summary_ac_state()
        #print("last_summary_ac: %s ac_status: %s"%(last_summary_ac,
                                        #self.get_ac_status()))
        if last_summary_ac != self.get_ac_status():
            self.write_summary()
        #record the current status
        self.write_current_ac_status()

class RawData(Tools):
    def __init__(self):
        global summary_dict
        # get summary data
        try:
            fsummary = file(SUMMARY_FILE,'r')
            data_str = fsummary.read()
            summary_dict = json.loads(data_str)
        except IOError:
            raise AcException("Summaary file read error in init of RawData")
        keylist = sorted(summary_dict.keys())
        print "Summary file:"
        for item in keylist:
            print item, summary_dict[item]

        global data_dict
        try:
            fd = file(DATA_FILE,'r')
            data_str = fd.read()
            data_dict = json.loads(data_str)
            fd.close()
        except IOError,e:
            logging.exception("failed to write data file. error:%s"% (e,))
            raise AcException("Datafile read error in RawData")
        keylist = sorted(data_dict.keys())
        print "Current data file:"
        for item in keylist:
            print item, data_dict[item]

if __name__ == "__main__":
    # if interactive from command line, do gui
    if len(sys.argv) == 1:
        pi = ShowPowerHistory()
    elif (len(sys.argv )== 2):
        # if coming from cron, the check for an action to do
        if sys.argv[1] == '--timeout':
            pa = CollectData()
        # dump the data in understandable form
        if sys.argv[1] == '--debug':
            pa = RawData()
        if sys.argv[1] == '--enable':
            tools = Tools()
            tools.enable()
        if sys.argv[1] == '--disable':
            tools = Tools()
            tools.disable()
            sys.exit(0)

    # pop up the GUI
    #Gtk.main()
    sys.exit(0)
