ObjectStack
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © cryptolinx - jango_blockchained

//@version=5

// @description 
library("ObjectStack", false)

//  ▪ Library Export ────
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

// ──── Init helper function. Recommanded, not required {   
export init() => 
    var label [] xLABEL = array.new_label(), var label [] fLABEL = array.new_label()
    var line [] xLINE = array.new_line(), var line [] fLINE = array.new_line()
    var box [] xBOX = array.new_box(), var box [] fBOX = array.new_box()
    var linefill [] xFILL = array.new_linefill(), var linefill [] fFILL = array.new_linefill()
    var table [] TABLE = array.new_table(), //
    [xLABEL, fLABEL, xLINE, fLINE, xBOX, fBOX, xFILL, fFILL, TABLE]
// }

// ──── Push object into corr. array. Equivalent to array.push() {
export push(series label [] _stack, series label _obj) => array.push(_stack, _obj)
export push(series line [] _stack, series line _obj) => array.push(_stack, _obj)
export push(series linefill [] _stack, series linefill _obj) => array.push(_stack, _obj)
export push(series box [] _stack, series box _obj) => array.push(_stack, _obj)
export push(series table [] _stack, series table _obj) => array.push(_stack, _obj)
// }

// ──── Get next stack index {
export nextIndex(series label [] _stack) => int(array.size(_stack))
export nextIndex(series line [] _stack) => int(array.size(_stack))
export nextIndex(series box [] _stack) => int(array.size(_stack))
export nextIndex(series linefill [] _stack) => int(array.size(_stack))
export nextIndex(series table [] _stack) => int(array.size(_stack))
// }

// ──── Delete forwarding stacks {
export delete(series line [] _arr) =>
    if array.size(_arr) > 0
        for i = 0 to array.size(_arr) - 1 by 1
            line.delete(array.get(_arr, i))
    array.clear(_arr)
// ──
export delete(series label [] _arr) =>
    if array.size(_arr) > 0
        for i = 0 to array.size(_arr) - 1 by 1
            label.delete(array.get(_arr, i))
    array.clear(_arr)
// ──
export delete(series box [] _arr) =>
    if array.size(_arr) > 0
        for i = 0 to array.size(_arr) - 1 by 1
            box.delete(array.get(_arr, i))
    array.clear(_arr)
// ──
export delete(series linefill [] _arr) =>
    if array.size(_arr) > 0
        for i = 0 to array.size(_arr) - 1 by 1
            linefill.delete(array.get(_arr, i))
    array.clear(_arr)
// ──
export delete(series table [] _arr) =>
    if array.size(_arr) > 0
        for i = 0 to array.size(_arr) - 1 by 1
            table.delete(array.get(_arr, i))
    array.clear(_arr)
// }  
    
// ──── Fixed stack: clean oldest entries {
export cleanOldest(series line [] _arrFix, series line [] _arrFor, series int _max = 500, series int _maxFixedPercent = 75) =>
    int arrFixSize = array.size(_arrFix), int arrForSize = array.size(_arrFor)
    int maxFixed = math.min(math.ceil(_max * _maxFixedPercent / 100), math.abs(_max - arrForSize))
    if arrFixSize > 0 and arrFixSize > maxFixed
        for i = (arrFixSize - maxFixed - 1) to 0 by 1
            line.delete(array.get(_arrFix, i)), array.remove(_arrFix, i)
// ──
export cleanOldest(series label [] _arrFix, series label [] _arrFor, series int _max = 500, series int _maxFixedPercent = 75) =>
    int arrFixSize = array.size(_arrFix), int arrForSize = array.size(_arrFor)
    int maxFixed = math.min(math.ceil(_max * _maxFixedPercent / 100), math.abs(_max - arrForSize))
    if arrFixSize > 0 and arrFixSize > maxFixed 
        for i = (arrFixSize - maxFixed - 1) to 0 by 1
            label.delete(array.get(_arrFix, i)), array.remove(_arrFix, i)
// ──
export cleanOldest(series box [] _arrFix, series box [] _arrFor, series int _max = 500, series int _maxFixedPercent = 75) =>
    int arrFixSize = array.size(_arrFix), int arrForSize = array.size(_arrFor)
    int maxFixed = math.min(math.ceil(_max * _maxFixedPercent / 100), math.abs(_max - arrForSize))
    if arrFixSize > 0 and arrFixSize > maxFixed
        for i = (arrFixSize - maxFixed - 1) to 0 by 1
            box.delete(array.get(_arrFix, i)), array.remove(_arrFix, i)
// ──
export cleanOldest(series linefill [] _arrFix, series linefill [] _arrFor, series int _max = 500, series int _maxFixedPercent = 75) =>
    int arrFixSize = array.size(_arrFix), int arrForSize = array.size(_arrFor)
    int maxFixed = math.min(math.ceil(_max * _maxFixedPercent / 100), math.abs(_max - arrForSize))
    if arrFixSize > 0 and arrFixSize > maxFixed
        for i = (arrFixSize - maxFixed - 1) to 0 by 1
            linefill.delete(array.get(_arrFix, i)), array.remove(_arrFix, i)
// ──
export cleanOldest(series table [] _arrFix, series table [] _arrFor, series int _max = 350, series int _maxFixedPercent = 75) =>
    int arrFixSize = array.size(_arrFix), int arrForSize = array.size(_arrFor)
    int maxFixed = math.min(math.ceil(_max * _maxFixedPercent / 100), math.abs(_max - arrForSize))
    if arrFixSize > 0 and arrFixSize > maxFixed
        for i = (arrFixSize - maxFixed - 1) to 0 by 1
            table.delete(array.get(_arrFix, i)), array.remove(_arrFix, i)
// }        

// ──
export cleanOldest(series string [] _arrReg, series label [] _arrFixed, series int _max = 400) =>
    int arrRegSize = array.size(_arrReg), int ctr = 0
    if arrRegSize > _max
        for i =  (arrRegSize - _max)  to 0 by 1
            label.delete(array.get(_arrFixed, ctr)), array.set(_arrFixed, ctr, na), ctr += 1
// }

// ──── Get Registry Index By Object Title {
export getIndexByTitle(series string [] _arrReg, series string _title) => int _return = array.indexof(_arrReg, _title)
// }

// ──── Get Next Registry Index {
export getNextIndex(series string [] _arrReg) => int _return = array.size(_arrReg)
// }

// ──── Show Last {
export getSeriesTitle(series string _title) => var int ctr = 0, ctr += 1, string(str.format('{0}_{1}_{2}', _title, ctr, timenow))
// }

// ──── Show Last {
export showLast(series int _bars) => bool(bar_index > (last_bar_index - (_bars <= 0 ? last_bar_index : _bars)))
// }

// ──── All-In-One Function - labelFix (eq. to label.new) {
export labelFixed(series string _title = na, series int _offsetX = na, series float _y = na, series string _text = na, series string _xloc = xloc.bar_index,
 series string _yloc = yloc.price, series color _color = color.blue, series string _style = label.style_label_center, series color _textcolor = color.white, 
 series string _size = size.normal, series string _textalign = text.align_center, series string _tooltip = na, series int _max = 400) =>
    string sTitle = getSeriesTitle(_title), var int max = _max, var label [] LABELS = array.new_label(5000, na), var string [] REG = array.new_string(), label labelId = na
    int index = getIndexByTitle(REG, sTitle), cleanOldest(REG, LABELS, max)
    if index == -1
        labelId := label.new(x = bar_index + _offsetX, y = _y, text = _text, color = _color, xloc = _xloc, yloc = _yloc, style = _style, textcolor = _textcolor, size = _size, textalign = _textalign, tooltip = _tooltip)
        index := getNextIndex(REG), array.push(REG, sTitle), array.set(LABELS, index, labelId)
    else if index > -1
        labelId := array.get(LABELS, index)
    [label(labelId), LABELS, REG]
    

// ──── ** New ** All-In-One Function - labelForward (eq. to label.new) {
export labelForward(series string _title, series int _offsetX = na, series float _y = na, series string _text = na, series string _xloc = xloc.bar_index,
 series string _yloc = yloc.price, series color _color = color.blue, series string _style = label.style_label_center, series color _textcolor = color.white, 
 series string _size = size.normal, series string _textalign = text.align_center, series string _tooltip = na, series int _max = 100) =>
    string sTitle = getSeriesTitle(_title), var label [] LABELS = array.new_label(5000, na), var string [] REG = array.new_string(), label labelId = na
    int index = getIndexByTitle(REG, sTitle), cleanOldest(REG, LABELS, _max)
    if index == -1
        labelId := label.new(x = bar_index + _offsetX, y = _y, text = _text, color = _color, xloc = _xloc, yloc = _yloc, style = _style, textcolor = _textcolor, size = _size, textalign = _textalign, tooltip = _tooltip)
        index := getNextIndex(REG), array.push(REG, sTitle), array.set(LABELS, index, labelId)
    else if index > -1
        labelId := array.get(LABELS, index), label.set_x(labelId, bar_index + _offsetX)
    [label(labelId), LABELS, REG]
// }  

//  ▪ EXAMPLE ────
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

// ! LIMITATION: Libraries are limited to ~100 objects. Indicator and strategies coming up with 500 objects for each type, by default. 
// !             If there is an official description, please let me know! :)
// !
// ! INDICATOR EXAMPLE https://www.tradingview.com/script/Pm3zSRnd/

// ──── Declare before indicator() / strategy() tag {
var int MaxLabelsCount = 50 //, var int MaxLinesCount = 100, var int MaxBoxesCount = 100
var int MaxFixedLabelsPercent = 80
// }

// ──── Init {
[xLABEL, fLABEL, xLINE, fLINE, _, _, _, _, _] = init()
// --
delete(fLABEL), delete(fLINE) //, delete(fBOX)
// }

// ──── Fixed Labels (xLABEL) (left-locked) {
var __BARS = input.int(0,"Show Last", minval = 0, maxval = 10000, step = 50, tooltip = '0 = Show All')
//--
var int ctr = 0
if bar_index % 25 == 1 and showLast(__BARS)
    ctr += 1, push(xLABEL, label.new(x=bar_index, y=0, text=str.format("FIXED (@xLABEL)\n{0}\n\nBar_Index: {1}\n\nRecursive Array_Index: {2}", ctr, bar_index, '###'), color=color.blue, textcolor=color.white, style=ctr % 2 == 1 ? label.style_label_down : label.style_label_up))
// }

// ──── Forwarding Labels (fLABEL) (right-locked) {
int labelIndex = na
if barstate.islast
    push(fLABEL, label.new(bar_index + 10, 0, text=str.format("FORWARD 1 (@fLABEL)\n\nShow Last: {0} bars\n\nChange Input 'Show Last'!", (__BARS == 0 ? "0 = All" : str.tostring(__BARS))), color=color.red, textcolor=color.white, style=label.style_label_right))
    // --
    labelIndex := nextIndex(fLABEL)
    push(fLABEL, label.new(bar_index + 15, 0, text="", color=color.orange, textcolor=color.black, style=label.style_label_left))
// }

// ──── Debug: before {
plotchar(array.size(fLABEL),"fLABEL before",char='',color=#FFFFFF00)
plotchar(array.size(xLABEL),"xLABEL before",char='',color=#FFFFFF00)
// }

// ──── After all object calls, we need to clean our fixed object stacks {
cleanOldest(xLINE, fLINE) // Use default values for _max and _maxFixedPercent
cleanOldest(xLABEL, fLABEL, MaxLabelsCount, MaxFixedLabelsPercent)
// }

// ──── Debug: after {
plotchar(array.size(fLABEL),"fLABEL after",char='',color=#FFFFFF00)
plotchar(array.size(xLABEL),"xLABEL after",char='',color=#FFFFFF00)
// }

// ──── Only for example {
if barstate.islast
    label.set_text(array.get(fLABEL, labelIndex), str.format("FORWARD 2 (@fLABEL)\n\nMax Labels Count: {6}\nMax Fixed Labels Percent: {7} %\nMax Fixed Labels Amount: {8}\n\n Current Fixed Labels Amount: {0}\nCurrent Fixed Label Index Range: {3} - {4}\nCurrent Forwarding Label Amount: {1}\nTotal Label Amount: {2}\nCurrent bar_index: {5}",
     array.size(xLABEL), array.size(fLABEL), (array.size(xLABEL) + array.size(fLABEL)), (ctr - array.size(xLABEL)), ctr, bar_index, MaxLabelsCount, MaxFixedLabelsPercent, (MaxLabelsCount * MaxFixedLabelsPercent / 100)))
    for i = 0 to array.size(xLABEL) - 1 by 1
        string rString = label.get_text(array.get(xLABEL, i))
        label.set_text(array.get(xLABEL, i), str.substring(rString, 0, str.length(rString) - 3) + str.tostring(i,"000"))
// }

// ──── } #EOF