# SPDX-License-Identifier: GPL-2.0

"""
Command line arguments handling
"""

import argparse
import json
import os
import subprocess
import sys

# for non-python-default modules
try:
    import yaml
except ModuleNotFoundError:
    # do nothing.  The yaml using functions should handle the exception
    # properly.
    pass

import _damo_subproc
import _damo_sysinfo
import _damon
import damo_report_pa_layout

# Kdamonds construction from command line arguments

def range_overlap_or_contig(range1, range2):
    s1, e1 = range1
    s2, e2 = range2
    if e1 == s2 or e2 == s1:
        return True
    if e1 <= s2:
        return False
    if e2 <= s1:
        return False
    return True

def merge_ranges(ranges):
    ranges.sort(key=lambda r: r[0])
    merged_ranges = []
    for start, end in ranges:
        if len(merged_ranges) > 0 and range_overlap_or_contig(
                merged_ranges[-1], [start, end]):
            merged_ranges[-1] = [min(merged_ranges[-1][0], start),
                                 max(merged_ranges[-1][1], end)]
        else:
            merged_ranges.append([start, end])
    return merged_ranges

def init_regions_for(args_regions, args_ops, args_numa_node):
    init_regions = []
    if args_regions:
        for region in args_regions.split():
            addrs = region.split('-')
            try:
                if len(addrs) != 2:
                    raise Exception ('two addresses not given')
                region = _damon.DamonRegion(addrs[0], addrs[1])
                if region.start >= region.end:
                    raise Exception('start >= end')
                if init_regions and init_regions[-1].end > region.start:
                    raise Exception('regions overlap')
            except Exception as e:
                return None, 'Wrong \'--regions\' argument (%s)' % e
            init_regions.append(region)

    if args_ops == 'paddr' and not init_regions:
        if args_numa_node != None:
            init_regions, err = damo_report_pa_layout.numa_addr_ranges(
                    args_numa_node)
            if err != None:
                return None, err
            init_regions = merge_ranges(init_regions)
        else:
            init_regions = [damo_report_pa_layout.default_paddr_region()]
        try:
            init_regions = [_damon.DamonRegion(r[0], r[1])
                    for r in init_regions]
        except Exception as e:
            return None, 'Wrong \'--regions\' argument (%s)' % e

    return init_regions, None

def override_vals(to_override, new_vals):
    if new_vals is None:
        return
    for idx, new_val in enumerate(new_vals):
        if new_val is not None:
            to_override[idx] = new_val

def damon_intervals_for(args_intervals, args_sample, args_aggr, args_updr,
                        args_intervals_goal, use_default_autotune=False):
    intervals = ['5ms', '100ms', '1s']
    override_vals(intervals, args_intervals)
    override_vals(intervals, [args_sample, args_aggr, args_updr])

    if use_default_autotune is True:
        args_intervals_goal = ['4%', '3', '5ms', '10s']
    elif args_intervals_goal is None:
        args_intervals_goal = ['0%', '0', '0us', '0us']

    intervals_goal = _damon.DamonIntervalsGoal(*args_intervals_goal)

    return _damon.DamonIntervals(*intervals, intervals_goal)

def damon_nr_regions_range_for(args_range, args_minr, args_maxr):
    nr_range = ['10', '1000']
    override_vals(nr_range, args_range)
    override_vals(nr_range, [args_minr, args_maxr])
    return _damon.DamonNrRegionsRange(*nr_range)

def schemes_option_to_damos(schemes):
    if os.path.isfile(schemes):
        with open(schemes, 'r') as f:
            schemes = f.read()

    try:
        kvpairs = json.loads(schemes)
        return [_damon.Damos.from_kvpairs(kv) for kv in kvpairs], None
    except Exception as json_err:
        return None, '%s' % json_err

def damos_filter_with_optional_args(ftype, fmatching, allow, optional_words):
    # return filter, error, and number of consumed words
    if ftype == 'memcg':
        if len(optional_words) < 1:
            return None, 'no memcg path is given', 0
        memcg_path = optional_words[0]
        return _damon.DamosFilter(ftype, fmatching, memcg_path=memcg_path,
                                  allow=allow), None, 1
    if ftype == 'addr':
        if len(optional_words) < 2:
            return None, 'no address range is given', 0
        try:
            addr_range = _damon.DamonRegion(optional_words[0], optional_words[1])
        except Exception as e:
            return None, 'wrong addr range (%s, %s)' % (optional_words, e), 0
        return _damon.DamosFilter(ftype, fmatching, allow=allow,
                                  address_range=addr_range), None, 2
    elif ftype == 'target':
        if len(optional_words) < 1:
            return None, 'no target argument', 0
        try:
            return _damon.DamosFilter(
                    ftype, fmatching, allow=allow,
                    damon_target_idx=optional_words[0]), None, 1
        except Exception as e:
            return None, 'target filter creation failed (%s, %s)' % (
                    optional_words[0], e), 0
    elif ftype == 'hugepage_size':
        if len(optional_words) < 2:
            return None, 'no range for hugepage sizes is given', 0
        hugepage_size = [optional_words[0], optional_words[1]]
        return _damon.DamosFilter(ftype, fmatching, allow=allow,
                                  hugepage_size=hugepage_size), None, 2

    return None, 'unsupported filter type', 0

def damos_options_to_filter_v2(words):
    '''
    Returns filter, error, and consumed number of words
    '''
    if len(words) < 2:
        return None, 'wrong number of words: %s' % words, 0
    if not words[0] in ['allow', 'reject', 'deny', 'block']:
        return None, 'unsupported allow-reject idntifier: %s' % words[0], 0
    allow = words[0] == 'allow'
    nr_consumed_words = 1
    word = words[nr_consumed_words]
    if word == 'none':
        fmatching = False
        nr_consumed_words += 1
    else:
        fmatching = True
    ftype = words[nr_consumed_words]
    nr_consumed_words += 1

    if ftype in ['anon', 'active', 'young', 'unmapped']:
        filter = _damon.DamosFilter(ftype, fmatching, allow=allow)
        return filter, None, nr_consumed_words
    else:
        filter, err, nr_words = damos_filter_with_optional_args(
                ftype, fmatching, allow, words[nr_consumed_words:])
        if err is not None:
            return None, 'handling %s fail (%s)' % (words, err), 0
        return filter, None, nr_consumed_words + nr_words

def damos_options_to_filters_v2(words):
    filters = []
    while len(words) > 0:
        filter, err, nr_consumed_words = damos_options_to_filter_v2(words)
        if err is not None:
            return None, err
        filters.append(filter)
        words = words[nr_consumed_words:]
    return filters, None

def convert_damos_filter_v1_to_v2(filter_args):
    if len(filter_args) < 2:
        return None, '<2 filter argument'
    filter_type = filter_args[0]
    matching = filter_args[1] == 'matching'
    optional_args = filter_args[2:]
    if len(optional_args) == 0:
        allow_reject = 'reject'
    else:
        if optional_args[-1] in ['allow', 'pass', 'reject', 'deny', 'block']:
            if optional_args[-1] in ['allow', 'pass']:
                allow_reject = 'allow'
            else:
                allow_reject = 'reject'
            optional_args = optional_args[:-1]
        else:
            allow_reject = 'reject'
    v2_args = [allow_reject]
    if matching is False:
        v2_args.append('none')
    return v2_args + [filter_type] + optional_args, None

def damos_options_to_filters(filters_args):
    filters = []
    if filters_args == None:
        return filters, None

    full_words = []
    for fields in filters_args:
        full_words += fields
    filters, v2_err = damos_options_to_filters_v2(full_words)
    if v2_err is None:
        return filters, None

    full_words = []
    for filter_args in filters_args:
        converted_filter_args, err = convert_damos_filter_v1_to_v2(filter_args)
        if err is not None:
            return None, 'converting format fail (%s, %s)' % (filter_args, err)
        full_words += converted_filter_args

    filters, v2_err = damos_options_to_filters_v2(full_words)
    return filters, v2_err

def damos_quotas_cons_arg(cmd_args):
    time_ms = 0
    sz_bytes = 0
    reset_interval_ms = 'max'
    weights = ['0 %', '0 %', '0 %']

    nr_cmd_args = len(cmd_args)
    if nr_cmd_args >= 1:
        time_ms = cmd_args[0]
    if nr_cmd_args >= 2:
        sz_bytes = cmd_args[1]
    if nr_cmd_args >= 3:
        reset_interval_ms = cmd_args[2]
    if nr_cmd_args >= 4:
        weights[0] = cmd_args[3]
    if nr_cmd_args >= 5:
        weights[1] = cmd_args[4]
    if nr_cmd_args >= 6:
        weights[2] = cmd_args[5]

    return [time_ms, sz_bytes, reset_interval_ms, weights]

def damos_options_to_quota_goal(garg):
    # garg is the user inputs
    # garg should be <metric> <target value> [<optional>...]
    # for user_input, one optional argument for "current value" is given.
    # for node_mem[cg]_{used,free}_bp, one optional argument for node id is
    # given.
    # for node_memcg_{used,free}_bp, one more optional argument for memcg path
    # is given.
    if not len(garg) in [2, 3, 4]:
        return None, 'Wrong --damos_quota_goal (%s)' % garg
    metric, target_value, optionals = garg[0], garg[1], garg[2:]
    current_value = 0
    nid = None
    memcg_path = None
    if metric in [_damon.qgoal_node_mem_used_bp,
                  _damon.qgoal_node_mem_free_bp]:
        if len(optionals) != 1:
            return None, 'nid is not given or something else is given'
        nid = optionals[0]
    elif metric in [_damon.qgoal_node_memcg_used_bp,
                    _damon.qgoal_node_memcg_free_bp]:
        if len(optionals) != 2:
            return None, 'nid and memcg_path required'
        nid = optionals[0]
        memcg_path = optionals[1]
    elif metric == 'user_input':
        if len(optionals) != 1:
            return None, 'current value is not given or something else is given'
        current_value = optionals[0]
    try:
        return _damon.DamosQuotaGoal(
                metric=metric, target_value=target_value,
                current_value=current_value,
                nid=nid, memcg_path=memcg_path), None
    except Exception as e:
        return None, 'DamosQuotaGoal creation fail (%s, %s)' % (garg, e)

def damos_options_to_quotas(quotas, fail_charge_ratio, goals, goal_tuner):
    gargs = goals
    goals = []
    for garg in gargs:
        goal, err = damos_options_to_quota_goal(garg)
        if err is not None:
            return None, 'quota goals parsing fail (%s)' % err
        goals.append(goal)

    qargs = quotas
    if len(qargs) > 6:
        return None, 'Wrong --damos_quotas (%s, >6 parameters)' % qargs
    num, denom = fail_charge_ratio
    try:
        quotas = _damon.DamosQuotas(
                *damos_quotas_cons_arg(qargs),
                fail_charge_num=num, fail_charge_denom=denom,
                goals=goals, goal_tuner=goal_tuner)
    except Exception as e:
        return None, 'Wrong --damos_quotas (%s, %s)' % (qargs, e)
    return quotas, None

def damos_options_to_scheme(
        sz_region, access_rate, age, action,
        apply_interval, quotas, quota_fail_charge_ratio, goals, goal_tuner,
        wmarks, target_nid, filters, dests, max_nr_snapshots):
    if quotas != None:
        quotas, err = damos_options_to_quotas(
                quotas, quota_fail_charge_ratio, goals, goal_tuner)
        if err is not None:
            return None, err

    if wmarks != None:
        wargs = wmarks
        try:
            wmarks = _damon.DamosWatermarks(wargs[0], wargs[1], wargs[2],
                    wargs[3], wargs[4])
        except Exception as e:
            return None, 'Wrong --damos_wmarks (%s, %s)' % (wargs, e)

    filters, err = damos_options_to_filters(filters)
    if err != None:
        return None, err

    if max_nr_snapshots is None:
        stats = _damon.DamosStats()
    else:
        stats = _damon.DamosStats(max_nr_snapshots=max_nr_snapshots)

    try:
        return _damon.Damos(
                access_pattern=_damon.DamosAccessPattern(sz_region,
                    access_rate, _damon.unit_percent, age, _damon.unit_usec),
                action=action, target_nid=target_nid,
                dests=dests, apply_interval_us=apply_interval, quotas=quotas,
                watermarks=wmarks, filters=filters, stats=stats), None
    except Exception as e:
        return None, 'Wrong \'--damos_*\' argument (%s)' % e

def set_args_damos_quotas(args):
    # DAMOS quotas can be set using
    # 1) only --damos_quotas (specify time, space, interval, weights at once),
    #    or
    # 2) --damos_quota_interval, --damos_quota_time, --damos_quota_space,
    #    and --damos_quota_weights (specify the components one by one).
    # Convert quotas specified in 2) way into --damos_quotas for easier
    # handling.
    if not args.damos_quota_interval:
        return
    for i, interval in enumerate(args.damos_quota_interval):
        t, s = 0, 0
        if i < len(args.damos_quota_time):
            t = args.damos_quota_time[i]
        if i < len(args.damos_quota_space):
            s = args.damos_quota_space[i]
        if i < len(args.damos_quota_weights):
            w1, w2, w3 = args.damos_quota_weights[i]
        else:
            w1, w2, w3 = 1, 1, 1
        args.damos_quotas.append([t, s, interval, w1, w2, w3])

def verify_set_damos_args_len(args):
    # return an error string if given args is wrong in terms of lengths.
    nr_schemes = len(args.damos_action)

    if len(args.damos_sz_region) > nr_schemes:
        return 'too much --damos_sz_region'
    if len(args.damos_access_rate) > nr_schemes:
        return 'too much --damos_access_rate'
    if len(args.damos_age) > nr_schemes:
        return 'too much --damos_age'
    if len(args.damos_apply_interval) > nr_schemes:
        return 'too much --damos_apply_interval'
    if len(args.damos_quotas) > nr_schemes:
        return 'too much --damos_quotas'

    if len(args.damos_quota_goal) > 0 and nr_schemes > 1:
        if len(args.damos_nr_quota_goals) == 0:
            return '--damos_nr_quota_goals required'
    if nr_schemes == 1 and args.damos_nr_quota_goals == []:
        args.damos_nr_quota_goals = [len(args.damos_quota_goal)]
    if sum(args.damos_nr_quota_goals) != len(args.damos_quota_goal):
        return 'wrong --damos_nr_quota_goals'

    if len(args.damos_wmarks) > nr_schemes:
        return 'too much --damos_wmarks'

    # for multiple schemes, number of filters per scheme is required
    if len(args.damos_filter) > 0 and nr_schemes > 1:
        if len(args.damos_nr_filters) == 0:
            return '--damos_nr_filters required'
    if nr_schemes == 1 and args.damos_nr_filters == []:
        args.damos_nr_filters = [len(args.damos_filter)]
    if sum(args.damos_nr_filters) != len(args.damos_filter):
        return 'wrong --damos_nr_filters'

    if len(args.damos_max_nr_snapshots) > nr_schemes:
        return 'too much --damos_max_nr_snapshots'
    return None

def fillup_default_damos_args(args):
    nr_schemes = len(args.damos_action)

    args.damos_sz_region += [['min', 'max']] * (
            nr_schemes - len(args.damos_sz_region))
    args.damos_access_rate += [['min', 'max']] * (
            nr_schemes - len(args.damos_access_rate))
    args.damos_age += [['min', 'max']] * (nr_schemes - len(args.damos_age))
    args.damos_apply_interval += [0] * (
            nr_schemes - len(args.damos_apply_interval))
    args.damos_quotas += [None] * (nr_schemes - len(args.damos_quotas))
    args.damos_quota_goal_tuner += [None] * (
            nr_schemes - len(args.damos_quota_goal_tuner))
    args.damos_quota_fail_charge_ratio += [[0, 0]] * (
            nr_schemes - len(args.damos_quota_fail_charge_ratio))
    args.damos_wmarks += [None] * (nr_schemes - len(args.damos_wmarks))
    args.damos_max_nr_snapshots += [None] * (
            nr_schemes - len(args.damos_max_nr_snapshots))

def damos_options_to_schemes(args):
    set_args_damos_quotas(args)
    err = verify_set_damos_args_len(args)
    if err is not None:
        return [], err
    fillup_default_damos_args(args)
    nr_schemes = len(args.damos_action)

    schemes = []

    for i in range(nr_schemes):
        action = args.damos_action[i][0]
        target_nid = None
        dests = []
        if _damon.is_damos_migrate_action(action):
            try:
                if len(args.damos_action[i]) == 2:
                    target_nid = int(args.damos_action[i][1])
                elif len(args.damos_action[i]) > 2:
                    dests = []
                    for j in range(1, len(args.damos_action[i]), 2):
                        dests.append(_damon.DamosDest(
                            args.damos_action[i][j],
                            args.damos_action[i][j + 1]))
            except:
                return [], '"%s" action require a numeric target_nid ' \
                        'or dests arguments.' \
                            % args.damos_action[i][0]
        elif len(args.damos_action[i]) > 1:
            return [], 'Wrong number of --damos_action arguments.' % action
        args.damos_action[i] = action

        qgoals = []
        if args.damos_quota_goal:
            goal_start_index = sum(args.damos_nr_quota_goals[:i])
            goal_end_index = goal_start_index + args.damos_nr_quota_goals[i]
            qgoals = args.damos_quota_goal[goal_start_index:goal_end_index]

        filters = []
        if args.damos_filter:
            filter_start_index = sum(args.damos_nr_filters[:i])
            filter_end_index = filter_start_index + args.damos_nr_filters[i]
            filters = args.damos_filter[filter_start_index:filter_end_index]
        scheme, err = damos_options_to_scheme(
                args.damos_sz_region[i],
                args.damos_access_rate[i], args.damos_age[i],
                args.damos_action[i], args.damos_apply_interval[i],
                args.damos_quotas[i], args.damos_quota_fail_charge_ratio[i],
                qgoals, args.damos_quota_goal_tuner[i], args.damos_wmarks[i],
                target_nid, filters, dests, args.damos_max_nr_snapshots[i])
        if err != None:
            return [], err
        schemes.append(scheme)
    return schemes, None

def damos_for(args):
    if args.damos_action:
        schemes, err = damos_options_to_schemes(args)
        if err != None:
            return None, err
        return schemes, None

    if not 'schemes' in args or args.schemes == None:
        return [], None

    schemes, err = schemes_option_to_damos(args.schemes)
    if err:
        return None, 'failed damo schemes arguments parsing (%s)' % err
    return schemes, None

def damon_target_for(args, idx, ops):
    init_regions, err = init_regions_for(
            args.regions[idx], ops, args.numa_node[idx])
    if err:
        return None, err

    try:
        obsolete = False
        if args.obsolete_targets is not None and idx in args.obsolete_targets:
            obsolete = True
        target = _damon.DamonTarget(
                args.target_pid[idx] if _damon.target_has_pid(ops) else None,
                init_regions, obsolete=obsolete)
    except Exception as e:
        return None, 'Wrong \'--target_pid\' argument (%s)' % e
    return target, None

def sample_control_to_ops_attrs_args(args, idx):
    sample_primitives = args.sample_primitives[idx]
    if sample_primitives is None:
        return None
    ops_use_reports = args.exp_ops_use_reports[idx]
    if sample_primitives is not None and ops_use_reports is not None:
        return '--sample_primitives and --exp_ops_use_reports used together'
    if sample_primitives == ['page_table']:
        args.exp_ops_use_reports[idx] = 'N'
    elif sample_primitives == ['page_fault']:
        args.exp_ops_use_reports[idx] = 'Y'
    else:
        return '--sample_primitives of %s is not supported for now' % \
                sample_primitives

def build_sample_control_ops_attrs(args, idx):
    '''
    Returns DamonSampleControl, OpsAttrs, and an error
    '''
    err = sample_control_to_ops_attrs_args(args, idx)
    if err is not None:
        return None, None, err
    if args.exp_ops_use_reports[idx] is not None:
        use_reports = args.exp_ops_use_reports[idx]
    else:
        use_reports = False
    if args.exp_ops_write_only[idx] is not None:
        write_only = args.exp_ops_write_only[idx]
    else:
        write_only = False
    if args.exp_ops_cpus[idx] is not None:
        cpus = args.exp_ops_cpus[idx]
    else:
        cpus = 'all'
    if args.exp_ops_tids[idx] is not None:
        tids = args.exp_ops_tids[idx]
    else:
        tids = ''
    use_sample_control = _damo_sysinfo.damon_feature_available(
            'sysfs/damon_sample_control')
    if use_sample_control is False:
        try:
            ops_attrs = _damon.OpsAttrs(
                    use_reports=use_reports, write_only=write_only, cpus=cpus,
                    tids=tids)
        except Exception as e:
            return None, None, 'invalid ops_attrs arguments (%s)' % e
        return None, ops_attrs, None

    if use_reports:
        page_table, page_fault = False, True
    else:
        page_table, page_fault = True, False
    primitives_enabled = _damon.DamonPrimitivesEnabled(
            page_table=page_table, page_fault=page_fault)
    sample_filters = []
    if write_only:
        sample_filters.append(_damon.DamonSampleFilter(
            filter_type=_damon.damon_filter_type_write, matching=True,
            allow=True))
    if cpus != 'all':
        sample_filters.append(_damon.DamonSampleFilter(
            filter_type=_damon.damon_filter_type_cpumask, matching=True,
            allow=True, cpumask=cpus))
    if tids != '':
        sample_filters.append(_damon.DamonSampleFilter(
            filter_type=_damon.damon_filter_type_threads, matching=True,
            allow=True, tid_arr=tids))
    sample_control = _damon.DamonSampleControl(
            primitives_enabled=primitives_enabled,
            sample_filters=sample_filters)
    return sample_control, None, None

def damon_ctx_for(args, idx):
    if args.ops[idx] is None:
        if args.target_pid[idx] is None:
            args.ops[idx] = 'paddr'
        else:
            args.ops[idx] = 'vaddr'

    try:
        intervals = damon_intervals_for(
                args.monitoring_intervals[idx], args.sample[idx],
                args.aggr[idx], args.updr[idx],
                args.monitoring_intervals_goal[idx],
                args.monitoring_intervals_autotune)
    except Exception as e:
        return None, 'invalid intervals arguments (%s)' % e
    try:
        nr_regions = damon_nr_regions_range_for(
                args.monitoring_nr_regions_range[idx],
                args.minr[idx], args.maxr[idx])
    except Exception as e:
        return None, 'invalid nr_regions arguments (%s)' % e
    ops = args.ops[idx]
    sample_control, ops_attrs, err = build_sample_control_ops_attrs(args, idx)
    if err is not None:
        return None, 'exp_ops_* handling fail (%s)' % err
    pause = False
    if args.pause_ctx is not None and idx in args.pause_ctx:
        pause = True
    addr_unit = args.damon_addr_unit[idx]
    if addr_unit is None:
        addr_unit = 1

    try:
        ctx = _damon.DamonCtx(
                ops, None, intervals, nr_regions, schemes=[],
                sample_control=sample_control, ops_attrs=ops_attrs,
                pause=pause, addr_unit=addr_unit)
        return ctx, None
    except Exception as e:
        return None, 'Creating context from arguments failed (%s)' % e

def get_nr_ctxs(args):
    candidates = []
    for v in [args.ops, args.nr_probes, args.sample, args.aggr, args.updr,
              args.minr, args.maxr, args.monitoring_intervals,
              args.monitoring_intervals_goal,
              args.monitoring_nr_regions_range]:
        if v is not None:
            candidates.append(len(v))
    if args.nr_targets is not None:
        candidates.append(len(args.nr_targets))
    if args.nr_schemes is not None:
        candidates.append(len(args.nr_schemes))
    if len(candidates) == 0:
        return 1
    return max(candidates)

def get_nr_targets(args):
    candidates = []
    for v in [args.target_pid, args.regions, args.numa_node]:
        if v is not None:
            candidates.append(len(v))
    candidates.append(get_nr_ctxs(args))
    return max(candidates)

def fillup_none_ctx_args(args):
    nr_ctxs = get_nr_ctxs(args)
    for attr_name in [
            'ops', 'exp_ops_use_reports', 'exp_ops_write_only', 'exp_ops_cpus',
            'exp_ops_tids', 'sample', 'aggr', 'updr', 'minr', 'maxr',
            'monitoring_intervals', 'monitoring_intervals_goal',
            'monitoring_nr_regions_range', 'sample_primitives',
            'damon_addr_unit']:
        attr_val = getattr(args, attr_name)
        if attr_val is None:
            setattr(args, attr_name, [None] * nr_ctxs)
        elif len(attr_val) < nr_ctxs:
            setattr(args, attr_name,
                    attr_val + [None] * (nr_ctxs - len(attr_val)))

def fillup_none_target_args(args):
    nr_targets = get_nr_targets(args)
    for attr_name in ['target_pid', 'regions', 'numa_node']:
        attr_val = getattr(args, attr_name)
        if attr_val is None:
            setattr(args, attr_name, [None] * nr_targets)
        elif len(attr_val) < nr_targets:
            print(attr_name, attr_val)
            setattr(args, attr_name,
                    attr_val + [None] * (nr_targets - len(attr_val)))

def valid_obsolete_targets_args(obsolete_targets, nr_targets):
    if obsolete_targets is None:
        return True
    for target_idx in obsolete_targets:
        if target_idx >= nr_targets:
            return False
    return True

def gen_assign_targets(ctxs, args):
    nr_targets = get_nr_targets(args)
    if not valid_obsolete_targets_args(args.obsolete_targets, nr_targets):
        return 'Invalid --obsolete_targets'
    targets = []
    if args.nr_targets is None:
        if len(ctxs) != 1:
            return '--nr_targets is required for multiple contexts'
        args.nr_targets = [nr_targets]
    for idx in range(nr_targets):
        for i in range(len(args.nr_targets)):
            if idx < sum(args.nr_targets[:i + 1]):
                ops = args.ops[i]
                break
        target, err = damon_target_for(args, idx, ops)
        if err is not None:
            return err
        targets.append(target)
    if sum(args.nr_targets) != len(targets):
        return '--nr_targets and number of targets mismatch (%d != %d)' % (
                sum(args.nr_targets), len(targets))
    if len(args.nr_targets) != len(ctxs):
        return '--nr_targets and number of ctxs mismatch (%d != %d)' % (
                len(args.nr_targets), len(ctxs))
    ctx_idx = 0
    target_idx = 0
    for nr in args.nr_targets:
        ctxs[ctx_idx].targets = targets[target_idx:target_idx + nr]
        ctx_idx += 1
        target_idx += nr
    return None

def gen_assign_schemes(ctxs, args):
    schemes, err = damos_for(args)
    if err is not None:
        return err
    if args.nr_schemes is None:
        if len(ctxs) != 1 and len(schemes) > 0:
            return '--nr_schemes is required for multiple contexts'
        args.nr_schemes = [len(schemes)]
        args.nr_schemes += [0] * (len(ctxs) - 1)
    if sum(args.nr_schemes) != len(schemes):
        return '--nr_schemes and number of schemes mismatch (%d != %d)' % (
                sum(args.nr_schemes), len(schemes))
    if len(args.nr_schemes) != len(ctxs):
        return '--nr_schemes and number of ctxs mismatch (%d != %d)' % (
                len(args.nr_schemes), len(ctxs))
    ctx_idx = 0
    scheme_idx = 0
    for nr in args.nr_schemes:
        ctxs[ctx_idx].schemes = schemes[scheme_idx:scheme_idx + nr]
        ctx_idx += 1
        scheme_idx += nr
    return None

def probe_preps_for(preps_args):
    preps = []
    for prep_action in preps_args:
        try:
            prep = _damon.DamonPrep(prep_action=prep_action)
        except Exception as e:
            return None, 'prep creation fail (%s)' % e
        preps.append(prep)
    return preps, None

def probe_filter_for(filter_arg_fields):
    fields = filter_arg_fields
    if len(fields) < 2:
        return None, 'expected >=2 fields but %d' % len(fields)
    if not fields[0] in ['allow', 'reject']:
        return None, 'expected allow|reject but %s' % fields[0]
    allow = fields[0] == 'allow'
    fields = fields[1:]
    matching = True
    if fields[0] == 'non':
        matching = False
        fields = fields[1:]
    if len(fields) < 1:
        return None, 'filter type is not given'
    filter_type = fields[0]
    fields = fields[1:]
    path = None
    if filter_type == 'memcg':
        if len(fields) < 1:
            return None, 'memcg path is not given'
        path = fields[0]
    try:
        filter = _damon.DamonFilter(filter_type=filter_type, matching=matching,
                                    allow=allow, path=path)
    except Exception as e:
        return None, 'filter creation fail (%s)' % e
    return filter, None

def probe_filters_for(filters_args):
    filters = []
    for filter_args in filters_args:
        filter, err = probe_filter_for(filter_args)
        if err is not None:
            return None, err
        filters.append(filter)
    return filters, None

def probes_for(args):
    preps, err = probe_preps_for(args.probe_prep)
    if err is not None:
        return None, err
    if args.nr_probe_preps is None:
        args.nr_probe_preps = [len(preps)]
    if sum(args.nr_probe_preps) != len(preps):
        return None, '--nr_probe_preps mismatches --probe_prep (%d != %d)' % (
                sum(args.nr_probe_preps), len(preps))

    filters, err = probe_filters_for(args.probe_filter)
    if err is not None:
        return None, err

    if len(preps) == 0 and len(filters) == 0:
        return [], None

    probes = []
    filter_idx = 0

    if args.nr_probe_filters is None:
        args.nr_probe_filters = [len(args.probe_filter)]
    if sum(args.nr_probe_filters) != len(filters):
        return None, \
                '--nr_probe_filters mismatches --probe_filter (%d != %d)' % (
                        sum(args.nr_probe_filters), len(filters))

    probe_weights = []
    for probe_weights_list in args.probe_weight:
        probe_weights += probe_weights_list
    if len(probe_weights) > len(args.nr_probe_filters):
        return None, \
                '--probe_weight mismatches --nr_probe_filters (%d > %d)' \
                % (len(probe_weights), len(args.nr_probe_filters))
    probe_weights += [0] * (
            len(args.nr_probe_filters) - len(probe_weights))

    nr_probes = len(args.nr_probe_filters)
    for i in range(nr_probes - len(preps)):
        args.nr_probe_preps.append(0)
    prep_idx = 0

    for probe_idx, nr in enumerate(args.nr_probe_filters):
        preps_for_probe = preps[
                prep_idx:prep_idx + args.nr_probe_preps[probe_idx]]
        filters_for_probe = filters[filter_idx:filter_idx + nr]
        weight = probe_weights[len(probes)]
        try:
            probe = _damon.DamonProbe(
                    preps=preps_for_probe, filters=filters_for_probe,
                    weight=weight)
        except Exception as e:
            return None, 'probe creation fail (%s)' % e
        probes.append(probe)
        filter_idx += nr
    return probes, None

def gen_assign_probes(ctxs, args):
    probes, err = probes_for(args)
    if err is not None:
        return err
    if args.nr_probes is None:
        if len(ctxs) != 1 and len(probes) > 0:
            return '--nr_probes is required'
        args.nr_probes = [len(probes)]
        args.nr_probes += [0] * (len(ctxs) - 1)
    if sum(args.nr_probes) != len(probes):
        return '--nr_probes mismatches number of probes (%d != %d)' % (
                sum(args.nr_probes), len(probes))
    if len(args.nr_probes) != len(ctxs):
        return '--nr_probes mismatches number of contexts (%d != %d)' % (
                len(args.nr_probes), len(ctxs))
    ctx_idx = 0
    probe_idx = 0
    for nr in args.nr_probes:
        ctxs[ctx_idx].probes = probes[probe_idx:probe_idx + nr]
        ctx_idx += 1
        probe_idx += nr
    return None

def damon_ctxs_for(args):
    fillup_none_ctx_args(args)
    fillup_none_target_args(args)
    ctxs = []
    for idx in range(get_nr_ctxs(args)):
        ctx, err = damon_ctx_for(args, idx)
        if err is not None:
            return None, err
        ctxs.append(ctx)

    err = gen_assign_targets(ctxs, args)
    if err is not None:
        return None, err

    err = gen_assign_schemes(ctxs, args)
    if err is not None:
        return None, err

    err = gen_assign_probes(ctxs, args)
    if err is not None:
        return None, err

    return ctxs, err

def kdamonds_from_json_arg(arg):
    try:
        if os.path.isfile(arg):
            with open(arg, 'r') as f:
                kdamonds_str = f.read()
        else:
            kdamonds_str = arg
        kdamonds_kvpairs = json.loads(kdamonds_str)['kdamonds']
        return [_damon.Kdamond.from_kvpairs(kvp)
                for kvp in kdamonds_kvpairs], None
    except Exception as e:
        return None, e

def kdamonds_from_yaml_arg(arg):
    try:
        assert os.path.isfile(arg)
        with open(arg, 'r') as f:
            kdamonds_str = f.read()
        kdamonds_kvpairs = yaml.safe_load(kdamonds_str)['kdamonds']
        return [_damon.Kdamond.from_kvpairs(kvp)
                for kvp in kdamonds_kvpairs], None
    except Exception as e:
        return None, e

target_type_explicit = 'explicit'
target_type_cmd = 'cmd'
target_type_pid = 'pid'
target_type_unknown = None

def deduced_target_type(target):
    if target in ['vaddr', 'paddr', 'fvaddr']:
        return target_type_explicit
    if _damo_subproc.avail_cmd(target.split()[0]):
        return target_type_cmd
    else:
        pass
    try:
        _ = int(target)
        return target_type_pid
    except:
        pass
    return target_type_unknown

def warn_option_override(option_name):
    print('warning: %s is overridden by <deducible target>' % option_name)

def deduce_target_update_args(args):
    'deducible target supports only single context/single kdamond'
    args.self_started_target = False
    target_type = deduced_target_type(args.deducible_target)
    if target_type == target_type_unknown:
        return 'target \'%s\' is not supported' % args.deducible_target
    if target_type == target_type_explicit and args.deducible_target == 'paddr':
        if not args.ops in [['paddr'], None]:
            warn_option_override('--ops')
        args.ops = ['paddr']
        if args.target_pid != None:
            warn_option_override('--target_pid')
        args.target_pid = None
        return None
    if target_type == target_type_cmd:
        p = subprocess.Popen(args.deducible_target, shell=True,
                executable='/bin/bash')
        pid = p.pid
        args.self_started_target = True
    elif target_type == target_type_pid:
        pid = int(args.deducible_target)
    if args.target_pid != None:
        print('warning: --target_pid will be ignored')
    args.target_pid = [pid]
    if not args.regions:
        if not args.ops in [['vaddr'], None]:
            warn_option_override('--ops')
        args.ops = ['vaddr']
    if args.regions:
        if not args.ops in [['fvaddr'], None]:
            print('warning: override --ops by <deducible target> and --regions')
        args.ops = ['fvaddr']

def update_args_defaults(args):
    # having default=[] makes complicated global shared states.  Instantiate
    # default values here.
    for arg_name in [
            'probe_prep', 'probe_filter', 'probe_weight', 'damos_action',
            'damos_sz_region', 'damos_access_rate', 'damos_age',
            'damos_apply_interval', 'damos_quotas', 'damos_quota_interval',
            'damos_quota_time', 'damos_quota_space', 'damos_quota_weights',
            'damos_quota_fail_charge_ratio', 'damos_quota_goal',
            'damos_quota_goal_tuner', 'damos_nr_quota_goals', 'damos_filter',
            'damos_nr_filters', 'damos_wmarks', 'damos_max_nr_snapshots',
            ]:
        if getattr(args, arg_name) is None:
            setattr(args, arg_name, [])

def warn_for(arg, feature):
    if _damo_sysinfo.damon_feature_available(feature):
        return
    sys.stderr.write('%s is passed but %s DAMON feature is not available\n' %
                     (arg, feature))

def warn_for_features(arg, features):
    for feature in features:
        if _damo_sysinfo.damon_feature_available(feature):
            return
    sys.stderr.write(
            '%s is passed but any of %s DAMON features is not available\n' %
            (arg, ', '.join(features)))

def warn_unsupported_damon_features_for(args):
    # damon/next
    if args.sample_primitives is not None:
        warn_for('--sample_primitives', 'sysfs/damon_sample_control')

    if len(args.probe_prep) > 0:
        warn_for('--probe_prep', 'sysfs/probe_preps')
    for probe_filter_args in args.probe_filter:
        if _damon.damon_filter_type_pgidle_unset in args.probe_filter:
            warn_for(
                    '--probe_filter of %s' %
                    _damon.damon_filter_type_pgidle_unset,
                    'sysfs/probe_type_pgidle_unset')

    # mm.git

    if args.probe_weight != []:
        warn_for('--probe_weight', 'sysfs/probe_weights')

    # 7.2
    if args.probe_filter != []:
        warn_for('--probe_filter', 'sysfs/attrs_monitoring')
    if _damon.damos_action_collapse in args.damos_action:
        warn_for('--damos_action collapse', 'sysfs/damos_action_collapse')
    for quota_goal in args.damos_quota_goal:
        metric = quota_goal[0]
        if metric == _damon.qgoal_node_eligible_mem_bp:
            warn_for('--damos_quota_goal %s' % metric,
                     'sysfs/damos_quota_goal_node_eligible_mem_bp')

    # 7.1
    if args.damos_quota_goal_tuner != []:
        warn_for('--damos_quota_goal_tuner', 'sysfs/damos_quota_goal_tuner')
    if args.pause_ctx is not None:
        warn_for('--pause_ctx', 'sysfs/ctx_pause')
    if len(args.damos_quota_fail_charge_ratio) > 0:
        warn_for('--damos_quota_fail_charge_ratio',
                 'sysfs/damos_quota_fail_charge_ratio')

    # 7.0
    for quota_goal in args.damos_quota_goal:
        metric = quota_goal[0]
        if metric in [_damon.qgoal_inactive_mem_bp,
                      _damon.qgoal_active_mem_bp]:
            warn_for('--damos_quota_goal %s' % metric,
                     'sysfs/damos_quota_goal_in_active_mem_bp')
    if args.damos_max_nr_snapshots != []:
        warn_for('--damos_max_nr_snapshots', 'sysfs/damos_max_nr_snapshots')

    # 6.19
    if args.obsolete_targets is not None:
        warn_for('--obsolete_targets', 'sysfs/obsolete_target')
    for quota_goal in args.damos_quota_goal:
        metric = quota_goal[0]
        if metric in [_damon.qgoal_node_memcg_used_bp,
                      _damon.qgoal_node_memcg_free_bp]:
            warn_for('--damos_quota_goal %s' % metric,
                     'sysfs/schemes_quota_goal_node_memcg_used_free')

    # 6.18
    if args.damon_addr_unit is not None:
        for addr_unit in args.damon_addr_unit:
            if addr_unit != 1:
                warn_for('--damon_addr_unit', 'sysfs/addr_unit')
                break

    # 6.17
    if args.refresh_stat is not None:
        warn_for('--refresh_stat', 'sysfs/refresh_ms')
    for damos_action in args.damos_action:
        action = damos_action[0]
        if not _damon.is_damos_migrate_action(action):
            continue
        if len(damos_action) > 2:
            warn_for('--damos_action %s' % damos_action, 'sysfs/schemes_dests')
    # 6.16
    for quota_goal in args.damos_quota_goal:
        metric = quota_goal[0]
        if metric in [_damon.qgoal_node_mem_used_bp,
                      _damon.qgoal_node_mem_free_bp]:
            warn_for('--damos_quota_goal %s' % metric,
                     'sysfs/schemes_quota_goal_node_mem_used_free')

    # 6.15
    if args.damos_filter != []:
        filters, err = damos_options_to_filters(args.damos_filter)
        if err is None:
            for filter in filters:
                if filter.filter_type == 'active':
                    warn_for('--damos_filter with active type',
                             'sysfs/schemes_filters_active')
                if filter.filter_type == 'unmapped':
                    warn_for('--damos_filter with unmapped type',
                             'sysfs/schemes_filters_unmapped')
                if filter.filter_type == 'hugepage_size':
                    warn_for('--damos_filter with hugepage_size type',
                             'sysfs/schemes_filters_hugepage_size')
    if args.monitoring_intervals_goal is not None:
        warn_for('--monitoring_intervals_goal', 'sysfs/intervals_goal')
    if args.monitoring_intervals_autotune is True:
        warn_for('--monitoring_intervals_autotune', 'sysfs/intervals_goal')

    # 6.14
    # allow_filter is supported in damo itself if the DAMON feature is
    # unavailable.
    # sz_ops_filter_passed should be checked on other place.

    # 6.11
    for damos_action in args.damos_action:
        action = damos_action[0]
        if _damon.is_damos_migrate_action(action):
            warn_for('--damos_action %s' % damos_action,
                     'sysfs/schemes_migrate')

    # 6.10
    if args.damos_filter != []:
        filters, err = damos_options_to_filters(args.damos_filter)
        if err is None:
            for filter in filters:
                if filter.filter_type == 'young':
                    warn_for('--damos_filter with young type',
                             'sysfs/schemes_filters_young')

    # 6.9
    for quota_goal in args.damos_quota_goal:
        metric = quota_goal[0]
        if metric == _damon.qgoal_some_mem_psi_us:
            warn_for('--damos_quota_goal %s' % metric,
                     'sysfs/schemes_quota_goal_some_psi')
    if args.damos_quota_goal != []:
        if metric != 'user_input':
            warn_for('--damos_quota_goal with non-user_input',
                     'sysfs/schemes_quota_goal_metric')
    # quota_effective_bytes should be checked in different places.

    # 6.8
    if args.damos_quota_goal != []:
        warn_for('--damos_quota_goal', 'sysfs/schemes_quota_goals')

    # 6.7
    if args.damos_apply_interval != []:
        warn_for('--damos_apply_interval', 'sysfs/schemes_apply_interval')

    # 6.6
    if args.damos_filter != []:
        filters, err = damos_options_to_filters(args.damos_filter)
        if err is None:
            for filter in filters:
                if filter.filter_type == 'target':
                    warn_for('--damos_filter with target type',
                             'sysfs/schemes_filters_target')
                if filter.filter_type == 'addr':
                    warn_for('--damos_filter with addr type',
                             'sysfs/schemes_filters_addr')
    # schemes_tried_regions_sz should be checked in different places.

    # 6.3
    if args.damos_filter != []:
        filters, err = damos_options_to_filters(args.damos_filter)
        if err is None:
            for filter in filters:
                if filter.filter_type == 'memcg':
                    warn_for('--damos_filter with memcg type',
                             'sysfs/schemes_filters_memcg')
                if filter.filter_type == 'anon':
                    warn_for('--damos_filter with anon type',
                             'sysfs/schemes_filters_anon')
    if args.damos_filter != []:
        warn_for('--damos_filter', 'sysfs/schemes_filters')

    # 6.2
    # schemes_tried_regions should be checked in different places.

    # 5.19
    if args.ops is not None and 'fvaddr' in args.ops:
        warn_for('--ops fvaddr', 'sysfs/fvaddr')
    if args.deducible_target == 'fvaddr':
        warn_for('fvaddr', 'sysfs/fvaddr')

    # 5.16-5.18
    if args.damos_wmarks != []:
        warn_for_features('--damos_wmarks',
                          ['sysfs/schemes_wmarks', 'debugfs/schemes_wmarks'])
    if args.damos_quota_space != []:
        warn_for_features(
                '--damos_quota_space',
                ['sysfs/schemes_size_quota', 'debugfs/schemes_size_quota'])
    if args.damos_quota_time != []:
        warn_for_features(
                '--damos_quota_time',
                ['sysfs/schemes_time_quota', 'debugfs/schemes_time_quota'])
    if args.regions is not None:
        warn_for_features(
                '--regions', ['sysfs/init_regions', 'debugfs/init_regions'])
    if args.ops is not None and 'paddr' in args.ops:
        warn_for_features('--ops paddr', ['sysfs/paddr', 'debugfs/paddr'])
    if args.deducible_target == 'paddr':
        warn_for_features('ops paddr', ['sysfs/paddr', 'debugfs/paddr'])
    if args.damos_action != []:
        warn_for_features(
                '--damos_action', ['sysfs/schemes', 'debugfs/schemes'])

    # 5.15-5.18
    # TODO: in future, we might support modules with 'damo start'...
    warn_for_features('damon control request',
                      ['interface/damon_debugfs', 'interface/damon_sysfs'])

def evaluate_args(args):
    update_args_defaults(args)
    warn_unsupported_damon_features_for(args)

    '''
    Verify if 'damons_action' is present when any 'damos_*' is specified
    '''
    if not args.damos_action:
        for key, value in args.__dict__.items():
            if key.startswith('damos_') and len(value):
                if key == 'damos_action': continue
                return False, '\'damos_action\' not specified while using --damos_* option(s)'

    if len(args.damos_quotas) > 0 and len(args.damos_quota_interval) > 0:
        return False, '--damos_quotas and --damos_quota_interval cannot passed together'

    '''
    Verify if 'reset_interval_ms' is specified in args when setting quota goals
    '''
    if args.damos_quota_goal:
        damos_quotas = args.damos_quotas

        if not len(damos_quotas) and not len(args.damos_quota_interval):
            return False, '\'reset_interval_ms\' not specified when setting quota goals'

        #reset_interval_ms is specified in --damos_quotas as 3rd arg
        for quota in damos_quotas:
            if len(quota) < 3:
                return False, '\'reset_interval_ms\' not specified when setting quota goals'

    return True, None

def verify_kdamonds(kdamonds):
    for kd in kdamonds:
        for ctx in kd.contexts:
            if ctx.ops != 'paddr' and ctx.addr_unit != 1:
                return 'only paddr supports non-one --damon_addr_unit'
    return None

def kdamonds_for(args):
    correct, err = evaluate_args(args)
    if err is not None:
        return None, err

    if args.kdamonds:
        return kdamonds_from_json_arg(args.kdamonds)

    if args.deducible_target:
        if args.deducible_target.endswith('.yaml'):
            kdamonds, e = kdamonds_from_yaml_arg(args.deducible_target)
        else:
            kdamonds, e = kdamonds_from_json_arg(args.deducible_target)
        if e == None:
            return kdamonds, e
        err = deduce_target_update_args(args)
        if err:
            return None, err

    ctxs, err = damon_ctxs_for(args)
    if err:
        return None, err

    if args.nr_ctxs is None:
        args.nr_ctxs = [len(ctxs)]
    if sum(args.nr_ctxs) != len(ctxs):
        return None, '--nr_ctxs and number of ctxs mismatch (%d != %d)' % (
                sum(args.nr_ctxs), len(ctxs))

    kdamonds = []
    ctx_idx = 0
    for nr in args.nr_ctxs:
        if (args.refresh_stat is not None and
            len(args.refresh_stat) > len(kdamonds)):
            refresh_ms = args.refresh_stat[len(kdamonds)]
        else:
            refresh_ms = 0
        try:
            kdamonds.append(_damon.Kdamond(
                state=None, pid=None, refresh_ms=refresh_ms,
                contexts=ctxs[ctx_idx:ctx_idx + nr]))
        except Exception as e:
            return None, 'kdamond creation fail (%s)' % e
        ctx_idx += nr

    err = verify_kdamonds(kdamonds)
    if err is not None:
        return None, err
    return kdamonds, None

def self_started_target(args):
    return 'self_started_target' in args and args.self_started_target

# Command line processing helpers

def is_ongoing_target(args):
    return args.deducible_target == 'ongoing'

def stage_kdamonds(args):
    kdamonds, err = kdamonds_for(args)
    if err:
        return None, 'cannot create kdamonds from args (%s)' % err
    err = _damon.stage_kdamonds(kdamonds)
    if err:
        return None, 'cannot apply kdamonds from args (%s)' % err
    return kdamonds, None

def damos_filter_invalidity(filter):
    # todo: support non-memcg filters
    if filter.filter_type != 'memcg':
        return None
    memcg_path = filter.memcg_path
    if not memcg_path.startswith('/'):
        return 'path should start with "/"'
    cgroup_mount_path = None
    with open('/proc/mounts', 'r') as f:
        for line in f:
            fields = line.split()
            if not fields[2] in ['cgroup', 'cgroup2']:
                continue
            cgroup_mount_path = fields[1]
            break
    if cgroup_mount_path is None:
        return 'cgroup is not mounted'
    memcg_realpath = os.path.join(cgroup_mount_path, memcg_path[1:])
    if not os.path.isdir(memcg_realpath):
        return '%s not found' % memcg_realpath
    return None

def infer_start_or_commit_fail_reason(kdamonds):
    for kidx, kd in enumerate(kdamonds):
        for cidx, ctx in enumerate(kd.contexts):
            for sidx, scheme in enumerate(ctx.schemes):
                for fidx, filter in enumerate(scheme.filters):
                    reason = damos_filter_invalidity(filter)
                    if reason is not None:
                        return ''.join([
                            'wrong %d/%d/%d/%d-th ' % (kidx, cidx, sidx, fidx),
                            'kdamond/context/scheme/filter (%s)' % reason])
    return None

def commit_kdamonds(args, commit_quota_goals_only):
    kdamonds, err = kdamonds_for(args)
    if err:
        return None, 'cannot create kdamonds to commit from args (%s)' % err
    err = _damon.commit(kdamonds, commit_quota_goals_only)
    if err:
        inferred_reason = infer_start_or_commit_fail_reason(kdamonds)
        if inferred_reason is not None:
            err = '%s (%s)' % (err, inferred_reason)
        return None, 'cannot commit kdamonds (%s)' % err
    return kdamonds, None

def turn_damon_on(args):
    kdamonds, err = stage_kdamonds(args)
    if err:
        return err, None
    err = _damon.turn_damon_on(
            ['%s' % kidx for kidx, k in enumerate(kdamonds)])
    if err is not None:
        inferred_reason = infer_start_or_commit_fail_reason(kdamonds)
        if inferred_reason is not None:
            err = '%s (%s)' % (err, inferred_reason)
        return err, None
    return None, kdamonds

# Commandline options setup helpers

def add_interface_debug_args(parser):
    parser.add_argument('--damon_interface_DEPRECATED',
            choices=['sysfs', 'debugfs', 'auto'],
            default='auto',
            # underlying DAMON interface to use (!! DEPRECATED)
            help=argparse.SUPPRESS)
    parser.add_argument('--debug_damon', action='store_true',
            help='Print debugging log')

def set_monitoring_attrs_pinpoint_argparser(parser, hide_help=False):
    # for easier pinpoint setup
    parser.add_argument(
            '-s', '--sample', metavar='<microseconds>', action='append',
            help='sampling interval (us)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '-a', '--aggr', metavar='<microseconds>', action='append',
            help='aggregate interval (us)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '-u', '--updr', metavar='<microseconds>', action='append',
            help='regions update interval (us)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '-n', '--minr', metavar='<# regions>', action='append',
            help='minimal number of regions'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '-m', '--maxr', metavar='<# regions>', action='append',
            help='maximum number of regions'
            if not hide_help else argparse.SUPPRESS)

def set_monitoring_attrs_argparser(parser, hide_help=False):
    # for easier total setup
    parser.add_argument('--monitoring_intervals_autotune', action='store_true',
                        help='use default monitoring intervals goal '
                        '(4%% 3 5ms 10s)')
    parser.add_argument('--monitoring_intervals', nargs=3, action='append',
                        metavar=('<sample>', '<aggr>', '<update>'),
                        help='monitoring intervals (us)'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--monitoring_intervals_goal', nargs=4, action='append',
            metavar=('<access_bp>', '<aggrs>', '<min_sample_us>',
                     '<max_sample_us>'),
            help='monitoring intervals auto-tuning goal'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--monitoring_nr_regions_range', nargs=2,
                        metavar=('<min>', '<max>'), action='append',
                        help='min/max number of monitoring regions'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--sample_primitives', action='append',
                        choices=['page_table', 'page_fault'], nargs='+',
                        help='access sampling primitives to use'
                        if not hide_help else argparse.SUPPRESS)

def set_monitoring_damos_common_args(parser, hide_help=False):
    parser.add_argument('--ops', choices=['vaddr', 'paddr', 'fvaddr'],
                        action='append',
                        help='monitoring operations set'
                        if not hide_help else argparse.SUPPRESS)

    parser.add_argument('--probe_prep', action='append',
                        metavar='<prep action>',
                        help='data attribute monitoring probe prep'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--nr_probe_preps', type=int, nargs='+',
                        metavar='<integer>',
                        help='number of preps for each probe (in order)'
                        if not hide_help else argparse.SUPPRESS)

    parser.add_argument('--probe_filter', nargs='+', action='append',
                        metavar='<<allow|reject> [non] <type> [option]...>',
                        help='data attribute monitoring probe filter'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--nr_probe_filters', type=int, nargs='+', metavar='<integer>',
            help='number of filters for each probe (in order)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--probe_weight', type=int, nargs='+', action='append',
                        metavar='<int>',
                        help='data attribute probe weight'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--nr_probes', type=int, nargs='+', metavar='<integer>',
            help='number of probes for each context (in order)'
            if not hide_help else argparse.SUPPRESS)

    parser.add_argument(
            '--pause_ctx', type=int, nargs='+', metavar='<context index>',
            help='contexts to pause')
    parser.add_argument('--exp_ops_use_reports', action='append',
                        metavar='<Y|N>',
                        help='use access reports (experimental)'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--exp_ops_write_only', action='append',
                        metavar='<Y|N>',
                        help='monitor writes only (experimental)'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--exp_ops_cpus', action='append', metavar='<cpulist>',
                        help='monitor for given cpus only (experimental)'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--exp_ops_tids', action='append', metavar='<thread ids list>',
            help='monitoring for given threads only (experimental)')
    parser.add_argument(
            '--refresh_stat', metavar='<milliseconds>', action='append',
            help='automatic kdamond internal stat refresh interval')
    parser.add_argument(
            '--damon_addr_unit', metavar='<bytes>', action='append',
            help='DAMON address unit')

def set_monitoring_argparser(parser, hide_help=False):
    parser.add_argument('--target_pid', type=int, metavar='<pid>',
                        action='append',
                        help='pid of monitoring target process'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('-r', '--regions', metavar='"<start>-<end> ..."',
                        type=str, action='append',
                        help='monitoring target address regions'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--numa_node', metavar='<node id>', type=int, nargs='+',
            action='append',
            help='if target is \'paddr\', limit it to the numa node'
            if not hide_help else argparse.SUPPRESS)
    set_monitoring_attrs_argparser(parser, hide_help)
    parser.add_argument(
            '--nr_targets', metavar='<number>', nargs='+', type=int,
            help='number of monitoring targets for each context (in order)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--obsolete_targets', metavar='<target index>',
                        nargs='+', type=int,
                        help='obsolte targets'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--nr_ctxs', metavar='<number>', nargs='+', type=int,
                        help='number of contexts for each kdamond (in order)'
                        if not hide_help else argparse.SUPPRESS)

def set_damos_argparser(parser, hide_help):
    parser.add_argument('--damos_action', metavar='<action>', nargs='+',
                        action='append',
                        help=' '.join([
                            'damos action to apply to the target regions.',
                            '<action> should be {%s}.' %
                            ','.join(_damon.damos_actions),
                            'for \'migrate_{hot,cold}\' actions,',
                            'single target node id or',
                            'multiple destination nodes input',
                            'should also be given.'
                            ]))
    parser.add_argument('--damos_sz_region', metavar=('<min>', '<max>'),
                        nargs=2, action='append',
                        help='min/max size of damos target regions (bytes)'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_access_rate', metavar=('<min>', '<max>'), nargs=2,
            action='append',
            help='min/max access rate of damos target regions (percent)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_age', metavar=('<min>', '<max>'), nargs=2,
            action='append',
            help='min/max age of damos target regions (microseconds)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_apply_interval', metavar='<microseconds>',
                        action='append',
                        help='the apply interval for the scheme'
                        if not hide_help else argparse.SUPPRESS)
    damos_quotas_help = ' '.join([
        'damos quotas (<time (ms)> [<size (bytes)> [<reset interval (ms)>',
        '[<size priority weight (permil)>',
        '[<access rate priority weight> (permil)',
        '[<age priority weight> (permil)]]]]])'])
    parser.add_argument(
            '--damos_quotas', metavar='<quota parameter>', nargs='+',
            action='append',
            help=damos_quotas_help if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_quota_interval', metavar='<milliseconds>',
                        action='append',
                        help='quota reset interval'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_quota_time', metavar='<milliseconds>',
                        action='append',
                        help='time quota'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_quota_space', metavar='<bytes>',
                        action='append', help='space quota'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_quota_weights', metavar='<permil>', nargs=3,
                        action='append',
                        help='quota\'s prioritization weights'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument('--damos_quota_fail_charge_ratio',
                        metavar=('<numerator>', '<denominator>'),
                        action='append', nargs=2,
                        help='quota failed regions charge ratio'
                        if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_quota_goal', nargs='+', action='append',
            metavar='<metric or value>',
            help=' '.join([
                'damos quota goal (<metric> <target value> [optional value]).',
                '<metric> should be {%s}.' %
                ','.join(_damon.qgoal_metrics)])
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_quota_goal_tuner', action='append',
            help='quota\'s goal-based tuner')
    parser.add_argument(
            '--damos_nr_quota_goals', type=int, nargs='+', metavar='<integer>',
            help='number of quota goals for each scheme (in order)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_filter', nargs='+', action='append',
            metavar='<<allow|reject> [none] <type> [option]...>',
            help='damos filter' if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_nr_filters', type=int, nargs='+', metavar='<integer>',
            help='number of filters for each scheme (in order)'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_wmarks', nargs=5, action='append',
            metavar=('<metric (none|free_mem_rate)>', '<interval (us)>',
                '<high mark (permil)>', '<mid mark (permil)>',
                '<low mark (permil)>'),
            help='damos watermarks'
            if not hide_help else argparse.SUPPRESS)
    parser.add_argument(
            '--damos_max_nr_snapshots', action='append',
            help='damos max_nr_snapshots')
    parser.add_argument('--nr_schemes', metavar='<number>', nargs='+', type=int,
                        help='number of schemes for each context (in order)'
                        if not hide_help else argparse.SUPPRESS)

def set_misc_damon_params_argparser(parser):
    parser.add_argument('-c', '--schemes', metavar='<json string or file>',
	    help='data access monitoring-based operation schemes')
    parser.add_argument('--kdamonds', metavar='<json/yaml string or file>',
            help=' '.join([
                'json or yaml format kdamonds specification to run DAMON for.',
                '\'damo args damon\' can help generating it.']))
    parser.add_argument(
            'deducible_target', type=str, metavar='<deducible string>',
            nargs='?',
            help=' '.join([
                'The implicit monitoring requests.',
                'It could be a command, process id, special keywords, or full',
                'DAMON parameters (same to that for --kdamonds)']))

def set_damon_params_argparser(parser, min_help):
    set_monitoring_damos_common_args(parser, min_help)
    set_monitoring_argparser(parser, min_help)
    set_damos_argparser(parser, min_help)
    set_misc_damon_params_argparser(parser)
    set_monitoring_attrs_pinpoint_argparser(parser, hide_help=True)

def set_argparser(parser, add_record_options, min_help):
    set_damon_params_argparser(parser, min_help)
    if add_record_options:
        parser.add_argument('-o', '--out', metavar='<file path>', type=str,
                default='damon.data', help='output file path')
    add_interface_debug_args(parser)
    if min_help:
        if parser.epilog is None:
            parser.epilog = ''
        parser.epilog += ' '.join([
            "DAMON parameters options are also supported.",
            "Do 'damo help damon_param_options -h' for the options."])
