// SPDX-License-Identifier: GPL-2.0
// Copyright (C) 2020 Martin Whitaker.
//
// Derived from memtest86+ patn.c:
//
// MemTest86+ V1.60 Specific code (GPL V2.0)
// By Samuel DEMEULEMEESTER, sdemeule@memtest.org
// http://www.x86-secret.com - http://www.memtest.org
// ----------------------------------------------------
// Pattern extension for memtest86
//
// Generates patterns for the Linux kernel's BadRAM extension that avoids
// allocation of faulty pages.
//
// Released under version 2 of the Gnu Public License.
//
// By Rick van Rein, vanrein@zonnet.nl
//
// What it does:
//  - Keep track of a number of BadRAM patterns in an array;
//  - Combine new faulty addresses with it whenever possible;
//  - Keep masks as selective as possible by minimising resulting faults;
//  - Print a new pattern only when the pattern array is changed.

#include <stdbool.h>
#include <stdint.h>

#include "display.h"

#include "badram.h"

//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------

#define MAX_PATTERNS 10
#define PATTERNS_SIZE (MAX_PATTERNS + 1)

// DEFAULT_MASK covers a uintptr_t, since that is the testing granularity.
#ifdef __x86_64__
#define DEFAULT_MASK (UINTPTR_MAX << 3)
#else
#define DEFAULT_MASK (UINTPTR_MAX << 2)
#endif

//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------

typedef struct {
    uintptr_t   addr;
    uintptr_t   mask;
} pattern_t;

//------------------------------------------------------------------------------
// Private Variables
//------------------------------------------------------------------------------

static pattern_t    patterns[PATTERNS_SIZE];
static int          num_patterns = 0;

//------------------------------------------------------------------------------
// Private Functions
//------------------------------------------------------------------------------

// New mask is 1 where both masks were 1 (b & d) and the addresses were equal ~(a ^ c).
// If addresses were unequal the new mask must be 0 to allow for both values.
#define COMBINE_MASK(a,b,c,d) ((b & d) & ~(a ^ c))

/*
 * Combine two addr/mask pairs to one addr/mask pair.
 */
static void combine(uintptr_t addr1, uintptr_t mask1, uintptr_t addr2, uintptr_t mask2, uintptr_t *addr, uintptr_t *mask)
{
    *mask = COMBINE_MASK(addr1, mask1, addr2, mask2);

    *addr  = addr1 | addr2;
    *addr &= *mask;    // Normalise to ensure sorting on .addr will work as intended
}

/*
 * Count the number of addresses covered with a mask.
 */
static uintptr_t addresses(uintptr_t mask)
{
    uintptr_t ctr = 1;
    int i = 8*sizeof(uintptr_t);
    while (i-- > 0) {
        if (! (mask & 1)) {
            ctr += ctr;
        }
        mask >>= 1;
    }
    return ctr;
}

/*
 * Count how many more addresses would be covered by addr1/mask1 when combined
 * with addr2/mask2.
 */
static uintptr_t combi_cost(uintptr_t addr1, uintptr_t mask1, uintptr_t addr2, uintptr_t mask2)
{
    uintptr_t cost1 = addresses(mask1);
    uintptr_t tmp, mask;
    combine(addr1, mask1, addr2, mask2, &tmp, &mask);
    return addresses(mask) - cost1;
}

/*
 * Determine if pattern is already covered by an existing pattern.
 * Return true if that's the case, else false.
 */
static bool is_covered(pattern_t pattern)
{
    for (int i = 0; i < num_patterns; i++) {
        if (combi_cost(patterns[i].addr, patterns[i].mask, pattern.addr, pattern.mask) == 0) {
            return true;
        }
    }
    return false;
}

/*
 * Find the pair of entries that would be the cheapest to merge.
 * Assumes patterns is sorted by .addr asc and that for each index i, the cheapest entry to merge with is at i-1 or i+1.
 * Return -1 if <= 1 patterns exist, else the index of the first entry of the pair (the other being that + 1).
 */
static int cheapest_pair()
{
    // This is guaranteed to be overwritten with >= 0 as long as num_patterns > 1
    int merge_idx = -1;

    uintptr_t min_cost = UINTPTR_MAX;
    for (int i = 0; i < num_patterns - 1; i++) {
        uintptr_t tmp_cost = combi_cost(
                patterns[i].addr,
                patterns[i].mask,
                patterns[i+1].addr,
                patterns[i+1].mask
        );
        if (tmp_cost <= min_cost) {
            min_cost = tmp_cost;
            merge_idx = i;
        }
    }
    return merge_idx;
}

/*
 * Remove entries at idx and idx+1.
 */
static void remove_pair(int idx)
{
    for (int i = idx; i < num_patterns - 2; i++) {
        patterns[i] = patterns[i + 2];
    }
    patterns[num_patterns - 1].addr = 0u;
    patterns[num_patterns - 1].mask = 0u;
    patterns[num_patterns - 2].addr = 0u;
    patterns[num_patterns - 2].mask = 0u;
    num_patterns -= 2;
}

/*
 * Get the combined entry of idx1 and idx2.
 */
static pattern_t combined_pattern(int idx1, int idx2)
{
    pattern_t combined;
    combine(
            patterns[idx1].addr,
            patterns[idx1].mask,
            patterns[idx2].addr,
            patterns[idx2].mask,
            &combined.addr,
            &combined.mask
    );
    return combined;
}

/*
 * Insert pattern at index idx, shuffling other entries on index towards the end.
 */
static void insert_at(pattern_t pattern, int idx)
{
    // Move all entries >= idx one index towards the end to make space for the new entry
    for (int i = num_patterns - 1; i >= idx; i--) {
        patterns[i + 1] = patterns[i];
    }

    patterns[idx] = pattern;
    num_patterns++;
}

/*
 * Insert entry (addr, mask) in patterns in an index i so that patterns[i-1].addr < patterns[i]
 * NOTE: Assumes patterns is already sorted by .addr asc!
 */
static void insert_sorted(pattern_t pattern)
{
    // Normalise to ensure sorting on .addr will work as intended
    pattern.addr &= pattern.mask;

    // Find index to insert entry into
    int new_idx = num_patterns;
    for (int i = 0; i < num_patterns; i++) {
        if (pattern.addr < patterns[i].addr) {
            new_idx = i;
            break;
        }
    }

    insert_at(pattern, new_idx);
}

//------------------------------------------------------------------------------
// Public Functions
//------------------------------------------------------------------------------

void badram_init(void)
{
    num_patterns = 0;

    for (int idx = 0; idx < PATTERNS_SIZE; idx++) {
        patterns[idx].addr = 0u;
        patterns[idx].mask = 0u;
    }
}

bool badram_insert(uintptr_t addr)
{
    pattern_t pattern = {
            .addr = addr,
            .mask = DEFAULT_MASK
    };

    // If covered by existing entry we return immediately
    if (is_covered(pattern)) {
        return false;
    }

    // Add entry in order sorted by .addr asc
    insert_sorted(pattern);

    // If we have more patterns than the max we need to force a merge
    if (num_patterns > MAX_PATTERNS) {
        // Find the pair that is the cheapest to merge
        // merge_idx will be -1 if num_patterns < 2, but that means MAX_PATTERNS = 0 which is not a valid state anyway
        int merge_idx = cheapest_pair();

        pattern_t combined = combined_pattern(merge_idx, merge_idx + 1);

        // Remove the source pair so that we can maintain order as combined does not necessarily belong in merge_idx
        remove_pair(merge_idx);

        insert_sorted(combined);
    }
    return true;
}

void badram_display(void)
{
    if (num_patterns == 0) {
        return;
    }

    check_input();

    clear_message_area();
    display_pinned_message(0, 0, "BadRAM Patterns");
    display_pinned_message(1, 0, "---------------");
    scroll();
    display_scrolled_message(0, "badram=");
    int col = 7;
    for (int i = 0; i < num_patterns; i++) {
        if (i > 0) {
            display_scrolled_message(col, ",");
            col++;
        }
        int text_width = 2 * (TESTWORD_DIGITS + 2) + 1;
        if (col > (SCREEN_WIDTH - text_width)) {
            scroll();
            col = 7;
        }
        display_scrolled_message(col, "0x%0*x,0x%0*x",
                                 TESTWORD_DIGITS, patterns[i].addr,
                                 TESTWORD_DIGITS, patterns[i].mask);
        col += text_width;
    }
}
