# SPDX-License-Identifier: GPL-2.0

"""
Contains core functions for DAMON debugfs control.
"""

import os

import _damo_fs
import _damo_sysinfo
import _damon
import _damon_features

debugfs_root = None

def get_debugfs_root():
    global debugfs_root

    if debugfs_root is None:
        debugfs_root = _damo_fs.dev_mount_point('debugfs')
    return debugfs_root

def get_damon_dir():
    '''Returns None if debugfs is not mounted'''
    if get_debugfs_root() is None:
        return None
    return os.path.join(get_debugfs_root(), 'damon')

def get_debugfs_monitor_on_path():
    path = os.path.join(get_damon_dir(), 'monitor_on')
    if os.path.isfile(path):
        return path
    path = os.path.join(get_damon_dir(), 'monitor_on_DEPRECATED')
    if os.path.isfile(path):
        return path
    return None

def supported():
    damon_dir = get_damon_dir()
    if damon_dir is None:
        return False
    return os.path.isdir(damon_dir)

def turn_damon_on(kdamonds_idxs):
    return _damo_fs.write_file(get_debugfs_monitor_on_path(), 'on')

def turn_damon_off(kdamonds_idxs):
    return _damo_fs.write_file(get_debugfs_monitor_on_path(), 'off')

def is_kdamond_running(kdamond_idx):
    content, err = _damo_fs.read_file(get_debugfs_monitor_on_path())
    if err != None:
        raise Exception('monitor_on file read failed: err')
    return content.strip() == 'on'

'Return error'
def update_schemes_stats(kdamond_idxs):
    # DAMON debugfs updates stats always
    return None

def update_schemes_tried_regions(kdamond_idxs):
    return 'DAMON debugfs doesn\'t support schemes tried regions'

def update_schemes_quota_effective_bytes(kdamond_idxs):
    return 'DAMON debugfs doesn\'t support schemes effective quota'

# for stage_kdamonds

# note that DAMON debugfs interface is deprecated[1], and hence newer DAMOS
# actions including _damon.damos_action_lru_prio and
# _damon.damos_action_lru_deprio are not supported.
#
# [1] https://git.kernel.org/torvalds/c/5445fcbc4cda
damos_action_to_int = {
        _damon.damos_action_willneed: 0,
        _damon.damos_action_cold: 1,
        _damon.damos_action_pageout: 2,
        _damon.damos_action_hugepage: 3,
        _damon.damos_action_nohugepage: 4,
        _damon.damos_action_stat: 5}

damos_wmark_metric_to_int = {
        _damon.damos_wmarks_metric_none: 0,
        _damon.damos_wmarks_metric_free_mem_rate: 1}

def damos_action_to_file_input(action_str):
    if not action_str in damos_action_to_int:
        raise Exception('\'%s\' DAMOS action is not supported' % action_str)
    return damos_action_to_int[action_str]

def damos_wmarks_metric_to_file_input(metric_str):
    if not metric_str in damos_wmark_metric_to_int:
        raise Exception('\'%s\' DAMOS watermark metric is not supported' %
                metric_str)
    return damos_wmark_metric_to_int[metric_str]

def file_content_to_damos_action(action_file_content):
    for action_str in damos_action_to_int:
        if damos_action_to_int[action_str] == action_file_content:
            return action_str

def file_content_to_damos_wmarks_metric(metric_file_content):
    for metric_str in damos_wmark_metric_to_int:
        if damos_wmark_metric_to_int[metric_str] == metric_file_content:
            return metric_str

def damos_to_debugfs_input(damos, intervals, quotas_wmarks_supported):
    pattern = damos.access_pattern.converted_for_units(
            _damon.unit_samples, _damon.unit_aggr_intervals,
            intervals)
    quotas = damos.quotas
    watermarks = damos.watermarks

    scheme_input = '%d\t%d\t%d\t%d\t%d\t%d\t%d' % (
            pattern.sz_bytes[0], pattern.sz_bytes[1],
            pattern.nr_acc_min_max[0].samples,
            pattern.nr_acc_min_max[1].samples,
            pattern.age_min_max[0].aggr_intervals,
            pattern.age_min_max[1].aggr_intervals,
            damos_action_to_file_input(damos.action))
    if not quotas_wmarks_supported:
        return scheme_input

    scheme_input = '%s\t' % scheme_input + '\t'.join(
            '%d' % x for x in [
                # quotas
                quotas.time_ms, quotas.sz_bytes, quotas.reset_interval_ms,
                quotas.weight_sz_permil, quotas.weight_nr_accesses_permil,
                quotas.weight_age_permil,
                # wmarks
                damos_wmarks_metric_to_file_input(watermarks.metric),
                watermarks.interval_us, watermarks.high_permil,
                watermarks.mid_permil, watermarks.low_permil])
    return scheme_input

def write_schemes(dir_path, schemes, intervals):
    scheme_file_input_lines = []
    for scheme in schemes:
        scheme_file_input_lines.append(damos_to_debugfs_input(scheme,
            intervals, _damo_sysinfo.damon_feature_available(
                'debugfs/schemes_time_quota')))
    scheme_file_input = '\n'.join(scheme_file_input_lines)
    if scheme_file_input == '':
        scheme_file_input = '\n'
    err = _damo_fs.write_file(
            os.path.join(dir_path, 'schemes'), scheme_file_input)
    return err

def write_target(dir_path, target, target_has_pid):
    if target_has_pid:
        err = _damo_fs.write_file(
                os.path.join(dir_path, 'target_ids'), '%s' % target.pid)
        if err is not None:
            return err
        tid = target.pid
    else:
        if not _damo_sysinfo.damon_feature_available('debugfs/paddr'):
            raise Exception('paddr is not supported')
        err = _damo_fs.write_file(
                os.path.join(dir_path, 'target_ids'), 'paddr\n')
        if err is not None:
            return err
        tid = 42
    if _damo_sysinfo.damon_feature_available(
            'debugfs/init_regions_target_idx'):
        tid = 0

    if _damo_sysinfo.damon_feature_available('debugfs/init_regions'):
        string = ' '.join(['%s %d %d' % (tid, r.start, r.end) for r in
            target.regions])
        err = _damo_fs.write_file(
                os.path.join(dir_path, 'init_regions'), string)
        if err is not None:
            return err
    return None

def attr_str_ctx(damon_ctx):
    intervals = damon_ctx.intervals
    nr_regions = damon_ctx.nr_regions
    return '%d %d %d %d %d ' % (intervals.sample, intervals.aggr,
            intervals.ops_update, nr_regions.minimum, nr_regions.maximum)

def write_kdamonds(dir_path, kdamonds):
    if len(kdamonds) > 1:
        raise Exception('Currently only <=one kdamond is supported')
    if len(kdamonds) == 1 and len(kdamonds[0].contexts) > 1:
        raise Exception('currently only <= one damon_ctx is supported')
    if (len(kdamonds) == 1 and len(kdamonds[0].contexts) == 1 and
            len(kdamonds[0].contexts[0].targets) > 1):
        raise Exception('currently only <= one target is supported')
    ctx = kdamonds[0].contexts[0]

    err = _damo_fs.write_file(
            os.path.join(dir_path, 'attrs'), attr_str_ctx(ctx))
    if err is not None:
        return err

    if len(ctx.targets) > 0:
        err = write_target(
                dir_path, ctx.targets[0], _damon.target_has_pid(ctx.ops))
        if err:
            return err
    if not _damo_sysinfo.damon_feature_available('debugfs/schemes'):
        return None

    err = write_schemes(dir_path, ctx.schemes, ctx.intervals)
    return err

def stage_kdamonds(kdamonds):
    '''Return error'''
    if _damon.any_kdamond_running():
        return 'DAMON debugfs doesn\'t support online staging'

    return write_kdamonds(get_damon_dir(), kdamonds)

# for current_kdamonds

def debugfs_schemes_output_fields_to_access_pattern(fields, intervals_us):
    sz_bytes = fields[:2]

    # convert nr_accesses from sample intervals to percent
    max_nr_accesses = intervals_us.aggr / intervals_us.sample
    nr_accesses = [float(fields[2]) * 100 / max_nr_accesses,
            float(fields[3]) * 100 / max_nr_accesses]
    nr_accesses_unit = _damon.unit_percent

    # convert ages in update_interval to us
    age = [intervals_us.aggr * fields[4], intervals_us.aggr * fields[5]]
    age_unit = _damon.unit_usec

    return _damon.DamosAccessPattern(sz_bytes, nr_accesses, nr_accesses_unit,
            age, age_unit)

def debugfs_output_to_damos(output, intervals_us):
    fields = [int(x) for x in output.strip().split()]
    if _damo_sysinfo.damon_feature_available('debugfs/schemes_stat_succ'):
        nr_stat_fields = 5
    else:
        nr_stat_fields = 2
    stat_fields = fields[-1 * nr_stat_fields:]
    fields = [int(x) for x in output.strip().split()][:-1 * nr_stat_fields]

    access_pattern = debugfs_schemes_output_fields_to_access_pattern(fields,
            intervals_us)
    action = file_content_to_damos_action(fields[6])

    if len(fields) == 7:
        damos = _damon.Damos(access_pattern=access_pattern, action=action)
    elif len(fields) == 18:
        damos = _damon.Damos(access_pattern=access_pattern, action=action)
        damos.quotas.time_ms = fields[7]
        damos.quotas.sz_bytes = fields[8]
        damos.quotas.reset_interval_ms = fields[9]
        damos.quotas.weight_sz_permil = fields[10]
        damos.quotas.weight_nr_accesses_permil = fields[11]
        damos.quotas.weight_age_permil = fields[12]
        damos.watermarks.metric = file_content_to_damos_wmarks_metric(
                fields[13])
        damos.watermarks.interval_us = fields[14]
        damos.watermarks.high_permil = fields[15]
        damos.watermarks.mid_permil = fields[16]
        damos.watermarks.low_permil = fields[17]
    damos.stats = _damon.DamosStats(*stat_fields)
    return damos

def files_content_to_kdamonds(files_content):
    if 'monitor_on' in files_content:
        state = files_content['monitor_on'].strip()
    else:
        state = files_content['monitor_on_DEPRECATED'].strip()
    attrs = [int(x) for x in files_content['attrs'].strip().split()]

    intervals = _damon.DamonIntervals(attrs[0], attrs[1], attrs[2])
    nr_regions = _damon.DamonNrRegionsRange(attrs[3], attrs[4])

    target_ids = [int(x) for x in files_content['target_ids'].strip().split()]
    regions_dict = {}
    # Reading init_regions fails when DAMON is running.  Do the parsing only
    # when DAMON is off.
    if state == 'off' and _damo_sysinfo.damon_feature_available(
            'debugfs/init_regions'):
        fields = [int(x) for x in files_content['init_regions'].strip().split()]
        for i in range(0, len(fields), 3):
            id_or_index = fields[i]
            if not id_or_index in regions_dict:
                regions_dict[id_or_index] = []
            regions_dict[id_or_index].append(_damon.DamonRegion(
                    fields[i + 1], fields[i + 2]))
    ops = 'vaddr'
    is_paddr = False
    if 42 in target_ids:
        ops = 'paddr'
    targets = []
    for idx, target_id in enumerate(target_ids):
        targets.append(_damon.DamonTarget(
            pid=target_id if not is_paddr else None,
            regions=regions_dict[idx
                if _damo_sysinfo.damon_feature_available(
                    'debugfs/init_regions_target_idx')
                else target_id] if len(regions_dict) > 0 else []))

    schemes = []
    if _damo_sysinfo.damon_feature_available('debugfs/schemes'):
        for line in files_content['schemes'].split('\n'):
            if line.strip() == '':
                continue
            schemes.append(debugfs_output_to_damos(line, intervals))

    ctx = _damon.DamonCtx(ops, targets, intervals, nr_regions, schemes)
    if 'monitor_on' in files_content:
        state = files_content['monitor_on'].strip()
    else:
        state = files_content['monitor_on_DEPRECATED'].strip()
    pid = files_content['kdamond_pid'].strip()
    return [_damon.Kdamond(state, pid, [ctx])]

def current_kdamonds():
    return files_content_to_kdamonds(
            _damo_fs.read_files(get_damon_dir()))

def nr_kdamonds():
    # TODO: Support manually created kdamonds
    return 1

# features

def values_for_restore(filepath, read_val):
    if read_val == '':
        return '\n'
    if os.path.basename(filepath) == 'target_ids' and read_val == '42\n':
        return 'paddr\n'
    return read_val

'''Return value to write back to the filepath for restoring, and error'''
def read_value_for_restore(filepath):
    err = True
    read_val, err = _damo_fs.read_file(filepath)
    if err != None:
        return None, err
    err = None
    return values_for_restore(filepath, read_val), err

def test_debugfs_file(path, input_str, expected):
    orig_val, err = read_value_for_restore(path)
    if err != None:
        return False
    err = _damo_fs.write_file(path, input_str)
    if err != None:
        return False
    content, err = _damo_fs.read_file(path)
    if err != None:
        return False
    if content == expected:
        passed = True
    else:
        passed = False
    err = _damo_fs.write_file(path, orig_val)
    if err != None:
        return False
    return passed

def get_schemes_file():
    return os.path.join(get_damon_dir(), 'schemes')

def test_debugfs_file_schemes(nr_fields):
    input_str = ' '.join(['1'] * nr_fields)
    expected = '%s 0 0\n' % input_str

    return test_debugfs_file(get_schemes_file(), input_str, expected)

def test_debugfs_file_schemes_stat_extended(nr_fields):
    input_str = ' '.join(['1'] * nr_fields)
    expected = '%s 0 0 0 0 0\n' % input_str

    return test_debugfs_file(get_schemes_file(), input_str, expected)

def get_target_ids_file():
    return os.path.join(get_damon_dir(), 'target_ids')

def get_init_regions_file():
    return os.path.join(get_damon_dir(), 'init_regions')

def test_init_regions_version(paddr_supported):
    # Save previous values
    orig_target_ids, err = read_value_for_restore(get_target_ids_file())
    if err != None:
        raise Exception('debugfs target_ids read failed')
    orig_init_regions, err = read_value_for_restore(get_init_regions_file())
    if err != None:
        raise Exception('debugfs init_regions read failed')

    # Test
    if paddr_supported:
        err = _damo_fs.write_file(get_target_ids_file(), 'paddr\n')
        if err != None:
            raise Exception(err)
        v1_input = '42 100 200'
    else:
        err = _damo_fs.write_file(get_target_ids_file(), '%d\n' % os.getpid())
        if err != None:
            raise Exception(err)
        v1_input = '%d 100 200' % os.getpid()

    # We check if the write was success below anyway, so ignore error
    err = _damo_fs.write_file(get_init_regions_file(), v1_input)
    read_val, err = _damo_fs.read_file(get_init_regions_file())
    if err != None:
        raise Exception(err)
    if read_val.strip() == v1_input:
        version = 1
    else:
        version = 2

    # Previous value might be invalid now (e.g., process terminated), so ignore
    # error
    err = _damo_fs.write_file(get_target_ids_file(), orig_target_ids)
    err = _damo_fs.write_file(get_init_regions_file(), orig_init_regions)

    return version

def mk_feature_supports_map():
    '''
    Returns feature supports info map and an error if failed.
    Read _damon_sysfs.mk_feature_supports_map() for more details.
    '''

    feature_supports = {x.name: False for x in _damon_features.features_list}

    need_schemes_file_test = False
    if os.path.isfile(get_schemes_file()):
        feature_supports['debugfs/schemes'] = True
        with open(get_schemes_file(), 'r') as f:
            nr_fields = len(f.read().strip().split())
        if nr_fields == 0:
            need_schemes_file_test = True
        elif nr_fields == 20:   # v5.16
            feature_supports['debugfs/schemes_size_quota'] = True
            feature_supports['debugfs/schemes_prioritization'] = True
            feature_supports['debugfs/schemes_wmarks'] = True
            feature_supports['debugfs/schemes_time_quota'] = True
        elif nr_fields == 23:   # v5.17 or later
            feature_supports['debugfs/schemes_size_quota'] = True
            feature_supports['debugfs/schemes_prioritization'] = True
            feature_supports['debugfs/schemes_wmarks'] = True
            feature_supports['debugfs/schemes_time_quota'] = True
            feature_supports['debugfs/schemes_stat_succ'] = True
            feature_supports['debugfs/schemes_stat_qt_exceed'] = True

    if _damon.any_kdamond_running():
        return None, \
                'debugfs feature update cannot be done while DAMON running'

    # virtual address space has supported since the beginning
    feature_supports['debugfs/vaddr'] = True
    if test_debugfs_file(get_target_ids_file(), 'paddr\n', '42\n'):
        feature_supports['debugfs/paddr'] = True

    if os.path.isfile(get_init_regions_file()):
        feature_supports['debugfs/init_regions'] = True
        init_regions_version = test_init_regions_version(
                feature_supports['debugfs/paddr'])
        if init_regions_version == 2:
            feature_supports['debugfs/init_regions_target_idx'] = True

    if need_schemes_file_test:
        # 'schemes' receives 18 numbers input and has three stats (v5.16)
        if test_debugfs_file_schemes(18):
            feature_supports['debugfs/schemes_size_quota'] = True
            feature_supports['debugfs/schemes_prioritization'] = True
            feature_supports['debugfs/schemes_wmarks'] = True
            feature_supports['debugfs/schemes_time_quota'] = True
        elif test_debugfs_file_schemes_stat_extended(18):
            feature_supports['debugfs/schemes_size_quota'] = True
            feature_supports['debugfs/schemes_prioritization'] = True
            feature_supports['debugfs/schemes_wmarks'] = True
            feature_supports['debugfs/schemes_time_quota'] = True
            feature_supports['debugfs/schemes_stat_succ'] = True
            feature_supports['debugfs/schemes_stat_qt_exceed'] = True

    return feature_supports, None
