#!/usr/bin/env python3

"""Simple FTDI EEPROM configurator.
"""

# Copyright (c) 2019-2024, Emmanuel Blot <emmanuel.blot@free.fr>
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from argparse import ArgumentParser, FileType
from io import StringIO
from logging import Formatter, StreamHandler, DEBUG, ERROR
from sys import exit as sys_exit, modules, stderr, stdout
from textwrap import fill
from traceback import format_exc
from pyftdi import FtdiLogger
from pyftdi.eeprom import FtdiEeprom
from pyftdi.ftdi import Ftdi
from pyftdi.misc import add_custom_devices, hexdump

# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements


def main():
    """Main routine"""
    debug = False
    try:
        argparser = ArgumentParser(description=modules[__name__].__doc__)
        argparser.add_argument('device', nargs='?', default='ftdi:///?',
                               help='serial port device name')

        files = argparser.add_argument_group(title='Files')
        files.add_argument('-i', '--input', type=FileType('rt'),
                           help='input ini file to load EEPROM content')
        files.add_argument('-l', '--load', default='all',
                           choices=('all', 'raw', 'values'),
                           help='section(s) to load from input file')
        files.add_argument('-o', '--output',
                           help='output ini file to save EEPROM content')
        files.add_argument('-V', '--virtual', type=FileType('r'),
                           help='use a virtual device, specified as YaML')

        device = argparser.add_argument_group(title='Device')
        device.add_argument('-P', '--vidpid', action='append',
                            help='specify a custom VID:PID device ID '
                                 '(search for FTDI devices)')
        device.add_argument('-M', '--eeprom',
                            help='force an EEPROM model')
        device.add_argument('-S', '--size', type=int,
                            choices=FtdiEeprom.eeprom_sizes,
                            help='force an EEPROM size')

        fmt = argparser.add_argument_group(title='Format')
        fmt.add_argument('-x', '--hexdump', action='store_true',
                         help='dump EEPROM content as ASCII')
        fmt.add_argument('-X', '--hexblock', type=int,
                         help='dump EEPROM as indented hexa blocks')

        config = argparser.add_argument_group(title='Configuration')
        config.add_argument('-s', '--serial-number',
                            help='set serial number')
        config.add_argument('-m', '--manufacturer',
                            help='set manufacturer name')
        config.add_argument('-p', '--product',
                            help='set product name')
        config.add_argument('-c', '--config', action='append',
                            help='change/configure a property as key=value '
                                 'pair')
        config.add_argument('--vid', type=lambda x: int(x, 16),
                            help='shortcut to configure the USB vendor ID')
        config.add_argument('--pid', type=lambda x: int(x, 16),
                            help='shortcut to configure the USB product ID')

        action = argparser.add_argument_group(title='Action')
        action.add_argument('-e', '--erase', action='store_true',
                            help='erase the whole EEPROM content')
        action.add_argument('-E', '--full-erase', action='store_true',
                            default=False,
                            help='erase the whole EEPROM content, including '
                                 'the CRC')
        action.add_argument('-u', '--update', action='store_true',
                            help='perform actual update, use w/ care')

        extra = argparser.add_argument_group(title='Extras')
        extra.add_argument('-v', '--verbose', action='count', default=0,
                           help='increase verbosity')
        extra.add_argument('-d', '--debug', action='store_true',
                           help='enable debug mode')
        args = argparser.parse_args()
        debug = args.debug

        if not args.device:
            argparser.error('Serial device not specified')

        loglevel = max(DEBUG, ERROR - (10 * args.verbose))
        loglevel = min(ERROR, loglevel)
        if debug:
            formatter = Formatter('%(asctime)s.%(msecs)03d %(name)-20s '
                                  '%(message)s', '%H:%M:%S')
        else:
            formatter = Formatter('%(message)s')
        FtdiLogger.set_formatter(formatter)
        FtdiLogger.set_level(loglevel)
        FtdiLogger.log.addHandler(StreamHandler(stderr))

        if args.virtual:
            # pylint: disable=import-outside-toplevel
            from pyftdi.usbtools import UsbTools
            # Force PyUSB to use PyFtdi test framework for USB backends
            UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', )
            # Ensure the virtual backend can be found and is loaded
            backend = UsbTools.find_backend()
            loader = backend.create_loader()()
            loader.load(args.virtual)

        try:
            add_custom_devices(Ftdi, args.vidpid, force_hex=True)
        except ValueError as exc:
            argparser.error(str(exc))

        eeprom = FtdiEeprom()
        eeprom.open(args.device, size=args.size, model=args.eeprom)
        if args.erase or args.full_erase:
            eeprom.erase()
        if args.input:
            eeprom.load_config(args.input, args.load)
        if args.serial_number:
            eeprom.set_serial_number(args.serial_number)
        if args.manufacturer:
            eeprom.set_manufacturer_name(args.manufacturer)
        if args.product:
            eeprom.set_product_name(args.product)
        for conf in args.config or []:
            if conf in ('?', 'help'):
                helpstr = ', '.join(sorted(eeprom.properties))
                print(fill(helpstr, initial_indent='  ',
                           subsequent_indent='  '))
                sys_exit(1)
            for sep in ':=':
                if sep in conf:
                    name, value = conf.split(sep, 1)
                    if not value:
                        argparser.error(f'Configuration {conf} without value')
                    if value == 'help':
                        value = '?'
                    helpio = StringIO()
                    eeprom.set_property(name, value, helpio)
                    helpstr = helpio.getvalue()
                    if helpstr:
                        print(fill(helpstr, initial_indent='  ',
                                   subsequent_indent='  '))
                        sys_exit(1)
                    break
            else:
                argparser.error(f'Missing name:value separator in {conf}')
        if args.vid:
            eeprom.set_property('vendor_id', args.vid)
        if args.pid:
            eeprom.set_property('product_id', args.pid)
        if args.hexdump:
            print(hexdump(eeprom.data))
        if args.hexblock is not None:
            indent = ' ' * args.hexblock
            for pos in range(0, len(eeprom.data), 16):
                hexa = ' '.join([f'{x:02x}' for x in eeprom.data[pos:pos+16]])
                print(indent, hexa, sep='')
        if args.update:
            if eeprom.commit(False, no_crc=args.full_erase):
                eeprom.reset_device()
        if args.verbose > 0:
            eeprom.dump_config()
        if args.output:
            if args.output == '-':
                eeprom.save_config(stdout)
            else:
                with open(args.output, 'wt') as ofp:
                    eeprom.save_config(ofp)

    except (ImportError, IOError, NotImplementedError, ValueError) as exc:
        print(f'\nError: {exc}', file=stderr)
        if debug:
            print(format_exc(chain=False), file=stderr)
        sys_exit(1)
    except KeyboardInterrupt:
        sys_exit(2)


if __name__ == '__main__':
    main()
