#!/data/data/com.termux/files/usr/bin/env python3
"""
PM3 Help 2 List

This script takes the full text help output from the PM3 client and converts it to a list to be used for readline autocomplete.

It is based on pm3_help2JSON.py by
Original Authors / Maintainers:
 - Samuel Windall

This version
 - Iceman

Note:
    This script is used as a helper script to generate the pm3line_vocabulary.h file.
    It need a working proxmark3 client to extract the help text.

    Ie: this script can't be used inside the normal build sequence.
"""

import re
import datetime
import argparse
import logging
from pathlib import Path
import subprocess

##############################################################################
# Script version data: (Please increment when making updates)

APP_NAME = 'PM3Help2List'

VERSION_MAJOR = 1
VERSION_MINOR = 0

##############################################################################
# Main Application Code:


def main():
    """The main function for the script"""
    args = build_arg_parser().parse_args()
    logging_format = '%(message)s'
    if args.debug:
        logging.basicConfig(level=logging.DEBUG, format=logging_format)
    else:
        logging.basicConfig(level=logging.WARN, format=logging_format)
    logging.info(f'{get_version()} starting...')
    help_text = args.input_file.read()
    command_data = parse_all_command_data(help_text)
    # Collect all git-tracked scripts
    paths = [('*.py', Path(__file__).parent),
             ('*.lua', Path(__file__).parent.parent / 'luascripts'),
             ('*.cmd', Path(__file__).parent.parent / 'cmdscripts')]
    try:
        for pattern, scripts_dir in paths:
            result = subprocess.run(['git', 'ls-files', pattern],
                                    cwd=scripts_dir,
                                    capture_output=True,
                                    text=True,
                                    check=True)
            for file_path in result.stdout.strip().split('\n'):
                if file_path:
                    file = Path(file_path)
                    command_data[file.name] = {'command': f'script run {file.name}', 'offline': False}
    except subprocess.CalledProcessError:
        print("Error, need git")

    args.output_file.write("""//-----------------------------------------------------------------------------
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
//
// 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.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
// readline auto complete utilities
//-----------------------------------------------------------------------------

#ifndef PM3LINE_VOCABULARY_H__
#define PM3LINE_VOCABULARY_H__

#ifdef __cplusplus
extern "C" {
#endif

#include <stdbool.h>

typedef struct vocabulary_s {
    bool offline;
    const char *name;
} vocabulary_t;

const static vocabulary_t vocabulary[] = {\n""")

    for key, values in command_data.items():
        offline = 0
        if (values['offline'] == True):
            offline = 1

        cmd = values['command']

        args.output_file.write('    {{ {}, "{}" }},\n'.format(offline, cmd))

    args.output_file.write("""    {0, NULL}\n};

#ifdef __cplusplus
}
#endif

#endif
""")

    logging.info(f'{get_version()} completed!')


def build_arg_parser():
    """Build the argument parser for reading the program arguments"""
    parser = argparse.ArgumentParser()
    parser.add_argument('input_file', type=argparse.FileType('r'), help='Source of full text help from the PM3 client.')
    parser.add_argument('output_file', type=argparse.FileType('w'), help='Destination for list output.')
    parser.add_argument('--version', '-v', action='version', version=get_version(), help='Version data about this app.')
    parser.add_argument('--debug', '-d', action='store_true', help='Log debug messages.')
    return parser


def build_help_regex():
    """The regex uses to parse the full text output of help data from the pm3 client."""
    # Reads the divider followed by the command itself
    re_command = r'-{87}\n(?P<command>.+)\n'

    # Reads if the command is available offline
    re_offline = r'available offline: (?P<offline>yes|no)\n+'

    return re.compile(re_command+re_offline, re.MULTILINE);


def parse_all_command_data(help_text):
    """Turns the full text output of help data from the pm3 client into a list of dictionaries"""
    command_dicts = {}
    # Strip out ANSI escape sequences
    help_text = remove_ansi_escape_codes(help_text)
    # Find all commands in the full text help output
    matches = build_help_regex().finditer(help_text)
    for match in matches:
        # Turn a match into a dictionary with keys for the extracted fields
        command_object = parse_command_data(match)
        # Store this command against its name for easy lookup
        command_dicts[command_object['command']] = command_object
    return command_dicts


def parse_command_data(match):
    """Turns a regex match of a command in the help text and converts it into a dictionary"""
    logging.info('Parsing new command...')
    # Get and clean the command string
    command = remove_extra_whitespace(match.group('command'))
    logging.info(f'    Command: {command}')

    # Get the online status as a boolean. Note: the regex only picks up 'yes' or 'no' so this check is safe.
    offline = (match.group('offline') == 'yes')
    logging.debug(f'    Offline: {offline}')

    # Construct the command dictionary
    command_data = {
        'command': command,
        'offline': offline,
    }
    logging.info('Completed parsing command!')
    return command_data


##############################################################################
# Helper Functions:


def get_version():
    """Get the version string for this script"""
    return f'{APP_NAME} v{VERSION_MAJOR}.{VERSION_MINOR:02}'


def remove_ansi_escape_codes(text):
    """Remove ANSI escape sequences that may be left in the text."""
    re_ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
    return re_ansi_escape.sub('', str(text)).lower()


def remove_extra_whitespace(text):
    """Removes extra whitespace that may be in the text."""
    # Ensure input is a string
    text = str(text)
    # Remove whitespace from the start and end of the text
    text = text.strip()
    # Deduplicate spaces in the string
    text = re.sub(r' +', ' ', text)
    return text


def text_to_oneliner(text):
    """Converts a multi line string into a single line string and removes extra whitespace"""
    # Ensure input is a string
    text = str(text)
    # Replace newlines with spaces
    text = re.sub(r'\n+', ' ', text)
    # Remove the extra whitespace
    text = remove_extra_whitespace(text)
    return text


def text_to_list(text):
    """Converts a multi line string into a list of lines and removes extra whitespace"""
    # Ensure input is a string
    text = str(text)
    # Get all the lines
    lines = text.strip().split('\n')
    # For each line clean up any extra whitespace
    return [remove_extra_whitespace(line) for line in lines]


##############################################################################
# Application entrypoint:

if __name__ == '__main__':
    main()
