--
-- Copyright (C) 2014-2020 by Fabian Lipp <fabian.lipp@gmx.de>
-- ------------------------------------------------------------
-- 
-- This file may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.2
-- of this license or (at your option) any later version.
-- The latest version of this license is in:
--
--    http://www.latex-project.org/lppl.txt
--
-- and version 1.2 or later is part of all distributions of LaTeX 
-- version 1999/12/01 or later.
--

require("lualibs")
--require("debugger")()
local inspect = require('inspect')

local point = require'path_point'
local pathLine = require'path_line'
--local bezier3 = require'path_bezier3'

luatodonotes = {}

-- strings used to switch to standard catcodes for LaTeX packages
local catcodeStart = "\\makeatletter"
local catcodeEnd = "\\makeatother"

local currentPage = 1

local const1In = string.todimen("1in") -- needed for calculations of page borders
                                       -- (used as a constant in TeX)
-- constants set in sty-file
-- + noteInnerSep (inner sep used for tikz nodes)
-- + noteInterSpace (vertical space between notes)
-- + routingAreaWidth (width of the track routing area for opo-leaders)
-- + minNoteWidth (width that must be available for labels to consider the left or
--     right border of the page for placing labels)
-- + distanceNotesPageBorder (distance from the page borders to the outmost point
--     of the labels)
-- + distanceNotesText (horizontal distance between the labels and the text area)
-- + rasterHeight (height of raster for po leader algorithm)
-- + todonotesDebug (activate debug outputs when true)
--
-- values are filled into local variables in function initTodonotes (from
-- corresponding fields luatodonotes.*)
local noteInnerSep = nil
local noteInterSpace = nil
local routingAreaWidth = nil
local minNoteWidth = nil
local distanceNotesPageBorder = nil
local distanceNotesText = nil
local rasterHeight = nil
local todonotesDebug = nil


-- stores information about available algorithms
local positioningAlgos = {}
local splittingAlgos = {}
local leaderTypes = {}

local positioning = nil
local splitting = nil
local leaderType = nil
function luatodonotes.setPositioningAlgo(algo)
    if positioningAlgos[algo] ~= nil then
        positioning = positioningAlgos[algo]
    else
        positioning = positioningAlgos["inputOrderStacks"]
        tex.print("\\PackageWarningNoLine{luatodonotes}{Invalid value for parameter positioning: " .. algo .. "}")
    end
end
function luatodonotes.setSplittingAlgo(algo)
    if splittingAlgos[algo] ~= nil then
        splitting = splittingAlgos[algo]
    else
        splitting = splittingAlgos["none"]
        tex.print("\\PackageWarningNoLine{luatodonotes}{Invalid value for parameter split: " .. algo .. "}")
    end
end
function luatodonotes.setLeaderType(typ)
    if leaderTypes[typ] ~= nil then
        leaderType = leaderTypes[typ]
    else
        leaderType = leaderTypes["opo"]
        tex.print("\\PackageWarningNoLine{luatodonotes}{Invalid value for parameter leadertype: " .. typ .. "}")
    end
end

-- stores the notes for the current page
luatodonotes.notesForPage = {}
local notesForPage = luatodonotes.notesForPage
luatodonotes.notesForNextPage = {}
local notesForNextPage = luatodonotes.notesForNextPage
-- Fields for each note:
-- index: numbers notes in whole document
-- indexOnPage: index of the note in the notesForPage array
-- textbox: links to a hbox that contains the text, which is displayed inside
--     the note
-- origInputX, origInputY: position in which the todo-command was issued
-- inputX, inputY: position to which the leader should be attached (can have a
--     certain offset to origInputX/Y)
-- heightLeft, heightRight: height of the contained text when placed on
--     left/right side
-- pageNr: absolute number of page on which site for note is placed
-- rightSide: true means the note should be placed on the right side;
--     otherwise left side is meant
-- fontsize: fontsize used for paragraph in that the note was defined
-- baselineskip: \baselineskip in the paragraph in that the note was defined
-- normalbaselineskip: \normalbaselineskip in the paragraph in that the note was defined
-- outputX, outputY: position on which the north west anchor of the note should
--     be placed
-- lineColor: color of line connecting note to text
-- backgroundColor: color of background of note
-- borderColor: color of border of note
-- leaderWidth: width of leader (used as argument for tikz line width)
-- sizeCommand: fontsize command given as parameter for this note
--
-- Additional fields for text area:
-- noteType: constant string "area"
-- origInputEndX, origInputEndY: position at which the todo area ends
-- pageNrEnd: absolute number of page on which todo area ends
-- lineCountInArea: highest line index in area
-- linesInArea: positions of lines in area (to detect page/column break)

-- stores the areas for the labels on the current page
-- (calculated in function calcLabelAreaDimensions())
local labelArea = {}

-- stores the positions of the text lines on every page
local linePositions = {}
-- Fields for every position:
-- 1: position of baseline
-- 2: upper bound of line (baseline + height)
-- 3: lower bound of line (baseline - depth)

-- variables used for construction of linePositions list when reading the file
local linePositionsCurPage = {}
local linePositionsPageNr = 0


-- *** metatable for note objects ***
local noteMt = {}
-- getHeight(): yield heightLeft or heightRight depending on rightSide (implemented by metatable)
function noteMt:getHeight()
    if self.rightSide then
        return self.heightRight
    else
        return self.heightLeft
    end
end
function noteMt:getLabelAnchorY()
    local leaderAnchor = positioning.leaderAnchor
    local y
    if leaderAnchor == "north east" then
        y = self.outputY
    elseif leaderAnchor == "east" then
        y = self.outputY - noteInnerSep - self:getHeight() / 2
    else
        error("Invalid anchor for algorithm")
    end

    if positioning.leaderShift then
        y = y + self.leaderShiftY
    end

    return y
end
function noteMt:getInTextAnchorTikz()
    return "(" .. self.inputX .. "sp," .. self.inputY .. "sp)"
end
function noteMt:getLabelAnchorTikz()
    local leaderAnchor = positioning.leaderAnchor
    if leaderAnchor == "north east" and self.rightSide then
        leaderAnchor = "north west"
    elseif leaderAnchor == "east" and self.rightSide then
        leaderAnchor = "west"
    end

    local shiftStr = ""
    if positioning.leaderShift then
        shiftStr = "[shift={(" .. self.leaderShiftX .. "sp," .. self.leaderShiftY .. "sp)}]"
    end

    return "(" .. shiftStr .. " @todonotes@" .. self.index .. " note." .. leaderAnchor ..  ")"
end
function noteMt:boxForNoteText(rightSide)
    local area = labelArea:getArea(rightSide)
    local noteWidth
    if area == nil then
        noteWidth = minNoteWidth
    else
        noteWidth = area.noteWidth - 2*noteInnerSep
    end
    local retval = "\\directlua{tex.box[\"@todonotes@notetextbox\"] = " ..
        "node.copy_list(luatodonotes.notesForPage[" .. self.indexOnPage .. "].textbox)}"
    retval = retval .. "\\parbox{" .. noteWidth .. "sp}" ..
        "{\\raggedright\\unhbox\\@todonotes@notetextbox}"
    return retval
end

function noteMt:getClipPathForTodoArea()
    -- detext which lines are in same column/page as start of area
    local lineCount = self.lineCountInArea
    local maxLine = 1
    local lines = self.linesInArea
    local lastY = lines[1]
    while maxLine < lineCount do
        if lines[maxLine + 1] < lastY then
            maxLine = maxLine + 1
            lastY = lines[maxLine]
        end
    end

    local function nodename(i, corner)
        return "(@todonotes@" .. self.index .. "@" .. i .. " area" .. corner .. ")"
    end
    local path = nodename(1, "NW")
    local pathLeft = ""
    if maxLine == 1 then
        -- only one line
        path = path .. " -- " .. nodename(1, "NE") ..
            " -- " .. nodename(1, "SE")
    else
        path = path .. " -- " .. nodename(1, "NE") ..
            " decorate[@todonotes@todoarea] { -- " .. nodename(1, "SE") .. "}"
    end
    for i = 2, maxLine do
        if i == lineCount then
            -- area does not use the whole line
            path = path .. " -| " .. nodename(i, "NE") ..
                " -- " .. nodename(i, "SE")
        else
            -- area uses whole line
            path = path .. " -- " .. nodename(i, "NE") ..
                " decorate[@todonotes@todoarea] { -- " .. nodename(i, "SE") .. "}"
        end
        pathLeft = " -- " .. nodename(i, "SW") ..
            " decorate[@todonotes@todoarea] { -- " .. nodename(i, "NW") .. "}" ..
            pathLeft
    end
    path = path .. pathLeft .. " -| " .. nodename(1, "SW") .. "-- cycle"
    return path
end



-- *** label areas ***
-- stores areas for placing labels on current page
function labelArea:getArea(rightSide)
    if rightSide then
        return self.right
    else
        return self.left
    end
end
-- yields the x-coordinate of the boundary of the label that is pointing
-- towards the text
function labelArea:getXTextSide(rightSide)
    if rightSide then
        return self.right.left
    else
        return self.left.right
    end
end
function labelArea:isOneSided()
    if self.right == nil or self.left == nil then
        return true
    else
        return false
    end
end



-- divides notes in two lists (for left and right side)
-- side must be stored in note.rightSide for every note before using this function
local function segmentNotes(notes)
    local availableNotesLeft = {}
    local availableNotesRight = {}
    for k, v in pairs(notes) do
        if v.rightSide == true then
            table.insert(availableNotesRight, k)
        else
            table.insert(availableNotesLeft, k)
        end
    end
    return availableNotesLeft, availableNotesRight
end



-- is called by the sty-file when all settings (algorithms etc.) are made
function luatodonotes.initTodonotes()
    -- fill local variables (defined at begin of file) with package options
    noteInnerSep = luatodonotes.noteInnerSep
    noteInterSpace = luatodonotes.noteInterSpace
    routingAreaWidth = luatodonotes.routingAreaWidth
    minNoteWidth = luatodonotes.minNoteWidth
    distanceNotesPageBorder = luatodonotes.distanceNotesPageBorder
    distanceNotesText = luatodonotes.distanceNotesText
    rasterHeight = luatodonotes.rasterHeight
    todonotesDebug = luatodonotes.todonotesDebug

    if positioning.needLinePositions then
        luatexbase.add_to_callback("post_linebreak_filter", luatodonotes.callbackOutputLinePositions, "outputLinePositions")
        tex.print("\\@starttoc{lpo}")
        tex.print("\\directlua{lpoFileStream = \\the\\tf@lpo}")
    end
end


-- valid values for noteType: nil/"" (for point in text), "area"
function luatodonotes.addNoteToList(index, drawLeader, noteType)
    if next(notesForPage) ~= nil
        and index <= notesForPage[#notesForPage].index then
        -- Index is the same as for one of the previous note.
        -- This can happen when commands are read multiple times
        -- => don't add anything to list in this case
        return
    end
    local newNote = {}
    newNote.index = index
    newNote.textbox = node.copy_list(tex.box["@todonotes@notetextbox"])
    newNote.baselineskip = tex.dimen["@todonotes@baselineskip"]
    newNote.normalbaselineskip = tex.dimen["@todonotes@normalbaselineskip"]
    newNote.fontsize = tex.dimen["@todonotes@fontsize"]
    newNote.lineColor = tex.toks["@todonotes@toks@currentlinecolor"]
    newNote.backgroundColor = tex.toks["@todonotes@toks@currentbackgroundcolor"]
    newNote.borderColor = tex.toks["@todonotes@toks@currentbordercolor"]
    newNote.leaderWidth = tex.toks["@todonotes@toks@currentleaderwidth"]
    newNote.sizeCommand = tex.toks["@todonotes@toks@sizecommand"]
    newNote.drawLeader = drawLeader
    if noteType == "area" then
        newNote.noteType = "area"
        newNote.lineCountInArea = 0
    -- else: newNote.noteType = nil (default value)
    end
    setmetatable(newNote, {__index = noteMt})
    newNote.indexOnPage = #notesForPage + 1
    notesForPage[newNote.indexOnPage] = newNote
end

function luatodonotes.clearNotes()
    -- delete the texts for the notes on this page from memory
    -- (garbage collection does not work for nodes)
    for _, v in pairs(notesForPage) do
        node.free(v.textbox)
    end
    luatodonotes.notesForPage = notesForNextPage
    notesForPage = luatodonotes.notesForPage
    -- update indexOnPage for the new notes
    for k, v in pairs(notesForPage) do
        v.indexOnPage = k
    end

    currentPage = currentPage + 1
end

function luatodonotes.processLastLineInTodoArea()
    -- LaTeX counter is accessed as TeX count by prefixing c@
    ind = tex.count["c@@todonotes@numberoftodonotes"]
    val = tex.count["c@@todonotes@numberofLinesInArea"]
    notesForPage[#notesForPage].lineCountInArea = val
end


-- *** constructing the linePositions list ***
function luatodonotes.linePositionsNextPage()
    linePositionsPageNr = linePositionsPageNr + 1
    linePositionsCurPage = {}
    linePositions[linePositionsPageNr] = linePositionsCurPage
end

function luatodonotes.linePositionsAddLine(ycoord, lineheight, linedepth)
    linePositionsCurPage[#linePositionsCurPage + 1] = {ycoord, lineheight, linedepth}
end


function luatodonotes.getInputCoordinatesForNotes()
    tex.sprint(catcodeStart)
    for k, v in ipairs(notesForPage) do
        local nodename = "@todonotes@" .. v.index .. " inText"

        tex.sprint("\\pgfextractx{\\@todonotes@extractx}{\\pgfpointanchor{" ..
            nodename .. "}{center}}")
        tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" ..
            nodename .. "}{center}}")
        tex.print("\\directlua{luatodonotes.notesForPage[" .. k .. "].origInputX = " ..
            "tex.dimen[\"@todonotes@extractx\"]}")
        tex.print("\\directlua{luatodonotes.notesForPage[" .. k .. "].origInputY = " ..
            "tex.dimen[\"@todonotes@extracty\"]}")

        if v.noteType == "area" then
            nodename = nodename .. "End"
            tex.sprint("\\pgfextractx{\\@todonotes@extractx}{\\pgfpointanchor{" ..
                nodename .. "}{center}}")
            tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" ..
                nodename .. "}{center}}")
            tex.print("\\directlua{luatodonotes.notesForPage[" .. k .. "].origInputEndX = " ..
                "tex.dimen[\"@todonotes@extractx\"]}")
            tex.print("\\directlua{luatodonotes.notesForPage[" .. k .. "].origInputEndY = " ..
                "tex.dimen[\"@todonotes@extracty\"]}")

            notesForPage[k].linesInArea = {}
            for i = 1, v.lineCountInArea do
                nodename = "@todonotes@" .. v.index .. "@" .. i .. " areaSW"
                    tex.sprint("\\pgfextracty{\\@todonotes@extracty}{\\pgfpointanchor{" ..
                        nodename .. "}{center}}")
                tex.print("\\directlua{luatodonotes.notesForPage[" .. k .. "].linesInArea[" ..
                    i .. "] = " .. "tex.dimen[\"@todonotes@extracty\"]}")
            end
        end
    end
    tex.sprint(catcodeEnd)
end

function luatodonotes.calcLabelAreaDimensions()
    local routingAreaSpace = 0
    if leaderType.needRoutingArea then
        routingAreaSpace = routingAreaWidth
    end

    local top = tex.voffset + tex.dimen.topmargin + const1In
    local bottom = top + tex.dimen.headheight + tex.dimen.headsep + tex.dimen.textheight + tex.dimen.footskip
    local currentsidemargin = tex.hoffset + tex.dimen["@todonotes@currentsidemargin"] + const1In

    local left = {}
    left.top = -top
    left.bottom = -bottom
    left.left = distanceNotesPageBorder
    left.right = currentsidemargin - distanceNotesText - routingAreaSpace
    if left.right - left.left < minNoteWidth then
        -- not enough space left of text
        left = nil
    else
        left.noteWidth = left.right - left.left
    end

    local right = {}
    right.top = -top
    right.bottom = -bottom
    right.left = currentsidemargin + tex.dimen.textwidth + distanceNotesText + routingAreaSpace
    right.right = tex.pagewidth - distanceNotesPageBorder
    if right.right - right.left < minNoteWidth then
        -- not enough space right of text
        right = nil
    else
        right.noteWidth = right.right - right.left
    end

    local text = {}
    text.left = currentsidemargin
    text.right = currentsidemargin + tex.dimen.textwidth

    labelArea.left = left
    labelArea.right = right
    labelArea.text = text
end

function luatodonotes.calcHeightsForNotes()
    -- function has to be called outside of a tikzpicture-environment
    tex.sprint(catcodeStart)
    for k, v in ipairs(notesForPage) do
        -- store height for note
        -- (is determined by creating a box with the text and reading its size)

        -- left side
        tex.sprint("\\savebox{\\@todonotes@heightcalcbox}" ..
            "{" .. v.sizeCommand .. v:boxForNoteText(false) .. "}")
        tex.sprint("\\@todonotes@heightcalcboxdepth=\\dp\\@todonotes@heightcalcbox")
        tex.sprint("\\@todonotes@heightcalcboxheight=\\ht\\@todonotes@heightcalcbox")
        tex.sprint("\\directlua{luatodonotes.notesForPage[" .. k .. "].heightLeft = " ..
            "tex.dimen[\"@todonotes@heightcalcboxheight\"]" ..
            " + tex.dimen[\"@todonotes@heightcalcboxdepth\"]}")

        -- right side
        tex.sprint("\\savebox{\\@todonotes@heightcalcbox}" ..
            "{" .. v.sizeCommand .. v:boxForNoteText(true) .. "}")
        tex.sprint("\\@todonotes@heightcalcboxdepth=\\dp\\@todonotes@heightcalcbox")
        tex.sprint("\\@todonotes@heightcalcboxheight=\\ht\\@todonotes@heightcalcbox")
        tex.sprint("\\directlua{luatodonotes.notesForPage[" .. k .. "].heightRight = " ..
            "tex.dimen[\"@todonotes@heightcalcboxheight\"]" ..
            " + tex.dimen[\"@todonotes@heightcalcboxdepth\"]}")

        -- store pageNr for note
        -- (is determined as reference to a label)
        tex.sprint("\\directlua{luatodonotes.notesForPage[" .. k .. "].pageNr = " ..
            "\\zref@extract{@todonotes@" .. v.index .. "}{abspage}}")
        if v.noteType == "area" then
            tex.sprint("\\directlua{luatodonotes.notesForPage[" .. k .. "].pageNrEnd = " ..
                "\\zref@extract{@todonotes@" .. v.index .. "@end}{abspage}}")
        end
    end
    tex.sprint(catcodeEnd)
end

local inputShiftX = string.todimen("-0.05cm") -- sensible value depends on shape of mark
function luatodonotes.printNotes()
    print("Drawing notes for page " .. currentPage)

    -- seperate notes that should be placed on another page
    -- This can occur when note is in a paragraph which doesn't fit on the
    -- current page and is thus moved to the next one. But the \todo-command is
    -- still read before the shipout of the current page is done
    luatodonotes.notesForNextPage = {}
    notesForNextPage = luatodonotes.notesForNextPage
    local k=1
    while k <= #notesForPage do
        local v = notesForPage[k]
        if v.pageNr == 0 then
            -- Notes without a page number occur when the zref label is not
            -- defined correctly. This happens with notes in a
            -- \caption-command, e.g.
            -- In this case two version of the note are stored and we drop the
            -- note that does not have a valid page number (the other note
            -- seems to have one).
            table.remove(notesForPage, k)
            if todonotesDebug then
                print("deleting note: " .. k .. " (" .. v.index .. ")")
            end
        elseif v.pageNr ~= currentPage then
            table.insert(notesForNextPage, v)
            table.remove(notesForPage, k)
            if todonotesDebug then
                print("moving note to next page: " .. k .. " (" .. v.index .. ")")
            end
        else
            -- update index here (needed if a note was deleted before)
            v.indexOnPage = k
            k = k + 1
        end
    end

    -- add offset to input coordinates
    for _, v in pairs(notesForPage) do
        if v.noteType ~= "area" then
            v.inputX = v.origInputX + inputShiftX
            local bls = v.baselineskip
            if v.baselineskip == 0 then
                bls = v.normalbaselineskip
            end
            v.inputY = v.origInputY - 1.3 * (bls - v.fontsize)
        else
            v.inputX = v.origInputX
            v.inputY = v.origInputY
        end
    end

    splitting.algo()
    if positioning.twoSided then
        local notesLeft, notesRight = segmentNotes(notesForPage)
        if #notesLeft > 0 then
            positioning.algo(notesLeft, false)
        end
        if #notesRight > 0 then
            positioning.algo(notesRight, true)
        end
    else
        positioning.algo()
    end
    for k, v in ipairs(notesForPage) do
        if todonotesDebug then
            local function outputWithPoints(val)
                if val ~= nil then
                    return val .. " (" .. number.topoints(val, "%s%s") .. ")"
                else
                    return ""
                end
            end
            print("-----------------")
            print(k .. ": ")
            print("index:        " .. v.index)
            print("origInputX:   " .. v.origInputX)
            print("origInputY:   " .. v.origInputY)
            if (v.noteType ~= nil) then
                print("noteType:     " .. v.noteType)
                print("origInputEndX:" .. v.origInputEndX)
                print("origInputEndY:" .. v.origInputEndY)
                print("lineCountInArea:" .. v.lineCountInArea)
                print("linesInArea  :" .. inspect(v.linesInArea))
            else
                print("noteType:     nil")
            end
            print("inputX:       " .. v.inputX)
            print("inputY:       " .. v.inputY)
            print("outputX:      " .. v.outputX)
            print("outputY:      " .. v.outputY)
            if (v.rasterSlots ~= nil) then
                print("rasterSlots:  " .. v.rasterSlots)
            end
            print("baselineskip: " .. outputWithPoints(v.baselineskip))
            print("nbaselineskip:" .. outputWithPoints(v.normalbaselineskip))
            print("fontsize:     " .. outputWithPoints(v.fontsize))
            print("textbox:      " .. inspect(v.textbox))
            print("height:       " .. outputWithPoints(v:getHeight()))
            print("heightLeft:   " .. outputWithPoints(v.heightLeft))
            print("heightRight:  " .. outputWithPoints(v.heightRight))
            print("rightSide:    " .. tostring(v.rightSide))
            if v.pageNr ~= nil then
                print("pageNr:       " .. v.pageNr)
            end
            print("lineColor:    " .. v.lineColor)
            print("backgroundColor:" .. v.backgroundColor)
            print("borderColor:  " .. v.borderColor)
            print("leaderWidth:  " .. v.leaderWidth)
            print("sizeCommand:  " .. v.sizeCommand)
            print("drawLeader:   " .. tostring(v.drawLeader))
        end

        -- print note
        tex.print(catcodeStart)
        tex.print("\\node[@todonotes@notestyle,anchor=north west," ..
            "fill=" .. v.backgroundColor .. ",draw=" .. v.borderColor .. "," ..
            "font=" .. v.sizeCommand .. "] " ..
            "(@todonotes@" .. v.index ..
            " note) at (" .. v.outputX .. "sp," .. v.outputY .. "sp) {" ..
            v:boxForNoteText(v.rightSide) .. "};")
        tex.print(catcodeEnd)


        -- output debugging hints on page
        if todonotesDebug then
            tex.print("\\node[anchor=north west,text=blue,fill=white,rectangle] at (@todonotes@" .. v.index .. " inText) {" .. v.index .. "};")
            tex.print("\\draw[green,fill] (@todonotes@" .. v.index .. " inText) circle(2pt);")
            tex.print("\\draw[black,fill] (@todonotes@" .. v.index .. " inText) circle(0.2pt);")
            if v.noteType == "area" then
                tex.print("\\draw[red,fill] (@todonotes@" .. v.index .. " inTextEnd) circle(2pt);")
            end

            if (v.noteType ~= nil) then
                print(v:getClipPathForTodoArea())
                tex.print("\\draw[blue] " .. v:getClipPathForTodoArea() .. ";")
                --for i=1, v.lineCountInArea do
                    --tex.print(" (@todonotes@" .. v.index .. "@" .. i .. " areaSW) -- ")
                --end
                --tex.print("cycle;")
            end
        end
    end
    -- draw leader
    leaderType.algo()

    -- draw mark in text
    for _, v in pairs(notesForPage) do
        if v.drawLeader ~= false and v.noteType ~= "area" then
            local shiftStr = "(" .. v.inputX .. "sp," .. v.inputY .. "sp)"
            tex.print("\\draw[@todonotes@textmark," ..
                "draw=" .. v.lineColor .. ",fill=" .. v.lineColor .. "," ..
                "shift={" .. shiftStr .. "}," ..
                "scale around={0.5:(-0,-0)},shift={(-0.5,-0.1)}]" ..
                "(1,0) .. controls (0.5,0.2) and (0.65,0.3) .." ..
                "(0.5,0.7) .. controls (0.35,0.3) and (0.5,0.2) .." .. 
                "(0,0) -- cycle;")
        end
    end

    --- draw label areas when requested
    if todonotesDebug then
        local area = labelArea.left
        if area ~= nil then
            tex.print("\\draw[blue] (" .. area.left .. "sp," .. area.top .. "sp) rectangle (" ..
                area.right .. "sp," .. area.bottom .. "sp);")
        end
        area = labelArea.right
        if area ~= nil then
            tex.print("\\draw[blue] (" .. area.left .. "sp," .. area.top .. "sp) rectangle (" ..
                area.right .. "sp," .. area.bottom .. "sp);")
            end
    end
end




-- ********** Helper Functions **********

-- * comparators for table.sort() *
-- (yields true if first parameter should be placed before second parameter in
-- sorted table)
local function compareNoteInputXAsc(note1, note2)
    if note1.inputX < note2.inputX then
        return true
    end
end

local function compareNoteIndInputXAsc(key1, key2)
    if notesForPage[key1].inputX < notesForPage[key2].inputX then
        return true
    end
end

local function compareNoteIndInputXDesc(key1, key2)
    if notesForPage[key1].inputX > notesForPage[key2].inputX then
        return true
    end
end

local function compareNoteIndInputYDesc(key1, key2)
    local v1 = notesForPage[key1]
    local v2 = notesForPage[key2]
    if v1.inputY > v2.inputY then
        return true
    elseif v1.inputY == v2.inputY then
        if v1.inputX < v2.inputX then
            return true
        end
    end
end

-- * callbacks for Luatex *
local function appendStrToTokenlist(tokenlist, str)
    str:gsub(".", function(c)
        tokenlist[#tokenlist + 1] = {12, c:byte(), 0}
    end)
end
-- writes commands into the node tree that print the absolute position on the
-- page to the output file (streamId is taken from lpoFileStream) at the
-- beginning of every line
-- should be called as post_linebreak_filter
local ID_GLYPH_NODE = node.id("glyph")
local ID_HLIST_NODE = node.id("hlist")
function luatodonotes.callbackOutputLinePositions(head)
    while head do
        if head.id == ID_HLIST_NODE then
            -- check if we are in the main text area (hlists in, e.g.,
            -- tikz nodes should have other widths)
            if head.width == tex.dimen.textwidth then
                -- check if there is a glyph in this hlist
                -- -> then we consider it a text line
                local foundGlyph = false
                local glyphTest = head.head
                while glyphTest do
                    if glyphTest.id == ID_GLYPH_NODE then
                        foundGlyph = true
                        break
                    end
                    glyphTest = glyphTest.next
                end

                if foundGlyph then
                    local w = node.new("whatsit", "write")
                    w.stream = lpoFileStream
                    local tokenlist = {
                        {12,  92, 0}, -- \
                        {12,  64, 0}, -- @
                        {12, 116, 0}, -- t
                        {12, 111, 0}, -- o
                        {12, 100, 0}, -- d
                        {12, 111, 0}, -- o
                        {12, 110, 0}, -- n
                        {12, 111, 0}, -- o
                        {12, 116, 0}, -- t
                        {12, 101, 0}, -- e
                        {12, 115, 0}, -- s
                        {12,  64, 0}, -- @
                        {12, 108, 0}, -- l
                        {12, 105, 0}, -- i
                        {12, 110, 0}, -- n
                        {12, 101, 0}, -- e
                        {12, 112, 0}, -- p
                        {12, 111, 0}, -- o
                        {12, 115, 0}, -- s
                        {12, 105, 0}, -- i
                        {12, 116, 0}, -- t
                        {12, 105, 0}, -- i
                        {12, 111, 0}, -- o
                        {12, 110, 0}, -- n
                        {12, 123, 0} -- {
                    }
                    local t = token.create("@todonotes@pdflastypos")
                    -- the token handling changed with newer LuaTeX versions
                    if tex.luatexversion > 81 then
                        tokenlist[#tokenlist + 1] = {0, t.tok}
                    else
                        tokenlist[#tokenlist + 1] = t
                    end
                    tokenlist[#tokenlist + 1] = {12, 125, 0} -- }
                    tokenlist[#tokenlist + 1] = {12, 123, 0} -- {
                    appendStrToTokenlist(tokenlist, tostring(head.height))
                    tokenlist[#tokenlist + 1] = {12, 125, 0} -- }
                    tokenlist[#tokenlist + 1] = {12, 123, 0} -- {
                    appendStrToTokenlist(tokenlist, tostring(head.depth))
                    tokenlist[#tokenlist + 1] = {12, 125, 0} -- }
                    w.data = tokenlist
                    head.head = node.insert_before(head.head,head.head,w)

                    -- the name of the whatsit node changed with newer LuaTeX versions
                    local whatsitName
                    if tex.luatexversion > 80 then
                        whatsitName = "save_pos"
                    else
                        whatsitName = "pdf_save_pos"
                    end
                    local w = node.new("whatsit", whatsitName)
                    head.head = node.insert_before(head.head,head.head,w)
                end
            end
        end
        head = head.next
    end
    return true
end





-- ********** Leader Drawing Algorithms **********

local function drawLeaderPath(note, path)
    if note.drawLeader == false then
        return
    end
    local clipPath
    if note.noteType == "area" then
        clipPath = note:getClipPathForTodoArea()
        tex.print("\\begin{scope}")
        tex.print("\\clip (current page.north west) rectangle (current page.south east) ")
        tex.print(clipPath)
        tex.print(";")
    end
    tex.print("\\draw[@todonotes@leader,draw=" .. note.lineColor ..
        ",line width=" .. note.leaderWidth .. ",name path=leader] " .. path .. ";")
    if note.noteType == "area" then
        tex.print("\\path[name path=clipping] " .. clipPath .. ";")
        tex.print("\\fill[@todonotes@leader,name intersections={of=leader and clipping, by=x,sort by=leader},fill=" .. note.lineColor .. "] (x) circle(3pt);")
        tex.print("\\end{scope}")
    end
end


-- ** leader drawing: s-leaders
local function drawSLeaders()
    for k, v in ipairs(notesForPage) do
        drawLeaderPath(v, v:getLabelAnchorTikz() ..
            " -- " .. v:getInTextAnchorTikz())
    end
end
leaderTypes["s"] = {algo = drawSLeaders}



-- ** leader drawing: opo-leaders
local function drawOpoLeader(v, opoShift, rightSide)
    if rightSide then
        opoShift = - opoShift
    end
    drawLeaderPath(v, v:getLabelAnchorTikz() .. " -- +(" .. opoShift .. "sp,0) " ..
        "|- " .. v:getInTextAnchorTikz())
end
local function drawOpoGroup(group, directionDown, rightSide)
    if directionDown == nil then
        for _, v2 in ipairs(group) do
            drawOpoLeader(notesForPage[v2], 0, rightSide)
        end
    else
        if #group == 1 then
            -- place p-section of leader in center of routing area
            local opoShift = distanceNotesText / 2 + routingAreaWidth / 2
            drawOpoLeader(notesForPage[group[1]], opoShift, rightSide)
        else
            local leaderDistance = routingAreaWidth / (#group - 1)

            -- initialise shift value
            local nextOpoShift, move
            if directionDown then
                nextOpoShift = distanceNotesText / 2 + routingAreaWidth
                move = -leaderDistance
            else
                nextOpoShift = distanceNotesText / 2
                move = leaderDistance
            end

            -- cycle through group
            for _, v2 in ipairs(group) do
                drawOpoLeader(notesForPage[v2], nextOpoShift, rightSide)
                nextOpoShift = nextOpoShift + move
            end
        end
    end
end
local function drawOpoLeadersSide(notes, rightSide)
    table.sort(notes, compareNoteIndInputYDesc)

    local lastDirectionDown = nil
    local group = {}
    local prevNote
    for _, ind in ipairs(notes) do
        local v = notesForPage[ind]

        local leaderAnchorY = v:getLabelAnchorY()
        if leaderAnchorY > v.inputY then
            newDirectionDown = true
        elseif leaderAnchorY < v.inputY then
            newDirectionDown = false
        else
            newDirectionDown = nil
        end

        if lastDirectionDown == newDirectionDown and
                prevNote ~= nil and
                -- following conditions check that leaders would really intersect
                -- otherwise we can start a new group
                ((newDirectionDown and leaderAnchorY >= prevNote.inputY) or
                (not newDirectionDown and v.inputY >= prevNote:getLabelAnchorY())) then
            -- note belongs to group
            table.insert(group, ind)
        else
            -- draw leaders for group
            drawOpoGroup(group, lastDirectionDown, rightSide)

            -- initialise new group with this note
            lastDirectionDown = newDirectionDown
            group = {ind}
        end

        prevNote = v
    end
    drawOpoGroup(group, lastDirectionDown, rightSide)
end
local function drawOpoLeaders()
    local notesLeft, notesRight = segmentNotes(notesForPage)
    if #notesLeft > 0 then
        drawOpoLeadersSide(notesLeft, false)
    end
    if #notesRight > 0 then
        drawOpoLeadersSide(notesRight, true)
    end
end
leaderTypes["opo"] = {algo = drawOpoLeaders,
    needRoutingArea = true}



-- ** leader drawing: po-leaders
local function drawPoLeaders()
    for _, v in ipairs(notesForPage) do
        drawLeaderPath(v, v:getLabelAnchorTikz() .. " -| " .. v:getInTextAnchorTikz())
    end
end
leaderTypes["po"] = {algo = drawPoLeaders}



-- ** leader drawing: os-leaders
local function drawOsLeaders()
    for _, v in ipairs(notesForPage) do
        local cornerX
        if v.rightSide then
            cornerX = labelArea.right.left - distanceNotesText / 2 - routingAreaWidth
        else
            cornerX = labelArea.left.right + distanceNotesText / 2 + routingAreaWidth
        end
        drawLeaderPath(v, v:getInTextAnchorTikz() ..
            " -- (" .. cornerX .. "sp,0 |- 0," .. v.inputY .. "sp) -- " ..
            v:getLabelAnchorTikz())
    end
end
leaderTypes["os"] = {algo = drawOsLeaders,
    needRoutingArea = true}



-- ** leader drawing: s-Bezier-leaders
-- additional fields for each note:
--   leaderArmY
--   movableControlPointX
--   optimalPositionX
--   currentForce
--   forceLimitDec
--   forceLimitInc

-- settings for algorithm
local maxIterations = 1000
local factorRepulsiveControlPoint = 1
local factorAttractingControlPoint = 1
local stopCondition = 65536 -- corresponds to 1pt

local function constructCurve(l)
    local curve = {}

    -- site
    curve[1] = {}
    curve[1].x = l.inputX
    curve[1].y = l.inputY

    -- unmovable control point (middle point of site and movable control point)
    curve[2] = {}
    curve[2].x = (l.inputX + l.movableControlPointX) / 2
    curve[2].y = (l.inputY + l.leaderArmY) / 2

    -- movable control point
    curve[3] = {}
    curve[3].x = l.movableControlPointX
    curve[3].y = l.leaderArmY

    -- port
    curve[4] = {}
    curve[4].x = labelArea:getXTextSide(l.rightSide)
    curve[4].y = l.leaderArmY

    return curve
end
local function getPointOnCurve(t, curve)
    if #curve ~= 4 then
        error("4 points needed for a Bezier-curve. Given size was: " .. #curve)
    end

    local x = (1 - t) * (1 - t) * (1 - t) * curve[1].x +
        3 * t * (1 - t) * (1 - t) * curve[2].x +
        3 * t * t * (1 - t) * curve[3].x +
        t * t * t * curve[4].x

    local y = (1 - t) * (1 - t) * (1 - t) * curve[1].y +
        3 * t * (1 - t) * (1 - t) * curve[2].y +
        3 * t * t * (1 - t) * curve[3].y +
        t * t * t * curve[4].y;

    return x, y
end
local function getDistance(line1, line2)
    local t1, t2 = pathLine.line_line_intersection(line1.x1, line1.y1, line1.x2, line1.y2,
            line2.x1, line2.y1, line2.x2, line2.y2)
    if 0 <= t1 and t1 <= 1 and 0 <= t2 and t2 <= 1 then
        -- the lines do intersect
        return 0
    end

    local d1 = pathLine.hit(line2.x1, line2.y1, line1.x1, line1.y1, line1.x2, line1.y2)
    local d2 = pathLine.hit(line2.x2, line2.y2, line1.x1, line1.y1, line1.x2, line1.y2)
    local d3 = pathLine.hit(line1.x1, line1.y1, line2.x1, line2.y1, line2.x2, line2.y2)
    local d4 = pathLine.hit(line1.x2, line1.y2, line2.x1, line2.y1, line2.x2, line2.y2)
    return math.sqrt(math.min(d1, d2, d3, d4))
end
local function checkCurveApproximation(curve1, curve2)
    -- these lists will contain the sections of the approximation of the two curves
    local sectionsCurve1 = {}
    local sectionsCurve2 = {}

    -- get line segments of the first curve
    local numberOfSectionsCurve1 = luatodonotes.numberOfCurvePartitions
    local temp1X, temp1Y = getPointOnCurve(0, curve1)
    local i = 1
    while i <= numberOfSectionsCurve1 do
        local t = i / numberOfSectionsCurve1
        local temp2X, temp2Y = getPointOnCurve(t, curve1)
        local line = {}
        line.x1 = temp1X
        line.y1 = temp1Y
        line.x2 = temp2X
        line.y2 = temp2Y
        table.insert(sectionsCurve1, line)
        temp1X, temp1Y = temp2X, temp2Y
        i = i + 1
    end

    -- get line segments of the second curve
    local numberOfSectionsCurve2 = luatodonotes.numberOfCurvePartitions
    temp1X, temp1Y = getPointOnCurve(0, curve2)
    i = 1
    while i <= numberOfSectionsCurve2 do
        local t = i / numberOfSectionsCurve2
        local temp2X, temp2Y = getPointOnCurve(t, curve2)
        local line = {}
        line.x1 = temp1X
        line.y1 = temp1Y
        line.x2 = temp2X
        line.y2 = temp2Y
        table.insert(sectionsCurve2, line)
        temp1X, temp1Y = temp2X, temp2Y
        i = i + 1
    end

    -- get the minimal distance of the 2 curve approximations
    local minDistance = math.huge
    for _, line1 in pairs(sectionsCurve1) do
        for _, line2 in pairs(sectionsCurve2) do
            local distance = getDistance(line1, line2)
            if distance <= minDistance then
                minDistance = distance
            end
        end
    end

    return minDistance
end
local function computeRepulsiveControlPointForces()
    for k1, l1 in pairs(notesForPage) do
        for k2, l2 in pairs(notesForPage) do
            if k1 ~= k2 then
                -- curves of the leaders
                local curve1 = constructCurve(l1)
                local curve2 = constructCurve(l2)

                local distance = checkCurveApproximation(curve1, curve2);

                -- check if R1 has to be increased or decreased to increase the distance of the 2 curves
                -- if curve1 is bent into the direction of curve2, R1 has to be decreased
                local actualR = math.abs(labelArea:getXTextSide(l1.rightSide) - l1.movableControlPointX)
                if ((l1.inputY < l1.leaderArmY and
                        l2.leaderArmY < l1.leaderArmY) or
                        (l1.inputY > l1.leaderArmY and
                        l2.leaderArmY > l1.leaderArmY)) then
                    -- R1 has to be increased
                    local desiredR = math.abs(labelArea:getXTextSide(l1.rightSide) - l1.optimalPositionX)
                    local diff = math.abs(desiredR - actualR)
                    if distance == 0 then
                        distance = 0.01
                    end
                    local force = diff / distance
                    local newForce = force * factorRepulsiveControlPoint
                    l1.currentForce = l1.currentForce + newForce
                    l1.forceLimitDec = math.min(l1.forceLimitDec, distance * 0.45)
                else
                    -- R1 has to be decreased
                    if distance == 0 then
                        distance = 0.01
                    end
                    local force = actualR / distance
                    local newForce = -force * factorRepulsiveControlPoint
                    l1.currentForce = l1.currentForce + newForce
                    local oldLim = l1.forceLimitInc
                    l1.forceLimitInc = math.min(l1.forceLimitInc, distance * 0.45)
                    --if oldLim ~= l1.forceLimitInc then
                        --print(k1 .. ": Reduced forceLimitInc from " .. oldLim .. " to " .. l1.forceLimitInc .. " because of " .. k2 .. " (distance: " .. distance .. ")")
                    --end
                end
            end
        end
    end
end
local function computeAttractingControlPointForces()
    for _, l in pairs(notesForPage) do
        local desiredR = math.abs(labelArea:getXTextSide(l.rightSide) - l.optimalPositionX)
        local actualR = math.abs(labelArea:getXTextSide(l.rightSide) - l.movableControlPointX)
        local newForce = (desiredR - actualR) * factorAttractingControlPoint
        l.currentForce = l.currentForce + newForce
    end
end
local function applyForces(v)
    --print("force on note " .. v.index .. ": " .. v.currentForce .. " (limit: +" .. v.forceLimitInc .. ", -" .. v.forceLimitDec .. ")")

    -- limit the force so the movable control point is between the port and the optimal position
    local actualR = math.abs(labelArea:getXTextSide(v.rightSide) - v.movableControlPointX)
    local differenceR = math.abs(labelArea:getXTextSide(v.rightSide) - v.optimalPositionX) - actualR
    if (v.currentForce < 0 and math.abs(v.currentForce) > actualR) then
        v.currentForce = (-1) * actualR
    end
    if (v.currentForce > 0 and v.currentForce > differenceR) then
        v.currentForce = differenceR
    end

    -- limit the force so 2 curves do not get too close to each other and do not cross
    if v.currentForce > v.forceLimitInc then
        v.currentForce = v.forceLimitInc
    end
    if v.currentForce < (-1) * v.forceLimitDec then
        v.currentForce = (-1) * v.forceLimitDec
    end
    v.forceLimitDec = math.huge
    v.forceLimitInc = math.huge

    if v.rightSide then
        v.movableControlPointX = v.movableControlPointX - v.currentForce
    else
        v.movableControlPointX = v.movableControlPointX + v.currentForce
    end

    --print("force on note " .. v.index .. ": " .. v.currentForce)
    local c = v.currentForce
    v.currentForce = 0
    return c
end
local function getAngle(centerX, centerY, x, y)
    local vectorX = x - centerX
    local vectorY = y - centerY
    local length = math.sqrt((vectorX ^ 2) + (vectorY ^ 2))

    vectorX = vectorX / length
    vectorY = vectorY / length

    local radAngle = math.acos(vectorX)
    local degAngle = (radAngle * 180) / math.pi

    if vectorY < 0 then
        degAngle = 360 - degAngle
    end

    return degAngle
end
local function solveQuadraticEquation(a, b, c)
    local discr = (b * b) - (4 * a * c)

    if discr < 0 then
        error("Fehler bei der Berechnung das optimalen Punktes")
    end

    local solution1 = ((-b) + math.sqrt(discr)) / (2 * a)
    local solution2 = ((-b) - math.sqrt(discr)) / (2 * a)

    if solution1 < 0 and solution2 < 0 then
        error("no positive solution")
    end

    if solution1 < solution2 then
        return solution2
    else
        return solution1
    end
end
local function computeOptimalPosition(v)
    local distance = point.distance(v.inputX, v.inputY, labelArea:getXTextSide(v.rightSide), v.leaderArmY)

    -- the angle at the port between the point and the movable control point
    local tempAngle = getAngle(v.inputX, v.inputY, labelArea:getXTextSide(v.rightSide), v.leaderArmY)

    local gamma
    if v.rightSide then
        if tempAngle < 180 then
            gamma = tempAngle
        else
            gamma = 360 - tempAngle
        end
    else
        if tempAngle < 180 then
            gamma = 180 - tempAngle
        else
            gamma = tempAngle - 180
        end
    end

    -- a quadratic formula has to be solved to get the optimal position
    local a = 3
    local b = 2 * distance * math.cos(math.rad(gamma))
    local c = -(distance * distance)

    local r = solveQuadraticEquation(a, b, c)

    if v.rightSide then
        v.optimalPositionX = labelArea:getXTextSide(v.rightSide) - r
    else
        v.optimalPositionX = labelArea:getXTextSide(v.rightSide) + r
    end
end
local function drawSBezierLeaders()
    for _, v in pairs(notesForPage) do
        -- initialise leader
        v.leaderArmY = v:getLabelAnchorY()
        v.movableControlPointX = labelArea:getXTextSide(v.rightSide)
        v.currentForce = 0
        v.forceLimitDec = math.huge
        v.forceLimitInc = math.huge
    end

    luatodonotes.numberOfCurvePartitions = #notesForPage * 3

    for _, v in pairs(notesForPage) do
        computeOptimalPosition(v)
    end

    -- main loop
    local proceed = true
    local loopCounter = 0
    while (proceed and loopCounter < maxIterations) do
        if todonotesDebug then
            print("Iteration " .. loopCounter)
        end

        -- compute forces
        computeRepulsiveControlPointForces()
        computeAttractingControlPointForces()

        -- apply forces
        proceed = false
        for _, l in pairs(notesForPage) do
            local diff = applyForces(l)
            if diff > stopCondition then
                proceed = true
            end
        end

        loopCounter = loopCounter + 1
    end

    if todonotesDebug then
        print("End of Force-directed algo, number of iterations: " .. loopCounter)
    end

    -- draw
    for _, v in pairs(notesForPage) do
        local curve = constructCurve(v)
        local unmovableStr = "(" .. curve[2].x .. "sp," .. curve[2].y .. "sp)"
        local movableStr = "(" .. curve[3].x .. "sp," .. curve[3].y .. "sp)"
        drawLeaderPath(v, v:getLabelAnchorTikz() .. " .. controls " ..
            movableStr .. " and " .. unmovableStr .. " .. " ..
            v:getInTextAnchorTikz())

        -- draw control points when requested
        if todonotesDebug then
            local optimalStr = "(" .. v.optimalPositionX .. "sp," .. v.leaderArmY .. "sp)"
            tex.print("\\node[anchor=north west,text=pink,fill=white,rectangle] at " .. optimalStr .. " {" .. v.index .. "};")
            tex.print("\\node[anchor=north west,text=red,fill=white,rectangle] at " .. movableStr .. " {" .. v.index .. "};")
            tex.print("\\node[anchor=north west,text=orange,fill=white,rectangle] at " .. unmovableStr .. " {" .. v.index .. "};")
            tex.print("\\draw[red,fill] " .. movableStr .. " circle(2pt);")
            tex.print("\\draw[orange,fill] " .. unmovableStr .. " circle(2pt);")
            tex.print("\\draw[pink,fill] " .. optimalStr .. " circle(1pt);")
        end
    end
end
leaderTypes["sBezier"] = {algo = drawSBezierLeaders}





-- ********** Positioning Algorithms **********

-- ** helper functions

-- finds the index in the list given as parameter with the minimum angle
-- the function used for computation of the angle is given as second parameter
-- (the alphaFormula gets the note, to which the angle should be computed, as
-- the only parameter)
local function findIndexMinAlpha(availableNotesIndex, alphaFormula)
    local minAlpha = math.huge -- infinity
    local minIndex = -1

    for k, v in pairs(availableNotesIndex) do
        local alpha = alphaFormula(notesForPage[v])
        if alpha < minAlpha then
            minAlpha = alpha
            minIndex = k
        end
    end

    return minIndex
end



-- ** partition into stacks
local function getMeanYHeight(stack)
    -- TODO: Alternative: nicht das arithmetische Mittel verwenden, sondern
    -- Mittelpunkt zwischen dem obersten und untersten Punkt
    local sumY = 0
    local height = 0
    for _, v in pairs(stack) do
        sumY = sumY + notesForPage[v].inputY
        height = height + notesForPage[v]:getHeight() + 2 * noteInnerSep + noteInterSpace
    end

    local area = labelArea:getArea(notesForPage[stack[1]].rightSide)

    local meanY = sumY / #stack
    local height = height - noteInterSpace
    if meanY + (height/2) > area.top then
        meanY = area.top - (height/2)
    elseif meanY - (height/2) < area.bottom then
        meanY = area.bottom + (height/2)
    end
    return meanY, height
end
local function stacksIntersect(stackTop, stackBottom)
    local topMeanY, topHeight = getMeanYHeight(stackTop)
    local topLower = topMeanY - topHeight/2

    local bottomMeanY, bottomHeight = getMeanYHeight(stackBottom)
    local bottomUpper = bottomMeanY + bottomHeight/2

    if topLower - bottomUpper < noteInterSpace then
        return true
    else
        return false
    end
end
local function findStacks(notesOnSide)
    local notes = table.copy(notesOnSide)
    table.sort(notes, compareNoteIndInputYDesc)

    -- list that contains stacks
    -- is initialized by putting all notes as single stacks ordered by their inputY
    local stacks = {}
    for _, v in pairs(notes) do
        table.insert(stacks, {v})
    end

    -- Collapse Stacks where needed
    local i = 1
    while i <= #stacks - 1 do
        if stacksIntersect(stacks[i], stacks[i+1]) then
            collapsedStacks = true
            for _, v in pairs(stacks[i+1]) do
                table.insert(stacks[i], v)
            end
            table.remove(stacks,i+1)
            if i > 1 then
                -- as stack i has increased in size we look at the previous
                -- stack again in next iteration
                i = i - 1
            end
        else
             -- look at next stack in next iteration
            i = i + 1
        end
    end

    return stacks
end


-- ** positioning: inText
local function posInText()
    -- trivial algorithm
    -- places notes in text on position where todo-command was issued
    for k, v in ipairs(notesForPage) do
        v.outputX = v.inputX
        v.outputY = v.inputY
    end
end
positioningAlgos["inText"] = {algo = posInText,
    leaderAnchor = "north west",
    leaderShift = false,
    twoSided = false}



-- ** positioning: inputOrderStacks
local function placeNotesInputOrder(stack, yStart, rightSide)
    local freeY = yStart

    for _, k in ipairs(stack) do
        local v = notesForPage[k]
        v.outputX = labelArea:getArea(rightSide).left
        v.outputY = freeY
        freeY = freeY - v:getHeight() - 2 * noteInnerSep - noteInterSpace
    end
end
local function posInputOrderStacks(notesOnSide, rightSide)
    table.sort(notesOnSide, compareNoteIndInputYDesc)

    local stacks = findStacks(notesOnSide)

    -- place stacks
    for k, stack in pairs(stacks) do
        local meanY, height = getMeanYHeight(stack)
        local stackStart = meanY + height / 2
        placeNotesInputOrder(stack, stackStart, rightSide)
    end
end
positioningAlgos["inputOrderStacks"] = {algo = posInputOrderStacks,
    leaderAnchor = "east",
    leaderShift = false,
    twoSided = true}



-- ** positioning: inputOrder
-- start at top and place notes below each other on left/right side
-- notes are placed in the order induced by their y-coordinates
local function posInputOrder(notes, rightSide)
    table.sort(notes, compareNoteIndInputYDesc)
    placeNotesInputOrder(notes, labelArea:getArea(rightSide).top, rightSide)
end
positioningAlgos["inputOrder"] = {algo = posInputOrder,
    leaderAnchor = "east",
    leaderShift = false,
    twoSided = true}



-- ** positioning: sLeaderNorthEast
local function posSLeaderNorthEast(notes, rightSide)
    local noteY = labelArea:getArea(rightSide).top

    local alphaFormula
    local noteX = labelArea:getXTextSide(rightSide)
    local outputX = labelArea:getArea(rightSide).left
    if rightSide then
        alphaFormula = function (note)
            return (noteY - note.inputY) / (noteX - note.inputX)
        end
    else
        alphaFormula = function (note)
            return (noteY - note.inputY) / (note.inputX - noteX)
        end
    end

    while #notes > 0 do
        local minIndex = findIndexMinAlpha(notes, alphaFormula)

        -- place note identified by minIndex
        local note = notesForPage[notes[minIndex]]
        note.outputX = outputX
        note.outputY = noteY
        noteY = noteY - note:getHeight() - 2 * noteInnerSep - noteInterSpace

        table.remove(notes, minIndex)
    end
end
positioningAlgos["sLeaderNorthEast"] = {algo = posSLeaderNorthEast,
    leaderAnchor = "north east",
    leaderShift = false,
    twoSided = true}



-- ** positioning: sLeaderNorthEastBelow
local function placeNotesNorthEastBelow(stack, yStart, rightSide)
    -- calculate minimum height of all notes
    local minHeight = math.huge -- (infinity)
    for _, v in pairs(stack) do
        if notesForPage[v]:getHeight() < minHeight then
            minHeight = notesForPage[v]:getHeight()
        end
    end
    local leaderShiftY = (- minHeight - 2 * noteInnerSep) / 2

    local noteY = yStart
    local availableNotes = table.copy(stack)

    local alphaFormula
    local noteX = labelArea:getXTextSide(rightSide)
    local outputX = labelArea:getArea(rightSide).left
    if rightSide == true then
        alphaFormula = function (note)
            return ((noteY + leaderShiftY) - note.inputY) / (noteX - note.inputX)
        end
    else
        alphaFormula = function (note)
            return ((noteY + leaderShiftY) - note.inputY) / (note.inputX - noteX)
        end
    end
    while #availableNotes > 0 do
        local minIndex = findIndexMinAlpha(availableNotes, alphaFormula)

        -- place note identified by minIndex
        local note = notesForPage[availableNotes[minIndex]]
        note.outputX = outputX
        note.outputY = noteY
        note.leaderShiftX = 0
        note.leaderShiftY = leaderShiftY
        noteY = noteY - note:getHeight() - 2 * noteInnerSep - noteInterSpace

        table.remove(availableNotes, minIndex)
    end
end
local function posSLeaderNorthEastBelow(notes, rightSide)
    placeNotesNorthEastBelow(notes, labelArea:getArea(rightSide).top, rightSide)
end
positioningAlgos["sLeaderNorthEastBelow"] = {algo = posSLeaderNorthEastBelow,
    leaderAnchor = "north east",
    leaderShift = true,
    twoSided = true}



-- ** positioning: sLeaderNorthEastBelowStacks
local function posSLeaderNorthEastBelowStacks(notesOnSide, rightSide)
    local stacks = findStacks(notesOnSide)

    -- place stacks
    for k, stack in pairs(stacks) do
        local meanY, height = getMeanYHeight(stack)
        local stackStart = meanY + height / 2
        placeNotesNorthEastBelow(stack, stackStart, rightSide)
    end
end
positioningAlgos["sLeaderNorthEastBelowStacks"] = {algo = posSLeaderNorthEastBelowStacks,
    leaderAnchor = "north east",
    leaderShift = true,
    twoSided = true}



-- ** positioning: sLeaderEast
local function posSLeaderEast(notes, rightSide)
    local leaderPosY
    local noteY = labelArea:getArea(rightSide).top

    local alphaFormula
    local noteX = labelArea:getXTextSide(rightSide)
    local outputX = labelArea:getArea(rightSide).left
    if rightSide == true then
        alphaFormula = function (note)
            return (leaderPosY - note.inputY) / (noteX - note.inputX)
        end
    else
        alphaFormula = function (note)
            return (leaderPosY - note.inputY) / (note.inputX - noteX)
        end
    end

    local placedNotes = {}
    while #notes > 0 do
        -- build a array with all distinct heights of the notes
        -- first create a set and then convert to sorted array
        local heights = {}
        for _, v in pairs(notes) do
            heights[notesForPage[v]:getHeight()] = true
        end
        heights = table.keys(heights)
        table.sort(heights)

        local chosenIndex = -1
        local chosenH = -1
        for _, h in pairs(heights) do
            if todonotesDebug then
                print("testing height: " .. h)
            end
            leaderPosY = noteY - noteInnerSep - h/2

            -- find point with highest angle
            local minIndex = findIndexMinAlpha(notes, alphaFormula)

            -- found a valid note
            if notesForPage[notes[minIndex]]:getHeight() <= h then
                chosenIndex = minIndex
                chosenH = h
                if todonotesDebug then
                    print("placed note " .. notesForPage[notes[chosenIndex]].index)
                end
                break
            end
        end

        -- place note identified by chosenIndex
        local note = notesForPage[notes[chosenIndex]]
        note.outputX = outputX
        -- let free space above note if needed (if chosenH ~= note:getHeight())
        note.outputY = noteY - (chosenH - note:getHeight()) / 2
        -- no extraordinary free space below note (even if chosenH ~= note:getHeight())
        noteY = note.outputY - note:getHeight() - 2 * noteInnerSep - noteInterSpace
        if todonotesDebug and chosenH ~= note:getHeight() then
            print("Creating free space above note " .. note.index)
        end

        table.insert(placedNotes, notes[chosenIndex])
        table.remove(notes, chosenIndex)
    end

    -- postprocessing: reduce spaces between notes where possible
    for ind, noteNr in pairs(placedNotes) do
        local note = notesForPage[noteNr]

        local aimedPos
        if ind == 1 then
            aimedPos = labelArea:getArea(rightSide).top
        else
            local prevNote = notesForPage[placedNotes[ind-1]]
            aimedPos = prevNote.outputY - prevNote:getHeight() - 2 * noteInnerSep - noteInterSpace
        end

        if todonotesDebug and aimedPos ~= note.outputY then
            print("note " .. note.index .. " got moved:")
            print("aimed: " .. aimedPos)
            print("real:  " .. note.outputY)
        end

        local aimedLeaderAnchorY = aimedPos - noteInnerSep - note:getHeight() / 2
        local realLeaderAnchorY = note.outputY - noteInnerSep - note:getHeight() / 2
        -- it holds: realLeaderAnchorY < aimedLeaderAnchorY (realLeaderAnchor is lower on page)

        -- check if there are points in triangle (aimedLeaderAnchor, note.input, realLeaderAnchor)
        -- we perform this check by calculating the angle of the points referred to note.input
        local pointsInTriangle = false
        local denom
        if rightSide then
            denom = noteX - note.inputX
        else
            denom = note.inputX - noteX
        end
        local aimedLeaderAnchorAngle = (aimedLeaderAnchorY - note.inputY) / denom
        local realLeaderAnchorAngle = (realLeaderAnchorY - note.inputY) / denom
        local minAngle = math.huge
        local minAngleIndex = -1 -- takes index of lowest point in triangle
        for otherInd, otherNote in pairs(notesForPage) do
            if otherInd ~= noteNr and
                ((not rightSide and otherNote.inputX < note.inputX) or
                 (rightSide and otherNote.inputX > note.inputX)) then
                local otherNoteAngle
                if rightSide then
                    otherNoteAngle = (otherNote.inputY - note.inputY) / (otherNote.inputX - note.inputX)
                else
                    otherNoteAngle = (otherNote.inputY - note.inputY) / (note.inputX - otherNote.inputX)
                end

                if (realLeaderAnchorAngle < otherNoteAngle)
                        and (otherNoteAngle < aimedLeaderAnchorAngle) then
                    pointsInTriangle = true
                    if otherNoteAngle < minAngle then
                        minAngle = otherNoteAngle
                        minAngleIndex = otherInd
                    end
                    if todonotesDebug then
                        print(otherNote.index .. " is in triangle for " .. note.index)
                    end
                end
            end
        end

        if not pointsInTriangle then
            -- no points in triangle
            -- => we can move this note to aimedPos
            note.outputY = aimedPos
        else
            -- move note upwards so that leader touches lowest point in triangle
            -- new point for leader anchor is determined by the ray from note.input through the lowest point in triangle (otherNote.input)
            -- TODO: force a certain distance between leader and other points (at the moment a leader can contain endpoints of other leaders)
            local otherNote = notesForPage[minAngleIndex]
            local aimedLeaderAnchorY = note.inputY - (note.inputY - otherNote.inputY) * (note.inputX - noteX) / (note.inputX - otherNote.inputX)
            note.outputY = aimedLeaderAnchorY + noteInnerSep + note:getHeight() / 2
        end
    end
end
positioningAlgos["sLeaderEast"] = {algo = posSLeaderEast,
    leaderAnchor = "east",
    leaderShift = false,
    twoSided = true}



-- ** positioning: poLeaders
local function getRasterAbsolute(rasterHeight, top, rasterIndex)
    return top - (rasterIndex - 1) * rasterHeight
end
 -- distance between line and leader that algorithm tries to reach when there is
 -- no neighbouring line
local poMinDistLine = string.todimen("4pt")
local function getPosAboveLine(linePositionsCurPage, lineInd)
    local line = linePositionsCurPage[lineInd]
    local posAbove
    if linePositionsCurPage[lineInd - 1] ~= nil then
        posAbove = (line[2] + linePositionsCurPage[lineInd - 1][3]) / 2
    end
    if posAbove == nil or posAbove - line[2] > poMinDistLine then
        posAbove = line[2] + poMinDistLine
    end
    return posAbove
end
local function getPosBelowLine(linePositionsCurPage, lineInd)
    local line = linePositionsCurPage[lineInd]
    local posBelow
    if linePositionsCurPage[lineInd + 1] ~= nil then
        posBelow = (line[3] + (linePositionsCurPage[lineInd + 1][2])) / 2
    end
    if posBelow == nil or line[3] - posBelow > poMinDistLine then
        posBelow = line[3] - poMinDistLine
    end
    return posBelow
end
local function posPoLeaders(notes, rightSide, avoidLines)
    local linePositionsCurPage
    if avoidLines then
        linePositionsCurPage = {}
        -- use the current pageheight to convert coordinates:
        -- savepos yields coordinates relativ to lower left corner of page,
        -- while our tikzpicture is anchored at the upper left corner
        for k, v in pairs(linePositions[currentPage] or {}) do
            local baseline = v[1] - tex.pageheight
            linePositionsCurPage[k] = {baseline, baseline + v[2], baseline - v[3]}
            if todonotesDebug then
                print("linePositionsAddLine, add: " .. v[1] .. ", pageheight: " .. tex.pageheight ..
                    ", result: " .. baseline)
            end
        end
    end

    -- number of slots on the whole page
    local area = labelArea:getArea(rightSide)
    local totalNumSlots = math.floor((area.top - area.bottom) / rasterHeight)

    -- calculate number of raster slots for each note
    for _, ind in pairs(notes) do
        local v = notesForPage[ind]
        local height = v:getHeight() + 2 * noteInnerSep + noteInterSpace
        v.rasterSlots = math.ceil(height / rasterHeight)
    end

    -- sort notes by inputY
    table.sort(notes, compareNoteIndInputYDesc)

    -- draw slots and line positions
    if todonotesDebug then
        for i = 1,totalNumSlots+1 do
            local pos = area.top - (i-1) * rasterHeight
            tex.print("\\draw[blue,dashed] (0," .. pos .. "sp) -- +(21cm,0);")
        end
        for ind, v in pairs(linePositionsCurPage) do
            local pos = v[1]
            tex.print("\\draw[red,dashed] (0," .. pos .. "sp) -- +(21cm,0);")
            tex.print("\\node[red] at (1cm," .. pos .. "sp) {" .. ind .. "};")
        end
    end

    -- initialise table opt for dynamic program
    -- opt[topPoint, bottomPoint, topSlot, bottomSlot, numberLabeledSites]
    -- opt[a][b][c][d][e] describes length-minimal placement of the labels for
    -- sites from a to b in the raster slots c to d
    -- the leftmost/rightmost e sites between a and b are selected until there
    -- are no more free slots
    local opt = {}
    for a = 1, #notes do
        opt[a] = {}
        for b = a, #notes do
            opt[a][b] = {}
            -- TODO: needed label slots are restricted by points
            -- only create tables for needed slots
            for i = 1, totalNumSlots do
                opt[a][b][i] = {}
                for j = i, totalNumSlots do
                    if i == j then
                        opt[a][b][i][j] = {}
                        opt[a][b][i][j][0] = {}
                        opt[a][b][i][j][0].totalLength = 0
                        opt[a][b][i][j][0].positions = {}
                        opt[a][b][i][j][0].leaderShiftY = {}
                    end
                end
            end
        end
    end

    -- constant use an partial solution without labeled points
    local optEmpty = {}
    optEmpty.totalLength = 0
    optEmpty.positions = {}
    optEmpty.leaderShiftY = {}

    -- fill table opt for dynamic program
    -- numberOfPoints is difference between topPoint and bottomPoint when computing opt
    for numberOfPoints = 1, #notes do
        for topPoint = 1, (#notes - numberOfPoints + 1) do
            -- compute opt[topStrip][bottomStrip]
            local bottomPoint = topPoint + numberOfPoints - 1

            local pointsBetween = {}
            for i = topPoint, bottomPoint do
                table.insert(pointsBetween, notes[i])
            end
            if rightSide then
                table.sort(pointsBetween, compareNoteIndInputXDesc)
            else
                table.sort(pointsBetween, compareNoteIndInputXAsc)
            end

            -- TODO: Einschränken, nicht alle Kombinationen von Slots notwendig
            --       (siehe auch oben)
            -- numberOfSlots is difference between topSlot and bottomSlot when computing opt
            for numberOfSlots = 1, totalNumSlots do
                for topSlot = 1, (totalNumSlots - numberOfSlots + 1) do
                    local bottomSlot = topSlot + numberOfSlots - 1

                    -- DEBUG
                    --print("computing opt[" .. topPoint .. "][" .. bottomPoint ..
                          --"][" .. topSlot .. "][" .. bottomSlot .. "]")

                    opt[topPoint][bottomPoint][topSlot][bottomSlot] = {}
                    opt[topPoint][bottomPoint][topSlot][bottomSlot][0] = {}
                    opt[topPoint][bottomPoint][topSlot][bottomSlot][0].totalLength = 0
                    opt[topPoint][bottomPoint][topSlot][bottomSlot][0].positions = {}
                    opt[topPoint][bottomPoint][topSlot][bottomSlot][0].leaderShiftY = {}

                    -- stelle fest, wie viele Punkte gelabelt werden (bestimme also r)
                    local labeledSites = {}
                    local usedSlots = 0
                    for _, v in pairs(pointsBetween) do
                        local note = notesForPage[v]
                        if usedSlots + note.rasterSlots <= numberOfSlots then
                            usedSlots = usedSlots + note.rasterSlots
                            table.insert(labeledSites, v)
                        else
                            break
                        end
                    end

                    -- TODO: Teste, ob bei kleinerer Anzahl an Slots gleiche Punkte gelabelt werden
                    --  -> dann kann Teillösung übernommen werden

                    -- Mögliche Aufteilungen (Positionierung des Labels für r) testen
                    -- und Optimum auswählen
                    if #labeledSites > 0 then
                        for numLabeledSites = 1, #labeledSites do
                            -- we place rightmost point: labeledSites[#labeledSites]
                            local rIndex = labeledSites[numLabeledSites]
                            local r = notesForPage[rIndex]

                            -- slotIndexR is the slot in that the label for r begins (topmost slot)
                            local bestVal = math.huge
                            local bestOpt = {}
                            -- try all label positions for r (leader should enter the label at east-anchor)
                            for slotIndexR = topSlot, (bottomSlot + 1 - r.rasterSlots) do
                                -- calculate position in which leader arm is placed
                                local labelTopR = getRasterAbsolute(rasterHeight, area.top, slotIndexR)
                                local leaderArmR, leaderShiftR
                                if avoidLines then
                                    leaderArmR = labelTopR - noteInnerSep - r:getHeight() / 2 -- east anchor

                                    -- find first line (from the top) whose lower bound is below leaderArmR
                                    local lineBelowInd
                                    for ind, v in pairs(linePositionsCurPage) do
                                        if v[3] <= leaderArmR then
                                            lineBelowInd = ind
                                            break
                                        end
                                    end

                                    -- choose the desired position for the leader arm
                                    -- (later we check if the label is high enough to shift the port to this position)
                                    local desiredPos = leaderArmR
                                    if lineBelowInd == nil then
                                        -- there is no line below the leaderArmR
                                        local lowestLine = linePositionsCurPage[#linePositionsCurPage]
                                        if lowestLine ~= nil and lowestLine[3] - leaderArmR < poMinDistLine then
                                            -- leader is too near to lowest line on page
                                            -- -> use the valid position below this line
                                            desiredPos = getPosBelowLine(linePositionsCurPage, #linePositionsCurPage)
                                        end
                                    else
                                        local lineBelow = linePositionsCurPage[lineBelowInd]
                                        local lineAbove = linePositionsCurPage[lineBelowInd - 1]
                                        if lineBelow ~= nil and leaderArmR - lineBelow[2] < poMinDistLine then
                                            -- leader is too near (or conflicting) to the line below
                                            -- -> move below or above this line (using the position closer to the original one)

                                            local posAbove = getPosAboveLine(linePositionsCurPage, lineBelowInd)
                                            local posBelow = getPosBelowLine(linePositionsCurPage, lineBelowInd)

                                            -- chose position which is closer to east anchor
                                            if posAbove - leaderArmR <= leaderArmR - posBelow then
                                                desiredPos = posAbove
                                            else
                                                desiredPos = posBelow
                                            end
                                        elseif lineAbove ~= nil and lineAbove[3] - leaderArmR < poMinDistLine then
                                            -- leader is too near to the line below
                                            -- -> use the valid position above this line
                                            desiredPos = getPosAboveLine(linePositionsCurPage, lineBelowInd)
                                        end
                                    end

                                    -- check if label is high enough to move leader to desired position
                                    if math.abs(desiredPos - leaderArmR) <= r:getHeight() / 2 + noteInnerSep then
                                        leaderShiftR = desiredPos - leaderArmR
                                        leaderArmR = desiredPos
                                    else
                                        leaderShiftR = 0
                                    end
                                else
                                    leaderArmR = labelTopR - noteInnerSep - r:getHeight() / 2
                                end

                                -- determine index of last point above arm
                                local pointAboveArm = 0
                                for k, ind in pairs(notes) do
                                    v = notesForPage[ind]
                                    if v.inputY >= leaderArmR then
                                        pointAboveArm = k
                                    else
                                        break
                                    end
                                end
                                local numPointsAbove = 0
                                local numPointsBelow = 0
                                for _, v in pairs(pointsBetween) do
                                    if v == rIndex then
                                        break
                                    end

                                    local note = notesForPage[v]
                                    if note.inputY >= leaderArmR then
                                        numPointsAbove = numPointsAbove + 1
                                    else
                                        numPointsBelow = numPointsBelow + 1
                                    end
                                end

                                local optAbove, optBelow
                                if pointAboveArm < topPoint then
                                    optAbove = optEmpty
                                elseif slotIndexR - 1 < topSlot then
                                    optAbove = optEmpty
                                elseif pointAboveArm > bottomPoint then
                                    optAbove = opt[topPoint][bottomPoint][topSlot][slotIndexR - 1][numPointsAbove]
                                else
                                    optAbove = opt[topPoint][pointAboveArm][topSlot][slotIndexR - 1][numPointsAbove]
                                end
                                if pointAboveArm + 1 > bottomPoint then
                                    optBelow = optEmpty
                                elseif slotIndexR + r.rasterSlots > bottomSlot then
                                    optBelow = optEmpty
                                elseif pointAboveArm + 1 < topPoint then
                                    optBelow = opt[topPoint][bottomPoint][slotIndexR + r.rasterSlots][bottomSlot][numPointsBelow]
                                else
                                    optBelow = opt[pointAboveArm + 1][bottomPoint][slotIndexR + r.rasterSlots][bottomSlot][numPointsBelow]
                                end

                                local partitionValid = true
                                if optAbove == nil or optBelow == nil then
                                    partitionValid = false
                                else
                                    local labeledAboveArm = table.keys(optAbove.positions)
                                    local labeledBelowArm = table.keys(optBelow.positions)
                                    if #labeledAboveArm + #labeledBelowArm + 1 ~= numLabeledSites then
                                        partitionValid = false
                                    else
                                        -- test if all of labeledNotes are in one of the sets
                                        -- last element (= r) must not be tested
                                        for testIndex = 1, (numLabeledSites - 1) do
                                            if not table.contains(labeledAboveArm, labeledSites[testIndex])
                                                and not table.contains(labeledBelowArm, labeledSites[testIndex]) then
                                                partitionValid = false
                                            end
                                        end
                                    end
                                end

                                if partitionValid then
                                    local newVal = math.abs(r.inputY - leaderArmR) + optAbove.totalLength + optBelow.totalLength
                                    if newVal < bestVal then
                                        bestVal = newVal
                                        bestOpt = {}
                                        bestOpt.totalLength = newVal
                                        bestOpt.positions = {}
                                        for k, v in pairs(optAbove.positions) do
                                            bestOpt.positions[k] = v
                                        end
                                        for k, v in pairs(optBelow.positions) do
                                            bestOpt.positions[k] = v
                                        end
                                        -- DEBUG
                                        if bestOpt.positions[rIndex] ~= nil then
                                            error("WARNING: Overwriting position of " .. rIndex .. " from " .. bestOpt.positions[rIndex] .. " to " .. slotIndexR)
                                        end
                                        bestOpt.positions[rIndex] = slotIndexR
                                        bestOpt.leaderShiftY = {}
                                        if avoidLines then
                                            for k, v in pairs(optAbove.leaderShiftY) do
                                                bestOpt.leaderShiftY[k] = v
                                            end
                                            for k, v in pairs(optBelow.leaderShiftY) do
                                                bestOpt.leaderShiftY[k] = v
                                            end
                                            bestOpt.leaderShiftY[rIndex] = leaderShiftR
                                        end
                                    end
                                end
                            end
                            if next(bestOpt) ~= nil then -- bestOpt is not an empty table
                                --print("setting opt[" .. topPoint .. "][" .. bottomPoint ..
                                      --"][" .. topSlot .. "][" .. bottomSlot .. "][" .. numLabeledSites .. "] = " .. inspect(bestOpt))
                                opt[topPoint][bottomPoint][topSlot][bottomSlot][numLabeledSites] = bestOpt
                            else
                                --print ("WARNING: Found no valid position for label in opt[" .. topPoint .. "][" .. bottomPoint ..
                                      --"][" .. topSlot .. "][" .. bottomSlot .. "][" .. numLabeledSites .. "] = " .. inspect(bestOpt))
                                opt[topPoint][bottomPoint][topSlot][bottomSlot][numLabeledSites] = nil
                            end
                        end
                    end
                end
            end
        end
    end

    if #notes > 0 then
        --DEBUG
        local maxPlaced = 0
        for i = 1,#notes do
            if opt[1][#notes][1][totalNumSlots][i] ~= nil then
                maxPlaced = i
            end
        end

        if maxPlaced < #notes then
            print("WARNING: could not fit all labels on page")
        end

        local res = opt[1][#notes][1][totalNumSlots][maxPlaced]
        local positions = res.positions
        local leaderShiftY = res.leaderShiftY
        if todonotesDebug then
            local length = res.totalLength
            -- in console
            print("----------------")
            print("po-leader algorithm: Using result: opt[1][" .. #notes ..
                                  "][1][" .. totalNumSlots .. "][" .. maxPlaced .. "]")

            print("resulting length: " .. length)
            print("resulting positions:")
            print(inspect(positions))
            print("resulting leaderShifts:")
            print(inspect(leaderShiftY))
            print("----------------")

            -- on page
            tex.print("\\node[text=blue,fill=white,rectangle,align=center] at (10.5cm,-27cm) {" ..
                "total length: " .. number.tocentimeters(length, "%s%s") .. "\\\\ " ..
                "rasterHeight: " .. number.tocentimeters(rasterHeight, "%s%s") ..
                "};")
        end

        for _, ind in pairs(notes) do
            local v = notesForPage[ind]
            v.outputX = area.left
            v.leaderShiftX = 0
            if positions[ind] == nil then
                print("did not define a position for note " .. v.index)
                v.outputY = 0
                v.leaderShiftY = 0
            else
                v.outputY = getRasterAbsolute(rasterHeight, area.top, positions[ind])
                if leaderShiftY[ind] ~= nil then
                    v.leaderShiftY = leaderShiftY[ind]
                end
            end
        end
    end
end
positioningAlgos["poLeaders"] = {algo = posPoLeaders,
    leaderAnchor = "east",
    leaderShift = false,
    twoSided = true}

local function posPoLeadersAvoid(notes, rightSide)
    posPoLeaders(notes, rightSide, true)
end
positioningAlgos["poLeadersAvoidLines"] = {algo = posPoLeadersAvoid,
    leaderAnchor = "east",
    leaderShift = true,
    twoSided = true,
    needLinePositions = true}




-- ********** Splitting Algorithms **********

-- ** splittingAlgorithm: none
-- place all notes on the wider side
local function splitNone()
    local rightSideSelected = false
    if labelArea.left == nil and labelArea.right == nil then
        error("Cannot place labels on any side of text (not enough space). " ..
            "Consider using the additionalMargin option of the package to " ..
            "extend the page margins " ..
            "or minNoteWidth to decrease the minimum width required")
    elseif labelArea.left == nil then
        rightSideSelected = true
    elseif labelArea.right ~= nil and
            labelArea.right.noteWidth > labelArea.left.noteWidth then
        rightSideSelected = true
    end

    for _, v in pairs(notesForPage) do
        v.rightSide = rightSideSelected
    end
end
splittingAlgos["none"] = {algo = splitNone}



-- ** splittingAlgorithm: middle
-- split on middle of page
local function splitMiddle()
    if labelArea:isOneSided() then
        splitNone()
    else
        local splitLine = (labelArea.text.right + labelArea.text.left)/2
        for _, v in pairs(notesForPage) do
            if v.inputX <= splitLine then
                v.rightSide = false
            else
                v.rightSide = true
            end
        end
    end
end
splittingAlgos["middle"] = {algo = splitMiddle}



-- ** splittingAlgorithm: median
-- split at median (sites sorted by x-coordinate)
local function splitMedian()
    if labelArea:isOneSided() then
        splitNone()
    else
        if #notesForPage == 0 then
            return
        end

        -- list that contains notes sorted by their inputX-coordinate
        local notesSorted = {}
        for _, v in pairs(notesForPage) do
            table.insert(notesSorted, v)
        end
        table.sort(notesSorted, compareNoteInputXAsc)

        local maxIndLeft
        if #notesSorted % 2 == 1 then
            maxIndLeft = math.ceil(#notesSorted / 2)
        else
            maxIndLeft = #notesSorted / 2
        end

        for k, v in pairs(notesSorted) do
            if k <= maxIndLeft then
                v.rightSide = false
            else
                v.rightSide = true
            end
        end
    end
end
splittingAlgos["median"] = {algo = splitMedian}



-- ** splittingAlgorithm: weightedMedian
-- split at weighted median (sites sorted by x-coordinate)
-- sum of heights of labels on both sides are similiar to each other
local function splitWeightedMedian()
    if labelArea:isOneSided() then
        splitNone()
    else
        if #notesForPage == 0 then
            return
        end

        -- list that contains notes sorted by their inputX-coordinate
        local notesSorted = {}
        for _, v in pairs(notesForPage) do
            table.insert(notesSorted, v)
        end
        table.sort(notesSorted, compareNoteInputXAsc)

        local heightLeft = 0
        local heightRight = 0
        while #notesSorted > 0 do
            if heightRight < heightLeft then
                -- place next note on the right side
                local note = notesSorted[#notesSorted]
                note.rightSide = true
                heightRight = heightRight + note:getHeight() + 2 * noteInnerSep + noteInterSpace
                table.remove(notesSorted, #notesSorted)
            else
                -- place next note on the left side
                local note = notesSorted[1]
                note.rightSide = false
                heightLeft = heightLeft + note:getHeight() + 2 * noteInnerSep + noteInterSpace
                table.remove(notesSorted, 1)
            end
        end
    end
end
splittingAlgos["weightedMedian"] = {algo = splitWeightedMedian}

