# * Copyright (C) 2020 Texas Instruments Incorporated - http://www.ti.com/
#  *
#  *
#  *  Redistribution and use in source and binary forms, with or without
#  *  modification, are permitted provided that the following conditions
#  *  are met:
#  *
#  *    Redistributions of source code must retain the above copyright
#  *    notice, this list of conditions and the following disclaimer.
#  *
#  *    Redistributions in binary form must reproduce the above copyright
#  *    notice, this list of conditions and the following disclaimer in the
#  *    documentation and/or other materials provided with the
#  *    distribution.
#  *
#  *    Neither the name of Texas Instruments Incorporated nor the names of
#  *    its contributors may be used to endorse or promote products derived
#  *    from this software without specific prior written permission.
#  *
#  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#  *
# */

import argparse
import questionary
import time
import sys, glob, serial, os
from os import path
import serial.tools.list_ports
import string

# Set the path for the files to the current directory
id_file = os.path.join('r', os.path.abspath("."), 'config_nv_regions.txt')
content_file = os.path.join('r', os.path.abspath("."), 'read_content.txt')
nv_multi_page_key_words = []

# Set up the CLI Menu
parser = argparse.ArgumentParser(description='Welcome to the TI Zigbee Network Properties Tool!')
parser = argparse.ArgumentParser(description='This tool allows for the cloning of a coordinator within a Zigbee 3.0 '
                                             'Mesh Network using the SimpleLink CC13x2 and CC26x2 devices. The task is '
                                             'accomplished through the Monitor and Test (MT) API. For more information '
                                             'please refer to the Cloning Z-Stack Network Properties Application Report '
                                             '(http://www.ti.com/lit/swra671) or the Z-Stack Users Guide at dev.ti.com')
parser.add_argument("-cf",  help="Path to configuration file for NV items", type=str, default=str(os.path.basename(id_file)))
parser.add_argument("-rf",  help="Path to file which will contain NV content read", type=str, default=str(os.path.basename(content_file)))
parser.add_argument("-nfn", help="Flag to not form the network before writing to ZC (default=True)", action='store_false', default=True)
parser.add_argument("-q",   help="Print quiet", action='store_true', default=False)
args = parser.parse_args()


def prelim_file_check(file):
    error = False
    if not path.exists(file):
        error = True
        print("The file, " + str(os.path.basename(file)) + ", was not found.")
    return error


def max_width_file(content):
    name = []
    for x in content:
        line = ((x.strip('\n')).split(':'))
        line[0] = line[0].strip()
        if line[0].find("NV Region") == -1:
            name.append(line[0])

    # Grab the max length of NV_Regions to format the output file
    if len(name) != 0:
        max_width = max(len(filename) for filename in name)
    else:
        max_width = 0
    return max_width


def table_add(name):
    name = name.strip('*')
    global nv_multi_page_key_words
    nv_multi_page_key_words.append(name)
    res = []
    for i in nv_multi_page_key_words:
        if i not in res:
            res.append(i)
    nv_multi_page_key_words = res
    return


def config_file_format_check(file_config):
    # The configuration file should be in a specific format
    error = False

    config_file = open(file_config, "r+")

    first_line = 0
    no_header = False

    content = config_file.readlines()
    max_width = max_width_file(content)

    for line in content:
        line = line.strip()

    # Check the content in the file
    if len(content) != 0:
        for lines in content:
            if not error:
                line = lines.strip("\n")
                if len(line.strip()) != 0 and line.find(":") == -1:
                    error = True
                    print("The file " + str(os.path.basename(file_config)) + " is not in the correct format.")
                    print("Make sure to include a separator between row information.")
                else:
                    line = line.split(":")
                    if len(line) != 0:
                        if len(line) == 5:
                            if line[0] != 'NV Region':
                                table_add(line[0])
                                # Check the ID
                                if line[1].find('0x') != -1 and len(line[1].strip()) == 6:
                                    # Check the System ID
                                    if line[2].find('0x') != -1 and len(line[2].strip()) == 4:
                                        # Check the Sub ID
                                        if line[3].find('0x') != -1 and len(line[3].strip()) == 6:
                                            # Check the number of entries
                                            if not (str(line[4]).strip()).isdigit():
                                                error = True
                                                print("The entry value for " + line[0] + " is incorrect.")
                                        else:
                                            error = True
                                            print("The Sub ID of " + line[0] + " is not in the correct format.")
                                            print("Make sure it is 0x followed by a four digit value.")
                                    else:
                                        error = True
                                        print("The System ID of " + line[0] + " is not in the correct format.")
                                        print("Make sure it is 0x followed by a two digit value.")
                                else:
                                    error = True
                                    print("The Item ID of " + line[0] + " is not in the correct format.")
                                    print("Make sure it is 0x followed by a four digit value.")
                        if len(line) == 2:
                            if line[0] != 'NV Region' and len(line[0].strip()) != 0:
                                # Check the ID
                                if line[1].find('0x') == -1 or len(line[1].strip()) != 6:
                                    error = True
                                    print("The Item ID of " + line[0] + " is not in the correct format.")
                        if len(line) == 3 or len(line) == 4 or len(line) > 5:
                            error = True
                            print("There was not enough/too much information provided for: ")
                    else:
                        error = True

    # Create the Header for the file if there is none
    if not error:
        for lines in content:
            # There is a problem with the header
            line = lines.strip("\n")
            if first_line == 0:
                if line.find("NV Region") == -1 or line.find("Item ID") == -1 or line.find("Sys ID") == -1\
                        or line.find("Sub ID") == -1 or line.find("Entries") == -1:
                    config_file.truncate(0)
                    config_file.seek(0,0)
                    config_file.write("NV Region:".ljust(max_width+4) + '%5s %5s %5s %5s' % (
                        "Item ID:  ", "Sys ID: ", "Sub ID: ", "Entries" + '\n'))
                    no_header = True
            first_line = 1
        if no_header:
            for x in content:
                line = (x.strip('\n'))
                config_file.write(line + '\n')
    return error


def write_file_format_check(write_file):
    # The configuration file should be in a specific format
    error = False

    if not prelim_file_check(write_file):
        # Check if the file is a text file
        if os.path.basename(write_file).endswith('.txt'):
            # Safe to open the file
            config_file = open(write_file, "r+")
        else:
            error = True
            print("The file chosen did not have the correct extension.")
    else:
        error = True

    line = []
    if not error:
        first_line = 0
        no_header = False

        content = config_file.readlines()
        max_width = max_width_file(content)

        # Check the content in the file
        if len(content) != 0:
            for lines in content:
                if not error:
                    line = lines.strip("\n")
                    if len(line.strip()) != 0 and line.find(":") == -1:
                        error = True
                        print("The file " + str(os.path.basename(write_file)) + " is not in the correct format.")
                        print("Make sure to include a separator between row information.")
                    else:
                        line = line.split(":")
                        if len(line) != 0:
                            if len(line) == 6:
                                if line[0].strip() != 'NV Region':
                                    line[0]= line[0].strip()
                                    table_add(line[0])
                                    # Check the ID
                                    if line[1].find('0x') != -1 and len(line[1].strip()) == 6:
                                        # Check the System ID
                                        if line[2].find('0x') != -1 and len(line[2].strip()) == 4:
                                            # Check the Sub ID
                                            if line[3].find('0x') != -1 and len(line[3].strip()) == 6:
                                                # Check the Length
                                                if line[4].find('0x') != -1 and (len(line[4].strip()) == 4 or
                                                                                 len(line[4].strip()) == 5):
                                                    # Check the Data
                                                    if (int(len(line[5].strip()) / 2)) != (int(line[4].strip(), 16)):
                                                        error = True
                                                        print("The data value for " + line[0] +
                                                              " does not match the length value specified.")
                                                else:
                                                    error = True
                                                    print("The length value for " + line[0] +
                                                          " should be two characters long.")
                                            else:
                                                error = True
                                                print("The Sub ID of " + line[0] + " is not in the correct format.")
                                                print("Make sure it is 0x followed by a four digit value.")
                                        else:
                                            error = True
                                            print("The System ID of " + line[0] +
                                                  " is not in the correct format.")
                                            print("Make sure it is 0x followed by a two digit value.")
                                    else:
                                        error = True
                                        print("The Item ID of " + line[0] + " is not in the correct format.")
                                        print("Make sure it is 0x followed by a four digit value.")
                            if len(line) == 4:
                                if line[0].strip() != 'NV Region' and len(line[0].strip()) != 0:
                                    # Check the ID
                                    if line[1].find('0x') != -1 and len(line[1].strip()) == 6:
                                        # Check the Length
                                        if line[2].find('0x') != -1 and (len(line[2].strip()) == 4 or
                                                                         len(line[2].strip()) == 5 or
                                                                         len(line[2].strip()) == 6):
                                            # Check the Data
                                            if (int(len(line[3].strip()) / 2)) != (int(line[2].strip(), 16)):
                                                error = True
                                                print("The data value for " + line[0] +
                                                      " does not match the length value specified.")
                                        else:
                                            error = True
                                            print("The length value for " + line[0] +
                                                  " should be two characters long.")
                                    else:
                                        error = True
                                        print("The Item ID of " + line[0] +
                                              " is not in the correct format.")
                            if len(line) > 1 and (len(line) == 5 or len(line) > 6):
                                error = True
                                print("There was not enough/ too much information provided for: ")
                                print(line[0])
                        else:
                            error = True

        # Create the Header for the file if there is none
        if not error:
            for lines in content:
                # There is a problem with the header
                line = lines.strip("\n")
                if first_line == 0:
                    if line.find("NV Region") == -1 or line.find("Item ID") == -1 or line.find("Sub ID") \
                            == -1 or line.find("Length") == -1 or line.find("Data") == -1:
                        config_file.truncate(0)
                        config_file.seek(0, 0)
                        config_file.write("NV Region" + '%5s %5s %5s %5s' % (
                            ": Item ID    " + ":", "Sub ID    " + ":", "Length" + ":", "Data" + ":" + '\n'))
                        no_header = True
                first_line = 1
            if no_header:
                for x in content:
                    line = (x.strip('\n'))
                    config_file.write(line + '\n')
    return error


def fcs_calc(frame):
    if frame[0:2] == 'fe':
        frame = frame.lstrip('fe')
    fcs_check = int(frame[:2], 16) ^ int(frame[2:4], 16)
    for x in range(4, len(frame), 2):
        fcs_check = fcs_check ^ int(frame[x:x + 2], 16)
    return fcs_check


def frame_append(message, value):
    # Converting the order of the bytes
    content = "".join(reversed([value[i:i + 2] for i in range(0, len(value), 2)]))
    message += chr(int(content[0:2], 16)).encode('latin-1')
    message += chr(int(content[2:4], 16)).encode('latin-1')
    return message


def read_response(port):
    buf_hex = []
    try:
        buf = port.read(2)  # Read the SOF and LEN
        buf = buf.hex()  # Convert read data to a hexadecimal string
        if buf[0:2] == 'fe':  # Verify the SOF byte
            buf2 = port.read(int(buf[2:4], 16) + 3)  # Read the rest of the frame + CMD0/CMD1/FCS
            buf_hex = buf + buf2.hex()  # Append the rest of the frame
            buf_hex = buf_hex[2: len(buf_hex)]  # Remove the SOF byte
    except ValueError:
        buf_hex = []
    return buf_hex


def sys_osal_nv_length(port, item_id):
    length = 0
    item_id = "%0.4X" % int(item_id, 16)
    command = [0x02, 0x21, 0x13]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message = frame_append(message, item_id)
    message += chr(fcs_calc(message.hex())).encode('latin-1')
    port.write(message)

    buf_hex = read_response(port)
    if len(buf_hex) != 0:
        fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]
        buf_hex = buf_hex[0: -2]
        length = buf_hex[6: 8]
        if int(hex(fcs_calc(buf_hex)), 16) != int(fcs_value, 16):
            error = True
    return length


def sys_nv_length(port, nv_region, item_id, sub_id):
    length = 0
    item_id = "%0.4X" % int(item_id, 16)
    sub_id = "%0.4X" % int(sub_id, 16)
    command = [0x05, 0x21, 0x32, 0x01]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message = frame_append(message, item_id)
    message = frame_append(message, sub_id)
    message += chr(fcs_calc(message.hex())).encode('latin-1')
    port.write(message)

    buf_hex = read_response(port)
    if len(buf_hex) != 0:
        fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]
        buf_hex = buf_hex[0: -2]
        length = buf_hex[6: 8]
        if int(hex(fcs_calc(buf_hex)), 16) != int(fcs_value, 16):
            error = True
    return length


def sys_nv_read(port, nv_region, item_id, sys_id, sub_id, length, entries, c_file, width, clone):
    wrote = False
    # Read the contents of each of the table entries
    sub_id = int(str(sub_id), 16)

    for y in range(sub_id, int(entries)):
        sub_id = "%0.4X" % int(hex(y), 16)
        sys_id = "%0.2X" % int(sys_id, 16)
        item_id = "%0.4X" % int(item_id, 16)
        length = "%0.2X" % int(length, 16)
        command = [0x08, 0x21, 0x33, int(sys_id, 16)]
        message = chr(0xFE).encode('latin-1')  # SOF
        for x in range(len(command)):
            message += (chr(command[x]).encode('latin-1'))  # Header
        message = frame_append(message, item_id)  # Item ID
        message = frame_append(message, sub_id)  # Sub ID
        message += chr(0x00).encode('latin-1')  # Offset (Byte 1)
        message += chr(0x00).encode('latin-1')  # Offset (Byte 2)
        message += chr(int(length, 16)).encode('latin-1')  # Length
        message += chr(fcs_calc(message.hex())).encode('latin-1')  # FCS
        port.write(message)  # Send the message to the device

        buf_hex = read_response(port)
        if len(buf_hex) != 0:
            status = buf_hex[6:8]
            if status == '00':
                fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate the FCS value
                buf_hex = buf_hex[0: -2]  # Remove FCS value from the rest of the frame
                data = buf_hex[10: 10 + int(buf_hex[8:10], 16) * 2]  # Isolate the data
                reversal = "".join(reversed([data[i:i + 2] for i in range(0, len(data), 2)]))
                buf_hex = buf_hex.replace(data, reversal)  # Replace the data content in the correct order
                if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16) and reversal != '':
                    # Write the NV Region, ID, LEN, and Value to a text file
                    if clone:
                        c_file.write((nv_region + "*").ljust(width + 5) + '%5s %5s %5s %5s %5s' %
                            (": " + "0x" + item_id + "       :", "0x" + sys_id +"       :",
                             "0x" + sub_id + "      :", "0x" + buf_hex[8:10] +
                            "        :", reversal + '\n'))
                    else:
                        c_file.write(nv_region.ljust(width + 5) + '%5s %5s %5s %5s' %
                                     (": " + "0x" + item_id + "       :", "0x" + sys_id + "      :",
                                      "0x" + sub_id + "      :",
                                      "0x" + buf_hex[8:10] +
                                      "        :", reversal + '\n'))
                    wrote = True
                else:
                    if not args.q: print("Incorrect FCS value for " + " " + nv_region)
    if wrote and not args.q: print("Success: Data Read for " + " " + nv_region)
    else:
        if not args.q: print('Failure: Could not read from:' + " " + nv_region)
    return


def sys_osal_nv_read(port, nv_region, item_id, c_file, width, clone):
    item_id = "%0.4X" % int(item_id, 16)
    command = [0x03, 0x21, 0x08]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message = frame_append(message, item_id)
    message += chr(0x00).encode('latin-1')
    message += chr(fcs_calc(message.hex())).encode('latin-1')
    port.write(message)

    buf_hex = read_response(port)
    if len(buf_hex) != 0:
        status = buf_hex[6:8]
        if status == '00':
            fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate the FCS value
            buf_hex = buf_hex[0: -2]  # Remove FCS value from the rest of the frame
            data = buf_hex[10: 10 + int(buf_hex[8:10], 16) * 2]  # Isolate the data
            # Reverse the bytes in the data
            data = data.strip()
            reversal = "".join(reversed([data[i:i + 2] for i in range(0, len(data), 2)]))
            buf_hex = buf_hex.replace(data, reversal)  # Replace the data content in the correct order
            if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16):  # Check if the FCS value is correct
                # Write the NV Region, ID, LEN, and Value to a text file
                reversal = reversal.strip()
                if clone:
                    c_file.write((nv_region + "*").ljust(width + 5) + '%5s %5s %2s' % (
                        ": " + "0x" + item_id + "                                  :", "0x" + buf_hex[8:10] + "        :",
                        reversal + '\n'))
                else:
                    c_file.write(nv_region.ljust(width + 5) + '%5s %5s %2s' % (
                        ": " + "0x" + item_id + "                                  :", "0x" + buf_hex[8:10] + "        :",
                        reversal + '\n'))
                if not args.q: print('Success: Data Read for' + " " + nv_region)
            else:
                if not args.q: print("Incorrect FCS value for" + " " + nv_region)
        if status == '01':
            if not args.q: print('Failure: Please re-try the operation' + " " + nv_region + " " + "Message Received was "
                   + buf_hex)
        if status == '02':
            if not args.q: print('Failure: Could not read' + " " + nv_region + " (" + buf_hex[2:8] + ")")
    else:
        if not args.q:print('Failure: Could not read' + " " + nv_region + "(Empty Return)")
    return


def sys_osal_nv_write(port, nv_region, item_id, length, value):
    if length == '0xf7':
        length = '0xDB'
        value = value[:-56]  # Remove an extra byte (UART buffer limitation)
    item_id = "%0.4X" % int(item_id, 16)
    length = "%0.2X" % int(length, 16)
    command = [int(length, 16) + 4, 0x21, 0x09]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message = frame_append(message, item_id)
    message += chr(0x00).encode('latin-1')  # Offset
    message += chr(int(length, 16)).encode('latin-1')  # LEN
    # Reverse the bytes in the data
    reversed_mes = "".join(reversed([value[i:i + 2] for i in range(0, len(value), 2)]))
    for x in range(0, len(reversed_mes), 2):
        message += chr(int(reversed_mes[x:x + 2], 16)).encode('latin-1')
    message += chr(fcs_calc(message.hex())).encode('latin-1')  # FCS
    port.write(message)  # Send the message to the device

    wrote = False
    buf_hex = read_response(port)
    if len(buf_hex) != 0:
        status = buf_hex[6:8]
        if status == '00':
            fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate FCS Value
            buf_hex = buf_hex[0: -2]  # Remove FCS
            if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16):  # Verify FCS Value
                wrote = True
                if not args.q: print('Success: Wrote' + " " + nv_region + " " + 'to Memory')
        else:
            if not args.q: print('Failure: Could not write to:' + " " + nv_region + " (" + buf_hex[2:8] + ")")
    else:
        if not args.q: print('Failure: Could not write to:' + " " + nv_region + " (Empty Return)")
    return wrote


def sys_nv_update(port, nv_region, item_id, sys_id, sub_id, length, value):
    item_id = "%0.4X" % int(item_id, 16)
    sys_id = "%0.2X" % int(sys_id, 16)
    sub_id = "%0.4X" % int(sub_id, 16)
    length = "%0.4X" % int(length, 16)
    command = [int(length, 16) + 6, 0x21, 0x35, int(sys_id, 16)]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message = frame_append(message, item_id)
    message = frame_append(message, sub_id)
    message += chr(int(length, 16)).encode('latin-1')  # LEN
    reversed_mes = "".join(reversed([value[i:i + 2] for i in range(0, len(value), 2)]))  # Reverse the bytes in data
    for x in range(0, len(reversed_mes), 2):
        message += chr(int(reversed_mes[x:x + 2], 16)).encode('latin-1')
    message += chr(fcs_calc(message.hex())).encode('latin-1')  # FCS
    port.write(message)

    buf_hex = read_response(port)
    wrote = False
    if len(buf_hex) != 0:
        status = buf_hex[6:8]
        if status == '00':
            fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate FCS Value
            buf_hex = buf_hex[0: -2]  # Remove FCS
            if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16):  # Verify FCS Value
                wrote = True
        else:
            if not args.q: print('Failure: Could not write to:' + " " + nv_region + " (" + buf_hex[2:8] + ")")
    else:
        if not args.q: print('Failure: Could not write to:' + " " + nv_region + " SubID: " + sub_id +" (Empty Return)")
    return wrote


def app_cnf_bdb_start_commissioning(port):
    com_mode = ['Network Formation']
    command = [0x01, 0x2F, 0x05, 0x04]  # Initialize the Network
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message += chr(fcs_calc(message.hex())).encode('latin-1')
    port.write(message)

    buf_hex = read_response(port)
    if len(buf_hex) != 0:
        status = buf_hex[6:8]
        if status == '00':
            fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate FCS Value
            buf_hex = buf_hex[0: -2]  # Remove FCS
            if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16):  # Verify FCS Value
                if not args.q: print('Success: Formed the Network')
        else:
            if not args.q: print('Failure: Could not form the Network' + buf_hex[2:8])
    else:
        if not args.q: print('Failure: Could not form the Network (No Response')
    return


def sys_ping(port):
    valid = 0
    command = [0x00, 0x21, 0x01]
    message = chr(0xFE).encode('latin-1')
    for x in range(len(command)):
        message += (chr(command[x]).encode('latin-1'))
    message += chr(fcs_calc(message.hex())).encode('latin-1')  # FCS
    port.write(message)

    try:
        buf_hex = read_response(port)
        if len(buf_hex) != 0:
            fcs_value = buf_hex[len(buf_hex) - 2:len(buf_hex)]  # Isolate the FCS value
            buf_hex = buf_hex[0: -2]  # Remove FCS value from the rest of the frame
            if int(hex(fcs_calc(buf_hex)), 16) == int(fcs_value, 16):  # Check if the FCS value is correct
                valid = 1
    except ValueError:
        valid = 0
    return valid


def serial_ports():
    if sys.platform.startswith('win'):
        ports = list(serial.tools.list_ports.comports())
        app_ports = []
        error = False
        # Filter the available ports as TI Application Ports
        print("Scanning for devices...")
        for p in ports:
            if p.manufacturer == 'Texas Instruments Incorporated' or p.manufacturer == 'Texas Instruments':
                if p.hwid[-1] == '0':
                    try:
                        ser = serial.Serial(port=p.device, baudrate=115200, parity=serial.PARITY_NONE,
                                            stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1)
                        if sys_ping(ser) == 1:
                            app_ports.append(p.device)
                        ser.close()
                    except IOError:
                        error = True
    elif sys.platform.startswith('darwin'):
        ports = serial.tools.list_ports.comports()
        app_ports = []
        ti_ports = []
        for p in ports:
            if p.manufacturer == 'Texas Instruments':
                ti_ports.append(p.device)
        ti_ports = list(dict.fromkeys(ti_ports))
        if len(ti_ports) != 0:
            print('Scanning for devices...')
        for p in ti_ports:
            try:
                ser = serial.Serial(port=p, baudrate=115200, parity=serial.PARITY_NONE,
                                    stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=.05)
                if sys_ping(ser) == 1:
                    app_ports.append(p)
                ser.close()
            except (OSError, serial.SerialException):
                pass
    elif sys.platform.starswith('linux') or sys.platform.startswith('cygwin'):
        print("Scanning for devices...")
        ports=glob.glob('/dev/tty[A-Za-z]*')
    else:
        raise EnvironmentError('Unsupported platform')
    return app_ports


def check_hex(value):
    for letter in value:
        if letter not in string.hexdigits:
            return True
    return False


def read_from_selections(ser, selections):
    error = False
    asterisk = False

    # Check if the file is available
    if len(selections) == 0:
        error = True
        print("No selections made")
    else:
        if "All Regions" in selections:
            selections.clear()
            selections = config_file_content()

    if not error:
        # Clear the file where info will be written to
        open(content_file, "w").close()
        c_file = open(content_file, "w+")

    # Initialize Variable
    if not error:
        # Grab the max length of NV_Regions to format the output file
        max_width = max(len(filename) for filename in selections)

        # Create the Header for the file
        c_file.write("NV Region".ljust(max_width + 5) + '%5s %5s %5s %5s %5s' % (
            ": Item ID    " + "  :", "Sys ID     " + ":", "Sub ID    " + "  :", "Length    " + "  :", "Data" + '\n'))

        # Open the Configuration file to map the information with the NV Regions
        with open(id_file, 'r') as file:
            for line in file.readlines():
                line = ((line.strip('\n')).split(':'))
                for x in range(len(line)):
                    line[x] = line[x].strip()
                if line[0] != ' ' and line[0] != 'NV Region':
                    if line[0].find('*') != -1:
                        asterisk = True
                    else:
                        asterisk = False
                    for y in range(len(selections)):
                        wrote = False
                        line[0] = line[0].strip('*')
                        # Check if there is a match between a selection and an item in the Configuration File
                        if selections[y] == line[0]:
                            # Separate reads by table and non-table reads
                            if any(x in line[0] for x in nv_multi_page_key_words) and line[1] != ''\
                                    and line[2] != '' and line[3] != '':  # Table Entry
                                # Find the length of the item
                                length = sys_nv_length(ser, line[0], line[1], line[2])
                                # Read the content
                                sys_nv_read(ser, line[0], line[1], line[2], line[3], length, line[4],
                                            c_file, max_width, asterisk)
                                wrote = True
                            else:  # Non-Table Entry
                                if line[0] != ' ' and line[1] != ' ':
                                    sys_osal_nv_read(ser, line[0], line[1], c_file, max_width, asterisk)
                                    wrote = True
        file.close()
    return error


def write_from_file(ser, file_dir_write):
    # Initialize Variable
    read_content = []
    error = False

    # Open the file and read the content to be written
    with open(file_dir_write, 'r') as file:
        for line in file.readlines():
            line = ((line.strip('\n')).split(':'))
            for x in range(len(line)):
                line[x] = line[x].strip()
            line[0] = line[0].strip()
            if line[0] != '' and line[0] != "NV Region":
                if line[0] not in read_content:
                    read_content.append(line[0])

    if not error:
        # Open the file and read the content to write
        with open(file_dir_write) as file:
            nv_region = ""
            for line in file.readlines():
                line = ((line.strip('\n')).split(':'))
                for x in range(len(line)):
                    line[x] = line[x].strip()
                if line[0].strip() != '' and line[0] != 'NV Region':
                    line[0] = line[0].strip()
                    # Separate table and non-table entries
                    if any(x in line[0] for x in nv_multi_page_key_words):  # Table Entry
                        if line[1].strip() != '' and line[2].strip() != '' and line[3].strip() != '' and \
                                line[4].strip() != '' and line[5].strip() != '':
                            line[0] = line[0].strip('*')
                            if not check_hex(line[5]):
                                # Check if writing was successful
                                updated = sys_nv_update(ser, line[0], line[1], line[2], line[3], line[4], line[5])
                                if updated and nv_region != line[0]:
                                    # Update the output that there was a successful write
                                    if not args.q: print('Success: Wrote' + " " + line[0] + " " + 'to Memory')
                            else:
                                if not args.q: print('Failure: Make sure ' + line[0])
                                if not args.q: print('only contains hexadecimal digits')
                                error = True
                            nv_region = line[0]
                    else:  # Non-Table Entry
                        if len(line) >= 4:
                            if line[0] != '' and line[1] != '' and line[2] != '' and line[3] != '':
                                line[0] = line[0].strip('*')
                                if not check_hex(line[3]):
                                    done = sys_osal_nv_write(ser, line[0], line[1], line[2], line[3])
                                    if not done:
                                        if line[0].find('NIB') != -1 or line[0].find('nib') != -1:  # Initialize the NIB
                                            print( 'Start the NWK (Tools -> Form Network) and try again.')
                                else:
                                    print('Failure: Make sure ' + line[0])
                                    print('only contains hexadecimal digits')
                                    error = True
                        else:
                            print("Not enough information in " + str(os.path.basename(file_dir_write)))
                            error = True
        file.close()
    return error


def clone_content_read(ser, file_dir_read, file_dir_write):
    error = False

    # Check if the file is available
    if not path.exists(file_dir_write):
        print("File " + str(os.path.basename(file_dir_write)) + "was not found")
        error = True

    if not error:
        # Clear the file where info will be written to
        open(file_dir_write, "w").close()
        c_file = open(file_dir_write, "w+")

    if not config_file_format_check(file_dir_read) and not error:
        # Initialize List
        read_content = []
        # Open the Configuration File to find the length of the content to be read
        file = open(file_dir_read, "r")
        for line in file.readlines():
            line = ((line.strip('\n')).split(':'))
            for x in range(len(line)):
                line[x] = line[x].strip()
                if line[0] != '' and line[0] != 'NV Region' and line[0][-1] == '*':
                    if line[0] not in read_content:
                        read_content.append(line[0])

        if len(read_content) != 0:
            # Grab the max length of NV_Regions to format the output file
            max_width = max(len(filename) for filename in read_content)

            # Create the Header for the file
            c_file.write("NV Region".ljust(max_width + 5) + '%5s %5s %5s %5s %5s' % (
                    ": Item ID    " + ":", "Sys ID   " + ":", "Sub ID    " + ":", "Length    " + ":", "Data" + '\n'))

            # Open the Configuration File to read the information of the NV Regions
            file = open(file_dir_read, "r")
            for line in file.readlines():
                line = ((line.strip('\n')).split(':'))
                for x in range(len(line)):
                    line[x] = line[x].strip()
                    # Find items mandatory for cloning
                    if line[0] != '' and line[0] != 'NV Region' and line[0][-1] == '*':
                        # Separate content by table and non-table entries
                        if any(x in line[0] for x in nv_multi_page_key_words):  # Table Entry
                            line[0] = line[0].strip('*')
                            if line[0] != ' ' and line[1] != ' ' and line[2] != ' ' and line[3] != ' ':
                                # Find the length of the NV item
                                length = sys_nv_length(ser, line[0], line[1], line[2])
                                # Read the content of the NV item
                                if length != 0 or length != '0':
                                    sys_nv_read(ser, line[0], line[1], line[2], line[3], length, line[4], c_file,
                                                max_width, True)
                        else:  # Non-Table Reading
                            line[0] = line[0].strip('*')
                            if line[0] != ' ' and line[1] != ' ':
                                sys_osal_nv_read(ser, line[0], line[1], c_file, max_width, True)
        else:
            print("No data to read from. Check the " + str(os.path.basename(id_file)))
            print("To clone data make sure the region name ends with a *")
            error = True
    return error


def clone_content_write(ser, file_dir_write):
    # Initialize List
    read_content = []
    error = False

    # Open the file to know how much content there is to write
    with open(file_dir_write, 'r') as file:
        for line in file.readlines():
            line = ((line.strip('\n')).split(':'))
            for x in range(len(line)):
                line[x] = line[x].strip()
            line[0] = line[0].strip()
            if line[0] != '' and line[0][-1] == '*':
                if line[0] not in read_content:
                    read_content.append(line[0])
    file.close()

    if not error:
        # Open the file to gather the information related to the NV Regions
        with open(file_dir_write) as file:
            nv_region = ""
            for line in file.readlines():
                line = ((line.strip('\n')).split(':'))
                for x in range(len(line)):
                    line[x] = line[x].strip()
                if line[0].strip() != '' and line[0] != 'NV Region' and line[0][-1] == '*':
                    line[0] = line[0].strip()
                    # Separate the content by table and non-table entries
                    if any(x in line[0] for x in nv_multi_page_key_words):  # Table Entry
                        if line[1].strip() != '' and line[2].strip() != '' and line[3].strip() != '' \
                                and line[4].strip() != '':
                            line[0] = line[0].strip('*')
                            # Write to the NV region and check if the write was successful
                            if not check_hex(line[5]):
                                updated = sys_nv_update(ser, line[0], line[1], line[2], line[3], line[4], line[5])
                                if updated and nv_region != line[0]:
                                    # Update the output display if write was successful
                                    if not args.q: print('Success: Wrote' + " " + line[0] + " " + 'to Memory')
                            else:
                                if not args.q: print('Failure: Make sure ' + line[0])
                                if not args.q: print('only contains hexadecimal digits')
                                error = True
                            nv_region = line[0]
                    else:  # Non-Table Entry
                        if len(line) >= 4:
                            if line[0] != '' and line[1] != '' and line[2] != '' and line[3] != '':
                                line[0] = line[0].strip('*')
                                if not check_hex(line[3]):
                                    sys_osal_nv_write(ser, line[0], line[1], line[2], line[3])
                                else:
                                    if not args.q: print('Failure: Make sure ' + line[0])
                                    if not args.q: print('only contains hexadecimal digits')
                                    error = True
        file.close()
    return


def config_file_content():
    nv_regions = ["All Regions"]
    with open(id_file, 'r') as file:  # Open the File
        content = file.readlines()
        for line in content:  # Read each line
            line = ((line.strip('\n')).split(':'))  # Split the Content
            for x in range(len(line)):  # Iterate through each element in the line
                line[x] = line[x].strip()  # Strip away the white space
            if line[0] != '' and line[0] != 'NV Region' and line[1] != '':
                line[0] = line[0].strip("*")  # Remove the (*) character
                nv_regions.append(line[0])
    file.close()
    return nv_regions


def start_options():
    # Check to make sure the arguments are correct
    if prelim_file_check(args.cf): exit()
    else: id_file = args.cf

    if prelim_file_check(args.rf): exit()
    else: content_file = args.rf

    print('Welcome to the TI Zigbee Network Properties Tool!')

    # Check to see if there are devices connected
    ports = serial_ports()
    if len(ports) == 0:
        print("No devices were detected. Please connect a ZC/ZNP with MT and NPI API integration and re-try.")
        exit()

    # Ask the user to choose an application for the tool
    app = questionary.select("Choose an application:", choices=['Read from ZC', 'Write to ZC', 'Clone ZC']).ask()

    # Proceed with reading if reading option selected
    if app == 'Read from ZC':
        read_port = questionary.select("Choose a reading port:", choices=ports).ask()
        read_content = questionary.checkbox('Select items to read', choices=config_file_content()).ask()
        if prelim_file_check(id_file): exit()
        if config_file_format_check(id_file): exit()
        if prelim_file_check(content_file): exit()
        try:
            ser = serial.Serial(port=read_port, baudrate=115200, parity=serial.PARITY_NONE,
                                stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1)
        except IOError:
            error = True
            print(read_port + " is no longer accessible.")
            exit()

        error = read_from_selections(ser, read_content)
        ser.close()
        if not error:
            print('Read Complete. Refer to ' + str(os.path.basename(content_file)) + " to view results.")

    # Proceed with writing if writing option selected
    elif app == 'Write to ZC':
        write_port = questionary.select("Choose a writing port:", choices=ports).ask()
        write_file = questionary.text("Please enter the path of the file to write from:").ask()
        if prelim_file_check(write_file): exit()
        if write_file_format_check(write_file): exit()
        if prelim_file_check(id_file): exit()
        if config_file_format_check(id_file): exit()
        error = False
        try:
            ser = serial.Serial(port=write_port, baudrate=115200, parity=serial.PARITY_NONE,
                                stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1)
        except IOError:
            error = True
            print(write_port + " is no longer accessible.")

        if not error:
            if args.nfn:
                app_cnf_bdb_start_commissioning(ser)
                ser.close()
                ser.open()
            error = write_from_file(ser, write_file)

        # Update the user the procedure is complete
        if not error:
            print('Writing procedure complete.')
            print('If cloning to a new ZC, then power cycle/restart the device to complete the procedure.')
            print('Ensure to do a physical reset if it is the first time restarting the device after')
            print('programming with an XDS110. If using a LaunchPad the reset button may be pressed.')

    # Proceed with cloning if cloning option selected
    elif app == 'Clone ZC':
        confirm = questionary.confirm("This options requires both ZC/ZNP devices present. Would you like to proceed?").ask()
        if confirm:
            read_port = questionary.select("Choose a reading port:", choices=ports).ask()
            write_port = questionary.select("Choose a writing port:", choices=ports).ask()
            if read_port == write_port:
                print("Read and write ports are the same. Please try again and choose different ports.")
                exit()
            if prelim_file_check(id_file): exit()
            if config_file_format_check(id_file): exit()
            if prelim_file_check(content_file): exit()
            try:
                ser = serial.Serial(port=read_port, baudrate=115200, parity=serial.PARITY_NONE,
                                    stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1)
            except IOError:
                error = True
                print(read_port + " is no longer accessible.")
                
            error = clone_content_read(ser, id_file, content_file)
            ser.close()
            if not error:
                print('Read Complete. Refer to ' + str(os.path.basename(content_file)) + " to view results.")
            try:
                ser = serial.Serial(port=write_port, baudrate=115200, parity=serial.PARITY_NONE,
                                    stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=1)
            except IOError:
                error = True
                print(write_port + " is no longer accessible.")
            
            if not error:
                if args.nfn:
                    app_cnf_bdb_start_commissioning(ser)
                    ser.close()
                    ser.open()
                clone_content_write(ser, content_file)
                print('Writing procedure complete.')
                print('Please Power Cycle/Restart the new ZC to complete the procedure.')
                print('Ensure to do a physical reset if it is the first time restarting the device after')
                print('programming with an XDS110. If using a LaunchPad the reset button may be pressed.')
        else:
            exit()
    return


if __name__ == '__main__':
    start_options()