"""Driver for plcpond"""

import threading
import sys
import json
import time
import logging
from random import randint
from device_base import deviceBase
from Channel import PLCChannel, read_tag, write_tag, TAG_DATAERROR_SLEEPTIME
from utilities import get_public_ip_address


_ = None

# LOGGING SETUP
from logging.handlers import RotatingFileHandler

log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(funcName)s(%(lineno)d) %(message)s')
logFile = './plcpond.log'
my_handler = RotatingFileHandler(logFile, mode='a', maxBytes=500*1024, backupCount=2, encoding=None, delay=0)
my_handler.setFormatter(log_formatter)
my_handler.setLevel(logging.INFO)
logger = logging.getLogger('plcpond')
logger.setLevel(logging.INFO)
logger.addHandler(my_handler)

console_out = logging.StreamHandler(sys.stdout)
console_out.setFormatter(log_formatter)
logger.addHandler(console_out)

logger.info("plcpond startup")

# GLOBAL VARIABLES
WATCHDOG_SEND_PERIOD = 3600  # Seconds, the longest amount of time before sending the watchdog status
PLC_IP_ADDRESS = "192.168.1.12"
CHANNELS = [
    PLCChannel(PLC_IP_ADDRESS, "cfgnumberofponds", "cfgNumberOfPonds", "REAL", 0.5, 600, map_=False, write_enabled=False, plc_type='Micro800'),

    PLCChannel(PLC_IP_ADDRESS, "pond1height", "pond1Height", "REAL", 0.5, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond2height", "pond2Height", "REAL", 0.5, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond3height", "pond3Height", "REAL", 0.5, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond4height", "pond4Height", "REAL", 0.5, 600, map_=False, write_enabled=False, plc_type='Micro800'),

    PLCChannel(PLC_IP_ADDRESS, "pond1volume", "pond1Volume", "REAL", 500.0, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond2volume", "pond2Volume", "REAL", 500.0, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond3volume", "pond3Volume", "REAL", 500.0, 600, map_=False, write_enabled=False, plc_type='Micro800'),
    PLCChannel(PLC_IP_ADDRESS, "pond4volume", "pond4Volume", "REAL", 500.0, 600, map_=False, write_enabled=False, plc_type='Micro800'),

    PLCChannel(PLC_IP_ADDRESS, "pondvolumetotal", "pondVolumeTotal", "REAL", 500.0, 600, map_=False, write_enabled=False, plc_type='Micro800')
]

CALIBRATION_TABLES = [[],[], [], [], []]  # position 0 is a dummy table




class start(threading.Thread, deviceBase):
    """Start class required by Meshify."""

    def __init__(self, name=None, number=None, mac=None, Q=None, mcu=None, companyId=None, offset=None, mqtt=None, Nodes=None):
        """Initialize the driver."""
        threading.Thread.__init__(self)
        deviceBase.__init__(self, name=name, number=number, mac=mac, Q=Q, mcu=mcu, companyId=companyId, offset=offset, mqtt=mqtt, Nodes=Nodes)

        self.daemon = True
        self.version = "2"
        self.finished = threading.Event()
        self.force_send = False
        threading.Thread.start(self)

    # this is a required function for all drivers, its goal is to upload some piece of data
    # about your device so it can be seen on the web
    def register(self):
        """Register the driver."""
        # self.sendtodb("log", "BOOM! Booted.", 0)
        pass

    def run(self):
        """Actually run the driver."""
        wait_sec = 30
        for i in range(0, wait_sec):
            print("plcpond driver will start in {} seconds".format(wait_sec - i))
            time.sleep(1)
        logger.info("BOOM! Starting plcpond driver...")

        public_ip_address = get_public_ip_address()
        self.sendtodbDev(1, 'public_ip_address', public_ip_address, 0, 'plcpond')
        watchdog = self.plcpond_watchdog()
        self.sendtodbDev(1, 'watchdog', watchdog, 0, 'plcpond')
        watchdog_send_timestamp = time.time()

        self.nodes["plcpond_0199"] = self

        send_loops = 0
        watchdog_loops = 0
        watchdog_check_after = 5000
        while True:
            if self.force_send:
                logger.warning("FORCE SEND: TRUE")

            for c in CHANNELS:
                v = c.read()
                if v is not None:  # read returns None if it fails
                    if c.check(v, self.force_send):
                        self.sendtodbDev(1, c.mesh_name, c.value, 0, 'plcpond')
                time.sleep(TAG_DATAERROR_SLEEPTIME) # sleep to allow Micro800 to handle ENET requests

            for pond_index in range(1, 5):
                self.read_pond_calibration(pond_index)

            # print("plcpond driver still alive...")
            if self.force_send:
                if send_loops > 2:
                    logger.warning("Turning off force_send")
                    self.force_send = False
                    send_loops = 0
                else:
                    send_loops += 1

            watchdog_loops += 1
            if watchdog_loops >= watchdog_check_after:
                test_watchdog = self.plcpond_watchdog()
                if not test_watchdog == watchdog or (time.time() - watchdog_send_timestamp) > WATCHDOG_SEND_PERIOD:
                    self.sendtodbDev(1, 'watchdog', test_watchdog, 0, 'plcpond')
                    watchdog = test_watchdog

                test_public_ip = get_public_ip_address()
                if not test_public_ip == public_ip_address:
                    self.sendtodbDev(1, 'public_ip_address', test_public_ip, 0, 'plcpond')
                    public_ip_address = test_public_ip
                watchdog_loops = 0


    def read_pond_calibration(self, pond_number):
        """Read all calibration values for a specific pond."""
        last_read_height = -1.0
        cal_values = []
        cal_index = 1
        while last_read_height != 0 and cal_index <=10:
            cal_tag_height = "pond{}CalibrationHeight[{}]".format(pond_number, cal_index)
            cal_tag_volume = "pond{}CalibrationVolume[{}]".format(pond_number, cal_index)
            read_height = read_tag(PLC_IP_ADDRESS, cal_tag_height, plc_type="Micro800")
            time.sleep(2)
            read_volume = read_tag(PLC_IP_ADDRESS, cal_tag_volume, plc_type="Micro800")
            time.sleep(2)
            if read_height and read_volume:
                if read_height[0] > 0.0:
                    cal_values.append({"height": read_height[0], "volume": read_volume[0]})
                last_read_height = read_height[0]
            cal_index += 1

        if cal_values != CALIBRATION_TABLES[pond_number] or self.force_send:
            calibration_channel = "pond{}calibration".format(pond_number)
            calibration_string = json.dumps(cal_values).replace('"', "'")
            self.sendtodbDev(1, calibration_channel, calibration_string, 0, 'plcpond')
            CALIBRATION_TABLES[pond_number] = cal_values

    def plcpond_watchdog(self):
        """Write a random integer to the PLC and then 1 seconds later check that it has been decremented by 1."""
        randval = randint(0, 32767)
        write_tag(str(PLC_IP_ADDRESS), 'watchdog_INT', randval, plc_type="Micro800")
        time.sleep(1)
        watchdog_val = read_tag(str(PLC_IP_ADDRESS), 'watchdog_INT', plc_type="Micro800")
        try:
            return (randval - 1) == watchdog_val[0]
        except (KeyError, TypeError):
            return False

    def plcpond_sync(self, name, value):
        """Sync all data from the driver."""
        self.force_send = True
        # self.sendtodb("log", "synced", 0)
        return True

    def plcpond_deletecalibrationpoint(self, name, value):
        """Delete a calibration point from a calibration table"""
        # {"pond": int, "point": int}
        value = value.replace("'", '"')
        parsed = json.loads(value)
        try:
            pond_number = int(parsed['pond'])
            point_number = int(parsed['point'])
            write_pond = write_tag(PLC_IP_ADDRESS, "inpPondNumber", pond_number, plc_type="Micro800")
            time.sleep(2)
            write_point = write_tag(PLC_IP_ADDRESS, "inpDeletePointIndex", point_number, plc_type="Micro800")
            time.sleep(2)
            if write_pond and write_point:
                write_cmd = write_tag(PLC_IP_ADDRESS, "cmdDeleteCalibrationPoint", 1, plc_type="Micro800")
                time.sleep(2)
                if write_cmd:
                    read_val = read_tag(PLC_IP_ADDRESS, "deleteSuccess", plc_type="Micro800")
                    if read_val[0] == 1:
                        self.read_pond_calibration(pond_number)
                        return True
                    return "Wrote everything successfully, but delete didn't succeed (Check pond and point values)."
                return "Didn't write delete command correctly."
            return "Didn't write pond or point correctly."
        except KeyError as e:
            return "Couldn't parse input value: {} -- {}".format(value, e)

    def plcpond_cfgnumberofponds(self, name, value):
        """Write the number of ponds to the plc."""
        value = int(value)
        return write_tag(PLC_IP_ADDRESS, "cfgNumberOfPonds", value, plc_type="Micro800")

    def plcpond_addcalibrationpoint(self, name, value):
        """Add a calibration point to the calibration table"""
        # value = {"pond": int, "height": float, "volume": float}
        value = value.replace("'", '"')
        parsed = json.loads(value)
        try:
            # parse json values, throw an error if one is missing
            pond_number = int(parsed['pond'])
            height = float(parsed['height'])
            volume = float(parsed['volume'])
            # write values to the tags
            write_pond = write_tag(PLC_IP_ADDRESS, "inpPondNumber", pond_number, plc_type="Micro800")
            time.sleep(2)
            write_height = write_tag(PLC_IP_ADDRESS, "inpPondHeight", height, plc_type="Micro800")
            time.sleep(2)
            write_volume= write_tag(PLC_IP_ADDRESS, "inpPondVolume", volume, plc_type="Micro800")
            time.sleep(2)
            if write_pond and write_height and write_volume:
                write_cmd = write_tag(PLC_IP_ADDRESS, "cmdAddCalibrationPoint", 1, plc_type="Micro800")
                time.sleep(2)
                if write_cmd:
                    read_val = read_tag(PLC_IP_ADDRESS, "addSuccess", plc_type="Micro800")
                    if read_val[0] == 1:
                        self.read_pond_calibration(pond_number)
                        return True
                    return "Wrote everything successfully, but delete didn't succeed (Check pond and point values)."
                return "Didn't write delete command correctly."
            return "Didn't write pond or point correctly."
        except KeyError as e:
            return "Couldn't parse input value: {} -- {}".format(value, e)

    def plcpond_writeplctag(self, name, value):
        """Write a value to the PLC."""
        new_val = json.loads(str(value).replace("'", '"'))
        tag_n = str(new_val['tag'])  # "cmd_Start"
        val_n = new_val['val']
        w = write_tag(str(PLC_IP_ADDRESS), tag_n, val_n)
        logger.info("Result of plcpond_writeplctag(self, {}, {}) = {}".format(name, value, w))
        if w is None:
            w = "Error writing to PLC..."
        return w
