# SPDX-License-Identifier: GPL-2.0

import argparse
import code
import copy
import json
import math
import os
import signal
import sys
import tempfile
import time

import _damo_ascii_color
import _damo_fmt_str
import _damo_print
import _damo_records
import _damon
import _damon_args

class Formatter:
    keyword = None
    format_fn = None
    help_msg = None

    def __init__(self, keyword, format_fn, help_msg):
            self.keyword = keyword
            self.format_fn = format_fn
            self.help_msg = help_msg

    def __str__(self):
        return '%s\n%s' % (self.keyword, self.help_msg)

class FormatFnParams:
    fmt = None
    damon_records = None    # _damo_records.DamonRecords
    record = None       # DamonRecord
    snapshot = None
    region = None
    region_idx = None

    def __init__(self, fmt, record, snapshot=None, region=None,
                 region_idx=None, damon_records=None):
        self.fmt = fmt
        self.damon_records = damon_records
        self.record = record
        self.snapshot = snapshot
        self.region = region
        self.region_idx = region_idx

    def get_probe_weights(self):
        damon_ctx = self.damon_records.damon_ctx_of(self.record)
        return [p.weight for p in damon_ctx.probes]

record_formatters = [
        Formatter(
            '<kdamond index>', lambda p: '%s' % p.record.kdamond_idx,
            'index of the record\'s kdamond'),
        Formatter(
            '<context index>', lambda p: '%s' % p.record.context_idx,
            'index of the record\'s DAMON context'),
        Formatter(
            '<scheme index>', lambda p: '%s' % p.record.scheme_idx,
            'index of the record\'s DAMOS scheme'),
        Formatter(
            '<target id>',
            lambda p: '%s' % p.record.target_id,
            'index of the record\'s DAMON target'),
        Formatter(
            '<abs start time>',
            lambda p: _damo_fmt_str.format_time_ns(
                p.record.snapshots[0].start_time, p.fmt.raw_number),
            'absolute time of the start of the record'),
        Formatter(
            '<duration>',
            lambda p: _damo_fmt_str.format_time_ns(
                p.record.snapshots[-1].end_time -
                p.record.snapshots[0].start_time, p.fmt.raw_number),
            'duration of the record'),
        Formatter('<intervals>',
                  lambda p: record_intervals(p.record, p.fmt.raw_number),
                  'monitoring intervals'),
        Formatter('<intervals goal>', lambda p:
                  p.record.intervals.intervals_goal.to_str(p.fmt.raw_number),
                  'monitoring intervals'),
        Formatter('<data source>', lambda p: p.record.data_source,
                  'data source of the record'),
        Formatter('<format strings>', lambda record, fmt: format_strings(fmt),
                  'current format strings')
        ]

snapshot_formatters = [
        Formatter(
            '<probes>', lambda p: format_probes(
                p.damon_records.damon_ctx_of(p.record), p.fmt.raw_number),
            'probes description'),
        Formatter(
            '<total bytes>', lambda p: _damo_fmt_str.format_sz(
                p.snapshot.total_bytes, p.fmt.raw_number),
            'total bytes of regions in the snapshot'),
        Formatter(
            '<duration>', lambda p:
            _damo_fmt_str.format_time_ns(
                p.snapshot.end_time - p.snapshot.start_time,
                p.fmt.raw_number),
            'access monitoring duration for the snapshot'),
        Formatter(
            '<start time>', lambda p: _damo_fmt_str.format_time_ns(
                p.snapshot.start_time - p.record.snapshots[0].start_time,
                p.fmt.raw_number),
            'access monitoring start time for the snapshot, ' \
                    'relative to the record start time'),
        Formatter(
            '<end time>', lambda p: _damo_fmt_str.format_time_ns(
                p.  snapshot.end_time - p.record.snapshots[0].start_time,
                p.fmt.raw_number),
            'access monitoring end time for the snapshot, ' \
                    'relative to the record end time'),
        Formatter(
            '<abs start time>', lambda p: _damo_fmt_str.format_time_ns(
                p.snapshot.start_time, p.fmt.raw_number),
            'absolute access monitoring start time for the snapshot'),
        Formatter(
            '<abs end time>', lambda p: _damo_fmt_str.format_time_ns(
                p.snapshot.end_time, p.fmt.raw_number),
            'absolute access monitoring end time for the snapshot'),
        Formatter(
            '<number of regions>', lambda p:
            _damo_fmt_str.format_nr(len(p.snapshot.regions), p.fmt.raw_number),
            'the number of regions in the snapshot'),
        Formatter(
            '<temperature-sz histogram>', lambda p: temperature_sz_hist_str(
                p.snapshot, p.record, p.get_probe_weights(), p.fmt, False),
            'temperature to total size of the regions histogram'),
        Formatter(
            '<temperature-df-passed-sz histogram>', lambda p:
            temperature_sz_hist_str(
                p.snapshot, p.record, p.get_probe_weights(), p.fmt, True),
            ' '.join(['temperature to total size of DAMOS filters (df)',
                      'passed regions histogram'])),
            Formatter(
                '<recency-sz histogram>', lambda p: recency_hist_str(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, False),
                'idle time to total size of the regions histogram'),
            Formatter(
                '<recency-df-passed-sz histogram>', lambda p: recency_hist_str(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, True),
                ' '.join(['idle time to total size of',
                          'DAMOS filters (df) passed regions histogram'])),
        Formatter(
                '<recency percentiles>', lambda p: recency_percentiles(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, False),
                'per-byte idle time distribution in percentiles'),
        Formatter(
                '<df-passed recency percentiles>', lambda p:
                recency_percentiles(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, True),
                ' '.join(['per-df-passed byte idle time',
                          'distribution in percentiles'])),
        Formatter(
                '<temperature percentiles>', lambda p: temperature_percentiles(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, False),
                'per-byte access temperature distribution in percentiles'),
        Formatter(
                '<df-passed temperature percentiles>', lambda p:
                temperature_percentiles(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt, True),
                ' '.join(['per-df-passed byte access temperature',
                          'distribution in percentiles'])),
        Formatter(
                '<heatmap>', lambda p:
                heatmap_str(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt),
                'heatmap of the snapshot'),
        Formatter(
                '<filters passed heatmap>',
                lambda p: df_passed_heatmap_str(
                    p.snapshot, p.record, p.get_probe_weights(), p.fmt),
                'heatmap of the snapshot for filter-passed regions'),
        Formatter(
                '<filters passed type>', lambda p:
                filters_pass_type_of(p.record),
                'type of <filters passed bytes> memory'),
        Formatter(
                '<filters passed bytes>', lambda p:
                filters_passed_bytes(p.snapshot, p.fmt),
                'bytes of regions that passed DAMOS filters'),
        Formatter(
                '<estimated memory bandwidth>', lambda p:
                estimated_mem_bw(p.snapshot, p.record, p.fmt),
                'estimated memory bandwidth'),
        Formatter(
                '<filters passed estimated memory bandwidth>',
                lambda p: estimated_mem_bw(p.snapshot, p.record, p.fmt,
                                           filter_passed_only=True),
                'estimated memory bandwidth'),
        Formatter(
                '<intervals tuning status>',
                lambda p: intervals_tuning_status(p.snapshot, p.record, p.fmt),
                'intervals tuning status'),
        Formatter(
                '<damos stats>',
                lambda p: damos_stats_str(p.snapshot, p.record, p.fmt),
                'DAMOS stats for the snapshot-retrieval scheme'),
        Formatter(
                '<snapshot intervals>',
                lambda p: snapshot_intervals_str(p.snapshot, p.record, p.fmt),
                'Snapshot intervals status'),
        ]

region_formatters = [
        Formatter(
            '<index>',
            lambda p: _damo_fmt_str.format_nr(p.region_idx, p.fmt.raw_number),
            'index of the region in the regions of the snapshot'),
        Formatter(
            '<start address>',
            lambda p: _damo_fmt_str.format_sz(
                p.region.start, p.fmt.raw_number),
            'start address of the region'),
        Formatter(
            '<end address>',
            lambda p: _damo_fmt_str.format_sz(p.region.end, p.fmt.raw_number),
            'end address of the region'),
        Formatter(
            '<size>', lambda p: _damo_fmt_str.format_sz(
                p.region.size(), p.fmt.raw_number),
            'size of the region'),
        Formatter(
            '<age>', lambda p: _damo_fmt_str.format_time_us(
                p.region.age.usec, p.fmt.raw_number),
            'how long the access pattern of the region has maintained'),
        Formatter(
            '<temperature>',
            lambda p: temperature_str(
                p.region, p.fmt.raw_number, p.get_probe_weights(), p.fmt),
            'access temperature of the region'),
        Formatter(
            '<probe hits>',
            lambda p: ' '.join(['%d' % h for h in p.region.probe_hits]),
            'data attributese probe hit counts'),
        Formatter(
            '<filters passed bytes>',
            lambda p: _damo_fmt_str.format_sz(
                p.region.sz_filter_passed, p.fmt.raw_number)
            if p.region.sz_filter_passed is not None else 'N/A',
            'bytes of the region that passed DAMOS filters'),
        ]

def record_intervals(record, raw_number):
    intervals = record.intervals
    return 'sample %s, aggr %s' % (
            _damo_fmt_str.format_time_us(intervals.sample, raw_number),
            _damo_fmt_str.format_time_us(intervals.aggr, raw_number))

def format_strings(fmt):
    return '\n'.join([
        'record head: "%s"' % fmt.format_record_head,
        'snapshot head: "%s"' % fmt.format_snapshot_head,
        'region: "%s"' % fmt.format_region,
        'snapshot tail: "%s"' % fmt.format_snapshot_tail,
        'record tail: "%s"' % fmt.format_record_tail,
        ])

def format_probes(ctx, raw_number):
    lines = []
    ctx.add_probes_str(lines, raw_number)
    if len(lines) == 0:
        return 'No probe'
    return '\n'.join(lines)

def filters_passed_bytes(snapshot, fmt):
    bytes = 0
    for region in snapshot.regions:
        if region.sz_filter_passed is None:
            return 'N/A'
        bytes += region.sz_filter_passed
    return _damo_fmt_str.format_sz(bytes, fmt.raw_number)

def intervals_tuning_status(snapshot, record, fmt):
    max_samples_per_region = record.intervals.aggr / record.intervals.sample
    max_observation = 0
    observation = 0
    for region in snapshot.regions:
        region.nr_accesses.add_unset_unit(record.intervals)
        max_observation += max_samples_per_region * region.size()
        observation += region.size() * region.nr_accesses.samples
    return '%s (%s / %s) %s %s' % (
            _damo_fmt_str.format_percent(
                observation * 100 / max(max_observation, 1), fmt.raw_number),
            _damo_fmt_str.format_sz(observation, fmt.raw_number),
            _damo_fmt_str.format_sz(max_observation, fmt.raw_number),
            _damo_fmt_str.format_time_us(record.intervals.sample,
                                         fmt.raw_number),
            _damo_fmt_str.format_time_us(record.intervals.aggr,
                                         fmt.raw_number),
            )

def damos_stats_str(snapshot, record, fmt):
    if snapshot.damos_stats is None:
        return 'none'
    return snapshot.damos_stats.to_str(fmt.raw_number)

def snapshot_monitoring_intervals(snapshot, record_intervals):
    '''
    Returns sampling and aggregation intervals for the snapshot, that may
    auto-tuned.
    '''
    if not record_intervals.intervals_goal.enabled():
        return [record_intervals.sample, record_intervals.aggr]

    aggr_sample_ratio = record_intervals.aggr / record_intervals.sample
    if snapshot.sample_interval_us is not None:
        return [snapshot.sample_interval_us,
                snapshot.sample_interval_us * aggr_sample_ratio]

    snapshot_aggr_us = (snapshot.end_time - snapshot.start_time) / 1000
    record_aggr_us = record_intervals.aggr
    # if error is small enough, use simpler number
    if abs(snapshot_aggr_us - record_aggr_us) / record_aggr_us < 0.1:
        return [record_intervals.sample, record_aggr_us]
    snapshot_sample_us = snapshot_aggr_us / aggr_sample_ratio
    return [snapshot_sample_us, snapshot_aggr_us]

def infer_aggr_time_us(snapshot, record):
    return snapshot_monitoring_intervals(snapshot, record.intervals)[1]

def max_access_hz(intervals):
    max_nr_accesses = intervals.aggr / intervals.sample
    aggr_sec = intervals.aggr / 1000000
    return max_nr_accesses / aggr_sec

def snapshot_intervals_str(snapshot, record, fmt):
    intervals = record.intervals
    aggr_sample_ratio = intervals.aggr // intervals.sample
    snapshot_aggr_us = infer_aggr_time_us(snapshot, record)
    snapshot_sample_us = snapshot_aggr_us / aggr_sample_ratio

    return 'sample %s aggr %s (max probe hits %s)' % (
        _damo_fmt_str.format_time_us(snapshot_sample_us, fmt.raw_number),
        _damo_fmt_str.format_time_us(snapshot_aggr_us, fmt.raw_number),
        _damo_fmt_str.format_nr(aggr_sample_ratio, fmt.raw_number))

def estimated_mem_bw(snapshot, record, fmt, filter_passed_only=False):
    access_bytes = 0
    for region in snapshot.regions:
        if filter_passed_only is True:
            bytes = region.sz_filter_passed
        else:
            bytes = region.size()
        access_bytes += bytes * region.nr_accesses.samples
    aggr_interval_sec = infer_aggr_time_us(snapshot, record) / 1000000
    bw_per_sec = access_bytes / aggr_interval_sec
    return '%s per second' % _damo_fmt_str.format_sz(
            bw_per_sec, fmt.raw_number)

def filters_pass_type_of(record):
    ops_filters = [f for f in record.scheme_filters if f.handled_by_ops()]
    if len(ops_filters) == 0:
        return 'n/a'
    text = ', '.join(['%s' % f for f in ops_filters])
    return text

def get_linearscale_hist_ranges(minv, maxv, nr_ranges, cumulate):
    hist_ranges = []
    total_interval = maxv + 1 - minv
    interval = max(math.ceil(total_interval / nr_ranges), 1)
    for i in range(nr_ranges):
        if cumulate:
            start_range = minv
        else:
            start_range = minv + interval * i
        hist_ranges.append([start_range, minv + interval * (i + 1)])
    return hist_ranges

def get_logscale_hist_ranges(minv, maxv, nr_ranges, cumulate):
    initial_interval = max(int((maxv - minv) / 2**nr_ranges), 1)
    # start from 1 second interval
    ranges = [[minv, minv + initial_interval]]
    while ranges[-1][-1] < maxv:
        last_interval = ranges[-1][1] - ranges[-1][0]
        if cumulate:
            start_range = minv
        else:
            start_range = ranges[-1][-1]
        ranges.append([start_range, ranges[-1][-1] + last_interval * 2])
    return ranges

def get_hist_ranges(minv, maxv, nr_ranges, logscale, cumulate):
    if logscale:
        return get_logscale_hist_ranges(minv, maxv, nr_ranges, cumulate)
    return get_linearscale_hist_ranges(minv, maxv, nr_ranges, cumulate)

def histogram_str(hist):
    """
    Format histogram string to show.
    Args:
        hist (list): List of lines of histogram to show.
    Returns:
        str: A string of the histogram that formatted in a user-friendly way.
    """
    yvals = [entry[2] for entry in hist]
    min_yval = min(yvals)
    max_yval = max(yvals)
    max_xval_range_str = max([len(entry[0]) for entry in hist])
    max_yval_str = max([len(entry[1]) for entry in hist])
    max_dots = 20
    yval_dot_interval = max(int((max_yval - min_yval) / max_dots), 1)
    lines = []
    for xrange_str, y_str, yval in hist:
        xrange_str = '%s%s' % (xrange_str,
                               ' ' * (max_xval_range_str - len(xrange_str)))
        y_str = '%s%s' % (y_str,
                           ' ' * (max_yval_str - len(y_str)))

        nr_dots = min(math.ceil((yval - min_yval) / yval_dot_interval),
                      max_dots)
        bar = '|%s%s|' % ('*' * nr_dots, ' ' * (max_dots - nr_dots))
        lines.append('%s %s %s' % (xrange_str, y_str, bar))
    return '\n'.join(lines)

def get_distribution(snapshot, fmt, get_metric_fn, probe_weights, aggr_us,
                     get_count_fn):
    dist = {}
    for region in snapshot.regions:
        metric_val = get_metric_fn(region, fmt, aggr_us, probe_weights)
        if not metric_val in dist:
            dist[metric_val] = 0
        dist[metric_val] += get_count_fn(region, fmt)
    return dist

def get_sorted_ranged_historgram(
        hist, fmt, fmt_x_fn, parse_x_fn, fmt_y_fn, y_aggr_fn):
    metrics = sorted(hist.keys())
    hist2 = []
    if fmt.hist_ranges is not None:
        hist_ranges = []
        for i in range(0, len(fmt.hist_ranges), 2):
            hist_ranges.append(
                    [parse_x_fn(fmt.hist_ranges[i]),
                     parse_x_fn(fmt.hist_ranges[i + 1])])
    else:
        min_metric, max_metric = metrics[0], metrics[-1]
        hist_ranges = get_hist_ranges(
                min_metric, max_metric, 10, fmt.hist_logscale,
                fmt.hist_cumulate)
    raw = fmt.raw_number
    for min_m, max_m in hist_ranges:
        yval = y_aggr_fn(
                [hist[x] for x in metrics if x >= min_m and x < max_m])
        metric_range_str = '[%s, %s)' % (
                fmt_x_fn(min_m, raw), fmt_x_fn(max_m, raw))
        yval_str = fmt_y_fn(yval, raw)
        hist2.append([metric_range_str, yval_str, yval])
    return hist2

def get_sz_region(region, fmt):
    return region.size()

def get_df_passed_sz_region(region, fmt):
    return region.sz_filter_passed

def sz_hist_str(snapshot, fmt, df_passed_sz, get_metric_fn, probe_weights,
                aggr_us, fmt_metric_fn, parse_metric_fn):
    if len(snapshot.regions) == 0:
        return 'no region in snapshot'
    if df_passed_sz is True:
        get_count_fn = get_df_passed_sz_region
    else:
        get_count_fn = get_sz_region
    dist = get_distribution(
            snapshot, fmt, get_metric_fn, probe_weights, aggr_us, get_count_fn)
    hist = get_sorted_ranged_historgram(
            dist, fmt, fmt_metric_fn, parse_metric_fn, _damo_fmt_str.format_sz,
            lambda sz_list: sum(sz_list))

    return histogram_str(hist)

def get_temperature(region, fmt, aggr_us, probe_weights):
    # set size weight zero
    weights = [fmt.temperature_weights[0], fmt.temperature_weights[1], 0]
    return attrs_temperate_of(region, probe_weights, weights)

def temperature_sz_hist_str(
        snapshot, record, probe_weights, fmt, df_passed_sz):
    return sz_hist_str(
            snapshot, fmt, df_passed_sz, get_temperature, probe_weights,
            infer_aggr_time_us(snapshot, record) , _damo_fmt_str.format_nr,
            _damo_fmt_str.text_to_nr)

def get_idle_time(region, fmt, aggr_interval, probe_weights=None):
    for hit in region.probe_hits:
        if hit > 0:
            return 0
    return region.age.usec + aggr_interval

def recency_hist_str(snapshot, record, probe_weights, fmt, df_passed_sz):
    if len(snapshot.regions) == 0:
        return 'no region in snapshot'

    return sz_hist_str(
            snapshot, fmt, df_passed_sz, get_idle_time, probe_weights,
            infer_aggr_time_us(snapshot, record), _damo_fmt_str.format_time_us,
            _damo_fmt_str.text_to_us)

def get_percentiles_to_show(fmt):
    percentiles_range = fmt.percentiles_range
    if percentiles_range is None:
        percentiles_to_show = [0, 1, 25, 50, 75, 99, 100]
    else:
        if len(percentiles_range) <= 3:
            percentiles_to_show = list(range(*percentiles_range))
        else:
            percentiles_to_show = sorted(percentiles_range)
    return percentiles_to_show

def get_percentile_values(snapshot, aggr_us, recency_or_temperature, df_passed,
                          percentiles_to_show, probe_weights, fmt):
    if recency_or_temperature == 'recency':
        get_metric_fn = get_idle_time
    else:
        get_metric_fn = get_temperature
    regions = sorted(snapshot.regions,
                     key=lambda r: get_metric_fn(
                         r, fmt, aggr_us, probe_weights))
    if df_passed is True:
        total_sz = sum(r.sz_filter_passed for r in regions)
    else:
        total_sz = sum(r.size() for r in regions)
    percentile_values = []
    percentile = 0
    for r in regions:
        if df_passed is True:
            percentile += r.sz_filter_passed * 100 / total_sz
        else:
            percentile += r.size() * 100 / total_sz
        while len(percentiles_to_show) > 0:
            if percentile < percentiles_to_show[0]:
                break
            percentile_values.append(
                    [percentiles_to_show[0], get_metric_fn(
                        r, fmt, aggr_us, probe_weights)])
            percentiles_to_show = percentiles_to_show[1:]
        if percentile >= 100.0:
            break
    if 100 in percentiles_to_show:
        percentile_values.append([100, get_metric_fn(
            r, fmt, aggr_us, probe_weights)])
    return percentile_values

def fmt_percentile_str_head(recency_or_temperature, df_passed, fmt):
    if recency_or_temperature == 'recency':
        if fmt.raw_number:
            legend_str = '<idle time (us)>'
        else:
            legend_str = '<idle time>'
    else:
        legend_str = '<temperature (weights: %s)>' % fmt.temperature_weights
    if df_passed is True:
        return '<df-passed percentile> %s' % legend_str
    else:
        return '<percentile> %s' % legend_str

def fmt_percentile_str(percentile_values, fmt, recency_or_temperature,
                       df_passed):
    lines = [fmt_percentile_str_head(recency_or_temperature, df_passed, fmt)]
    if recency_or_temperature == 'recency':
        fmt_fn = _damo_fmt_str.format_time_us
    else:
        fmt_fn = _damo_fmt_str.format_nr
    percentile_txts = []
    idletime_txts = []
    percentile_txts = ['%d' % p for p, v in percentile_values]
    max_percentile_txt_len = max([len(p) for p in percentile_txts])
    idletime_txts = [fmt_fn(v, fmt.raw_number) for p, v in percentile_values]
    max_idletime_txt_len = max([len(i) for i in idletime_txts])

    min_val = percentile_values[0][-1]
    max_val = percentile_values[-1][-1]
    max_dots = 20
    if max_val != min_val:
        val_per_dot = (max_val - min_val) / max_dots
    else:
        val_per_dot = 1

    for idx, percentile_txt in enumerate(percentile_txts):
        percentile_padding = ' ' * (max_percentile_txt_len -
                                    len(percentile_txt))
        idletime_txt = idletime_txts[idx]
        idletime_padding = ' ' * (max_idletime_txt_len - len(idletime_txt))
        bar_length = int((percentile_values[idx][1] - min_val) / val_per_dot)
        bar = '|%s%s|' % ('*' * bar_length, ' ' * (max_dots - bar_length))
        lines.append('%s%s  %s%s  %s' % (
            percentile_padding, percentile_txt,
            idletime_padding, idletime_txt, bar))
    return '\n'.join(lines)

def percentiles_str(
        snapshot, record, probe_weights, fmt, df_passed,
        recency_or_temperature):
    if len(snapshot.regions) == 0:
        return 'no region in snapshot'

    percentiles_to_show = get_percentiles_to_show(fmt)
    percentile_values = get_percentile_values(
            snapshot, infer_aggr_time_us(snapshot, record),
            recency_or_temperature, df_passed, percentiles_to_show,
            probe_weights, fmt)
    return fmt_percentile_str(percentile_values, fmt, recency_or_temperature,
                              df_passed)

def recency_percentiles(snapshot, record, probe_weights, fmt, df_passed):
    return percentiles_str(
            snapshot, record, probe_weights, fmt, df_passed, 'recency')

def temperature_percentiles(snapshot, record, probe_weights, fmt, df_passed):
    return percentiles_str(
            snapshot, record, probe_weights, fmt, df_passed, 'temperature')

def temperature_str(region, raw, probe_weights, fmt):
    temperature = attrs_temperate_of(
            region, probe_weights, fmt.temperature_weights)
    return _damo_fmt_str.format_nr(temperature, raw)

def region_in(start, end, regions):
    for region in regions:
        if region.end <= start:
            continue
        if end <= region.start:
            break
        return True
    return False

def region_after(addr, regions):
    for region in regions:
        if region.start > addr:
            return region
    return None

class HeatPixel:
    start = None
    end = None
    total_heat = None
    temperature = None
    is_void = None

    def __init__(self, start, end, regions, temperature_weights,
                 probe_weights=None):
        self.start = start
        self.end = end

        self.total_heat = 0
        self.is_void = True
        for region in regions:
            if region.end <= start:
                continue
            if end <= region.start:
                break
            self.is_void = False
            self.add_temperature(region, probe_weights, temperature_weights)
        self.temperature = self.total_heat / (self.end - self.start)

    def add_temperature(self, region, probe_weights, weights):
        start = self.start
        end = self.end
        # caller should ensure region intersects with self
        if start <= region.start and region.end <= end:
            # region is in the pixel
            pass
        elif region.start <= start and end <= region.end:
            # pixel is in the region
            region = copy.deepcopy(region)
            region.start = start
            region.end = end
        elif region.start < start:
            # region intersecting right part
            # <region>
            #    <pixel>
            region = copy.deepcopy(region)
            region.start = start
        else:
            # region intersecting left part
            #    <region>
            # <pixel>
            region = copy.deepcopy(region)
            region.end = end
        self.total_heat += attrs_temperate_of(
                region, probe_weights,
                hits_age_sz_weights=weights) * region.size()

def heatmap_pixels_minmax_temps(snapshot, sz_unit, probe_weights, fmt):
    pixels = []
    min_temperature = None
    max_temperature = None
    start = snapshot.regions[0].start
    while start < snapshot.regions[-1].end:
        end = start + sz_unit
        pixels.append(HeatPixel(
            start, end, snapshot.regions, fmt.temperature_weights,
            probe_weights=probe_weights))
        if pixels[-1].is_void is True:
            next_region = region_after(end, snapshot.regions)
            if next_region is None:
                break
            start = next_region.start // sz_unit * sz_unit
            pixels[-1].end = start
        else:
            pixel = pixels[-1]
            if min_temperature is None or pixel.temperature < min_temperature:
                min_temperature = pixel.temperature
            if max_temperature is None or pixel.temperature > max_temperature:
                max_temperature = pixel.temperature
            start = end
    return pixels, min_temperature, max_temperature

def heatmap_dots(
        pixels, min_temperature, max_temperature, fmt, sz_unit, static):
    max_color_level = _damo_ascii_color.max_color_level()
    temperature_unit = (max_temperature - min_temperature) / max_color_level
    # single region?
    if temperature_unit == 0:
        temperature_unit = max_temperature / max_color_level
    if temperature_unit == 0:
        temperature_unit = 1
    dots = []
    for pixel in pixels:
        if pixel.is_void is True:
            nr_dots = (pixel.end - pixel.start) / sz_unit
            if not static and nr_dots > 4:
                dots.append('[...]')
            else:
                dots += ['.'] * int(nr_dots)
            continue
        temp_level = int(
                (pixel.temperature - min_temperature) / temperature_unit)
        dots.append(_damo_ascii_color.colored(
            '%d' % temp_level, fmt.snapshot_heatmap_colorset, temp_level))
    return ''.join(dots)

def heatmap_str(snapshot, record, probe_weights, fmt):
    if len(snapshot.regions) == 0:
        return 'n/a (no region)'
    raw = fmt.raw_number
    # whether to display monitoring gaps as size-relative or not
    static = fmt.snapshot_heatmap_static
    if static:
        total_sz = snapshot.regions[-1].end - snapshot.regions[0].start
    else:
        total_sz = 0
        for region in snapshot.regions:
            total_sz += region.size()
    map_length = fmt.snapshot_heatmap_width
    sz_unit = total_sz / map_length

    pixels, min_temperature, max_temperature = heatmap_pixels_minmax_temps(
            snapshot, sz_unit, probe_weights, fmt)
    dots = heatmap_dots(
            pixels, min_temperature, max_temperature, fmt, sz_unit, static)

    comment = '# min/max temperatures: %s, %s, column size: %s' % (
            _damo_fmt_str.format_nr(min_temperature, raw),
            _damo_fmt_str.format_nr(max_temperature, raw),
            _damo_fmt_str.format_sz(sz_unit, raw))
    return '%s\n%s' % (dots, comment)

def df_passed_heatmap_str(snapshot, record, probe_weights, fmt):
    regions = []
    for region in snapshot.regions:
        cp_region = copy.deepcopy(region)
        if region.sz_filter_passed == 0:
            continue
        regions.append(cp_region)
    return heatmap_str(
            _damo_records.DamonSnapshot(
                snapshot.start_time, snapshot.end_time, regions,
                total_bytes=None),
            record, probe_weights, fmt)

def apply_min_chars(min_chars, field_name, txt):
    # min_chars: [[<field name>, <number of min chars>]...]
    for name, nr in min_chars:
        try:
            nr = int(nr)
        except:
            print('wrong min_chars: %s' % min_chars)

        if name == field_name:
            if len(txt) >= nr:
                return txt
            txt += ' ' * (nr - len(txt))
            return txt
    return txt

def format_output(template, formatters, format_params):
    if template == '':
        return
    for formatter in formatters:
        if template.find(formatter.keyword) == -1:
            continue
        txt = formatter.format_fn(format_params)
        txt = apply_min_chars(
                format_params.fmt.min_chars_for, formatter.keyword, txt)
        template = template.replace(formatter.keyword, txt)
    template = template.replace('\\n', '\n')
    return template

def probe_hits_wsum(region, probe_weights):
    attrs_wsum = 0
    for idx, hit in enumerate(region.probe_hits):
        attrs_wsum += hit * probe_weights[idx]
    return attrs_wsum

def attrs_temperate_of(region, probe_weights, hits_age_sz_weights):
    attrs_wsum = probe_hits_wsum(region, probe_weights)
    hits_weight, age_weight, sz_weight = hits_age_sz_weights
    hits_score = attrs_wsum * hits_weight
    age_score = region.age.usec * age_weight
    sz_score = region.size() * sz_weight
    score = hits_score + age_score + sz_score
    if attrs_wsum <= 0:
        score = -score
    return score

def sorted_regions(regions, sort_fields, sort_dsc_keys, temperature_weights,
                   fmt_fn_params):
    for field in sort_fields:
        dsc = sort_dsc_keys != None and ('all' in sort_dsc_keys or
                                         field in sort_dsc_keys)
        if field == 'address':
            regions = sorted(regions, key=lambda r: r.start, reverse=dsc)
        elif field == 'probe_hits_wsum':
            probe_weights = fmt_fn_params.get_probe_weights()
            regions = sorted(
                    regions, key=lambda r: probe_hits_wsum(r, probe_weights),
                    reverse=dsc)
        elif field == 'age':
            regions = sorted(regions, key=lambda r: r.age.usec, reverse=dsc)
        elif field == 'size':
            regions = sorted(regions, key=lambda r: r.size(), reverse=dsc)
        elif field == 'temperature':
            probe_weights = fmt_fn_params.get_probe_weights()
            regions = sorted(
                    regions, reverse=dsc,
                    key=lambda r: attrs_temperate_of(
                        r, probe_weights, temperature_weights))
    return regions

def fmt_damon_records(fmt, damon_records):
    '''
    damon_records: _damo_records.DamonRecords
    Return list of strings of lines for the formatted output
    '''

    # required for histograms.  Should be removed in future.
    for record in damon_records.record_list:
        for snapshot in record.snapshots:
            for region in snapshot.regions:
                region.age.add_unset_unit(record.intervals)

    outputs = []
    for record in damon_records.record_list:
        record_fmt_params = FormatFnParams(
                fmt=fmt, damon_records=damon_records, record=record)
        outputs.append(
                format_output(
                    fmt.format_record_head, record_formatters,
                    record_fmt_params))
        snapshots = record.snapshots

        for sidx, snapshot in enumerate(snapshots):
            snapshot_fmt_params = FormatFnParams(
                    fmt=fmt, damon_records=damon_records, record=record,
                    snapshot=snapshot)
            outputs.append(
                    format_output(
                        fmt.format_snapshot_head, snapshot_formatters,
                        snapshot_fmt_params))
            for idx, r in enumerate(
                    sorted_regions(
                        snapshot.regions, fmt.sort_regions_by,
                        fmt.sort_regions_dsc, fmt.temperature_weights,
                        snapshot_fmt_params)):
                params = FormatFnParams(
                        fmt=fmt, damon_records=damon_records, record=record,
                        snapshot=snapshot, region=r, region_idx=idx)
                outputs.append(
                        format_output(
                            fmt.format_region, region_formatters, params))
            outputs.append(
                    format_output(
                        fmt.format_snapshot_tail, snapshot_formatters,
                        snapshot_fmt_params))

            if sidx < len(snapshots) - 1 and not fmt.total_sz_only():
                outputs.append('')
        outputs.append(
                format_output(
                    fmt.format_record_tail, record_formatters,
                    record_fmt_params))
    return [o for o in outputs if o is not None]

def fmt_records(fmt, damo_records):
    outputs = []
    for damo_record in damo_records:
        outputs += fmt_damon_records(fmt, damo_record.damon_records)
    return '\n'.join(outputs)

def pr_records_raw_form(records, raw_number):
    lines = []
    for record in records:
        snapshots = record.snapshots
        if len(snapshots) == 0:
            continue

        base_time = snapshots[0].start_time
        lines.append('base_time_absolute: %s' %
                _damo_fmt_str.format_time_ns(base_time, raw_number))
        lines.append('data source: %s\n' % record.data_source)

        for snapshot in snapshots:
            lines.append('monitoring_start:    %16s' %
                    _damo_fmt_str.format_time_ns(
                        snapshot.start_time - base_time, raw_number))
            lines.append('monitoring_end:      %16s' %
                    _damo_fmt_str.format_time_ns(
                        snapshot.end_time - base_time, raw_number))
            lines.append('monitoring_duration: %16s' %
                    _damo_fmt_str.format_time_ns(
                        snapshot.end_time - snapshot.start_time,
                        raw_number))
            lines.append('target_id: %s' % record.target_id)
            lines.append('nr_regions: %s' % len(snapshot.regions))
            lines.append('# %10s %12s  %12s  %5s probe_hits' %
                    ('start_addr', 'end_addr', 'length', 'age'))
            for r in snapshot.regions:
                probe_hits = ' '.join(['%d' % h for h in r.probe_hits])
                lines.append("%012x-%012x (%12s) %5d %s" %
                        (r.start, r.end,
                         _damo_fmt_str.format_sz(r.size(), raw_number),
                         r.age.aggr_intervals if r.age.aggr_intervals != None
                         else -1, probe_hits))
            lines.append('')
    lines.append('')
    _damo_print.pr_with_pager_if_needed('\n'.join(lines))

def pr_records(fmt, damo_records, dont_use_pager):
    records = []
    for damo_record in damo_records:
        records += damo_record.damon_records.record_list

    if fmt.json:
        _damo_print.pr_with_pager_if_needed(
                json.dumps(
                    [r.to_kvpairs(fmt.raw_number) for r in damo_records],
                    indent=4))
    elif fmt.raw:
        pr_records_raw_form(records, fmt.raw_number)
    else:
        to_show = fmt_records(fmt, damo_records)
        if dont_use_pager:
            print(to_show)
        else:
            _damo_print.pr_with_pager_if_needed(fmt_records(fmt, damo_records))

class ReportFormat:
    sort_regions_by = None
    sort_regions_dsc = None
    temperature_weights = None
    dont_merge_regions = None

    hist_logscale = None
    hist_cumulate = None
    hist_ranges = None

    format_record_head = None
    format_record_tail = None
    format_snapshot_head = None
    format_snapshot_tail = None
    format_region = None

    snapshot_heatmap_width = None
    snapshot_heatmap_colorset = None
    snapshot_heatmap_static = None

    min_chars_for = None
    raw_number = None
    json = None
    raw = None


    @classmethod
    def from_args(cls, args):
        self = cls()
        self.sort_regions_by = args.sort_regions_by
        self.sort_regions_dsc = args.sort_regions_dsc
        self.temperature_weights = args.temperature_weights
        self.dont_merge_regions = args.dont_merge_regions
        self.hist_logscale = args.hist_logscale
        self.hist_cumulate = args.hist_cumulate
        self.hist_ranges = args.hist_ranges
        self.percentiles_range = args.percentiles_range
        self.snapshot_heatmap_width = args.snapshot_heatmap_width
        self.snapshot_heatmap_static = args.snapshot_heatmap_static
        self.snapshot_heatmap_colorset = args.snapshot_heatmap_colorset
        self.min_chars_for = args.min_chars_for
        self.raw_number = args.raw_number
        self.json = args.json
        self.raw = args.raw_form
        return self

    def total_sz_only(self):
        return (
                self.format_snapshot_head == '' and
                self.format_region == '' and
                self.format_snapshot_tail == '<total bytes>')

def has_ops_filters(records):
    for record in records:
        for scheme_filter in record.scheme_filters:
            if scheme_filter.handled_by_ops():
                return True
    return False

def set_formats_hist_style(args, fmt, records):
    if args.style == 'temperature-sz-hist':
        legend = '<temperature>'
        hist_keyword = '<temperature-sz histogram>'
        filter_passed_hist_keyword = '<temperature-df-passed-sz histogram>'
    else:
        # args.style == 'recency-sz-hist':
        legend = '<idle time (us)>'
        hist_keyword = '<recency-sz histogram>'
        filter_passed_hist_keyword = '<recency-df-passed-sz histogram>'

    snapshot_head_content = []
    if has_ops_filters(records):
        snapshot_head_content += [
                '# damos filters (df): <filters passed type>',
                '%s <df-passed size>' % legend, filter_passed_hist_keyword, '']
    snapshot_head_content += ['%s <total size>' % legend, hist_keyword]
    fmt.format_snapshot_head = '\n'.join(snapshot_head_content)
    fmt.format_region = ''

def has_multiple_snapshots(records):
    for record in records:
        if len(record.snapshots) > 1:
            return True
    return False

def set_formats_percentiles(args, fmt, records, recency_or_temperature):
    snapshot_head_content = []
    if has_multiple_snapshots(records):
        snapshot_head_content.append(
                'snapshot time: [<start time>, <end time>] (<duration>)')
    if has_ops_filters(records):
        snapshot_head_content += [
                '# damos filters (df): <filters passed type>',
                '# df-passed %s percentiles' % recency_or_temperature]
        if recency_or_temperature == 'recency':
            snapshot_head_content.append('<df-passed recency percentiles>')
        else:
            snapshot_head_content.append('<df-passed temperature percentiles>')
        snapshot_head_content.append('')
    if recency_or_temperature == 'recency':
        snapshot_head_content += [
                '# total recency percentiles',
                '<recency percentiles>',
                ]
    else:
        snapshot_head_content += [
                '# total temperature percentiles',
                '<temperature percentiles>',
                ]
    fmt.format_snapshot_head = '\n'.join(snapshot_head_content)
    fmt.format_region = ''

def set_formats_handle_format_set_arg(fmt, format_arg):
    '''
    Apply --format inputs except 'append' ones, update fmt (ReportFormat).
    Returns updated fmt and error if failed.
    '''
    if format_arg is None:
        return fmt, None
    for format_fields in format_arg:
        if len(format_fields) != 3:
            return None, 'wrong nr of --format option (%s)' % format_fields
        action, target_area, fmt_string = format_fields
        if not action in ['set', 'append']:
            return None, 'wrong --format action (%s)' % format_fields
        if not target_area in ['record_head', 'snapshot_head', 'region',
                               'snapshot_tail', 'record_tail']:
            return None, 'wrong --format target area (%s)' % format_fields
        if action == 'append':
            continue
        if target_area == 'record_head':
            fmt.format_record_head = fmt_string
        elif target_area == 'snapshot_head':
            fmt.format_snapshot_head = fmt_string
        elif target_area == 'region':
            fmt.format_region = fmt_string
        elif target_area == 'snapshot_tail':
            fmt.format_snapshot_tail = fmt_string
        elif target_area == 'record_tail':
            fmt.format_record_tail = fmt_string
    return fmt, None

def set_formats_handle_styles(fmt, args, damo_records):
    '''
    Set output formats that are dependent to the --style
    '''
    if args.style is None:
        return

    records = []
    for damo_record in damo_records:
        records += damo_record.damon_records.record_list

    if args.style in ['temperature-sz-hist', 'recency-sz-hist',
                        'cold-memory-tail']:
        set_formats_hist_style(args, fmt, records)
    elif args.style in ['recency-percentiles', 'idle-time-percentiles']:
        set_formats_percentiles(args, fmt, records, 'recency')
    elif args.style == 'temperature-percentiles':
        set_formats_percentiles(args, fmt, records, 'temperature')

def intervals_goal_enabled(records):
    for record in records:
        if record.intervals is None:
            continue
        if record.intervals.intervals_goal.enabled():
            return True
    return False

def set_formats_record_default(fmt, records):
    if fmt.format_record_head == None:
        if len(records) > 1:
            fmt.format_record_head = \
                    'kdamond <kdamond index> / context <context index>' \
                    ' / scheme <scheme index> / target id <target id> ' \
                    '/ recorded for <duration> from <abs start time>'
        else:
            fmt.format_record_head = ''
    if fmt.format_record_tail is None:
        tail_lines = ['record DAMON intervals: <intervals>']
        if intervals_goal_enabled(records):
            tail_lines.append('# <intervals goal>')
        show_data_source = False
        for record in records:
            if record.data_source == \
                    _damo_records.record_data_source_damon_stat:
                show_data_source = True
                break
        if show_data_source:
            tail_lines.append('# data source: <data source>')

        fmt.format_record_tail = '\n'.join(tail_lines)

def get_min_chars_for(name, min_chars_list):
    for n, val in min_chars_list:
        if n == name:
            return val

def region_legend_line(min_chars_for, ops_filters_installed):
    words = ['#']
    words.append(' ' * (get_min_chars_for('<index>', min_chars_for)))
    words.append('addr')
    words.append(' ' * (get_min_chars_for('<start address>', min_chars_for) -
                 len('addr') + 1))
    words.append('size')
    words.append(' ' * (get_min_chars_for('<size>', min_chars_for) -
                 len('size') + 2))
    if ops_filters_installed is True:
        words.append('df-passed')
        words.append(' ' * (get_min_chars_for(
            '<filters passed bytes>', min_chars_for) - len('df-passed') + 2))

    words.append('age')
    words.append(' ' *
                 (get_min_chars_for('<age>', min_chars_for) - len('age') + 1))

    words.append('probe_hits')
    return ''.join(words)

def set_fmt_snapshot_head_default(fmt, records, args, ops_filters_installed):
    if fmt.format_snapshot_head is not None:
        return
    need_snapshot_time = False
    for record in records:
        if len(record.snapshots) > 1:
            need_snapshot_time = True
            break
    lines = []
    if need_snapshot_time:
        lines.append('snapshot time: [<start time>, <end time>] (<duration>)')
    lines.append('heatmap: <heatmap>')
    lines.append('<probes>')
    lines.append('intervals: <snapshot intervals>')
    if ops_filters_installed:
        lines.append('# damos filters (df): <filters passed type>')
        lines.append('df-pass: <filters passed heatmap>')

    if fmt.format_region is None:
        lines.append(region_legend_line(
            fmt.min_chars_for, ops_filters_installed))

    fmt.format_snapshot_head = '\n'.join(lines)

def set_fmt_snapshot_tail_default(fmt, records, args, ops_filters_installed):
    has_damos_snapshot = False
    for record in records:
        if record.scheme_filters:
            has_damos_snapshot = True
            break
    if fmt.format_snapshot_tail is None:
        fmt.format_snapshot_tail = \
                'memory bw estimate: <estimated memory bandwidth>\n' \
                'total size: <total bytes>'
        if ops_filters_installed:
            fmt.format_snapshot_tail = \
                    'memory bw estimate: <estimated memory bandwidth>  ' \
                    'df-passed: <filters passed estimated memory bandwidth>' \
                    '\ntotal size: <total bytes>  ' \
                    'df-passed <filters passed bytes>'
        # further check if scheme action is not stat
        if has_damos_snapshot is True:
            fmt.format_snapshot_tail += '\nscheme stats\n<damos stats>'

def set_formats_snapshot_default(fmt, records, args, ops_filters_installed):
    set_fmt_snapshot_head_default(fmt, records, args, ops_filters_installed)
    set_fmt_snapshot_tail_default(fmt, records, args, ops_filters_installed)

def set_formats_region_default(fmt, records, args, ops_filters_installed):
    if fmt.format_region is not None:
        return

    default_region_format = '<index> <start address> <size>'
    if ops_filters_installed:
        default_region_format += '  <filters passed bytes>'
    default_region_format += '  <age> <probe hits>'
    fmt.format_region = default_region_format

def set_formats_update_default_formats(fmt, records, args):
    ops_filters_installed = False
    for record in records:
        if len(record.scheme_filters) > 0:
            ops_filters_installed = True
            break

    set_formats_record_default(fmt, records)
    set_formats_snapshot_default(fmt, records, args, ops_filters_installed)
    set_formats_region_default(fmt, records, args, ops_filters_installed)

def set_formats_handle_format_append_arg(fmt, format_args):
    if format_args is None:
        return
    for action, target_area, fmt_string in format_args:
        if action != 'append':
            continue
        if target_area == 'record_head':
            fmt.format_record_head += fmt_string
        elif target_area == 'snapshot_head':
            fmt.format_snapshot_head += fmt_string
        elif target_area == 'region':
            fmt.format_region += fmt_string
        elif target_area == 'snapshot_tail':
            fmt.format_snapshot_tail += fmt_string
        elif target_area == 'record_tail':
            fmt.format_record_tail += fmt_string

def set_formats(args, damo_records):
    # Setup basic formats as user specified, including
    # --format_{record_head,record_tail,snapshot_head,snapshot_tail,region}
    # options.  --format is not yet applied.
    fmt = ReportFormat.from_args(args)

    fmt, err = set_formats_handle_format_set_arg(fmt, args.format)
    if err is not None:
        return fmt, err
    set_formats_handle_styles(fmt, args, damo_records)

    if args.total_sz_only:
        fmt.format_snapshot_head = ''
        fmt.format_region = ''
        fmt.format_snapshot_tail = '<total bytes>'

    records = []
    for damo_record in damo_records:
        records += damo_record.damon_records.record_list

    set_formats_update_default_formats(fmt, records, args)
    set_formats_handle_format_append_arg(fmt, args.format)
    return fmt, None

def handle_ls_keywords(args):
    if args.ls_record_format_keywords:
        print("use 'damo help access_format_options record_format_keywords' " \
                "instead")
        return True
    if args.ls_snapshot_format_keywords:
        print("use 'damo help access_format_options " \
                "snapshot_format_keywords' instead")
        return True
    if args.ls_region_format_keywords:
        print("use 'damo help access_format_options " \
                "region_format_keywords' instead")
        return True
    return False

signal_received = False

def sighandler(signum, frame):
    global signal_received
    print('\nsignal %s received' % signum)
    signal_received = True

def recursive_del(dirpath):
    for filename in os.listdir(dirpath):
        filepath = os.path.join(dirpath, filename)
        if os.path.isdir(filepath):
            recursive_del(filepath)
        else:
            os.remove(filepath)
    os.rmdir(dirpath)

def handle_exec(cmd, damo_records):
    if cmd == 'interpreter':
        code.interact(
                local=locals(),
                banner="damo records are available as 'damo_records'",
                exitmsg='Exit the session.')
        return None
    cmd_fields = cmd.split()
    if not os.path.isfile(cmd_fields[0]):
        return 'first token is not file'
    script_dir_path = tempfile.mkdtemp(prefix='damo_report_attrs_script_')
    script_module_name = 'damo_report_attrs_script'
    script_file_name = '%s.py' % script_module_name
    with open(cmd_fields[0], 'r') as f:
        with open(os.path.join(script_dir_path, script_file_name), 'w') as f2:
            f2.write(f.read())
    sys.path.append(script_dir_path)
    import damo_report_attrs_script
    damo_report_attrs_script.main(damo_records, cmd_fields)
    recursive_del(script_dir_path)
    return None

def parse_input_option(input_args_list):
    '''
    Parse --input option.  Returns input files, schemes tried regions, and
    error string.
    '''
    if input_args_list is None:
        return [], [], None

    tried_regions_of_list = []
    input_files = []
    for input_args in input_args_list:
        if input_args[0] == 'tried_regions_of':
            if len(input_args) != 4:
                return None, None, \
                        'wrong number of args for tried_regions_of (%s)' % \
                        input_args
            try:
                tried_regions_of_list.append([int(x) for x in input_args[1:]])
            except:
                return None, None, \
                        'tried_regions_of should get only ints (%s)' % \
                        input_args
        elif len(input_args) == 1 and os.path.isfile(input_args[0]):
            input_files.append(input_args[0])
        else:
            return None, None, 'unsupported input (%s)' % input_args
    return input_files, tried_regions_of_list, None

def read_and_show(args):
    input_files, tried_regions_of_list, err = parse_input_option(args.input)
    if err is not None:
        print('--input handling failed (%s)' % err)
        exit(1)
    if len(tried_regions_of_list) == 0:
        tried_regions_of_list = None

    if len(input_files) == 0:
        _damon.ensure_root_and_initialized(args)
        if _damon.any_kdamond_running() is False:
            if os.path.exists('damon.data'):
                input_files = ['damon.data']

    record_filter, err = _damo_records.args_to_filter(args)
    if err != None:
        print(err)
        exit(1)

    dfilters, err = _damon_args.damos_options_to_filters(
            args.snapshot_damos_filter)
    if err is not None:
        print('wrong --snapshot_damos_filter (%s)' % err)
        exit(1)

    if args.repeat is None:
        repeat_delay = 0
        repeat_count = 1
    elif len(args.repeat) == 0:
        repeat_delay = 1
        repeat_count = -1
    elif len(args.repeat) == 2:
        repeat_delay = _damo_fmt_str.text_to_sec(args.repeat[0])
        repeat_count = _damo_fmt_str.text_to_nr(args.repeat[1])
    else:
        print('--repeat receives only zero or two arguments')
        exit(1)

    signal.signal(signal.SIGINT, sighandler)
    global signal_received

    read_show_count = 0
    while read_show_count < repeat_count or repeat_count == -1:
        if signal_received is True:
            break
        damo_records, err = _damo_records.get_damo_records(
                    tried_regions_of=tried_regions_of_list,
                    record_files=input_files,
                    snapshot_damos_filters=dfilters,
                    record_filter=record_filter,
                    total_sz_only=args.total_sz_only,
                    dont_merge_regions=args.dont_merge_regions)
        if err != None:
            print(err)
            exit(1)
        if signal_received is True:
            break
        zero_damon_records = True
        for damo_record in damo_records:
            if len(damo_record.damon_records.record_list) > 0:
                zero_damon_records = False
                break
        if zero_damon_records and len(input_files) > 0 and repeat_count == 1:
            print('No record in %s' % input_files)

        if args.exec:
            err = handle_exec(args.exec, damo_records)
            if err is not None:
                print('format_script handling fail (%s)' % err)
                exit(1)
            time.sleep(repeat_delay)
            read_show_count += 1
            continue

        fmt, err = set_formats(args, damo_records)
        if err is not None:
            print('format setting failed (%s)' % err)
            exit(1)

        try:
            pr_records(fmt, damo_records,
                       dont_use_pager = args.repeat is not None)
        except BrokenPipeError:
            # maybe user piped to 'less' like pager, and quit from it
            pass

        time.sleep(repeat_delay)
        read_show_count += 1

def main(args):
    handled = handle_ls_keywords(args)
    if handled:
        return

    read_and_show(args)

def add_fmt_args(parser, hide_help=False):
    # how to show, in simple selection
    parser.add_argument(
            '--style', choices=['detailed',
                                'temperature-sz-hist',
                                # cold-memory-tail is just another name of
                                # recency-sz-hist
                                'recency-sz-hist', 'cold-memory-tail',
                                # idle-time-percentiles is just another name of
                                # recency-percentiles
                                'recency-percentiles', 'idle-time-percentiles',
                                'temperature-percentiles',
                                ],
            default='detailed',
            help='output format selection among pre-configures ones')
    # how to show, in highly tunable way
    parser.add_argument(
            '--percentiles_range', nargs='+', type=int, metavar='<number>',
            help='Percentiles to show.  If <=3 arguments are given, '
            'ranges are made with Python range() function. '
            'If >3 argumenta are given, show percentiles of each argument.')
    parser.add_argument(
            '--sort_regions_by', nargs='+',
            choices=['address', 'probe_hits_wsum', 'age', 'size',
                     'temperature'],
            default=['address'],
            help='fields to sort regions by'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--sort_regions_dsc',
            choices=['address', 'probe_hits_wsum', 'age', 'size',
                     'temperature', 'all'],
            nargs='+',
            help='sort regions in descending order for the given keys'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--temperature_weights', type=int, metavar='<int>', nargs=3,
            default=[100, 100, 0],
            help=' '.join(
                ['region temperature weights for probe hits weighted sum, age',
                'and size']) if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--hist_logscale', action='store_true',
                        help='draw histograms in logscale'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--hist_ranges', nargs='+',
                        help='histogram range values'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--hist_cumulate', action='store_true',
                        help='draw histogram in cumulative way')

    fmt_flexible_metavar='<set|append> ' \
            '<record_head|snapshot_head|region|snapshot_tail|record_tail> ' \
            '<format string>'

    parser.add_argument('--format', metavar=fmt_flexible_metavar,
                        action='append', nargs='+',
                        help='customize report content format')

    # for snapshot heatmap
    parser.add_argument(
            '--snapshot_heatmap_width', metavar='<number>', default=80,
            type=int,
            help='width of snapshot heatmap'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--snapshot_heatmap_colorset', default='gray',
            choices=_damo_ascii_color.colorsets.keys(),
            help='snapshot heatmap colorset'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--snapshot_heatmap_static', action='store_true',
            help='draw snapshot heatmap as static'
            if not hide_help else argparse.SUPPRESS)

    # misc control
    parser.add_argument(
            '--min_chars_for', nargs=2,
            metavar=('<keyword>', '<number>'), action='append',
            default=[['<index>', 3],
                     ['<start address>', 12],['<end address>', 11],
                     ['<size>', 11],
                     ['<age>', 13],
                     ['<filters passed bytes>', 11],
                     ['<filters passed type>', 10],
                     ],
            help='minimum character for each keyword of the format'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--total_sz_only', action='store_true',
            help='print only total size of the regions for each snapshot')
    parser.add_argument('--dont_merge_regions', action='store_true',
            help='don\'t merge contiguous regions of same access pattern'
            if not hide_help else argparse.SUPPRESS)

    parser.add_argument('--raw_number', action='store_true',
            help='use machine-friendly raw numbers')
    parser.add_argument('--json', action='store_true',
            help='print in json format')
    parser.add_argument('--raw_form', action='store_true',
                        help='print in raw format'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--ls_record_format_keywords', action='store_true',
                        # help='list available record format keywords'
                        help=argparse.SUPPRESS)
    parser.add_argument('--ls_snapshot_format_keywords', action='store_true',
                        # help='list available snapshot format keywords'
                        help=argparse.SUPPRESS)
    parser.add_argument('--ls_region_format_keywords', action='store_true',
                        # help='list available region format keywords'
                        help=argparse.SUPPRESS)
    if hide_help:
        if parser.epilog is None:
            parser.epilog = ''
        else:
            parser.epilog += ' '
        parser.epilog += ' '.join([
            "More options for customized format are available.",
            "Do 'damo help access_format_options -h' for those options.",
            ])

def set_argparser(parser):
    parser.description = 'Show DAMON-monitored data attributes information.'
    parser.epilog=' '.join([
        'If --input is not provided and DAMON is running, capture snapshot.',
        'If --input is not provided, DAMON is not running,',
        'and "damon.data" file exists, use "damon.data" as --input.'])

    _damon_args.add_interface_debug_args(parser)

    # what to show
    _damo_records.set_filter_argparser(parser, hide_help=True)

    parser.add_argument(
            '--input', '-i', metavar='<file or special words>', nargs='+',
            action='append',
            help=' '.join([
                'Source of the attributes information to show.',
                'Can be file or',
                '"tried_regions_of <kdamond idx> <context idx> <scheme idx>"'
                ]))
    _damo_records.set_snapshot_damos_filters_option(parser)
    add_fmt_args(parser, hide_help=True)

    parser.add_argument('--exec', metavar='<command or \'interpreter\'>',
                        help='execute python code with the records')
    parser.add_argument(
            '--repeat', nargs='*', metavar=('<delay>', '<count>'),
            help='repeat <count> times with <delay> time interval')
