cache
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Geoff Hammond

//@version=5

// @description A simple cache library to store key value pairs
library("cache")


// █▀▀ ▄▀█ █▀▀ █ █ █▀▀   █   █ █▄▄ █▀█ ▄▀█ █▀█ █▄█
// █▄▄ █▀█ █▄▄ █▀█ ██▄   █▄▄ █ █▄█ █▀▄ █▀█ █▀▄  █

// @TODO: Expiry.


// ██╗ ███╗   ███╗ ██████╗   █████╗  ██████╗  ████████╗  ██████╗
// ██║ ████╗ ████║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝
// ██║ ██╔████╔██║ ██████╔╝ ██║  ██║ ██████╔╝    ██║    ╚█████╗
// ██║ ██║╚██╔╝██║ ██╔═══╝  ██║  ██║ ██╔══██╗    ██║     ╚═══██╗
// ██║ ██║ ╚═╝ ██║ ██║      ╚█████╔╝ ██║  ██║    ██║    ██████╔╝
// ╚═╝ ╚═╝     ╚═╝ ╚═╝       ╚════╝  ╚═╝  ╚═╝    ╚═╝    ╚═════╝

import GeoffHammond/assert/3


// ██╗  ██╗███████╗██╗     ██████╗ ███████╗██████╗  ██████╗
// ██║  ██║██╔════╝██║     ██╔══██╗██╔════╝██╔══██╗██╔════╝
// ███████║█████╗  ██║     ██████╔╝█████╗  ██████╔╝╚█████╗
// ██╔══██║██╔══╝  ██║     ██╔═══╝ ██╔══╝  ██╔══██╗ ╚═══██╗
// ██║  ██║███████╗███████╗██║     ███████╗██║  ██║██████╔╝
// ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝     ╚══════╝╚═╝  ╚═╝╚═════╝

// COPY THESE HELPERS INTO YOUR SCRIPT

// [__CACHE_KEYS, __CACHE_VALUES] = cache.init()
// cache_set(key, value) => cache.set(__CACHE_KEYS, __CACHE_VALUES, key, value)
// cache_has(key)        => cache.has(__CACHE_KEYS, __CACHE_VALUES, key)
// cache_get(key)        => cache.get(__CACHE_KEYS, __CACHE_VALUES, key)
// cache_remove(key)     => cache.remove(__CACHE_KEYS, __CACHE_VALUES, key)
// cache_count()         => cache.count(__CACHE_KEYS, __CACHE_VALUES)
// cache_loop()          => cache.loop(__CACHE_KEYS, __CACHE_VALUES)
// cache_next()          => cache.next(__CACHE_KEYS, __CACHE_VALUES)
// cache_clear()         => cache.clear(__CACHE_KEYS, __CACHE_VALUES)
// cache_debug() =>
//     while cache_loop()
//         [k, v] = cache_next()
//         debug(k, v) // see log library for this function


// ███████╗ ██╗   ██╗ ███╗  ██╗  █████╗  ████████╗ ██╗  █████╗  ███╗  ██╗  ██████╗
// ██╔════╝ ██║   ██║ ████╗ ██║ ██╔══██╗ ╚══██╔══╝ ██║ ██╔══██╗ ████╗ ██║ ██╔════╝
// █████╗   ██║   ██║ ██╔██╗██║ ██║  ╚═╝    ██║    ██║ ██║  ██║ ██╔██╗██║ ╚█████╗
// ██╔══╝   ██║   ██║ ██║╚████║ ██║  ██╗    ██║    ██║ ██║  ██║ ██║╚████║  ╚═══██╗
// ██║      ╚██████╔╝ ██║ ╚███║ ╚█████╔╝    ██║    ██║ ╚█████╔╝ ██║ ╚███║ ██████╔╝
// ╚═╝       ╚═════╝  ╚═╝  ╚══╝  ╚════╝     ╚═╝    ╚═╝  ╚════╝  ╚═╝  ╚══╝ ╚═════╝

check_integrity(string[] keys, float[] values) =>
    if array.size(keys) != array.size(values)
        runtime.error('Cache: Keys and values array sizes are not equal!')


// ██╗     ██╗██████╗ ██████╗  █████╗ ██████╗ ██╗   ██╗
// ██║     ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
// ██║     ██║██████╦╝██████╔╝███████║██████╔╝ ╚████╔╝
// ██║     ██║██╔══██╗██╔══██╗██╔══██║██╔══██╗  ╚██╔╝
// ███████╗██║██████╦╝██║  ██║██║  ██║██║  ██║   ██║
// ╚══════╝╚═╝╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝

// @function Initialises the syncronised cache key and value arrays
// @param persistant bool, toggles data persistance between bars and ticks
// @returns [string[], float[]], a tuple of both arrays
export init(bool persistant = false) =>
    varip keys = array.new_string()
    varip values = array.new_float()
    [persistant ? keys : array.new_string(), persistant ? values : array.new_float()]

// @function Sets a value into the cache
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
// @param key string, the cache key to create or update
// @param value float, the value to set
export set(string[] keys, float[] values, string key, float value) =>
    check_integrity(keys, values)
    index = array.indexof(keys, key)
    if index > -1
        array.set(values, index, value)
    else
        array.push(keys, key)
        array.push(values, value)

// @function Checks if the cache has a key
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
// @param key string, the cache key to check
// @returns bool, true only if the key is found
export has(string[] keys, float[] values, string key) =>
    check_integrity(keys, values)
    array.includes(keys, key)

// @function Gets a keys value from the cache
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
// @param key string, the cache key to get
// @returns float, the stored value
export get(string[] keys, float[] values, string key) =>
    check_integrity(keys, values)
    index = array.indexof(keys, key)
    index > -1 ? array.get(values, index) : float(na)

// @function Removes a key and value from the cache
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
// @param key string, the cache key to remove
export remove(string[] keys, float[] values, string key) =>
    check_integrity(keys, values)
    index = array.indexof(keys, key)
    if index > -1
        array.remove(keys, index)
        array.remove(values, index)

// @function Counts how many key value pairs in the cache
// @returns int, the total number of pairs
export count(string[] keys, float[] values) =>
    check_integrity(keys, values)
    array.size(values)

// @function Returns true for each value in the cache (use as the while loop expression)
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
export loop(string[] keys, float[] values) =>
    check_integrity(keys, values)
    varip i = 0
    if i < array.size(values)
        i += 1
        true
    else
        i := 0
        false

// @function Returns each key value pair on successive calls (use in the while loop)
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
// @returns [string, float], tuple of each key value pair
export next(string[] keys, float[] values) =>
    check_integrity(keys, values)
    size = array.size(values)
    varip i = 0
    if size == 0
        [string(na), float(na)]
    else
        offset = i
        i := i + 1 == size ? 0 : i + 1
        [array.get(keys, offset), array.get(values, offset)]

// @function Clears all key value pairs from the cache
// @param keys string[], the array of cache keys
// @param values float[], the array of cache values
export clear(string[] keys, float[] values) =>
    array.clear(keys)
    array.clear(values)


// ██╗   ██╗ ███╗  ██╗ ██╗ ████████╗    ████████╗ ███████╗  ██████╗ ████████╗  ██████╗
// ██║   ██║ ████╗ ██║ ██║ ╚══██╔══╝    ╚══██╔══╝ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██╔════╝
// ██║   ██║ ██╔██╗██║ ██║    ██║          ██║    █████╗   ╚█████╗     ██║    ╚█████╗
// ██║   ██║ ██║╚████║ ██║    ██║          ██║    ██╔══╝    ╚═══██╗    ██║     ╚═══██╗
// ╚██████╔╝ ██║ ╚███║ ██║    ██║          ██║    ███████╗ ██████╔╝    ██║    ██████╔╝
//  ╚═════╝  ╚═╝  ╚══╝ ╚═╝    ╚═╝          ╚═╝    ╚══════╝ ╚═════╝     ╚═╝    ╚═════╝

// @function Cache module unit tests, for inclusion in parent script test suite. Usage: log.unittest_cache(__ASSERTS)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
export unittest_cache(string[] case) =>
    assert.new_case(case, 'Cache:init')
    [ks, vs] = init()
    assert.is_false(has(ks, vs, 'test'), case, 'has false')

    assert.new_case(case, 'Cache:set #1')
    set(ks, vs, 'test', 1)
    assert.equal(array.size(ks), 1, case, 'keys size')
    assert.equal(array.size(vs), 1, case, 'values size')
    assert.is_true(has(ks, vs, 'test'), case, 'has')
    assert.equal(get(ks, vs, 'test'), 1, case, 'get')

    assert.new_case(case, 'Cache:set #2')
    set(ks, vs, 'test', 2)
    assert.equal(array.size(ks), 1, case, 'keys size')
    assert.equal(array.size(vs), 1, case, 'values size')
    assert.is_true(has(ks, vs, 'test'), case, 'has')
    assert.equal(get(ks, vs, 'test'), 2, case, 'get')

    assert.new_case(case, 'Cache:set #3')
    set(ks, vs, 'test2', 1.1)
    assert.equal(array.size(ks), 2, case, 'keys size')
    assert.equal(array.size(vs), 2, case, 'values size')
    assert.is_true(has(ks, vs, 'test2'), case, 'has')
    assert.equal(get(ks, vs, 'test2'), 1.1, case, 'get')

    assert.new_case(case, 'Cache:remove')
    remove(ks, vs, 'test')
    assert.equal(array.size(ks), 1, case, 'keys size')
    assert.equal(array.size(vs), 1, case, 'values size')
    assert.is_false(has(ks, vs, 'test'), case, 'has')
    assert.nan(get(ks, vs, 'test'), case, 'get')
    assert.is_true(has(ks, vs, 'test2'), case, 'has test2')
    assert.equal(get(ks, vs, 'test2'), 1.1, case, 'get test2')

    assert.new_case(case, 'Cache:clear')
    clear(ks, vs)
    assert.equal(array.size(ks), 0, case, 'keys size')
    assert.equal(array.size(vs), 0, case, 'values size')
    assert.is_false(has(ks, vs, 'test'), case, 'has test')
    assert.is_false(has(ks, vs, 'test2'), case, 'has test2')

    assert.new_case(case, 'Cache:count')
    assert.equal(count(ks, vs), 0, case, 'count is 0')
    set(ks, vs, 'test', 1)
    assert.equal(count(ks, vs), 1, case, 'count is 1')
    set(ks, vs, 'test2', 2)
    assert.equal(count(ks, vs), 2  , case, 'count is 2')

    assert.new_case(case, 'Cache:loop')
    i = 0
    while loop(ks, vs)
        i += 1
    assert.equal(i, 2, case, 'loop')

    // These static calls of next are only valid on the first bar and should not be used outside a while loop
    assert.new_case(case, 'Cache:next')
    [key1, value1] = next(ks, vs)
    assert.str_equal(key1, 'test', case, 'key1')
    assert.equal(value1, 1, case, 'value1')
    [key2, value2] = next(ks, vs)
    assert.str_equal(key2, 'test', case, 'key2')
    assert.equal(value2, 1, case, 'value2')

    i := 0
    while loop(ks, vs)
        i += 1
        [k, v] = next(ks, vs)
        assert.str_equal(k, i == 1 ? 'test' : i == 2 ? 'test2' : 'unknown', case, 'loop key' + str.tostring(i))
        assert.equal(v, i, case, 'loop value' + str.tostring(i))

// @function Run the cache module unit tests as a stand alone. Usage: cache.unittest()
// @param verbose bool, optionally disable the full report to only display failures
export unittest(bool verbose = true) =>
    if assert.once()
        __ASSERTS = assert.init()
        unittest_cache(__ASSERTS)
        assert.report(__ASSERTS, verbose)


// ██████╗  ██╗   ██╗ ███╗  ██╗
// ██╔══██╗ ██║   ██║ ████╗ ██║
// ██████╔╝ ██║   ██║ ██╔██╗██║
// ██╔══██╗ ██║   ██║ ██║╚████║
// ██║  ██║ ╚██████╔╝ ██║ ╚███║
// ╚═╝  ╚═╝  ╚═════╝  ╚═╝  ╚══╝

unittest()

//         _.-'~~`~~'-._
//      .'`  B   E   R  `'.
//     / I               T \
//   /`       .-'~"-.       `\
//  ; L      / `-    \      Y ;
// ;        />  `.  -.|        ;
// |       /_     '-.__)       |
// |        |-  _.' \ |        |
// ;        `~~;     \\        ;
//  ;  INGODWE /      \\)P    ;
//   \  TRUST '.___.-'`"     /
//    `\                   /`
//      '._   1 9 9 7   _.'
//         `'-..,,,..-'`  - No! No! No! That's not what I meant!
