#!python
""" ups-mon  -  Displays current status of all active UPSs.

    A utility to give the current state of all compatible UPSs. The default
    behavior is to continuously update a text based table in the current window
    until Ctrl-C is pressed.  With the *--gui* option, a table of relevant
    parameters will be updated in a Gtk window.  You can specify the delay
    between updates with the *--sleep N* option where N is an integer > 10 that
    specifies the number of seconds to sleep between updates.  The *--log*
    option is used to write all monitor data to a psv log file.  When writing
    to a log file, the utility will indicate this in red at the top of the
    window with a message that includes the log file name.  The *--status*
    option will output a table of the current status.  By default, unresponsive
    UPSs will not be displayed, but the *--show_unresponsive* can be used to
    force their display.  The logger is enabled with the *--debug* option.

    Copyright (C) 2019  RicksLab

    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 3 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, see <https://www.gnu.org/licenses/>.
"""
__author__ = 'RicksLab'
__copyright__ = 'Copyright (C) 2019 RicksLab'
__license__ = 'GNU General Public License'
__program_name__ = 'ups-mon'
__maintainer__ = 'RicksLab'
__docformat__ = 'reStructuredText'
# pylint: disable=multiple-statements
# pylint: disable=line-too-long
# pylint: disable=bad-continuation

import argparse
import threading
import os
import sys
import re
import time
import logging
import signal
from typing import TextIO, Any, Callable
try:
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import GLib, Gtk
except ModuleNotFoundError as error:
    print('gi import error: {}'.format(error))
    print('gi is required for %s', __program_name__)
    print('   In a venv, first install vext:  pip install --no-cache-dir vext')
    print('   Then install vext.gi:  pip install --no-cache-dir vext.gi')
    sys.exit(0)
from UPSmodules import UPSmodule as UPS
from UPSmodules import env
from UPSmodules import UPSgui
from UPSmodules import __version__, __status__, __credits__

set_gtk_prop = UPSgui.GuiProps.set_gtk_prop
LOGGER = logging.getLogger('ups-utils')


def ctrl_c_handler(target_signal: Any, _frame: Any) -> None:
    """
    Signal catcher for ctrl-c to exit monitor loop.

    :param target_signal: Target signal name
    :param _frame: Ignored
    """
    LOGGER.debug('ctrl_c_handler (ID: %s) has been caught. Setting quit flag...', target_signal)
    print('Setting quit flag...')
    env.UT_CONST.quit = True


def ctrl_u_handler(target_signal: Any, _frame: Any) -> None:
    """
    Signal catcher for ctrl-c to exit monitor loop.

    :param target_signal: Target signal name
    :param _frame: Ignored
    """
    LOGGER.debug('ctrl_u_handler (ID: %s) has been caught. Setting refresh daemon flag...', target_signal)
    print('Setting refresh daemon flag...')
    env.UT_CONST.refresh_daemon = True


class MonitorWindow(Gtk.Window):
    """
    Class defining Monitor Window
    """
    max_width = 23

    def __init__(self, ups_list: UPS.UpsList, gc: UPSgui.GuiComp):
        """ Initialize the main UPS monitor window.

        :param ups_list:  The ups list object
        :param gc: A dictionary of Gtk components and values
        """
        self.quit: bool = False

        # RuntimeError: Gtk couldn't be initialized. Use Gtk.init_check() if you want to handle this case.
        Gtk.Window.__init__(self, title='{} - Monitor'.format(env.UT_CONST.gui_window_title))
        init_chk_value = Gtk.init_check(sys.argv)
        LOGGER.debug('init_check: %s', init_chk_value)
        if not init_chk_value[0]:
            print('Gtk Error, Exiting')
            sys.exit(-1)
        self.set_border_width(0)
        UPSgui.GuiProps.set_style()

        if env.UT_CONST.icon_file:
            LOGGER.debug('Icon file: [%s]', env.UT_CONST.icon_file)
            if os.path.isfile(env.UT_CONST.icon_file):
                self.set_icon_from_file(env.UT_CONST.icon_file)

        grid = Gtk.Grid()
        self.add(grid)

        col = 0
        row = 0
        num_ups_dict = ups_list.num_upss()
        num_ups = num_ups_dict['total'] if env.UT_CONST.show_unresponsive else num_ups_dict['responsive']

        # Set logging details at top of table if logging enabled
        if LOGGER.getEffectiveLevel() == logging.DEBUG:
            log_label = Gtk.Label(name='warn_label')
            log_label.set_markup('<big><b> DEBUG Logger Active </b></big>')
            lbox = Gtk.Box(spacing=6, name='warn_box')
            set_gtk_prop(log_label, top=1, bottom=1, right=1, left=1)
            lbox.pack_start(log_label, True, True, 0)
            grid.attach(lbox, 0, row, num_ups + 1, 1)
        row += 1
        if env.UT_CONST.log:
            log_label = Gtk.Label(name='warn_label')
            log_label.set_markup('<big><b> Logging to:    </b>' + env.UT_CONST.log_file + '</big>')
            lbox = Gtk.Box(spacing=6, name='warn_box')
            set_gtk_prop(log_label, top=1, bottom=1, right=1, left=1)
            lbox.pack_start(log_label, True, True, 0)
            grid.attach(lbox, 0, row, num_ups + 1, 1)
        row += 1
        row_start = row

        # Set first column of table to static values
        row = row_start
        row_labels = {'display_name': Gtk.Label(name='white_label')}
        row_labels['display_name'].set_markup('<b>UPS Parameters</b>')
        # Set row labels for header items
        for param_name in UPS.UpsItem.ordered_table_items:
            if param_name not in UPS.UpsItem.table_list: continue
            param_label = UPS.UpsItem.param_labels[param_name]
            if param_name == 'display_name':
                row_labels[param_name] = Gtk.Label(name='white_label', halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
            else:
                row_labels[param_name] = Gtk.Label(name='white_label')
            row_labels[param_name].set_markup('<b>{}</b>'.format(param_label))
        # Set boxes for each row label
        for row_label_item in row_labels.values():
            lbox = Gtk.Box(spacing=6, name='head_box')
            set_gtk_prop(lbox, top=1, bottom=1, right=1, left=1)
            set_gtk_prop(row_label_item, top=1, bottom=1, right=4, left=4, align=(0.0, 0.5))
            lbox.pack_start(row_label_item, True, True, 0)
            grid.attach(lbox, col, row, 1, 1)
            row += 1

        def_box_css = "{ background-image: image(%s); }" % UPSgui.GuiProps.color_name_to_hex('slate_md')
        # for item_name, item_ups in ups.get_ups_list(errups=env.UT_CONST.show_unresponsive).items():
        col += 1
        for ups in ups_list.upss():
            row = row_start
            # Header items do not need to be in gui component since they are static
            label = Gtk.Label(name='white_label', halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
            label.set_markup('<b>{}</b>'.format(ups['display_name']))
            label.set_use_markup(True)
            lbox = Gtk.Box(spacing=6, name='head_box')
            set_gtk_prop(lbox, top=1, bottom=1, right=1, left=1)
            set_gtk_prop(label, top=1, bottom=1, right=3, left=3, width_chars=self.max_width)
            lbox.pack_start(label, True, True, 0)
            grid.attach(lbox, col, row, 1, 1)
            row += 1
            for param_name in UPS.UpsItem.ordered_table_items:
                if param_name not in UPS.UpsItem.table_list: continue
                if param_name == 'display_name': continue
                label = Gtk.Label(label=ups[param_name], name='white_label')
                label.set_width_chars(self.max_width)
                set_gtk_prop(label, top=1, bottom=1, right=3, left=3, width_chars=self.max_width)
                set_gtk_prop(label, width_chars=self.max_width)
                box_name = '{}-{}'.format(param_name, ups.prm.uuid)
                lbox = Gtk.Box(spacing=6, name=box_name)
                UPSgui.GuiProps.set_style(css_str="#{} {}".format(box_name, def_box_css))
                set_gtk_prop(lbox, top=1, bottom=1, right=1, left=1)
                lbox.pack_start(label, True, True, 0)
                grid.attach(lbox, col, row, 1, 1)
                gc.add(ups.prm.uuid, param_name, label, lbox, box_name)
                row += 1
            col += 1

            gc.refresh_gui_data(ups.prm.uuid)
        if LOGGER.getEffectiveLevel() == logging.DEBUG:
            log_message = ''
            for k, v in gc.items():
                if log_message: log_message = '{}\n\n{}:'.format(log_message, k)
                else: log_message = '{}:\n\n'.format(k)
                for k2, v2 in v.items():
                    log_message = '{}\n    {}:{}'.format(log_message, k2, v2)
            LOGGER.debug('Device dict:\n %s', log_message)

    def set_quit(self, _arg2: Any, _arg3: Any) -> None:
        """ Function called when quit monitor is executed.  Sets flag to end update loop.

        :param _arg2: Ignored
        :param _arg3: Ignored
        :return: None
        """
        self.quit = True


def update_data(ups_list: UPS.UpsList, gc: UPSgui.GuiComp) -> None:
    """ Function that updates data in MonitorWindow  with call to read data from ups.

    :param ups_list:  The main ups module object
    :param gc: A dictionary of Gtk components and values
    :return: None
    """
    ups_list.read_all_ups_list_items(UPS.UpsComm.MIB_group.dynamic, errups=env.UT_CONST.show_unresponsive)
    gc.all_refresh_gui_data()
    if env.UT_CONST.log:
        print_log(env.UT_CONST.log_file_ptr, ups_list)

    # update gui
    for dev_uuid, dev_data in gc.items():
        ups = ups_list[dev_uuid]
        if not ups['valid']:
            state_style = UPS.UpsItem.TXT_style.warn
            mib_name = 'mib_system_status'
            gui_comp = dev_data[mib_name]
            lv = gui_comp['label']
            box_name = gui_comp['box_name']
            lv.set_markup('<b>{}</b>'.format('Unresponsive'))
            UPSgui.GuiProps.set_style(css_str="#%s { background-image: image(%s); }" % (
                box_name, UPSgui.GuiProps.color_name_to_hex('yellow')))
            continue
        for mib_name, gui_comp in dev_data.items():
            box_name = gui_comp['box_name']
            state_style = UPS.UpsItem.TXT_style.normal
            data_value = str(ups[mib_name])[:MonitorWindow.max_width]
            lv = gui_comp['label']
            if mib_name in ups_list.daemon.daemon_param_dict:
                state_style = ups_list.daemon.daemon_format(mib_name, ups[mib_name], gui_text_style=True)
            if mib_name == 'mib_battery_status':
                if re.match(env.UT_CONST.PATTERNS['NORMAL'], data_value):
                    state_style = UPS.UpsItem.TXT_style.green
                else:
                    state_style = UPS.UpsItem.TXT_style.crit
            elif mib_name == 'mib_system_status':
                if re.match(env.UT_CONST.PATTERNS['ONLINE'], data_value):
                    state_style = UPS.UpsItem.TXT_style.green
                else:
                    state_style = UPS.UpsItem.TXT_style.crit
            elif mib_name == 'display_name':
                state_style = UPS.UpsItem.TXT_style.bold

            if data_value in {None, 'None', ''}:
                if mib_name == 'mib_ups_name':
                    state_style = UPS.UpsItem.TXT_style.crit
                    data_value = 'Unresponsive'
                else:
                    state_style = UPS.UpsItem.TXT_style.normal
                    data_value = '---'
            set_gtk_prop(lv, width_chars=MonitorWindow.max_width + 1)
            if state_style == UPS.UpsItem.TXT_style.crit:
                lv.set_markup('<b>{}</b>'.format(data_value))
                UPSgui.GuiProps.set_style(css_str="#%s { background-image: image(%s); }" % (
                    box_name, UPSgui.GuiProps.color_name_to_hex('red')))
            elif state_style == UPS.UpsItem.TXT_style.normal:
                lv.set_markup('<b>{}</b>'.format(data_value))
                UPSgui.GuiProps.set_style(css_str="#%s { background-image: image(%s); }" % (
                    box_name, UPSgui.GuiProps.color_name_to_hex('slate_md')))
            elif state_style == UPS.UpsItem.TXT_style.warn:
                lv.set_markup('<b>{}</b>'.format(data_value))
                UPSgui.GuiProps.set_style(css_str="#%s { background-image: image(%s); }" % (
                    box_name, UPSgui.GuiProps.color_name_to_hex('yellow')))
            elif state_style == UPS.UpsItem.TXT_style.green:
                lv.set_markup('<b>{}</b>'.format(data_value))
                UPSgui.GuiProps.set_style(css_str="#%s { background-image: image(%s); }" % (
                    box_name, UPSgui.GuiProps.color_name_to_hex('green_dk')))
            elif state_style == UPS.UpsItem.TXT_style.bold:
                lv.set_markup('<b>{}</b>'.format(data_value))
            else:
                lv.set_text(data_value)

    while Gtk.events_pending():
        Gtk.main_iteration_do(True)


def refresh(refresh_time: int, updater: Callable, ups_list: UPS.UpsList,
            gc: UPSgui.GuiComp, umonitor: MonitorWindow) -> None:
    """ Function that continuously updates the Gtk monitor window.

    :param refresh_time:  Delay time in seconds between monitor display refreshes
    :param updater:  Function to update data
    :param ups_list:  UPS list object
    :param gc: A dictionary of Gtk components and values
    :param umonitor: The main Gtk monitor window.
    :return: None
    """
    while True:
        if umonitor.quit:
            print('Quitting...')
            Gtk.main_quit()
            sys.exit(0)
        GLib.idle_add(updater, ups_list, gc)
        for _sleep_cnt in range(0, refresh_time):
            if umonitor.quit:
                print('Quitting...')
                Gtk.main_quit()
                sys.exit(0)
            time.sleep(1)


def print_monitor_table(ups_list: UPS.UpsList) -> bool:
    """ Print the monitor table in format optimized for terminal window.

    :param ups_list:  The main ups module object
    :return: True on success
    """
    hrw = 29  # Row header item width
    irw = MonitorWindow.max_width + 1  # Row data item width

    print('┌', '─'.ljust(hrw, '─'), sep='', end='')
    for _ in ups_list:
        print('┬', '─'.ljust(irw, '─'), sep='', end='')
    print('┐')

    color = env.UT_CONST.mark_up_codes['bcyan']
    reset = env.UT_CONST.mark_up_codes['reset']
    print('│{}{}{}'.format(color, 'UPS Parameters'.ljust(hrw, ' '), reset), sep='', end='')
    for ups in ups_list.upss():
        print('│{}{}{}'.format(color, ups['display_name'].center(irw), reset),
              sep='', end='')
    print('│')

    print('├', '─'.ljust(hrw, '─'), sep='', end='')
    for _ in ups_list:
        print('┼', '─'.ljust(irw, '─'), sep='', end='')
    print('┤')

    for param_name in UPS.UpsItem.ordered_table_items:
        if param_name not in UPS.UpsItem.table_list: continue
        param_label = UPS.UpsItem.param_labels[param_name]
        color = env.UT_CONST.mark_up_codes['bcyan']
        print('│{}{}{}'.format(color, param_label.ljust(hrw, ' ')[:hrw], reset), sep='', end='')
        for ups in ups_list.upss():
            try:
                color = ''
                if param_name == 'mib_system_status':
                    color = 'ok' if re.match(env.UT_CONST.PATTERNS['ONLINE'], ups[param_name]) else 'error'
                    color = env.UT_CONST.mark_up_codes[color]
                elif param_name == 'mib_battery_status':
                    color = 'ok' if re.match(env.UT_CONST.PATTERNS['NORMAL'], ups[param_name]) else 'error'
                    color = env.UT_CONST.mark_up_codes[color]
                elif param_name in UPS.UpsDaemon.daemon_param_dict:
                    text_format = ups_list.daemon.daemon_format(param_name, ups[param_name])
                    LOGGER.debug('%s, %s: %s', param_name, ups[param_name], text_format)
                    color = env.UT_CONST.mark_up_codes[text_format]
                value = '---' if ups[param_name] is None else str(ups[param_name])
                item_str = '{}{}{}'.format(color, value[:irw].center(irw), reset)
            except KeyError:
                item_str = ' '.ljust(irw, ' ')[:irw]
            print('│', item_str, sep='', end='')
        print('│')

    print('└', '─'.ljust(hrw, '─'), sep='', end='')
    for _ in ups_list:
        print('┴', '─'.ljust(irw, '─'), sep='', end='')
    print('┘')
    return True


def print_log(fileptr: TextIO, ups_list: UPS.UpsList) -> None:
    """ Print the logfile data line.

    :param fileptr:  The logfile fileptr
    :param ups_list: A UPS list structure
    """
    time_str = (str(env.UT_CONST.now(ltz=True).strftime('%c')).strip())
    for ups in ups_list.upss():
        print('{}'.format(time_str), file=fileptr, end='')
        for param_name in UPS.UpsItem.param_labels:
            if param_name not in UPS.UpsItem.table_list: continue
            print('|{}'.format(ups[param_name]), file=fileptr, end='')
        print('', file=fileptr)


def print_log_header(fileptr: TextIO) -> None:
    """ Print the logfile header line.

    :param fileptr:  The logfile fileptr
    """
    print('time', file=fileptr, end='')
    for param_name in UPS.UpsItem.param_labels:
        if param_name not in UPS.UpsItem.table_list: continue
        print('|{}'.format(param_name), file=fileptr, end='')
    print('', file=fileptr)


def main() -> None:
    """ Main function
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--about', help='README', action='store_true', default=False)
    parser.add_argument('--verbose', help='Output execution exception notices', action='store_true', default=False)
    parser.add_argument('--status', help='Display table of current status of UPSs', action='store_true', default=False)
    parser.add_argument('--show_unresponsive', help='Display unresponsive UPSs', action='store_true', default=False)
    parser.add_argument('--gui', help='Display GTK Version of Monitor', action='store_true', default=False)
    parser.add_argument('--log', help='Write all monitor data to logfile', action='store_true', default=False)
    parser.add_argument('--sleep', help='Number of seconds to sleep between updates',
                        type=int, default=UPS.UpsDaemon.daemon_param_defaults['read_interval']['monitor'])
    parser.add_argument('-d', '--debug', help='Debug output', action='store_true', default=False)
    args = parser.parse_args()

    # About me
    if args.about:
        print(__doc__)
        print('Author: ', __author__)
        print('Copyright: ', __copyright__)
        print('Credits: ', *['\n      {}'.format(item) for item in __credits__])
        print('License: ', __license__)
        print('Version: ', __version__)
        print('Install Type: ', env.UT_CONST.install_type)
        print('Maintainer: ', __maintainer__)
        print('Status: ', __status__)
        sys.exit(0)

    env.UT_CONST.set_env_args(args, __program_name__)
    LOGGER.debug('########## %s %s', __program_name__, __version__)

    if not env.UT_CONST.check_env():
        env.UT_CONST.process_message('Error in environment. Exiting...', log_flag=True)
        sys.exit(-1)

    print('Reading and verifying UPSs listed in {}. '.format(env.UT_CONST.ups_json_file))
    ups_list = UPS.UpsList()
    num_ups = ups_list.num_upss()

    if not num_ups['total']:
        env.UT_CONST.process_message('No UPSs specified in {}'.format(env.UT_CONST.ups_json_file), log_flag=True)
        print('    For more information: `man {0}`, exiting...'.format(env.UT_CONST.ups_json_file))
        sys.exit(-1)
    print(ups_list)
    if num_ups['total'] != num_ups['responsive']:
        print('    Check the {} file.\n'.format(env.UT_CONST.ups_json_file))

    if int(args.sleep) >= ups_list.daemon.daemon_param_defaults['read_interval']['limit']:
        env.UT_CONST.SLEEP = int(args.sleep)
    else:
        env.UT_CONST.process_message('Invalid value for sleep specified.  Must be an integer >= {}'.format(
            ups_list.daemon.daemon_param_defaults['read_interval']['limit']), log_flag=True)
        env.UT_CONST.fatal = True

    if env.UT_CONST.fatal:
        env.UT_CONST.process_message('Fatal Error. Exiting...', log_flag=True)
        sys.exit(-1)

    ups_list.read_all_ups_list_items(UPS.UpsComm.MIB_group.monitor, errups=args.show_unresponsive, display=False)

    env.UT_CONST.show_unresponsive = args.show_unresponsive

    if args.log:
        env.UT_CONST.log = True
        env.UT_CONST.log_file = './log_monitor_{}.txt'.format(
            env.UT_CONST.now(ltz=env.UT_CONST.use_ltz).strftime('%m%d_%H%M%S'))
        env.UT_CONST.log_file_ptr = open(env.UT_CONST.log_file, 'w', 1)
        print_log_header(env.UT_CONST.log_file_ptr)

    if args.gui:
        signal.signal(signal.SIGINT, ctrl_c_handler)
        signal.signal(signal.SIGUSR1, ctrl_u_handler)
        # Display Gtk style Monitor
        gui_components = UPSgui.GuiComp(ups_list, MonitorWindow.max_width)
        umonitor = MonitorWindow(ups_list, gui_components)
        umonitor.connect('delete-event', umonitor.set_quit)
        umonitor.show_all()

        # Start thread to update Monitor
        _monthread = threading.Thread(target=refresh, daemon=True, args=[env.UT_CONST.SLEEP,
                                      update_data, ups_list, gui_components, umonitor]).start()

        Gtk.main()
    else:
        # Display text style Monitor
        color = '{}{}'.format(env.UT_CONST.mark_up_codes['red'],
                              env.UT_CONST.mark_up_codes['bold'])
        try:
            while not env.UT_CONST.quit:
                ups_list.read_all_ups_list_items(UPS.UpsComm.MIB_group.dynamic,
                                                 errups=args.show_unresponsive, display=False)
                if args.status:
                    print_monitor_table(ups_list)
                    sys.exit(0)
                if LOGGER.getEffectiveLevel() != logging.DEBUG:
                    os.system('clear')
                if LOGGER.getEffectiveLevel() == logging.DEBUG:
                    print('{}Debug Logger Active{}'.format(color, env.UT_CONST.mark_up_codes['reset']))
                if env.UT_CONST.log:
                    print('{}Logging to:  {}{}'.format(color, env.UT_CONST.log_file,
                                                       env.UT_CONST.mark_up_codes['reset']))
                    print_log(env.UT_CONST.log_file_ptr, ups_list)
                print_monitor_table(ups_list)
                time.sleep(env.UT_CONST.SLEEP)
        except KeyboardInterrupt:
            if env.UT_CONST.log:
                env.UT_CONST.log_file_ptr.close()
            sys.exit(0)


if __name__ == '__main__':
    main()
