debugger
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
//
// SUMMARY
//    DEBUGGER is a library to help print debug messages to a console.
//
// DESCRIPTION 
//    This library provides an easy-to-use interface to print your debugging messages 
//    to a console in the chart. Special attention has been given to printing series 
//    and arrays easily.
//
//    A debugger is a valuable tool when working on scripts and getting into trouble.  
//    Unfortunately, TradingView does not provide an interactive debugger, and does 
//    not provide a console to use the oldest trick in the debugging book: print statements.  
//    This library provides you with the latter tool, print statements.
//
//    As a bonus, the library also provides a way to show labels in the chart next 
//    to the pricing action.
//
//  INTERFACE OVERVIEW
//    These are the main interface functions:
//        debugger         (consoleSize=20) -> [c, f]          Returns a debugger containing the console and the window frame
//        print            (c, f, message, condition)          Prints a debug message as string
//        print_series     (c, f, message, series, condition)  Prints a debug message for a float/int series
//        print_series_bool(c, f, message, series, condition)  Prints a debug message for a bool series
//        print_array      (c, f, message, array, condition)   Prints a debug message for an array of float/int
//        print_array_bool (c, f, message, array, condition)   Prints a debug message for an array of bool
//        show_label       (message, condition)                Shows a new debug message as a label
//        show_bar_index   ()                                  Shows bar indexes on the chart
//
// EXAMPLES
//    This is how you define and use a debug console and some usages.  The console will
//    show on the left hand side of the chart.
// 
//        import ed56/debugger/2 as db
//        [c, f] = db.debugger()
//
//        // Print a simple message for each bar
//        db.print(c, f, "Print a debug message")
//
//        // Print a message every 100 bars
//        db.print(c, f, str.format("Processed bar {0}", bar_index), bar_index % 100 == 0)
//
//        // Print the last 20 prices of a series as [1, 2, 3, ...]
//        db.print_series(c, f, "Close prices", close, size=20)
//
//        // Print the last 5 prices of an array as [1, 2, 3, ...]
//        db.print_array(c, f, "Array values", my_array, size=5)
//
//    This is how you define a label console, so we show messages in labels instead of the text console.
//
//        // Show a label above the price bar
//        db.show_label("Print a debug message")
// 
//        // Show a label below the price bar with an alternative color
//        db.show_label("Low" + str.tostring(low), (bar_index + 5) % 10 == 0, 
//                 aboveBar=false, textColor=color.maroon, bgColor=color.red)
//
//    And you can easily show the bar indexes on the chart as follows
//
//        db.show_bar_index()
//
// REMARKS
//    Ideally debugger() should just return the console, and there should be no need
//    to return the underlying table. The table should be available from the chart DOM using
//    table.all. The issue is that Pine Script v5 does not provide any way to access 
//    the contents or properties of the table to ensure we can identify the console table.
// 
//@version=5

library(title='debugger', overlay=true)

// ****************************************************************************************************************
// ****************************************************************************************************************
// 
// DEBUGGER CONSOLE METHODS
//
// ****************************************************************************************************************
// ****************************************************************************************************************

// @function debugger      Returns a debugger containing the console and the window frame
//                         
//                         You can declare and use a debugger as follows
//
//                             import ed56/Debugger/1 as db
//                             [c, f] = db.debugger()
//                             db.print(c, f, "Print a debug message")
//                             db.print_series(c, f, "Print the last 10 close prices", close, size=10)
//
// @param    consoleSize   The maximum number of lines in the debugger console
// @returns  dbConsole     The console containing debug messages
//           dbFrame       The window frame that will show the console
export debugger(simple int consoleSize=20) => 
    var int calls = 1
    var string[] dbConsole = array.new_string(consoleSize, '')
    var table dbFrame = table.new(
         position=position.bottom_left, 
         columns=1, 
         rows=1, 
         frame_color=na, 
         frame_width=1, 
         border_color=na, 
         border_width=1)
    if calls == 1
        table.cell(
             table_id=dbFrame,
             column=0,
             row=0,
             text="",
             width=100,
             height=100,
             text_color=chart.fg_color,
             text_halign=text.align_left,
             text_valign=text.align_bottom,
             text_size=size.normal)    
    calls += 1
    [dbConsole, dbFrame]

// @function print         Prints a debug message as string
//
// @param    dbConsole     The debugger console as returned by the debugger() method
//           dbFrame       The debugger frame as returned by the debugger() method
//           message       The message to print as string
//           condition     If true (default) then print the message to the console; if false then ignore the message
//           showBarIndex  If true (default) shows the current bar_index
export print(series string[] dbConsole, 
              series table   dbFrame, 
              series string  message, 
              series bool    condition = true, 
              bool           showBarIndex = true) => 
    if condition
        visibleMessage = message
        if showBarIndex
            visibleMessage := str.format("{0}: {1}", str.tostring(bar_index), message)
        array.shift(dbConsole)
        array.push(dbConsole, visibleMessage)
        
        debugText = array.join(dbConsole, '\n')
        table.cell_set_text(dbFrame, 0, 0, debugText)


// @function print_series  Prints a debug message for a float/int series.
//                         The message and series are only printed if the condition is true.
//                         The series is printed using the format [1, 2, 3, ...].
//                         The size parameter limits the values to print from the series.
//
// @param    dbConsole     The debugger console as returned by the debugger() method
//           dbFrame       The debugger frame as returned by the debugger() method
//           message       The message to print as string, before the series
//           source        The float/int series to print
//           condition     If true (default) then print the message to the console; if false then ignore the message
//           size          The max number of values to print from the series (default 10)
//           showBarIndex  If true (default) shows the current bar_index
export print_series(series string[] dbConsole, 
                     series table   dbFrame, 
                     series string  message, 
                     series float   source, 
                     series bool    condition = true, 
                     simple int     size = 10, 
                     simple bool    showBarIndex = true) => 
    visibleMessage = message
    if condition
        itemsStr = ""
        numItems = bar_index > size ? size : bar_index
        if numItems > 0
            for i = 0 to numItems - 1
                itemsStr := itemsStr + (i > 0 ? ", " : "") + str.tostring(source[i])
        visibleMessage := str.format("{0}: [{1}]", message, itemsStr)
    print(dbConsole, dbFrame, visibleMessage, condition, showBarIndex)

// @function print_series_bool  Prints a debug message for a bool series.
//                              The message and series are only printed if the condition is true.
//                              The series is printed using the format [true, false, true, ...].
//                              The size parameter limits the values to print from the series.
//
// @param    dbConsole     The debugger console as returned by the debugger() method
//           dbFrame       The debugger frame as returned by the debugger() method
//           message       The message to print as string, before the series
//           source        The bool series to print
//           condition     If true (default) then print the message to the console; if false then ignore the message
//           size          The max number of values to print from the series (default 10)
//           showBarIndex  If true (default) shows the current bar_index
export print_series_bool(series string[] dbConsole, 
                         series table    dbFrame, 
                         series string   message, 
                         series bool     source, 
                         series bool     condition = true, 
                         simple int      size = 10, 
                         simple bool     showBarIndex = true) => 
    visibleMessage = message
    if condition
        itemsStr = ""
        numItems = bar_index > size ? size : bar_index
        if numItems > 0
            for i = 0 to numItems - 1
                itemsStr := itemsStr + (i > 0 ? ", " : "") + str.tostring(source[i])
        visibleMessage := str.format("{0}: [{1}]", message, itemsStr)
    print(dbConsole, dbFrame, visibleMessage, condition, showBarIndex)

// @function print_array   Prints a debug message for an array of float/int.
//                         The message and array values are only printed if the condition is true.
//                         The array is printed using the format [1, 2, 3, ...].
//                         The size parameter limits the values to print from the array.
//
// @param    dbConsole     The debugger console as returned by the debugger() method
//           dbFrame       The debugger frame as returned by the debugger() method
//           message       The message to print as string, before the array
//           source        The float/int array to print
//           condition     If true (default) then print the message to the console; if false then ignore the message
//           size          The max number of values to print from the array (default 10)
//           showBarIndex  If true (default) shows the current bar_index
export print_array(series string[] dbConsole, 
                   series table    dbFrame, 
                   series string   message, 
                   series float[]  source, 
                   series bool     condition = true, 
                   simple int      size = 10, 
                   simple bool     showBarIndex = true) => 
    visibleMessage = message
    if condition
        itemsStr = ""
        numItems = array.size(source) > size ? size : array.size(source)
        if numItems > 0
            for i = 0 to numItems - 1
                itemsStr := itemsStr + (i > 0 ? ", " : "") + str.tostring(array.get(source, i))
        visibleMessage := str.format("{0}: (size={1}) [{2}]", message, array.size(source), itemsStr)
    print(dbConsole, dbFrame, visibleMessage, condition, showBarIndex)

// @function print_array_bool   Prints a debug message for an array of bool.
//                              The message and array values are only printed if the condition is true.
//                              The array is printed using the format [true, false, true, ...].
//                              The size parameter limits the values to print from the array.
//
// @param    dbConsole     The debugger console as returned by the debugger() method
//           dbFrame       The debugger frame as returned by the debugger() method
//           message       The message to print as string, before the array
//           source        The bool array to print
//           condition     If true (default) then print the message to the console; if false then ignore the message
//           size          The max number of values to print from the array (default 10)
//           showBarIndex  If true (default) shows the current bar_index
export print_array_bool(series string[] dbConsole, 
                         series table   dbFrame, 
                         series string  message, 
                         series bool[]  source, 
                         series bool    condition = true, 
                         simple int     size = 10, 
                         simple bool    showBarIndex = true) => 
    visibleMessage = message
    if condition
        itemsStr = ""
        numItems = array.size(source) > size ? size : array.size(source)
        if numItems > 0
            for i = 0 to numItems - 1
                itemsStr := itemsStr + (i > 0 ? ", " : "") + str.tostring(array.get(source, i))
        visibleMessage := str.format("{0}: (size={1}) [{2}]", message, array.size(source), itemsStr)
    print(dbConsole, dbFrame, visibleMessage, condition, showBarIndex)


// ****************************************************************************************************************
// ****************************************************************************************************************
// 
// LABEL MESSAGES 
//
// ****************************************************************************************************************
// ****************************************************************************************************************

// @function show_label    Shows a new debug message as a label
//                         The value of the label will be shown either above or below the current price bar (default above)
//
// @param    message       The message to print as string,
// @param    condition     If true (default) then print the message to the console; if false then ignore the message
// @param    aboveBar      If true (default) show the message above the price bar; if falst then show it below the price bar
// @param    simpleStyle   If true, we show the label as a text message on the chart using the color provided.  
//                         If false, we decorate the label with a background of the color provided, and black foreground.
// @param    color         The color of the label
export show_label(series string message, 
                  series bool   condition = true, 
                  simple bool   aboveBar = true,
                  simple bool   simpleStyle = true,
                  simple color  color = color.yellow) =>
    if condition
        ylocation    = aboveBar ? yloc.abovebar : yloc.belowbar
        labelStyle   = aboveBar ? label.style_label_down : label.style_label_up
        bgColor      = simpleStyle ? na : color
        textColor    = simpleStyle ? color : color.black
        labelMessage = not simpleStyle 
                             ? message 
                             : aboveBar ? str.format("{0}\n▼", message)
                             : str.format("▲\n{0}", message)
        newLabel   = label.new(x         = bar_index, 
                               y         = 0, 
                               yloc      = ylocation,
                               text      = labelMessage, 
                               textcolor = textColor, 
                               textalign = text.align_center,
                               color     = bgColor,
                               size      = size.normal, 
                               style     = labelStyle)
//}

// ****************************************************************************************************************
// ****************************************************************************************************************
// 
// BAR_INDEX
//
// ****************************************************************************************************************
// ****************************************************************************************************************

// @function show_bar_index  Shows bar indexes in the chart
//  
//                           Sometimes when debugging or refining your strategies over a specific date range, 
//                           when you want quickly find out the bar range in terms of bar_index.  
//
// @param    step            Step determines the separation between bars identified in the chart.  
//                           For example, step=10 will show every 1 in every 10 bars indexes.
// @param    from            Show bars from this index
// @param    until           Show bars until this index.  
//                           The default until=0 will show all bars 
// @param    color           The color of the bar index text on the chart
// @param    size            The size of the text
export show_bar_index(simple int    step=10, 
                      simple int    from=0,
                      simple int    until=0, 
                      simple color  color = color.gray,
                      simple string size = size.small) =>
    bar_until = until == 0 ? bar_index : until                     
    show = (bar_index % step == 0) and (bar_index >= from) and (bar_index <= bar_until)
    if show 
        label.new(x         = bar_index,
                  y         = 0,
                  yloc      = yloc.abovebar,
                  text      = str.format("{0}\n▼", bar_index),
                  tooltip   = str.format("Bar Index = {0}", bar_index),
                  textcolor = color,
                  size      = size,
                  color     = na,
                  style     = label.style_none)

// ****************************************************************************************************************
// 
// EXAMPLES
//
// ****************************************************************************************************************

show_text_conditional   = input.bool(true, title="Debug text message conditionally", group="Console Message")
show_text_if_block      = input.bool(true, title="Debug text message from IF block", group="Console Message")
show_text_spam          = input.bool(false, title="Write a lot of messages, cap to console size", group="Console Message")

show_float_series       = input.bool(true, title="Debug float/int series", group="Series")
show_float_series_start = input.bool(false, title="Debug float/int series for first few bars", group="Series")
show_bool_series        = input.bool(true, title="Debug bool series", group="Series")

show_float_array        = input.bool(false, title="Debug float/int array", group="Arrays")
show_float_array_start  = input.bool(false, title="Debug float/int array for first few bars", group="Arrays")
show_bool_array         = input.bool(true, title="Debug bool array", group="Arrays")

show_label_above        = input.bool(true, title="Show labels above price line", group="Labels")
show_label_below        = input.bool(false, title="Show labels below price line", group="Labels")
_show_simple_style      = input.string("Simple", title="Style", group="Labels", options=["Simple", "Background"])        
show_simple_style = "Simple" == _show_simple_style

show_barindex          = input.bool(false, title="Show bar index", group="Bar Index")
barindex_step          = input.int(10, title="Step", group="Bar Index")
barindex_from          = input.int(0, title="From", group="Bar Index")
barindex_until         = input.int(0, title="Until", group="Bar Index", tooltip="Use 0 to show until last bar")
barindex_color         = input.color(color.gray, "Style", group="Bar Index", inline="BI")
barindex_size          = input.string(size.small, "", group="Bar Index", inline="BI", options=[size.small, size.normal, size.large])

//******************************************************************************
// Declare the debugger
//
[c, f] = debugger()

//******************************************************************************
// Print message according to a condition
//
if show_text_conditional
    print(c, f, "Only show for first bar", barstate.isfirst)
    print(c, f, "Only show for last bar", barstate.islast)


//******************************************************************************
// Print messages within a conditional block
//
if show_text_if_block
    var numPrintsLeft = 3
    if numPrintsLeft > 0
        print(c, f, "We will print this debug message " + str.tostring(numPrintsLeft) + " times")
        numPrintsLeft -= 1
    
    // Print debug message from an conditional block
    if barstate.islast
        print(c, f, "This is the last bar printed from conditional block")

//******************************************************************************
// Print a lot of messages, cap it to debugger size limit
//
if show_text_spam
    if bar_index % 10 == 0
        print(c, f, "Bar Index=" + str.tostring(bar_index))

//******************************************************************************
// Print float/int series
//
if show_float_series
    print_series(c, f, "Print float series", close, barstate.islast)
    print_series(c, f, "Print int series", bar_index, barstate.islast, size=5)
    print_series(c, f, "Print High Prices every 2000 bars", high, bar_index % 2000 == 0, size=5)

//******************************************************************************
// Print float/int series from bar_index=0 (just a few bars)
//
if show_float_series_start
    print_series(c, f, "Print float series for first few bars", close, bar_index < 7)

//******************************************************************************
// Print bool series 
//
if show_bool_series
    var bool_series = true
    bool_series := bar_index % 2 == 0 ? true : false
    print_series_bool(c, f, "Print bool series", bool_series, barstate.islast, size=5)

//******************************************************************************
// Print float/int array
// 
if show_float_array
    // Print float array
    var float_array = array.new_float()
    if (array.size(float_array) >= 20) 
        array.pop(float_array)
    array.unshift(float_array, close[0])
    print_array(c, f, "Print float array", float_array, barstate.islast, size=10)

    // Print int array
    var int_array = array.new_int()
    if (array.size(int_array) >= 20) 
        array.pop(int_array)
    array.unshift(int_array, bar_index)
    print_array(c, f, "Print int array", int_array, barstate.islast, size=10)

//******************************************************************************
// Print float/int array from the start (just a few elements in the array)
// 
if show_float_array_start
    var float_array = array.new_float()
    if (array.size(float_array) >= 20) 
        array.pop(float_array)
    array.unshift(float_array, close[0])
    print_array(c, f, "Print float array", float_array, bar_index < 8, size=10)

//******************************************************************************
// Print bool array
// 
if show_bool_array
    var bool_array = array.new_bool()
    if (array.size(bool_array) >= 20) 
        array.pop(bool_array)
    array.unshift(bool_array, bar_index % 2 == 0)
    print_array_bool(c, f, "Print bool array", bool_array, barstate.islast, size=10)

//******************************************************************************
// Declare label debugger
// 

//******************************************************************************
// Show labels above price bars
// 
if show_label_above
    show_label("Close = " + str.tostring(close), bar_index % 40 == 0,
               simpleStyle = show_simple_style)

//******************************************************************************
// Show labels below price bars, and some customisation of the label
// 
if show_label_below
    show_label("Low Price = " + str.tostring(low), (bar_index + 5) % 40 == 0, 
               simpleStyle = show_simple_style,
               aboveBar    = false, 
               color       = color.red)

//******************************************************************************
// Show bar index
// 
if show_barindex
    show_bar_index(step  = barindex_step, 
                   from  = barindex_from, 
                   until = barindex_until, 
                   color = barindex_color, 
                   size  = barindex_size)

