[-_-] Dictionary
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © -_-

//@version=5
indicator("[-_-] Dictionary", overlay=true)

// Custom type that has two string arrays (one for storing keys, another for storing values)
type Dictionary
    array<string> keys
    array<string> values

// Convert key or value to string to be stored in dictionary
_convertTo(string key_or_value) => "DTS_" + key_or_value
_convertTo(int key_or_value) => "DTI_" + str.tostring(key_or_value)
_convertTo(float key_or_value) => "DTF_" + str.tostring(key_or_value)
_convertTo(bool key_or_value) => "DTB_" + (key_or_value ? 'T' : 'F')
_convertTo(color key_or_value) => 
    r = str.tostring(color.r(key_or_value))
    g = str.tostring(color.g(key_or_value))
    b = str.tostring(color.b(key_or_value))
    a = str.tostring(color.t(key_or_value))
    "DTC_" + r + "." + g + "." + b + "." + a

// Convert key or value stored in dictionary as string to string
_convertFromString(string key_or_value) => 
    if not na(key_or_value)
        str.substring(key_or_value, 4)
    else
        na

// Convert key or value stored in dictionary as string to integer
_convertFromInt(string key_or_value) => 
    if not na(key_or_value)
        int(str.tonumber(str.substring(key_or_value, 4)))
    else
        na

// Convert key or value stored in dictionary as string to float
_convertFromFloat(string key_or_value) => 
    if not na(key_or_value)
        str.tonumber(str.substring(key_or_value, 4))
    else
        na

// Convert key or value stored in dictionary as string to boolean
_convertFromBool(string key_or_value) => 
    if not na(key_or_value)
        (str.substring(key_or_value, 4, 5) == 'T') ? true : false
    else
        na

// Convert key or value stored in dictionary as string to color
_convertFromColor(string key_or_value) => 
    if not na(key_or_value)
        components = str.split(str.substring(key_or_value, 4), '.')
        r = str.tonumber(array.get(components, 0))
        g = str.tonumber(array.get(components, 1))
        b = str.tonumber(array.get(components, 2))
        a = str.tonumber(array.get(components, 3))
        color.new(color.rgb(r, g, b), a)
    else
        na

// Without this the script will display error "Can't call array methods when ID of array is na"
method init(Dictionary dict) =>
    dict.keys := array.new<string>(0, na)
    dict.values := array.new<string>(0, na)

// Set value by key (key and value can have any of the following types: string, int, float, bool, color); if key already exists, changes the value for that key to new one
method set(Dictionary dict, key, value) =>
    key_ = _convertTo(key)
    value_ = _convertTo(value)
    id_ = dict.keys.indexof(key_)
    if id_ == -1
        dict.values.push(value_)
        dict.keys.push(key_)
    else    
        dict.values.set(id_, value_)
        dict.keys.set(id_, key_)

// Converts key to string representation used by dictionary and checks if such key exists
method _get(Dictionary dict, key) =>
    key_ = _convertTo(key)
    id_ = dict.keys.indexof(key_)
    if id_ != -1
        dict.values.get(id_)
    else
        na

// Get a string value by key of string/int/float/bool/color
method getS(Dictionary dict, string key) => _convertFromString(dict._get(key))
method getS(Dictionary dict, int key) => _convertFromString(dict._get(key))
method getS(Dictionary dict, float key) => _convertFromString(dict._get(key))
method getS(Dictionary dict, bool key) => _convertFromString(dict._get(key))
method getS(Dictionary dict, color key) => _convertFromString(dict._get(key))

// Get an integer value by key of string/int/float/bool/color
method getI(Dictionary dict, string key) => _convertFromInt(dict._get(key))
method getI(Dictionary dict, int key) => _convertFromInt(dict._get(key))
method getI(Dictionary dict, float key) => _convertFromInt(dict._get(key))
method getI(Dictionary dict, bool key) => _convertFromInt(dict._get(key))
method getI(Dictionary dict, color key) => _convertFromInt(dict._get(key))

// Get a float value by key of string/int/float/bool/color
method getF(Dictionary dict, string key) => _convertFromFloat(dict._get(key))
method getF(Dictionary dict, int key) => _convertFromFloat(dict._get(key))
method getF(Dictionary dict, float key) => _convertFromFloat(dict._get(key))
method getF(Dictionary dict, bool key) => _convertFromFloat(dict._get(key))
method getF(Dictionary dict, color key) => _convertFromFloat(dict._get(key))

// Get a boolean value by key of string/int/float/bool/color
method getB(Dictionary dict, string key) => _convertFromBool(dict._get(key))
method getB(Dictionary dict, int key) => _convertFromBool(dict._get(key))
method getB(Dictionary dict, float key) => _convertFromBool(dict._get(key))
method getB(Dictionary dict, bool key) => _convertFromBool(dict._get(key))
method getB(Dictionary dict, color key) => _convertFromBool(dict._get(key))

// Get a color value by key of string/int/float/bool/color
method getC(Dictionary dict, string key) => _convertFromColor(dict._get(key))
method getC(Dictionary dict, int key) => _convertFromColor(dict._get(key))
method getC(Dictionary dict, float key) => _convertFromColor(dict._get(key))
method getC(Dictionary dict, bool key) => _convertFromColor(dict._get(key))
method getC(Dictionary dict, color key) => _convertFromColor(dict._get(key))
 
// Removes element from dictionary
method remove(Dictionary dict, string key) =>
    key_ = _convertTo(key)
    id_ = dict.keys.indexof(key_)
    if id_ != -1
        dict.keys.remove(id_)
        dict.values.remove(id_)

// Returns length of the dictionary (number of keys)
method len(Dictionary dict) => dict.keys.size()


// Create dictionary and put some values in it
d = Dictionary.new()
d.init()

d.set('12', 'something')
d.set(2, color.rgb(0, 100, 240))
d.set(true, false)
d.set(12.24, 18)
d.set(color.rgb(24, 12, 298), 0.982)

// Get values from dictionary
v1 = d.getS('12')

v2 = d.getC(2)
v2_str = 'RGBA(' + str.tostring(color.r(v2)) + ', ' + str.tostring(color.g(v2)) + ', ' + str.tostring(color.b(v2)) + ', ' + str.tostring(color.t(v2)) + ')'

v3 = d.getB(true)
v3_str = str.tostring(v3)

v4 = d.getI(12.24)
v4_str = str.tostring(v4)

v5 = d.getF(color.rgb(24, 12, 298))
v5_str = str.tostring(v5)


// Testing if the value of an already existing key will be changed for new one)
d2 = Dictionary.new()
d2.init()
// Add a key 2 with value 'value'
d2.set(2, 'value')
// Change the value of key 2 for 'new value'
d2.set(2, true)
// Get the value by key 2
v6 = d2.getB(2)
v6_str = str.tostring(v6)


// Testing if an item will be removed from dictionary
d3 = Dictionary.new()
d3.init()
// Add 2 items
d3.set('key1', 'Value 1')
d3.set('key2', 'Value 2')
// Remove key1
d3.remove('key1')
// Try to get value by key 'key1'
v7 = d3.getS('key1')
v8 = d3.getS('key2')


// Get dictionary length
v9 = d3.len()
v9_str = str.tostring(v9)


// Display label
if barstate.islast
    te = "GET VALUES:\n• '12' = " + v1 + '\n• 2 = ' + v2_str + '\n• true = ' + v3_str + '\n• 12.24 = ' + v4_str + '\n• color.rgb(24, 12, 298) = ' + v5_str + '\n\n'
    te := te + "CHANGE EXISTING ITEM:\n• " + v6_str + '\n\n'
    te := te + "REMOVE ITEM:\n• key1 = " + v7 + '\n• key2 = ' + v8 + '\n\n'
    te := te + "LENGTH:\n• " + v9_str

    var label lbl = label.new(bar_index + 50, close, text=te, textalign=text.align_left)