modes = ["data", "pipe"]

methods = ["GET", "INCR", "LPOP", "LPUSH", "LPUSH (needed to benchmark LRANGE)",
           "PING_BULK", "PING_INLINE", "SADD", "SET", "SPOP",
           "LRANGE_100 (first 100 elements)"
           "LRANGE_300 (first 300 elements)"
           "LRANGE_500 (first 450 elements)"
           "LRANGE_600 (first 600 elements)"]
suffix = "-one"

base = "/files/redis-benchmark-json"

# We sort version-specific compiler comparisons from low to high score
sortValue = (pa,pb) ->
    [_, a] = pa
    [_, b] = pb
    a - b

# Sort version strings properly, considering rc, patch level, and beta versions
sortVersion = (pa,pb) ->
    [a, _] = pa
    [b, _] = pb
    [aversion, _] = a.split '_'
    [bversion, _] = b.split '_'
    [a1, a2, a3] = aversion.split '.'
    [b1, b2, b3] = bversion.split '.'
    if a1 == b1 and a2 == b2
        a3s = a3.split '-'
        b3s = b3.split '-'
        subd = a3s[0] - b3s[0]
        # If we know they won't be equal, then return subtracted:
        if subd != 0 then return subd
        # Here, a3s[0] == b3s[0], so we need to
        # compare sub-versions of either -N, -rcN, or -betaN
        a3sl = a3s.length > 1
        b3sl = b3s.length > 1
        if a3sl and b3sl
            a3s1 = a3s[1][0]
            b3s1 = b3s[1][0]
            # If they match rc tags or beta tags, compare at tag level
            # Beta sorts before RC sorts before Regular before Patch
            if (a3s1 == 'r' and b3s1 == 'r') or (a3s1 == 'b' and b3s1 == 'b')
                if a3s[1] > b3s[1] then return 1
                if a3s[1] < b3s[1] then return -1
                return 0
            else if (a3s1 == 'b' and b3s1 == 'r') then return -1
            else if (a3s1 == 'r' and b3s1 == 'b') then return 1
            else if (a3s1 == 'r' or a3s1 == 'b') then return -1
            else if (b3s1 == 'b' or b3s1 == 'r') then return 1
            return a3s1 - b3s1
        if a3sl and not b3sl
            # If a has a dash but b doesn't
            if a3s[1][0] == 'r' or a3s[1][0] == 'b' then return -1
            # Else, we have a patch release, so sort above the normal version
            return 1
        else if b3sl and not a3sl
            if b3s[1][0] == 'r' or b3s[1][0] == 'b' then return 1
            return -1
        # By here, we have covered every possible case for the third level
    else if a1 == b1 and a2 > b2 then return 1
    else if a1 == b1 and a2 < b2 then return -1
    else if a1 > b1 then return 1
    else if a1 < b1 then return -1
    else return 0

toCompilerStr = (version, mode, method) ->
    "COMPILER-#{version}-#{mode}-#{method}"

toCompiler = (version, mode, method, totalMethod) ->
    fileBase = toCompilerStr version, mode, method
    c = $.get "#{base}/#{fileBase}.json"
    c.done (j) ->
        datas[fileBase] = j.sort(sortValue)
        if not extremes[mode]
            extremes[mode] = {}
        # here, "method" is just the version number, so we don't
        # conflict with proper "methods" in the methods array.
        extremes[mode][totalMethod] =
            min: j[0][1]
            max: j[j.length-1][1]

fetchData = ->
    for mode in modes
        for method in methods
            x = $.get "#{base}/#{mode}-#{method}#{suffix}.json"
            # Have to properly capture mode/method.  A normal
            # closure won't work because JS is capturing the references,
            # not the values, so at the end, all the variables point
            # to the same value, which isn't what we want at all.
            c = (m1, m2) ->
                x.done (j) ->
                    sorted = false
                    if not datas[m1]
                        datas[m1] = {}
                    if options.sortValue
                        datas[m1][m2] = j.sort(sortValue)
                        sorted = true
                    else
                        datas[m1][m2] = j.sort(sortVersion)
                    if not extremes[m1]
                        extremes[m1] = {}
                    minMax = []
                    if sorted
                        minMax = j
                    else
                        minMax = j.slice(0)
                        minMax.sort(sortValue)
                    extremes[m1][m2] =
                        min: minMax[0][1]
                        max: minMax[minMax.length-1][1]
            c mode, method

# Split dashes, add spaces, then replace "gcc -4.9" with "gcc-4.9"
fixFlags = (compilerflags) ->
    compilerflags.split('-').join(' -').replace(' -4.9', '-4.9')

versionOptions = (vercompiler) ->
    s = vercompiler.split '_'
    if s.length > 1
        # We have a Redis Version and Compiler Flags
        [version, compilerflags] = s
        "#{version} (#{fixFlags compilerflags})"
    else
        # We only have Compiler Flags
        fixFlags vercompiler

maxGraphs = 64
createGraphDivs = ->
    s = $("style")
    for num in [maxGraphs..0]
        s.after("<div id='graph_#{num}' class='graph'/>")

originals = {}
createdAxes = {}
createXAxis = (data) ->
    xaxis = []
    if createdAxes[data]
        return createdAxes[data]

    for [version, score], idx in data
        # Replace version information with index
        if not originals[idx]
            originals[idx] = []
        originals[idx].push(data[idx][0]) # store original unbroken versioncompilerflags
        data[idx][0] = idx
        # Add x-axis idx->version lookup
        xaxis.push([idx, versionOptions version])
    createdAxes[data] = xaxis

origSettings = []
origZoom = null
drawGraph = (container, idx, method, mode, data, secondary, label, opts) ->
    if not secondary
        # Save original settings so we can toggle back from compiler view
        # "not secondary" because we don't want to cache the compiler view args
        origSettings[idx] = [container, idx, method, mode, data, secondary, label, opts]
        origZoom = options.scaleFactor
    Flotr.draw(container, [
        mouse:
            track: true
            relative: true
            trackFormatter: hoverShow
        data: data
        label: label
        lines:
            show:false
    ], opts)
    Flotr.EventAdapter.observe container, "flotr:click", (e) ->
        # e.hit is only populated if we click on a data point.  Other graph
        # clicks also trigger here, but we only want info clicks.
        if not e.hit then return
        # :sadface: hacky toggle() function
        if secondary
            # if secondary, switch back to primary graph using original settings
            options.scaleFactor = origZoom
            drawGraph.apply(this, origSettings[idx])
        else
            dataPoint = e.hit.series.xaxis.ticks[e.hit.index].label
            ver = dataPoint.split(' ')
            compilerStr = toCompilerStr ver[0], mode, method

            totalMethod = "#{ver[0]} #{method}"
            localDraw = ->
                data = datas[compilerStr]
                options.scaleFactor = null
                draw data, container, idx, mode, totalMethod, true

            if datas[compilerStr]
                localDraw()
            else
                originalTerm = originals[idx][e.hit.index]
#                console.log("original: ", dataPoint, originalTerm, method, mode)
                x = toCompiler ver[0], mode, method, totalMethod
                $.when(x).done -> localDraw()

hoverShow = (obj) ->
    tick = obj.nearest.xaxis.ticks[parseInt(obj.x)]
    if tick then tick.label + " = " + obj.y

draw = (data, selector, idx, mode, method, secondary) ->
    xaxis = createXAxis data
    console.log mode, method, extremes[mode][method]
    ymin = ymax = null
    autoscale = true
    if not (options.scaleFactor is null)
        min = extremes[mode][method].min
        max = extremes[mode][method].max
        ymin = min / options.scaleFactor
        ymax = max * ((options.scaleFactor - 1)/2 + 1)
        autoscale = false
    drawGraph $(selector)[0], idx, method, mode, data, secondary, null,
        title: method
        subtitle: mode
        fontSize: 15
        HtmlText: false  # required to use labelsAngle
        bars:
            show: true
            horizontal: false
            shadowSize: 3
            barWidth: 0.7
            fill: true
            align: "center"
        xaxis:
            ticks: xaxis
            noticks: xaxis.length
            labelsAngle: -45
            autoscale: true
            title: "Redis Version with Compiler and Options"
        yaxis:
            autoscale: autoscale
#            autoscaleMargin: 0
            min: ymin
            max: ymax
            title: "Operations per second"
        y2axis:
            autoscale: true
            titleAngle: 270
            autoscaleMargin: 0
        grid:
            verticalLines: false
            minorVerticalLines: false

options =
    scaleFactor: 1.01
    sortValue: false

drawEverything = () ->
    i = 0
    $(".graph").css('display', 'block')
    for mode in modes
        for method in methods
            console.log("Using", mode, method)
            draw datas[mode][method], "#graph_#{i}", i, mode, method
            i++

# datas = cache of all retrieved data
datas = {}
extremes = {}

initGraphs = (hide) ->
    waitForThese = fetchData()
    # Merge all the wait lists together so we can single-done deferred wait
    waitFor = $.map waitForThese, (e) -> e

    $.when.apply($, waitFor).done ->
        drawEverything()
        # We hide the generated graphs at first so people
        # don't have a tiny scroll bar before they reach the graphs.
        $(".graph").css('display', 'none') if hide

$ ->
    createGraphDivs()
    initGraphs true

    $('#useful').click (e) ->
        options.scaleFactor = 1.09
        drawEverything()

    $('#maxchange').click (e) ->
        options.scaleFactor = null
        drawEverything()

    $('#sortall').click (e) ->
        options.sortValue = not options.sortValue
        options.scaleFactor = null
        if options.sortValue
            $(this).text 'Sort by Version Number'
        else
            $(this).text 'Sort from Low to High'
        # Yes, it's inefficient to re-fetch every graph JSON to re-sort the data
        # but at this point the data storage is pretty baked into the API and
        # it's easier to re-fetch instead of re-sort our cached data (and the
        # re-un-sort it) depending on user state.
        # Spoiler: The JSON URLs never expire, so the re-fetch should be
        # entirely from your local cache.
        initGraphs()