#!/usr/bin/python3.6

# This file is a part of sedbgmux, an open source DebugMux client.
# Copyright (c) 2023  Vadim Yanitskiy <fixeria@osmocom.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
import argparse
import math
import sys
import re

from sedbgmux.io import *
from sedbgmux import DbgMuxFrame

# local logger for this module
log = logging.getLogger(__name__)


def dumpio_auto(fname: str, *args, **kw) -> DumpIO:
    ''' Detect dump format automatically '''
    if fname.endswith('.socat.dump'):
        return DumpIOSocat(fname, *args, **kw)
    elif fname.endswith('.dump'):
        return DumpIONative(fname, *args, **kw)
    elif fname.endswith(('.pcap', '.pcapng', '.pcap.gz', '.pcapng.gz')):
        return DumpIOBtPcap(fname, *args, **kw)
    raise DumpIOError('Automatic format detection failed')


class SEDbgMuxDumpApp:
    FORMATS = {
        'auto'      : (dumpio_auto, 'Automatic dump format detection (by filename)'),
        'native'    : (DumpIONative, 'Native binary dump format for this package'),
        'socat'     : (DumpIOSocat, 'ASCII hexdump generated by socat (-x option)'),
        'btpcap'    : (DumpIOBtPcap, 'PCAP file with Bluetooth RFCOMM packets (requires pyshark)'),
    }

    def __init__(self, argv) -> None:
        if argv.verbose > 0:
            logging.root.setLevel(logging.DEBUG)
        if argv.verbose_module is not None:
            logger = logging.getLogger(argv.verbose_module)
            logger.setLevel(logging.DEBUG)

        if argv.command == 'parse':
            self.do_parse(argv)
        elif argv.command == 'convert':
            self.do_convert(argv)
        elif argv.command == 'format-info':
            self.do_format_info(argv)
        elif argv.command == 'list-formats':
            self.do_list_formats()

    def parse_args(self, args: str) -> dict:
        ''' Parse comma-separated key=value arguments '''
        l = re.findall(r'(\w+)(=\w+)?,?', args)
        return {key : val[1:] for (key, val) in l}

    def do_parse(self, argv):
        fmt = self.FORMATS[argv.format][0]
        fmt_args = self.parse_args(argv.format_args)
        dump = fmt(argv.input, readonly=True, **fmt_args)
        num_records: int = 0
        while num_records < argv.num_records:
            try:
                record: dict = dump.read()
                record['nr'] = num_records
                num_records += 1
            except DumpIOEndOfFile:
                break
            self._print_record(record)

    def do_convert(self, argv):
        # input
        fmt = self.FORMATS[argv.input_format][0]
        fmt_args = self.parse_args(argv.input_format_args)
        di = fmt(argv.input, readonly=True, **fmt_args)
        # output
        fmt = self.FORMATS[argv.output_format][0]
        fmt_args = self.parse_args(argv.output_format_args)
        do = fmt(argv.output, readonly=False, **fmt_args)
        num_records: int = 0
        while num_records < argv.num_records:
            try:
                record: dict = di.read()
                do.write(record)
                num_records += 1
            except DumpIOEndOfFile:
                break
        log.info('Converted %u records', num_records)

    def do_format_info(self, argv) -> None:
        fmt = self.FORMATS[argv.format][0]
        print('Description: %s' % fmt.__doc__)
        if fmt == dumpio_auto or not fmt.ARGS:
            return
        print('Arguments:')
        for name, desc in fmt.ARGS.items():
            print('  %s\t\t%s' % (name, desc))

    def do_list_formats(self) -> None:
        for name, desc in self.FORMATS.items():
            print('%s\t\t%s' % (name, desc[1]))

    def _print_record(self, record: dict) -> None:
        frame = DbgMuxFrame.Frame.parse(record['data'])
        if argv.conn_data:  # print only ConnData messages
            if frame['MsgType'] != DbgMuxFrame.MsgType.ConnData:
                return
        # Record information
        print('Record #{nr:04d} @ {timestamp:f} {dir}'.format(**record),
              record['data'].hex())
        # DebugMux frame header
        print('  DebugMux {} frame'.format(record['dir']),
              '(Ns={TxCount:03d}, Nr={RxCount:03d}, fcs=0x{FCS:04x})'.format(**frame),
              frame['MsgType'], frame['MsgData'].hex())
        fcs: int = DbgMuxFrame.fcs_func(record['data'][:-2])
        if fcs != frame['FCS']:
            msg: str = 'Indicated 0x{ind:04x} != calculated 0x{calc:04x}' \
                       .format(ind=frame['FCS'], calc=fcs)
            if not argv.ignore_bad_fcs:
                raise DumpIOFcsError(msg)
            print('  Bad FCS: ' + msg)
        # DebugMux frame payload
        if argv.decode_payload:
            msg = DbgMuxFrame.Msg.parse(frame['MsgData'], MsgType=frame['MsgType'])
            if msg != b'':
                print('  {}'.format(msg))


ap = argparse.ArgumentParser(prog='sedbgmux-dump',
                             description='DebugMux dump management utility')
sp = ap.add_subparsers(dest='command', metavar='command', required=True,
                       help='sub-command help')

ap.add_argument('-v', '--verbose', action='count', default=0,
                help='print debug logging')
ap.add_argument('-vm', '--verbose-module', metavar='MODULE', type=str,
                help='print debug logging for a specific module')

parse = sp.add_parser('parse', help='parse a dump file')
parse.add_argument('input', metavar='INPUT', type=str,
                   help='input file to be parsed')
parse.add_argument('-f', '--format', type=str, default='auto',
                   choices=[*SEDbgMuxDumpApp.FORMATS.keys()],
                   help='input file format (default: %(default)s)')
parse.add_argument('-fa', '--format-args', type=str, default='',
                   help='format specific argument(s) (example: foo=1,bar=2,zoo)')
parse.add_argument('-dp', '--decode-payload', action='store_true',
                   help='decode DebugMux frame contents')
parse.add_argument('-cd', '--conn-data', action='store_true',
                   help='show only ConnData messages')
parse.add_argument('-nr', '--num-records', type=int, default=math.inf,
                   help='number of records to parse (default: all)')
parse.add_argument('--ignore-bad-fcs', action='store_true',
                   help='do not abort parsing on FCS mismatch')

convert = sp.add_parser('convert', help='convert between different formats')
convert.add_argument('input', metavar='INPUT', type=str,
                     help='input file to be converted')
convert.add_argument('output', metavar='OUTPUT', type=str,
                     help='output file')
convert.add_argument('-if', '--input-format', type=str, required=True,
                     choices=[*SEDbgMuxDumpApp.FORMATS.keys()],
                     help='input file format')
convert.add_argument('-ifa', '--input-format-args', type=str, default='',
                     help='input file format argument(s) (example: foo=1,bar=2,zoo)')
convert.add_argument('-of', '--output-format', type=str, required=True,
                     choices=[*SEDbgMuxDumpApp.FORMATS.keys()],
                     help='output file format')
convert.add_argument('-ofa', '--output-format-args', type=str, default='',
                     help='output file format argument(s) (example: foo=1,bar=2,zoo)')
convert.add_argument('-nr', '--num-records', type=int, default=math.inf,
                     help='number of records to convert (default: all)')

format_info = sp.add_parser('format-info', help='show format info')
format_info.add_argument(dest='format', metavar='FORMAT', type=str,
                         choices=[*SEDbgMuxDumpApp.FORMATS.keys()],
                         help='dump file format')

sp.add_parser('list-formats', help='list all supported formats')

logging.basicConfig(
    format='[%(levelname)s] %(filename)s:%(lineno)d %(message)s', level=logging.INFO)

if __name__ == '__main__':
    argv = ap.parse_args()
    app = SEDbgMuxDumpApp(argv)
