pson
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © marspumpkin
//@version=5

// @description: PineScript Object Notation. 
// A workaround not having objects in pinescript. 
// This is a Json-look-alike interpreter. 
// Format: '{attr={a={b=c:d={e=f}}}:attr1=value1:attr3={obj=val}}'
// Please only use whitespaces in the values
library("pson")

LINE_BREAK = ':'
EQUALS = '='
OBJ_BOUNDS_0 = '{'
OBJ_BOUNDS_1 = '}'

valid_syntax(string pson) => // more syntax checks to be added later
    str.startswith(pson, OBJ_BOUNDS_0) and str.endswith(pson, OBJ_BOUNDS_1)

get_pson_string(string pson) =>
    str.substring(pson, 1, str.length(pson) - 1)

char_at(string my_string, int i) =>
    if (i < str.length(my_string))
        str.substring(my_string, i, i + 1)
    else
        na
        
export get_iterable(string pson) =>
    itterable = array.new_string(0)
    
    if valid_syntax(pson)
        pson_string = get_pson_string(pson)
        
        if not (str.contains(pson_string, '{') and str.contains(pson_string, '}'))
            str.split(pson_string, ':')
        else // object within
            attr_start = 0
            depth = 0
            
            length = str.length(pson_string)
            for i = 0 to length
                char = char_at(pson_string, i)
                if char == OBJ_BOUNDS_0
                    depth := depth + 1
                if char == OBJ_BOUNDS_1
                    depth := depth - 1
                    
                if depth == 0 and (char == LINE_BREAK or i == length)
                    array.push(itterable, str.substring(pson_string, attr_start, i))
                    attr_start := i + 1
                    
            itterable            
    else
        itterable

export set(string pson, string attribute, string value) =>
    if valid_syntax(pson)
        result = ''
        for a in get_iterable(pson)
            attr = str.substring(a, 0, str.pos(a, EQUALS))
            val = str.substring(a, str.pos(a, EQUALS) + 1, str.length(a))
        
            if (attribute == attr)
                val := value
                
            result := result + attr + EQUALS + val + LINE_BREAK
        
        result := OBJ_BOUNDS_0 + str.substring(result, 0, str.length(result) - 1) + OBJ_BOUNDS_1
        result
    else
        na

export set_values(string pson, string[] attributes, string[] values) => // attributes and values must have the same length
    if valid_syntax(pson)
        result = ''
        iterable = get_iterable(pson)
        for i = 0 to array.size(iterable) - 1 by 1
            iterable_string = array.get(iterable, i)
            attr = str.substring(iterable_string, 0, str.pos(iterable_string, EQUALS))
            val = str.substring(iterable_string, str.pos(iterable_string, EQUALS) + 1, str.length(iterable_string))
        
            if (array.get(attributes, i) == attr)
                val := array.get(values, i)
                
            result := result + attr + EQUALS + val + LINE_BREAK
        
        result := OBJ_BOUNDS_0 + str.substring(result, 0, str.length(result) - 1) + OBJ_BOUNDS_1
        result
    else
        na
        
export add(string pson, string attr, string value) =>
    if valid_syntax(pson)
        result = ''
        pson_string = get_pson_string(pson)
        
        if str.contains(pson, attr + EQUALS)
            result := set(pson, attr, value)
        else
            if pson_string == ''
                result := OBJ_BOUNDS_0 + attr + EQUALS + value + OBJ_BOUNDS_1
            else
                result := OBJ_BOUNDS_0 + pson_string + LINE_BREAK + attr + EQUALS + value + OBJ_BOUNDS_1
                
        result
    else 
        na

export get(string pson, string attribute) =>
    result = ''
    for a in get_iterable(pson)
        attr = str.substring(a, 0, str.pos(a, EQUALS))
        val = str.substring(a, str.pos(a, EQUALS) + 1, str.length(a))
        
        if (attribute == attr)
            result := val
            break
    
    result

// loops from the last added down to the first and removes the attribute, returns updated pson
export remove(string pson, string attribute) =>
    result = ''
    iterable = get_iterable(pson)
    for i = array.size(iterable) - 1 to 0 by 1
        attr_val = array.get(iterable, i)
        if not str.startswith(attr_val, attribute)
            continue
        
        attr = str.substring(attr_val, 0, str.pos(attr_val, EQUALS))
        val = str.substring(attr_val, str.pos(attr_val, EQUALS) + 1, str.length(attr_val))
        
        if (str.contains(pson, LINE_BREAK + attr + EQUALS + val))
            result := str.replace(pson, LINE_BREAK + attr + EQUALS + val, '')
        else
            result := str.replace(pson, attr + EQUALS + val, '')
        break
    
    result

export get_values(string pson) =>
    result = array.new_string(0)
    for a in get_iterable(pson)
        val = str.substring(a, str.pos(a, EQUALS) + 1, str.length(a))
        array.push(result, val)
    
    result
    
export get_number(string pson, string attr) =>
    str.tonumber(get(pson, attr))

export get_int(string pson, string attr) =>
    math.round(get_number(pson, attr))

export get_bool(string pson, string attr) =>
    get(pson, attr) == 'true'
    
export exists(string pson, string key) =>
    str.contains(pson, key + EQUALS)

// sample code below
if barstate.islast
    my_object = '{attr={a={b=c:d={e=f}}}:attr1=value1:attr2={obj=val}}'
    output = get(my_object, 'attr')
    output := get(output, 'a') // {b=c:d={e=f}}
    output := get(output, 'd') // {e=f}
    
    arr = get_values(my_object)
    output := ''
    for v in arr
        output := output + v + ','
        
    
    attributes = array.from('attr', 'attr1', 'attr2')
    values = get_values(my_object) //array.from('{a=b}', '{b=c}', '{d=e}')
    
    output := set_values(my_object, attributes, values)
    
    // output := add('{}', 'key', 'value') // {key=value}
    // output := add(output, 'key', 'value1') // {key=value1}
    // output := add(output, 'key1', 'value2') // {key=value1:key1=value2}
    // itter = get_iterable(my_object)
    // output := array.get(itter, 0) // attr={a={b=c:d={e=f}}}
    
    output := remove(output, 'attr1')
    
    label.new(x=bar_index, y=high, textcolor=color.yellow, text=output, size=size.normal, style=label.style_none)
    
plot(close)