r"""jc - JSON Convert `/usr/bin/time` command output parser

Output from `/usr/bin/time` is sent to `STDERR`, so the `-o` option can be
used to redirect the output to a file that can be read by `jc`.

Alternatively, the output from `/usr/bin/time` can be redirected to `STDOUT`
so `jc` can receive it.

> Note: `/usr/bin/time` is similar but different from the Bash builtin
> `time` command.

Usage (cli):

    $ /usr/bin/time -o timefile.out sleep 2; cat timefile.out | \\
      jc --time -p

Usage (module):

    import jc
    result = jc.parse('time', time_command_output)

Schema:

Source: https://www.freebsd.org/cgi/man.cgi?query=getrusage,
https://man7.org/linux/man-pages/man1/time.1.html

    {
      "real_time":                          float,
      "user_time":                          float,
      "system_time":                        float,
      "elapsed_time":                       string,
      "elapsed_time_hours":                 integer,
      "elapsed_time_minutes":               integer,
      "elapsed_time_seconds":               integer,
      "elapsed_time_centiseconds":          integer,
      "elapsed_time_total_seconds":         float,
      "cpu_percent":                        integer,   # null if ?
      "average_shared_text_size":           integer,
      "average_unshared_data_size":         integer,
      "average_unshared_stack_size":        integer,
      "average_shared_memory_size":         integer,
      "maximum_resident_set_size":          integer,
      "block_input_operations":             integer,   # [0]
      "block_output_operations":            integer,   # [1]
      "major_pagefaults":                   integer,
      "minor_pagefaults":                   integer,
      "swaps":                              integer,
      "page_reclaims":                      integer,
      "page_faults":                        integer,
      "messages_sent":                      integer,
      "messages_received":                  integer,
      "signals_received":                   integer,
      "voluntary_context_switches":         integer,
      "involuntary_context_switches":       integer
      "command_being_timed":                string,
      "average_stack_size":                 integer,
      "average_total_size":                 integer,
      "average_resident_set_size":          integer,
      "signals_delivered":                  integer,
      "page_size":                          integer,
      "exit_status":                        integer
    }

    [0] aka File system inputs
    [1] aka File system outputs

Examples:

    $ /usr/bin/time --verbose -o timefile.out sleep 2; cat timefile.out | \\
      jc --time -p
    {
      "command_being_timed": "sleep 2",
      "user_time": 0.0,
      "system_time": 0.0,
      "cpu_percent": 0,
      "elapsed_time": "0:02.00",
      "average_shared_text_size": 0,
      "average_unshared_data_size": 0,
      "average_stack_size": 0,
      "average_total_size": 0,
      "maximum_resident_set_size": 2084,
      "average_resident_set_size": 0,
      "major_pagefaults": 0,
      "minor_pagefaults": 72,
      "voluntary_context_switches": 2,
      "involuntary_context_switches": 1,
      "swaps": 0,
      "block_input_operations": 0,
      "block_output_operations": 0,
      "messages_sent": 0,
      "messages_received": 0,
      "signals_delivered": 0,
      "page_size": 4096,
      "exit_status": 0,
      "elapsed_time_hours": 0,
      "elapsed_time_minutes": 0,
      "elapsed_time_seconds": 2,
      "elapsed_time_centiseconds": 50,
      "elapsed_time_total_seconds": 2.5
    }

    $ /usr/bin/time --verbose -o timefile.out sleep 2; cat timefile.out | \\
      jc --time -p -r
    {
      "command_being_timed": "\"sleep 2\"",
      "user_time": "0.00",
      "system_time": "0.00",
      "cpu_percent": "0",
      "elapsed_time": "0:02.00",
      "average_shared_text_size": "0",
      "average_unshared_data_size": "0",
      "average_stack_size": "0",
      "average_total_size": "0",
      "maximum_resident_set_size": "2084",
      "average_resident_set_size": "0",
      "major_pagefaults": "0",
      "minor_pagefaults": "72",
      "voluntary_context_switches": "2",
      "involuntary_context_switches": "0",
      "swaps": "0",
      "block_input_operations": "0",
      "block_output_operations": "0",
      "messages_sent": "0",
      "messages_received": "0",
      "signals_delivered": "0",
      "page_size": "4096",
      "exit_status": "0"
    }
"""
import jc.utils


class info():
    """Provides parser metadata (version, author, etc.)"""
    version = '1.5'
    description = '`/usr/bin/time` command parser'
    author = 'Kelly Brazil'
    author_email = 'kellyjonbrazil@gmail.com'
    compatible = ['linux', 'darwin', 'cygwin', 'aix', 'freebsd']
    tags = ['command']


__version__ = info.version


def _process(proc_data):
    """
    Final processing to conform to the schema.

    Parameters:

        proc_data:   (List of Dictionaries) raw structured data to process

    Returns:

        Dictionary. Structured data to conform to the schema.
    """
    int_list = {
        'cpu_percent', 'average_shared_text_size', 'average_unshared_data_size',
        'average_unshared_stack_size', 'average_shared_memory_size', 'maximum_resident_set_size',
        'block_input_operations', 'block_output_operations', 'major_pagefaults', 'minor_pagefaults',
        'swaps', 'page_reclaims', 'page_faults', 'messages_sent', 'messages_received',
        'signals_received', 'voluntary_context_switches', 'involuntary_context_switches',
        'average_stack_size', 'average_total_size', 'average_resident_set_size',
        'signals_delivered', 'page_size', 'exit_status'
    }

    float_list = {'real_time', 'user_time', 'system_time'}

    if 'command_being_timed' in proc_data:
        proc_data['command_being_timed'] = proc_data['command_being_timed'][1:-1]

    if 'elapsed_time' in proc_data:
        *hours, minutes, ss = proc_data['elapsed_time'].split(':')
        if '.' in ss:
            seconds, centiseconds = ss.split('.')
        else:
            seconds = ss
            centiseconds = '0'

        if hours:
            proc_data['elapsed_time_hours'] = jc.utils.convert_to_int(hours[0])
        else:
            proc_data['elapsed_time_hours'] = 0

        proc_data['elapsed_time_minutes'] = jc.utils.convert_to_int(minutes)
        proc_data['elapsed_time_seconds'] = jc.utils.convert_to_int(seconds)
        proc_data['elapsed_time_centiseconds'] = jc.utils.convert_to_int(centiseconds)
        proc_data['elapsed_time_total_seconds'] = (proc_data['elapsed_time_hours'] * 3600) + \
                                                  (proc_data['elapsed_time_minutes'] * 60) + \
                                                  (proc_data['elapsed_time_seconds']) + \
                                                  (proc_data['elapsed_time_centiseconds'] / 100)

    # convert ints and floats
    for key in proc_data:
        if key in int_list:
            proc_data[key] = jc.utils.convert_to_int(proc_data[key])

        if key in float_list:
            proc_data[key] = jc.utils.convert_to_float(proc_data[key])

    return proc_data


def parse(data, raw=False, quiet=False):
    """
    Main text parsing function

    Parameters:

        data:        (string)  text data to parse
        raw:         (boolean) unprocessed output if True
        quiet:       (boolean) suppress warning messages if True

    Returns:

        Dictionary. Raw or processed structured data.
    """
    jc.utils.compatibility(__name__, info.compatible, quiet)
    jc.utils.input_type_check(data)

    raw_output = {}

    if jc.utils.has_data(data):
        time_type = None     # linux_brief, linux_long, bsd_brief, bsd_long, posix

        for line in filter(None, data.splitlines()):
            # linux default style:
            # 0.00user 0.00system 0:03.00elapsed 0%CPU (0avgtext+0avgdata 2148maxresident)k
            # 0inputs+0outputs (0major+71minor)pagefaults 0swaps
            if time_type != 'linux_brief' and 'elapsed' in line:
                time_type = 'linux_brief'

            # BSD/OSX default style:
            #         0.00 real         0.00 user         0.00 sys
            elif time_type != 'bsd_brief' and ' user ' in line:
                time_type = 'bsd_brief'

            elif time_type != 'linux_long' and 'Command' in line:
                time_type = 'linux_long'

            elif time_type != 'bsd_long' and 'maximum resident set size' in line:
                time_type = 'bsd_long'

            # POSIX compliant output:
            # real         0.00
            # user         0.00
            # sys          0.00
            elif time_type != 'posix' and line.startswith('real '):
                time_type = 'posix'

            # start parsing lines
            if time_type == 'linux_brief':
                if 'elapsed' in line:
                    line_num = 0
                else:
                    line_num = 1

                new_line = line.replace('+', ' ').replace('(', ' ').replace(')', ' ')\
                               .replace('user', ' ').replace('system', ' ').replace('elapsed', ' ')\
                               .replace('elapsed', ' ').replace('%CPU', ' ').replace('avgtext', ' ')\
                               .replace('avgdata', ' ').replace('maxresident', ' ').replace('inputs', ' ')\
                               .replace('outputs', ' ').replace('major', ' ').replace('minor', ' ')\
                               .replace('pagefaults', ' ').replace('swaps', ' ').replace('k', ' ')

                linux_brief_line = new_line.split()

                if line_num == 0:
                    raw_output['user_time'] = linux_brief_line[0]
                    raw_output['system_time'] = linux_brief_line[1]
                    raw_output['elapsed_time'] = linux_brief_line[2]
                    raw_output['cpu_percent'] = None if linux_brief_line[3] == '?' else linux_brief_line[3]
                    raw_output['average_shared_text'] = linux_brief_line[4]
                    raw_output['average_unshared_data_size'] = linux_brief_line[5]
                    raw_output['maximum_resident_set_size'] = linux_brief_line[6]
                else:
                    raw_output['block_input_operations'] = linux_brief_line[0]
                    raw_output['block_output_operations'] = linux_brief_line[1]
                    raw_output['major_pagefaults'] = linux_brief_line[2]
                    raw_output['minor_pagefaults'] = linux_brief_line[3]
                    raw_output['swaps'] = linux_brief_line[4]

            if time_type == 'posix':
                posix_line = line.split()
                if 'real' in line:
                    raw_output['real_time'] = posix_line[1]
                if 'user' in line:
                    raw_output['user_time'] = posix_line[1]
                if 'sys' in line:
                    raw_output['system_time'] = posix_line[1]

            if time_type == 'bsd_brief':
                bsd_brief_line = line.split()
                raw_output['real_time'] = bsd_brief_line[0]
                raw_output['user_time'] = bsd_brief_line[2]
                raw_output['system_time'] = bsd_brief_line[4]

            if time_type == 'bsd_long':
                bsd_long_line = line.split(maxsplit=1)
                key = bsd_long_line[1].replace(' ', '_')

                # fixup some key names
                if key == 'average_shared_text':
                    key = 'average_shared_text_size'

                value = bsd_long_line[0]
                raw_output[key] = value

            if time_type == 'linux_long':
                # cleanup key names: (h:mm:ss or m:ss)
                # line = line.replace('h:mm:ss', '', 1).replace('m:ss', '')
                linux_long_line = line.split(': ', maxsplit=1)
                key = linux_long_line[0].strip().lower().replace(' ', '_').replace('(', '').replace(')', '')\
                                        .replace('/', '_').replace(':', '_').replace('_kbytes', '')\
                                        .replace('_seconds', '').replace('socket_', '').replace('_bytes', '')

                # fixup some key names
                if key == 'file_system_inputs':
                    key = 'block_input_operations'

                if key == 'file_system_outputs':
                    key = 'block_output_operations'

                if key == 'percent_of_cpu_this_job_got':
                    key = 'cpu_percent'

                if key == 'elapsed_wall_clock_time_h_mm_ss_or_m_ss':
                    key = 'elapsed_time'

                if key == 'major_requiring_i_o_page_faults':
                    key = 'major_pagefaults'

                if key == 'minor_reclaiming_a_frame_page_faults':
                    key = 'minor_pagefaults'

                value = linux_long_line[1].replace('%', '')
                raw_output[key] = value

    if raw:
        return raw_output
    else:
        return _process(raw_output)
