#!/usr/bin/python2 -u
# coding=utf-8
import os
import re
import struct
import serial
import logging
from sys import argv, exit
from datetime import datetime
from pymodbus.pdu import ModbusRequest, ModbusResponse, ExceptionResponse
from pymodbus.other_message import ReportSlaveIdRequest
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse
from pymodbus.factory import ClientDecoder
from pymodbus.client import ModbusSerialClient as Modbus
logging.basicConfig(level=logging.INFO)



# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
# noinspection PyUnreachableCode
if False:
    from typing import List, Optional, NoReturn

RESET_REGISTER = 0x2087
FIRMWARE_VERSION_REGISTER = 1054
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
OUTPUT_DIR = '/data/innovenergy'


class ReadLogRequest(ModbusRequest):

    function_code = 0x42
    _rtu_frame_size = 5  # not used

    def __init__(self, address = None, **kwargs):

        ModbusRequest.__init__(self, **kwargs)
        self.sub_function = 0 if address is None else 1
        self.address = address

        # FUGLY as hell, but necessary bcs PyModbus cannot deal
        # with responses that have lengths depending on the sub_function.
        # it goes without saying that this isn't thread-safe
        ReadLogResponse._rtu_frame_size = 9 if self.sub_function == 0 else 9+128

    def encode(self):

        if self.sub_function == 0:
            return struct.pack('>B', self.sub_function)
        else:
            return struct.pack('>BI', self.sub_function, self.address)

    def decode(self, data):
        self.sub_function = struct.unpack('>B', data)

    def execute(self, context):
        print("EXECUTE1")

    def get_response_pdu_size(self):
        return ReadLogResponse._rtu_frame_size - 3

    def __str__(self):
        return "ReadLogAddressRequest"


class ReadLogResponse(ModbusResponse):

    function_code = 0x42
    _rtu_frame_size = 9  # the WHOLE frame incl crc

    def __init__(self, sub_function=0, address=b'\x00', data=None, **kwargs):
        ModbusResponse.__init__(self, **kwargs)
        self.sub_function = sub_function
        self.address = address
        self.data = data

    def encode(self):
        pass

    def decode(self, data):
        self.address, self.address = struct.unpack_from(">BI", data)
        self.data = data[5:]

    def __str__(self):
        arguments = (self.function_code, self.address)
        return "ReadLogAddressResponse(%s, %s)" % arguments

# unfortunately we have to monkey-patch this global table because
# the current (victron) version of PyModbus does not have a
# way to "register" new function-codes yet
ClientDecoder.function_table.append(ReadLogResponse)


class LockTTY(object):

    def __init__(self, tty):
        # type: (str) -> None
        self.tty = tty

    def __enter__(self):
        os.system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        os.system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)


def wrap_try_except(error_msg):
    def decorate(f):
        def applicator(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except:
                print(error_msg)
                exit(1)
        return applicator
    return decorate


def init_modbus(tty):
    # type: (str) -> Modbus

    return Modbus(
        port='/dev/' + tty,
        method='rtu',
        baudrate=115200,
        stopbits=1,
        bytesize=8,
        timeout=0.5,  # seconds
        parity=serial.PARITY_ODD)


@wrap_try_except("Failed to download BMS log!")
def download_log(modbus, node_id, battery_id):
    # type: (Modbus, int, str) -> NoReturn

    # Get address of latest log entry
    # request = ReadLogRequest(unit=slave_id)

    print ('downloading BMS log from node ' + str(node_id) + ' ...')

    progress = -1
    log_file = battery_id + "-node" + str(node_id) + "-"  + datetime.now().strftime('%d-%m-%Y') + ".bin"
    print(log_file)

    with open(log_file, 'w') as f:

        eof = 0x200000
        record = 0x40
        for address in range(0, eof, 2*record):

            percent = int(100*address/eof)

            if percent != progress:
                progress = percent
                print('\r{}% '.format(progress),end='')

            request = ReadLogRequest(address, slave=node_id)
            result = modbus.execute(request)  # type: ReadLogResponse

            address1 = "{:06X}".format(address)
            address2 = "{:06X}".format(address+record)

            data1 = result.data[:record]
            data2 = result.data[record:]

            line1 = address1 + ":" + ''.join('{:02X}'.format(byte) for byte in data1)
            line2 = address2 + ":" + ''.join('{:02X}'.format(byte) for byte in data2)

            lines = line1 + "\n" + line2 + "\n"
            f.write(lines)

    print("\r100%")
    print("done")
    print("wrote log to " + log_file)

    return True


@wrap_try_except("Failed to contact battery!")
def identify_battery(modbus, node_id):
    # type: (Modbus, int) -> str

    target = 'battery #' + str(node_id)
    print('contacting ' + target + ' ...')

    request = ReportSlaveIdRequest(slave=node_id)
    response = modbus.execute(request)

    index_of_ff = response.identifier.find(b'\xff')
    sid_response = response.identifier[index_of_ff + 1:].decode('utf-8').split(' ')

    response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=node_id)

    fw = '{0:0>4X}'.format(response.registers[0])
    print("log string is",sid_response[0]+"-"+sid_response[1]+"-"+fw)
        
    #return re.sub(" +", "-", sid + " " + fw)
    return sid_response[0]+"-"+sid_response[1]+"-"+fw


def is_int(value):
    # type: (str) -> bool
    try:
        _ = int(value)
        return True
    except ValueError:
        return False


def print_usage():
    print ('Usage:   ' + __file__ + ' <slave id> <serial device>')
    print ('Example: ' + __file__ + ' 2 ttyUSB0')
    print ('')
    print ('You can omit the "ttyUSB" prefix of the serial device:')
    print ('  ' + __file__ + ' 2 0')
    print ('')
    print ('You can omit the serial device entirely when the "com.victronenergy.battery.<serial device>" service is running:')
    print ('  ' + __file__ + ' 2')
    print ('')


def get_tty_from_battery_service_name():
    # type: () -> Optional[str]

    import dbus
    bus = dbus.SystemBus()

    tty = (
        name.split('.')[-1]
        for name in bus.list_names()
        if name.startswith('com.victronenergy.battery.')
    )

    return next(tty, None)


def parse_tty(tty):
    # type: (Optional[str]) -> str

    if tty is None:
        return get_tty_from_battery_service_name()

    if is_int(tty):
        return 'ttyUSB' + argv[1]
    else:
        return tty


def parse_cmdline_args(argv):
    # type: (List[str]) -> (str, int)

    slave_id = element_at_or_none(argv, 0)
    tty = parse_tty(element_at_or_none(argv, 1))

    if slave_id is None or tty is None:
        print_usage()
        exit(2)

    print("tty=",tty)
    print("slave id= ",slave_id)

    return tty, int(slave_id)


def element_at_or_none(lst, index):
    return next(iter(lst[index:]), None)


def main(argv):
    # type: (List[str]) -> ()

    tty, node_id = parse_cmdline_args(argv)

    with init_modbus(tty) as modbus:
        battery_id = identify_battery(modbus, node_id)
        download_log(modbus, node_id, battery_id)

    exit(0)


main(argv[1:])