# The rare bits touch by Zhaoming Luo are under MIT License
#
# ===
#
# MIT License
# 
# Copyright (c) 2021 rbaron
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import argparse
import serial
import time

BAUD = 115200 * 2

SLEEP_AFTER_RESET_IN_S = 0.02
# This is modified when reading RAM and flash.
SLEEP_BETWEEN_READ_AND_WRITE_IN_S = 0.5

RAM_SIZE = 0x3ffff
FLASH_SIZE = 0x7d000

serial_port = None


def hexdump(bytearr):
    return ' '.join(f'{b:02x}' for b in bytearr)


def make_read_request(addr, n_bytes):
    return [0x5a, addr >> 16, (addr >> 8) & 0xff, addr & 0xff, 0x80] + [0xff] * (n_bytes + 1)


def make_write_request(addr, data):
    return [0x5a, addr >> 16, (addr >> 8) & 0xff, addr & 0xff, 0x00] + data + [0xff]


def request_payload_size(request):
    "Discounts the leading 5 bytes and trailing 1 byte from read and write requests."
    return len(request) - 6


def validate_raw_response(request, raw_response):
    return all([
        raw_response[0] == 0x02,
        raw_response[1] == request[0],
        raw_response[2] == 0x00,
        raw_response[3] == request[1],
        raw_response[4] == 0x00,
        raw_response[5] == request[2],
        raw_response[6] == 0x00,
        raw_response[7] == request[3],
        raw_response[8] == 0x00,
        raw_response[9] == request[4],
        raw_response[-2] == 0x02,
        raw_response[-1] == 0xff,
    ])


def parse_response(raw_response, n_bytes):
    "Extracts the data bytes from the raw response."
    return [
        raw_response[(i+5)*2 + 1]
        for i in range(n_bytes)
    ]


def write_and_read(data):
    serial_port.write(data)
    time.sleep(SLEEP_BETWEEN_READ_AND_WRITE_IN_S)
    return serial_port.read_all()


def write_and_read_cmd(cmd, data=None):
    cmd_bytes = bytearray([0x55, cmd] if data is None else [0x55, cmd] + data)
    return write_and_read(cmd_bytes)


def write_and_read_data(request):
    payload_size = request_payload_size(request)
    data_bytes = bytearray(request)
    raw_response = write_and_read(data_bytes)
    if not validate_raw_response(request, raw_response):
        raise Exception(
            f"Invalid request/raw response pair:\n\t{hexdump(request)}\n\t{hexdump(raw_response)}")
    return parse_response(raw_response, payload_size)


def get_soc_id():
    res = write_and_read_data(make_read_request(0x007e, 2))
    return res[1] << 8 | res[0]
    # res = write_and_read_data(make_read_request(0x007e, 1))
    # return res[0]

def set_speed(speed):
    """The SWS speed is set in the 0x00b2 register by means of
        specifying the number of clocks per bit."""
    # return write_and_read_data(make_write_request(0x00b0, [0x00, speed, 0x80, 0x00]))
    return write_and_read_data(make_write_request(0x00b2, [speed]))


def set_pgm_speed(speed):
    return write_and_read_cmd(0x05, [speed])


def find_suitable_sws_speed():
    for speed in range(2, 0x7f):
        time.sleep(0.02)
        print(f'Trying speed {speed}')
        set_speed(speed)
        # Try to make a read request. If we get a valid response, assume we've
        # found a suitable SWS speed.
        try:
            get_soc_id()
        except Exception:
            continue
        else:
            print(f'Found and set suitable SWS speed: {speed}')
            return speed
    raise RuntimeError("Unable to find a suitable SPI speed")


def send_flash_write_enable():
    # CNS low.
    write_and_read_data(make_write_request(0x0d, [0x00]))
    # Write enable.
    write_and_read_data(make_write_request(0x0c, [0x06]))
    # CNS high.
    write_and_read_data(make_write_request(0x0d, [0x01]))


def send_flash_erase():
    # CNS low.
    write_and_read_data(make_write_request(0x0d, [0x00]))
    # Write enable.
    write_and_read_data(make_write_request(0x0c, [0x60]))
    # CNS high.
    write_and_read_data(make_write_request(0x0d, [0x01]))


def send_flash_get_status():
    # CNS low.
    write_and_read_data(make_write_request(0x0d, [0x00]))
    # Get flash status command.
    write_and_read_data(make_write_request(0x0c, [0x05]))
    # Start SPI.
    write_and_read_data(make_write_request(0x0c, [0xff]))
    # Read the status byte.
    res = write_and_read_data(make_read_request(0x0c, 1))
    # CNS high.
    write_and_read_data(make_write_request(0x0d, [0x01]))
    return res

def send_cpu_stop():
    return write_and_read_data(make_write_request(0x0602, [0x05]))


def send_csn_high():
    return write_and_read_data(make_write_request(0x000d, [0x01]))


def send_csn_low():
    return write_and_read_data(make_write_request(0x000d, [0x00]))


def dump_ram():
    contents = []
    CHUNK_SIZE = 16
    for addr in range(0x00, RAM_SIZE, CHUNK_SIZE):
        # Report progress.
        if addr & 0xff == 0:
            print(f'0x{addr:04x} {100 * addr / RAM_SIZE:05.2f}%')
        contents.extend(write_and_read_data(
            make_read_request(addr, CHUNK_SIZE)))
    return contents


def read_flash(addr, chunk_size):
    contents = []
    # send_flash_write_enable()
    # CNS low.
    write_and_read_data(make_write_request(0x0d, [0x00]))
    # Read command.
    write_and_read_data(make_write_request(0x0c, [0x03]))
    write_and_read_data(make_write_request(0x0c, [(addr >> 16) & 0xff]))
    write_and_read_data(make_write_request(0x0c, [(addr >> 8) & 0xff]))
    write_and_read_data(make_write_request(0x0c, [addr & 0xff]))

    # FIFO mode.
    write_and_read_data(make_write_request(0xb3, [0x80]))

    for i in range(chunk_size):
        write_and_read_data(make_write_request(0x0c, [0xff]))
        res = write_and_read_data(make_read_request(0x0c, 1))
        assert len(res) == 1
        contents.extend(res)

    # RAM mode.
    write_and_read_data(make_write_request(0xb3, [0x00]))

    # CNS high.
    write_and_read_data(make_write_request(0x0d, [0x01]))
    return contents


def write_flash(addr, data):
    send_flash_write_enable()

    # CNS low.
    write_and_read_data(make_write_request(0x0d, [0x00]))

    # Write command.
    write_and_read_data(make_write_request(0x0c, [0x02]))
    write_and_read_data(make_write_request(0x0c, [(addr >> 16) & 0xff]))
    write_and_read_data(make_write_request(0x0c, [(addr >> 8) & 0xff]))
    write_and_read_data(make_write_request(0x0c, [addr & 0xff]))

    # FIFO mode.
    write_and_read_data(make_write_request(0xb3, [0x80]))

    # Write data
    # CPU stop?
    write_and_read_data(make_write_request(0x0c, data))

    # # RAM mode.
    write_and_read_data(make_write_request(0xb3, [0x00]))

    # CNS high.
    write_and_read_data(make_write_request(0x0d, [0x01]))


def dump_flash(debug):
    contents = []
    CHUNK_SIZE = 16
    for addr in range(0x00, FLASH_SIZE, CHUNK_SIZE):
        # Report progress.
        if addr & 0xff == 0:
            print(f'0x{addr:06x} {100 * addr / FLASH_SIZE:05.2f}%')
        # Retry the same address in case something goes wrong.
        while True:
            try:
                res = read_flash(addr, CHUNK_SIZE)
                if debug:
                    print(f'Read: {hexdump(res)}')
                contents.extend(res)
                break
            except Exception as e:
                print(f"Retrying 0x{addr:08x}... {e}")

    return contents


def write_to_file(filename, contents):
    print(f"Writing {len(contents)} bytes to {filename}")
    with open(filename, 'wb') as f:
        f.write(bytes(contents))


def init_soc(sws_speed=None):
    # Set RST to low - turns off the SoC.
    write_and_read_cmd(0x00)

    # Configure SWire command length.  TLSR8251 uses a variant of
    # SWire protocol.  The difference is the number of bytes of
    # address.  TLSR8251 SWire protocol uses three bytes address.
    # Therefore, sw_cmd_len needs to be 1(START byte) plus 3
    # (address bytes) which is 4.
    write_and_read_cmd(0x08, [0x04])

    # Configure the cpu stop SWire command, which is used by the
    # programmer during activate.
    write_and_read_cmd(0x06, [0x5a, 0x00, 0x06, 0x02, 0x00, 0x05, 0xff])

    # Configure the read SWire register clock division command, which
    # is used by the programmer during activate.
    write_and_read_cmd(0x07, [0x5a, 0x00, 0x00, 0xb2, 0x80, 0xff, 0xff])

    # Configure programmer SWire speed
    set_pgm_speed(0x03)

    # Send an "activate" command. The STM32 will receive this command and put the Telink
    # in a suitable state. The STM32 will stop the Telink CPU by writing the value
    # 0x05 to Telink's 0x0602 register. It will also set a default SWS speed, but we
    # will override it later when we find a suitable SWS speed.
    serial_port.write(bytearray([0x55, 0x02, 0x02, 0x66]))

    # Give some time for the reset capacitor to charge and turn the chip on.
    time.sleep(0.70)

    raw = serial_port.read_all()

    if sws_speed is not None:
        set_speed(sws_speed)
    else:
        find_suitable_sws_speed()

    write_and_read_data(make_read_request(0xb2, 1))



def dump_flash_main(args):
    init_soc(args.sws_speed)
    print(f'Dumping flash to {args.filename}...')
    # Speed things up a little bit.
    global SLEEP_BETWEEN_READ_AND_WRITE_IN_S
    SLEEP_BETWEEN_READ_AND_WRITE_IN_S = 0.001
    write_to_file(args.filename, dump_flash(args.debug))


def erase_flash_main(args):
    init_soc(args.sws_speed)
    print(f'Erasing flash...')
    send_flash_write_enable()
    send_flash_erase()
    while True:
        res = send_flash_get_status()
        print(f'Flash status: {hexdump(res)}')
        if res[0] == 0:
            break
        time.sleep(1)

    # CNS high.
    # write_and_read_data(make_write_request(0x0d, [0x01]))


def write_flash_main(args):
    init_soc(args.sws_speed)
    time.sleep(0.02)

    print(f'Erasing flash...')
    send_flash_write_enable()
    send_flash_erase()
    while True:
        res = send_flash_get_status()
        print(f'Flash status: {hexdump(res)}')
        if res[0] == 0:
            break
        time.sleep(1)

    print(f'Writing flash from {args.filename}...')
    global SLEEP_BETWEEN_READ_AND_WRITE_IN_S
    # SLEEP_BETWEEN_READ_AND_WRITE_IN_S = 0.008
    SLEEP_BETWEEN_READ_AND_WRITE_IN_S = 0.005

    CHUNK_SIZE = 16
    with open(args.filename, 'rb') as f:
        contents = f.read()
        size = len(contents)
        for addr in range(0x00, size, CHUNK_SIZE):
            # Report progress.
            if addr & 0xff == 0:
                print(f'0x{addr:04x} {100 * addr / size:05.2f}%')
            data = contents[addr:min(addr + CHUNK_SIZE, size)]
            if args.debug:
                print(f'writing: {hexdump(data)}')
            write_flash(addr, list(data))

    while True:
        res = send_flash_get_status()
        print(f'Flash status: {hexdump(res)}')
        if res[0] == 0:
            break
        time.sleep(1)
    # Set RST to low - turns off the SoC.
    write_and_read_cmd(0x00)
    # Set RST to high - turns on the SoC.
    write_and_read_cmd(0x01)


def dump_ram_main(args):
    init_soc(args.sws_speed)
    print(f'Dumping ram to {args.filename}...')
    # Speed things up a little bit.
    global SLEEP_BETWEEN_READ_AND_WRITE_IN_S
    SLEEP_BETWEEN_READ_AND_WRITE_IN_S = 0.02
    write_to_file(args.filename, dump_ram())


def get_soc_id_main(args):
    init_soc(args.sws_speed)
    print(f'SOC ID: 0x{get_soc_id():04x}')

def cpu_run_main(args):
    init_soc(args.sws_speed)
    # Tell CPU to run.
    write_and_read_data(make_write_request(0x0602, [0x88]))


def main():
    args_parser = argparse.ArgumentParser(description='TLSR')
    args_parser.add_argument('--serial-port', type=str, required=True,
                             help="Serial port to use - this should be the USB CDC port that is connected to the STM32 (e.g.: /dev/cu.usbmodem6D8E448E55511.")
    args_parser.add_argument(
        '--sws-speed', type=int, help="SWS speed in the range [0x02, 0x7f]. If not provided, the script will try to find a suitable SWS speed automatically.")
    args_parser.add_argument(
        '--debug', action="store_true", help="Enabled debugging information.")
    subparsers = args_parser.add_subparsers(dest="cmd", required=True)

    dump_flash_parser = subparsers.add_parser('dump_flash')
    dump_flash_parser.set_defaults(func=dump_flash_main)
    dump_flash_parser.add_argument('filename', type=str)

    dump_ram_parser = subparsers.add_parser('dump_ram')
    dump_ram_parser.set_defaults(func=dump_ram_main)
    dump_ram_parser.add_argument('filename', type=str)

    get_soc_id_parser = subparsers.add_parser('get_soc_id')
    get_soc_id_parser.set_defaults(func=get_soc_id_main)

    write_flash_parser = subparsers.add_parser('write_flash')
    write_flash_parser.set_defaults(func=write_flash_main)
    write_flash_parser.add_argument('filename', type=str)

    erase_flash_parser = subparsers.add_parser('erase_flash')
    erase_flash_parser.set_defaults(func=erase_flash_main)

    erase_flash_parser = subparsers.add_parser('cpu_run')
    erase_flash_parser.set_defaults(func=cpu_run_main)

    args = args_parser.parse_args()

    # Initialize the serial port
    global serial_port
    serial_port = serial.Serial(
        args.serial_port, BAUD, serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE)

    args.func(args)


if __name__ == "__main__":
    main()
