#!/usr/bin/env python3
import os
import sys
import signal
import subprocess
import time
from threading import Thread, Lock
from journal import journal

try:
    import ubus
except ImportError:
    journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "Failed import ubus")
    sys.exit(-1)

fl_run_main = True
lib_sensors = {}
all_device = {}
cnt_max_dev = {}
curr_val_1wire = {}
uci_config_1wire = "owrt-1wire-protocol"
lock_curr_1wire = Lock()
dir_owfs = "/mnt/owfs/"


def stop_run(signum, frame):
    global fl_run_main
    journal.WriteLog("OWRT-1wire-protocol", "Normal", "notice", "Received termination signal!")
    fl_run_main = False


signal.signal(signal.SIGTERM, stop_run)


def get_list_1wire(dir_mnt):
    list_1w = []
    try:
        with os.scandir(dir_mnt) as files:
            subdir = [file.name for file in files if file.is_dir()]
            for i in range(len(subdir)):
                dir_name = subdir[i].split(".")
                if len(dir_name) == 2:
                    try:
                        int(dir_name[0], 16)
                        int(dir_name[1], 16)
                        list_1w.append(subdir[i])
                    except ValueError:
                        pass
    except FileNotFoundError:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "Not found mount dir OWFS")
        raise RuntimeError

    return list_1w


def poll_sensor(id_1w, rd_resourse, period):
    global curr_val_1wire

    while True:
        for name_file in rd_resourse:
            full_name = dir_owfs + "uncached/" + id_1w + "/" + name_file
            if os.path.isfile(full_name):
                result = subprocess.run(["cat", full_name], capture_output=True, text=True)
                lock_curr_1wire.acquire()
                curr_val_1wire[id_1w][name_file] = result.stdout
                lock_curr_1wire.release()
        time.sleep(period)


def run_poll_sensors():
    global curr_val_1wire
    tmp_dict = {}
    lock_curr_1wire.acquire()
    copy_config_1wire = all_device.copy()
    lock_curr_1wire.release()
    for id, resources in copy_config_1wire.items():
        tmp_dict.clear()
        # id = 10.001122 resoures = {rd... wr... period... line...}
        rd_file = resources['rd']
        name_rd_file = rd_file.split("/")
        for i in range(len(name_rd_file)):
            tmp_dict[name_rd_file[i]] = "-99999"
        lock_curr_1wire.acquire()
        curr_val_1wire[id] = tmp_dict.copy()
        lock_curr_1wire.release()
        try:
            period = float(resources['period'])
        except:
            journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                             "run_poll_sensors() error struct: period " + resources['period'])
            raise RuntimeError

        th = Thread(target=poll_sensor, args=(id, tmp_dict.copy(), period), daemon=True)
        th.start()


def ubus_init():
    def get_1wire_list_callback(event, data):
        list_1w = []
        ret_val = {}

        lim_max_dev()

        lock_curr_1wire.acquire()
        for id, resources in all_device.items():
            list_1w.append(id)
        lock_curr_1wire.release()

        ret_val["1wire_dev"] = list_1w

        event.reply(ret_val)

    def get_1wire_resources_callback(event, data):
        ret_val = {}
        id_1w = data['id_1wire']
        lock_curr_1wire.acquire()
        try:
            resources_1wire = all_device[id_1w].copy()
            for key, value in resources_1wire.items():
                if key != "rd" and key != "wr":
                    continue
                list_val = []
                name_resource = value.split("/")
                for i in range(len(name_resource)):
                    if name_resource[i] == "":
                        continue
                    list_val.append(name_resource[i])
                ret_val[key] = list_val
        except KeyError:
            ret_val[id_1w] = "Device not found"
        lock_curr_1wire.release()

        event.reply(ret_val)

    def get_value_1wire_callback(event, data):
        ret_val = {}
        lock_curr_1wire.acquire()
        try:
            ret_val['value'] = curr_val_1wire[data['id_1wire']][data['name_val']]
            ret_val['status'] = "0"
        except KeyError:
            ret_val['value'] = "-99999"
            ret_val['status'] = "-1"
        lock_curr_1wire.release()

        event.reply(ret_val)

    def set_value_1wire_callback(event, data):
        ret_val = {}

        lock_curr_1wire.acquire()
        res = all_device.get(data['id_1wire'])
        lock_curr_1wire.release()
        if res is not None:
            full_name = dir_owfs + "uncached/" + data['id_1wire'] + "/" + data['resource']
            if os.path.isfile(full_name):
                cmd = "echo " + data['value'] + " > " + full_name
                result = subprocess.run(cmd, shell=True)
                if result.returncode == 0:
                    ret_val["result"] = "0"
                else:
                    ret_val["result"] = "-1"
            else:
                ret_val["result"] = "-1"
        else:
            ret_val["result"] = "-1"

        event.reply(ret_val)

    ubus.add(
        'owrt-1wire-dev', {
            'get_1wire_list': {
                'method': get_1wire_list_callback,
                'signature': {
                    'ubus_rpc_session': ubus.BLOBMSG_TYPE_STRING
                }
            },
            'get_1wire_resources': {
                'method': get_1wire_resources_callback,
                'signature': {
                    'id_1wire': ubus.BLOBMSG_TYPE_STRING,
                    'ubus_rpc_session': ubus.BLOBMSG_TYPE_STRING
                }
            },
            'get_value_1wire': {
                'method': get_value_1wire_callback,
                'signature': {
                    'id_1wire': ubus.BLOBMSG_TYPE_STRING,
                    'name_val': ubus.BLOBMSG_TYPE_STRING,
                    'ubus_rpc_session': ubus.BLOBMSG_TYPE_STRING
                }
            },
            'set_value_1wire': {
                'method': set_value_1wire_callback,
                'signature': {
                    'id_1wire': ubus.BLOBMSG_TYPE_STRING,
                    'resource': ubus.BLOBMSG_TYPE_STRING,
                    'value': ubus.BLOBMSG_TYPE_STRING,
                    'ubus_rpc_session': ubus.BLOBMSG_TYPE_STRING
                }
            }
        }
    )


def parse_lib_sensors(confvalues):
    global lib_sensors
    sensor_pattern = {}

    try:
        for confdict in list(confvalues[0]['values'].values()):
            if confdict['.type'] == "info":
                if confdict['bus'] == "sensor":
                    if 'rd' in confdict:
                        sensor_pattern['rd'] = confdict['rd']
                    else:
                        sensor_pattern['rd'] = ''

                    if 'wr' in confdict:
                        sensor_pattern['wr'] = confdict['wr']
                    else:
                        sensor_pattern['wr'] = ''

                    if 'period' in confdict:
                        sensor_pattern['period'] = confdict['period']
                    else:
                        sensor_pattern['period'] = ''

                    lib_sensors[confdict['family_id']] = sensor_pattern.copy()
    except KeyError:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                         "parse_lib_sensors() error struct " + uci_config_1wire +
                         " config info '" + confdict['.name'] + "'")
        raise RuntimeError


def get_srch_dev(list_1wire, confdict):
    dict_dev = {}

    for dev_1w in list_1wire:
        id_fam_dev = dev_1w.split(".")
        if len(id_fam_dev) == 2:
            res = lib_sensors.get(id_fam_dev[0])
            if res is not None:
                if confdict['bus'] == "i2c":
                    res['line'] = str(confdict['i2c_bus'] + ":" + confdict['i2c_addr'])
                elif confdict['bus'] == "fake":
                    res['line'] = "fake"
                dict_dev[dev_1w] = res.copy()

    return dict_dev


def search_all_dev(confdict):
    global cnt_max_dev

    param_owfs = ["owfs", "-m", dir_owfs]
    srch_dev = {}
    flag_except = False

    if confdict['bus'] == "i2c":
        param_owfs.append("--i2c=/dev/" + confdict['i2c_bus'] + ":" + confdict['i2c_addr'])

        res = cnt_max_dev.get(str(confdict['i2c_bus'] + ":" + confdict['i2c_addr']))
        if res is None:
            cnt_max_dev[str(confdict['i2c_bus'] + ":" + confdict['i2c_addr'])] = int(confdict['max_device'])
        else:
            journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "search_all_dev() -- duplication bus "
                             + confdict['i2c_bus'] + ":" + confdict['i2c_addr'])
            raise RuntimeError

    elif confdict['bus'] == "fake":
        param_owfs.append("--fake=" + confdict['fake_id'])
    else:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "search_all_dev() -- error bus " + confdict['bus'])
        raise RuntimeError

    try:
        os.mkdir(dir_owfs)
    except FileExistsError:
        pass

    try:
        result = subprocess.run(param_owfs, check=True)
    except FileNotFoundError:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "search_all_dev() -- Failed run OWFS")
        raise RuntimeError
    except subprocess.CalledProcessError:
        # If i2c disable then owfs not running
        return None

    # Time for scanning OWFS 1wire devices on bus (f = 15,6 kbit/s)
    time.sleep(0.1)

    try:
        srch_dev = get_srch_dev(get_list_1wire(dir_owfs), confdict)
    except RuntimeError:
        flag_except = True
    finally:
        subprocess.run(["killall", "owfs"])
        if flag_except:
            raise RuntimeError

    return srch_dev


def get_all_dev(confvalues):
    global all_device

    try:
        for confdict in list(confvalues[0]['values'].values()):
            if confdict['.type'] == "info":
                if confdict['bus'] == "i2c" or confdict['bus'] == "fake":
                    tmp_dev = search_all_dev(confdict)
                    if tmp_dev is not None:
                        lock_curr_1wire.acquire()
                        all_device.update(tmp_dev)
                        lock_curr_1wire.release()
    except (KeyError, ValueError, RuntimeError) as e:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                         "get_all_dev() error struct " + uci_config_1wire +
                         " config info '" + confdict['.name'] + "'")
        raise RuntimeError


def parseconfig():
    list_param = []
    fake_bus = False

    try:
        confvalues = ubus.call("uci", "get", {"config": uci_config_1wire})
    except RuntimeError:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                         "parseconfig() error get " + uci_config_1wire)
        raise RuntimeError

    try:
        parse_lib_sensors(confvalues)
        get_all_dev(confvalues)
    except RuntimeError:
        raise RuntimeError

    try:
        for confdict in list(confvalues[0]['values'].values()):
            if confdict['.type'] == "info":
                if confdict['bus'] == "fake":
                    if fake_bus:
                        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                                         "parseconfig() error duplication bus " + confdict['bus'])
                        raise RuntimeError
                    fake_bus = True
                    list_param.append("--fake=" + confdict['fake_id'])
                if confdict['bus'] == "i2c":
                    list_param.append("--i2c=/dev/" + confdict['i2c_bus'] + ":" + confdict['i2c_addr'])
    except KeyError:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err",
                         "parseconfig() error struct " + uci_config_1wire)
        raise RuntimeError

    return list_param


def lim_max_dev():
    try:
        list_1w = get_list_1wire(dir_owfs)
    except RuntimeError:
        raise RuntimeError

    lock_curr_1wire.acquire()
    copy_all_device = all_device.copy()
    lock_curr_1wire.release()
    copy_cnt_max_dev = cnt_max_dev.copy()
    for id, resources in copy_all_device.items():
        if id in list_1w:
            line_dev = resources['line']
            if line_dev == "fake":
                continue
            if copy_cnt_max_dev[line_dev] > 0:
                copy_cnt_max_dev[line_dev] = copy_cnt_max_dev[line_dev] - 1
            else:
                lock_curr_1wire.acquire()
                all_device.pop(id, None)
                lock_curr_1wire.release()
        else:
            lock_curr_1wire.acquire()
            all_device.pop(id, None)
            lock_curr_1wire.release()


if __name__ == '__main__':
    param_owfs = ["owfs", "-m", dir_owfs]

    if not ubus.connect("/var/run/ubus.sock"):
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "Failed connect to ubus")
        sys.exit(-1)

    try:
        param_owfs.extend(parseconfig())
        for param in param_owfs:
            if "--fake=" in param:
                break
        else:
            # If i2c disable then owfs not running,
            # then added "--fake=" for running owfs.
            param_owfs.append("--fake=")

        try:
            os.mkdir(dir_owfs)
        except FileExistsError:
            pass
        result = subprocess.run(param_owfs, check=True)
    except (FileNotFoundError, RuntimeError, subprocess.CalledProcessError):
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "Failed run OWFS")
        ubus.disconnect()
        sys.exit(-1)

    try:
        lim_max_dev()
    except (RuntimeError, KeyError):
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "err", "lim_max_dev() -- ERROR")
        subprocess.run(["killall", "owfs"])
        ubus.disconnect()
        sys.exit(-1)

    journal.WriteLog("OWRT-1wire-protocol", "Normal", "notice", "Start module!")

    try:
        run_poll_sensors()
    except RuntimeError:
        fl_run_main = False
    ubus_init()

    try:
        while fl_run_main:
            ubus.loop(1)
    except KeyboardInterrupt:
        pass
    finally:
        journal.WriteLog("OWRT-1wire-protocol", "Normal", "notice", "Stop module!")

    subprocess.run(["killall", "owfs"])
    ubus.disconnect()
