Gann Spiral / Square of 9
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © S4M44R

//@version=5
indicator(title="Gann Spiral / Square of 9", shorttitle="Spiral / Sq9", overlay=true, max_lines_count=500)

// ----------------------
// --       Tips       --
// ----------------------

modeTip = "Set which direction to draw price lines based on whether the input price is considered to be low, high, or both"
fTimeTip = "Set whether time lines are plotted in the future or the past."
priceTip = "Set the price value to use to draw price levels"
dateTip = "Set the time to use to draw time levels"
timeTip = "Set the timeframe to use to draw time levels"
denomTip = "ONLY USE IF YOU KNOW WHAT YOU'RE DOING! \n \nThis changes the value used to determine what constitutes as a full revolution. \n \nInputting 2π or ~ 6.28 will allow you to base your calculations on radians"

pLenTip = "Set the number of price levels to plot."
tLenTip = "Set the number of time levels to plot"

priceDegreeTip = "Set the degrees to rotate around a spiral to draw price levels. 1 Revolution = 360° \n \nThis is influenced by the Full Revolution Value"
timeDegreeTip = "Set the degrees to rotate around a spiral to draw time levels. 1 Revolution = 360° \n \nThis is influenced by the Full Revolution Value"

// ---------------------------
// --       User Inputs     --
// ---------------------------

var iMode = input.string("Low", "Price Direction", options = ["Low", "High", "Both"], tooltip = modeTip, group = "Modes")
var fTime = input.string("Future", "Time Flip", options = ["Future", "Past"], tooltip = fTimeTip, group = "Modes")
var iPrice = input.price(1, "Price", confirm = true, tooltip = priceTip, group = "Price & Time")
var iDate = input.time(timestamp("20 Jul 2021 00:00 +0300"), "Date", confirm = true, tooltip = dateTip, group = "Price & Time")
var iTime = input.timeframe("D", "Timeframe", tooltip = timeTip, group = "Price & Time")
var iFullRev = input.float(360, "Full Revolution Value", tooltip = denomTip, group = "Price & Time")
pLen = input.int(16, "Price Lines", maxval = 500, minval = 1, tooltip = pLenTip, group = "Lines")
tLen = input.int(4, "Time Lines", maxval = 500, minval = 1, tooltip = tLenTip, group = "Lines")
var iPriceDegree = input.float(45, "Price Rotation Degree", tooltip = priceDegreeTip, group = "Degrees")
var iTimeDegree = input.float(45, "Time Rotation Degree", tooltip = timeDegreeTip, group = "Degrees")
var tPrice = input.bool(true, "Toggle Price", group = "Customization")
var tTime = input.bool(true, "Toggle Time", group = "Customization")
var cPrice = input.color(color.purple, "Price Color", group = "Customization")
var cTime = input.color(color.teal, "Time Color", group = "Customization")
var cMidLine = input.color(color.yellow, "Midline Color", group = "Customization")
var iPriceStyle = input.string("Dotted", "Price Line Style", options = ["Dotted", "Solid", "Dashed"], group = "Customization")
var iTimeStyle = input.string("Dotted", "Time Line Style", options = ["Dotted", "Solid", "Dashed"], group = "Customization")
var iTableLocation = input.string("Top Right", "Table Position", options = ["Top Right", "Top Center", "Middle Right"])

// ----------------------------
// --       Time to Unix     --
// ----------------------------

// Borrow some code from TV docs to convert input timeframe to Unix
var float inputTf =
      timeframe.multiplier * (
      timeframe.isseconds ? 1. / 60             :
      timeframe.isminutes ? 1.                  :
      timeframe.isdaily   ? 60. * 24            :
      timeframe.isweekly  ? 60. * 24 * 7        :
      timeframe.ismonthly ? 60. * 24 * 30.4375  : na)
float unixTF = 60000 * request.security(syminfo.tickerid, iTime, inputTf)

// ---------------------------
// --       Initializer     --
// ---------------------------

// Convert price to degrees for future use
initializer = iFullRev * math.sqrt(iPrice)

// Initialize arrays
var pLowValues = array.new_float()
var pHighValues = array.new_float()
var tValues = array.new_float()

// Calculate key levels for price and time to store in arrays
if barstate.islastconfirmedhistory
    array.clear(pLowValues)
    array.clear(pHighValues)
    array.clear(tValues)
    if tPrice
        if iMode == "Low" or iMode == "Both"
            for i = iMode == "Both" ? 1 : 0 to pLen
                array.push(pLowValues, math.pow(((initializer + (iPriceDegree * i))/iFullRev), 2))
        if iMode == "High" or iMode == "Both"
            for i = iMode == "Both" ? 1 : 0 to iMode == "Both" ? pLen : pLen-1
                dInc = (initializer - (iPriceDegree * i))
                if dInc < 0
                    break
                array.push(pHighValues, math.pow((dInc/iFullRev), 2))
    if tTime
        if fTime == "Future"
            for i = 0 to tLen
                array.push(tValues, math.pow((((iTimeDegree * i))/iFullRev), 2) * unixTF + iDate)
        else
            for i = 0 to tLen
                array.push(tValues, iDate - math.pow((((iTimeDegree * i))/iFullRev), 2) * unixTF)

// ------------------------------
// --       Plotting Lines     --
// ------------------------------

// Variables to track # of lines plotted
pLowLineNum = 0
pHighLineNum = 0
tLineNum = 0

// Arrays to hold lines
var iLine = array.new_line()
var pLine = array.new_line()
var tLine = array.new_line()

// Determine style from user input
var pStyle = iPriceStyle == "Dotted" ? line.style_dotted : iPriceStyle == "Solid" ? line.style_solid : iPriceStyle == "Dashed" ? line.style_dashed : na
var tStyle = iTimeStyle == "Dotted" ? line.style_dotted : iTimeStyle == "Solid" ? line.style_solid : iTimeStyle == "Dashed" ? line.style_dashed : na

// Draw mid line if "Both" is selected
if barstate.islastconfirmedhistory and iMode == "Both" and tPrice
    array.push(iLine, line.new(0, iPrice, bar_index, iPrice, xloc=xloc.bar_time, color=cMidLine, extend=extend.both, style=pStyle))

// Loop to draw price lines
if barstate.islastconfirmedhistory and tPrice
    pLowLineNum := 0
    pHighLineNum := 0
    if iMode == "Low" or iMode == "Both"
        for i = 0 to pLen-1
            if pLowLineNum >= 500 and iMode != "Both"
                break
            else if pLowLineNum >=250
                break
            array.push(pLine, line.new(0, array.get(pLowValues, i), bar_index, array.get(pLowValues, i), xloc=xloc.bar_time, color=cPrice, extend=extend.both, style=pStyle))
            pLowLineNum += 1
    if iMode == "High" or iMode == "Both"
        for i = 0 to array.size(pHighValues)-1
            if pHighLineNum >= 500 and iMode != "Both"
                break
            else if pHighLineNum >= 250
                break
            array.push(pLine, line.new(0, array.get(pHighValues, i), bar_index, array.get(pHighValues, i), xloc=xloc.bar_time, color=cPrice, extend=extend.both, style=pStyle))
            pHighLineNum += 1

// Loop to draw time lines
if barstate.islastconfirmedhistory and tTime
    array.clear(tLine)
    tLineNum := 0
    for i = 0  to tLen-1
        if (pLowLineNum + pHighLineNum) >= 500 or tLineNum >= 500
            break
        array.push(tLine, line.new(int(array.get(tValues, i)), high, int(array.get(tValues, i)), low, xloc=xloc.bar_time, color=cTime, extend=extend.both, style=tStyle))
        tLineNum += 1

// -----------------------------------
// --       Information Display     --
// -----------------------------------

tableLocation = iTableLocation == "Top Right" ? position.top_right : iTableLocation == "Middle Right" ? position.middle_right : position.top_center

var squareNine = table.new(tableLocation, 3, 5, frame_width = 2, frame_color = color.black)

table.cell(squareNine, 1, 0, "Price", text_color = #5C87FF)
table.cell(squareNine, 2, 0, "Time", text_color =  #5C87FF)

table.cell(squareNine, 0, 1, "Plotted Lines:", text_color = #D55D48)
table.cell(squareNine, 1, 1, str.tostring(pLowLineNum + pHighLineNum), text_color = #D55D48)
table.cell(squareNine, 2, 1, str.tostring(tLineNum), text_color = #D55D48)

table.cell(squareNine, 0, 2, "Degree:", text_color = color.green)
table.cell(squareNine, 1, 2, str.tostring(iPriceDegree, '#.###'), text_color = color.green)
table.cell(squareNine, 2, 2, str.tostring(iTimeDegree, '#.###'), text_color = color.green)

table.cell(squareNine, 0, 3, "Starting Degree:", text_color = color.teal)
table.cell(squareNine, 1, 3, str.tostring(initializer, '#.##'), text_color = color.teal)

table.cell(squareNine, 0, 4, "Price:", text_color = color.orange)
table.cell(squareNine, 1, 4, str.tostring(iPrice), text_color = color.orange)