/*
 * Copyright (C) 2020  Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Vojtech Trefny <vtrefny@redhat.com>
 */

#include <glib.h>
#include <blockdev/utils.h>

#include "vdo_stats.h"

#define VDO_SYS_PATH "/sys/kvdo"


G_GNUC_INTERNAL gboolean
get_stat_val64 (GHashTable *stats, const gchar *key, gint64 *val) {
    const gchar *s;
    gchar *endptr = NULL;

    s = g_hash_table_lookup (stats, key);
    if (s == NULL)
        return FALSE;

    *val = g_ascii_strtoll (s, &endptr, 0);
    if (endptr == NULL || *endptr != '\0')
        return FALSE;

    return TRUE;
}

G_GNUC_INTERNAL gboolean
get_stat_val64_default (GHashTable *stats, const gchar *key, gint64 *val, gint64 def) {
    if (!get_stat_val64 (stats, key, val))
        *val = def;
    return TRUE;
}

G_GNUC_INTERNAL gboolean
get_stat_val_double (GHashTable *stats, const gchar *key, gdouble *val) {
    const gchar *s;
    gchar *endptr = NULL;

    s = g_hash_table_lookup (stats, key);
    if (s == NULL)
        return FALSE;

    *val = g_ascii_strtod (s, &endptr);
    if (endptr == NULL || *endptr != '\0')
        return FALSE;

    return TRUE;
}

static void add_write_ampl_r_stats (GHashTable *stats) {
    gint64 bios_meta_write, bios_out_write, bios_in_write;

    if (! get_stat_val64 (stats, "bios_meta_write", &bios_meta_write) ||
        ! get_stat_val64 (stats, "bios_out_write", &bios_out_write) ||
        ! get_stat_val64 (stats, "bios_in_write", &bios_in_write))
        return;

    if (bios_in_write <= 0)
        g_hash_table_replace (stats, g_strdup ("writeAmplificationRatio"), g_strdup ("0.00"));
    else
        g_hash_table_replace (stats,
                              g_strdup ("writeAmplificationRatio"),
                              g_strdup_printf ("%.2f", (gfloat) (bios_meta_write + bios_out_write) / (gfloat) bios_in_write));
}

static void add_block_stats (GHashTable *stats) {
    gint64 physical_blocks, block_size, data_blocks_used, overhead_blocks_used, logical_blocks_used;
    gint64 savings;

    if (! get_stat_val64 (stats, "physical_blocks", &physical_blocks) ||
        ! get_stat_val64 (stats, "block_size", &block_size) ||
        ! get_stat_val64 (stats, "data_blocks_used", &data_blocks_used) ||
        ! get_stat_val64 (stats, "overhead_blocks_used", &overhead_blocks_used) ||
        ! get_stat_val64 (stats, "logical_blocks_used", &logical_blocks_used))
        return;

    g_hash_table_replace (stats, g_strdup ("oneKBlocks"), g_strdup_printf ("%"G_GINT64_FORMAT, physical_blocks * block_size / 1024));
    g_hash_table_replace (stats, g_strdup ("oneKBlocksUsed"), g_strdup_printf ("%"G_GINT64_FORMAT, (data_blocks_used + overhead_blocks_used) * block_size / 1024));
    g_hash_table_replace (stats, g_strdup ("oneKBlocksAvailable"), g_strdup_printf ("%"G_GINT64_FORMAT, (physical_blocks - data_blocks_used - overhead_blocks_used) * block_size / 1024));
    g_hash_table_replace (stats, g_strdup ("usedPercent"), g_strdup_printf ("%.0f", 100.0 * (gfloat) (data_blocks_used + overhead_blocks_used) / (gfloat) physical_blocks + 0.5));
    savings = (logical_blocks_used > 0) ? (gint64) (100.0 * (gfloat) (logical_blocks_used - data_blocks_used) / (gfloat) logical_blocks_used) : 100;
    g_hash_table_replace (stats, g_strdup ("savings"), g_strdup_printf ("%"G_GINT64_FORMAT, savings));
    if (savings >= 0)
        g_hash_table_replace (stats, g_strdup ("savingPercent"), g_strdup_printf ("%"G_GINT64_FORMAT, savings));
}

static void add_journal_stats (GHashTable *stats) {
    gint64 journal_entries_committed, journal_entries_started, journal_entries_written;
    gint64 journal_blocks_committed, journal_blocks_started, journal_blocks_written;

    if (! get_stat_val64 (stats, "journal_entries_committed", &journal_entries_committed) ||
        ! get_stat_val64 (stats, "journal_entries_started", &journal_entries_started) ||
        ! get_stat_val64 (stats, "journal_entries_written", &journal_entries_written) ||
        ! get_stat_val64 (stats, "journal_blocks_committed", &journal_blocks_committed) ||
        ! get_stat_val64 (stats, "journal_blocks_started", &journal_blocks_started) ||
        ! get_stat_val64 (stats, "journal_blocks_written", &journal_blocks_written))
        return;

    g_hash_table_replace (stats, g_strdup ("journal_entries_batching"), g_strdup_printf ("%"G_GINT64_FORMAT, journal_entries_started - journal_entries_written));
    g_hash_table_replace (stats, g_strdup ("journal_entries_writing"), g_strdup_printf ("%"G_GINT64_FORMAT, journal_entries_written - journal_entries_committed));
    g_hash_table_replace (stats, g_strdup ("journal_blocks_batching"), g_strdup_printf ("%"G_GINT64_FORMAT, journal_blocks_started - journal_blocks_written));
    g_hash_table_replace (stats, g_strdup ("journal_blocks_writing"), g_strdup_printf ("%"G_GINT64_FORMAT, journal_blocks_written - journal_blocks_committed));
}

static void add_computed_stats (GHashTable *stats) {
    const gchar *s;

    s = g_hash_table_lookup (stats, "logical_block_size");
    g_hash_table_replace (stats,
                          g_strdup ("fiveTwelveByteEmulation"),
                          g_strdup ((g_strcmp0 (s, "512") == 0) ? "true" : "false"));

    add_write_ampl_r_stats (stats);
    add_block_stats (stats);
    add_journal_stats (stats);
}

static gchar* _dm_node_from_name (const gchar *map_name, GError **error) {
    gchar *dev_path = NULL;
    gchar *ret = NULL;
    gchar *dev_mapper_path = g_strdup_printf ("/dev/mapper/%s", map_name);

    dev_path = bd_utils_resolve_device (dev_mapper_path, error);
    g_free (dev_mapper_path);
    if (!dev_path)
        /* error is already populated */
        return NULL;

    ret = g_path_get_basename (dev_path);
    g_free (dev_path);

    return ret;
}

G_GNUC_INTERNAL GHashTable *
vdo_get_stats_full (const gchar *name, GError **error) {
    GHashTable *stats;
    GDir *dir;
    gchar *stats_dir;
    const gchar *direntry;
    gchar *s;
    gchar *val = NULL;
    g_autofree gchar *dm_node = NULL;
    GError *l_error = NULL;

    /* try "new" (kvdo >= 8) path first -- /sys/block/dm-X/vdo/statistics */
    dm_node = _dm_node_from_name (name, error);
    if (dm_node == NULL) {
        g_prefix_error (error, "Failed to get DM node for %s: ", name);
        return NULL;
    }

    stats_dir = g_build_path (G_DIR_SEPARATOR_S, "/sys/block", dm_node, "vdo/statistics", NULL);
    dir = g_dir_open (stats_dir, 0, &l_error);
    if (dir == NULL) {
        bd_utils_log_format (BD_UTILS_LOG_INFO,
                             "Failed to read VDO stats using the new API, falling back to %s: %s",
                             VDO_SYS_PATH, l_error->message);
        g_free (stats_dir);
        g_clear_error (&l_error);

        /* lets try /sys/kvdo */
        stats_dir = g_build_path (G_DIR_SEPARATOR_S, VDO_SYS_PATH, name, "statistics", NULL);
        dir = g_dir_open (stats_dir, 0, error);
        if (dir == NULL) {
            g_prefix_error (error, "Error reading statistics from %s: ", stats_dir);
            g_free (stats_dir);
            return NULL;
        }
    }

    stats = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    while ((direntry = g_dir_read_name (dir))) {
        s = g_build_filename (stats_dir, direntry, NULL);
        if (! g_file_get_contents (s, &val, NULL, error)) {
            g_prefix_error (error, "Error reading statistics from %s: ", s);
            g_free (s);
            g_hash_table_destroy (stats);
            stats = NULL;
            break;
        }
        g_hash_table_replace (stats, g_strdup (direntry), g_strdup (g_strstrip (val)));
        g_free (val);
        g_free (s);
    }
    g_dir_close (dir);
    g_free (stats_dir);

    if (stats != NULL)
        add_computed_stats (stats);

    return stats;
}
