let txtjs = {}

Ox.load({UI: {loadCSS: false}}, function() {
    Ox.$parent.bindMessage(function(data, event) {
        txtjs.onMessage(data, event)
    })
})

txtjs.open = function(url) {
    fetch(url).then(function(response) {
        return response.text()
    }).then(txtjs.renderText)
}

txtjs.mark = function(notes) {
    notes.forEach(function(note) {
        if (!txtjs.notes.includes(note)) {
            txtjs.notes.push(note)
        }
        txtjs.renderNote(note)
    })
}

txtjs.notes = []

txtjs.addNoteFromSelection = function() {
    let note = txtjs.getNoteFromSelection()
    if (!note || txtjs.noteExists(note)) {
        return
    }
    txtjs.renderNote(note)
    txtjs.notes.push(note)
    txtjs.selectNote(note.id)
    getSelection().removeAllRanges()
    txtjs.postMessage('addNote', note)
}

txtjs.beginEdit = function() {
    let selected = txtjs.getSelectedNote()
    if (!selected || !selected.elements[0].classList.contains('editable')) {
        return
    }
    selected.elements.forEach(function(element) {
        element.classList.add('editing')
    })
}

txtjs.cancelEdit = function() {
    let editing = Array.from(document.querySelectorAll('g.editing'))
    editing.forEach(function(element) {
        element.classList.remove('editing')
    })
}

txtjs.createSVGElement = function(name) {
    return document.createElementNS('http://www.w3.org/2000/svg', name)
}

txtjs.editNote = function() {
    let editing = Array.from(document.querySelectorAll('g.editing'))
    let note = txtjs.getNoteFromSelection()
    if (editing.length == 0 || !note) {
        return
    }
    let id = txtjs.getNoteId(editing[0])
    note = Object.assign(Ox.getObjectById(txtjs.notes, id), {
        position: note.position,
        text: note.text
    })
    editing.forEach(function(element) {
        element.parentElement.removeChild(element)
    })
    txtjs.renderNote(note)
    getSelection().removeAllRanges()
    txtjs.postMessage('editNote', {
        id: id,
        position: note.position,
        text: note.text
    })
    txtjs.selectNote(note.id)
}

txtjs.getNewId = function() {
    let ids = txtjs.notes.map(function(note) {
        return note.id
    })
    let i = 1
    while (ids.includes(Ox.encodeBase26(i))) {
        i++
    }
    return Ox.encodeBase26(i)
}

txtjs.getNoteId = function(element) {
    let classNames = Array.from(element.classList).filter(function(className) {
        return className.startsWith('note-')
    })
    if (classNames.length == 0) {
        return
    }
    return classNames[0].substr(5)
}

txtjs.getNoteFromSelection = function() {
    let selection = getSelection()
    try {
        var range = selection.getRangeAt(0)
    } catch(e) {
        return
    }
    if (range.collapsed) {
        return
    }
    let container = range.commonAncestorContainer
    if (container.id != 'txt') {
        while (container != document.body) {
            container = container.parentElement
            if (container.id == 'txt') {
                break
            }
        }
    }
    if (container.id != 'txt') {
        return
    }
    let position = txtjs.getPosition(range)
    let pos = position.split(',').map(function(v) {
        return parseInt(v)
    })
    let note = {
        id: txtjs.getNewId(),
        position: position,
        text: txtjs.text.substr(pos[0], pos[1] - pos[0]),
        editable: true
    }
    if (txtjs.noteExists(note)) {
        return
    }
    return note
}

txtjs.getPosition = function(range) {
    let container = document.querySelector('#txt')
    let nodes = Array.from(container.childNodes)
    let startNodeIndex = range.startContainer == container
        ? range.startOffset : nodes.indexOf(range.startContainer)
    let endNodeIndex = range.endContainer == container
        ? range.endOffset : nodes.indexOf(range.endContainer)
    let startOffset = range.startContainer == container ? 0 : range.startOffset
    let endOffset = range.endContainer == container ? 0 : range.endOffset
    let index = 0
    let start = 0
    let end = 0
    for (let i = 0; i <= endNodeIndex; i++) {
        if (i == startNodeIndex) {
            start = index + startOffset
        }
        if (i == endNodeIndex) {
            end = index + endOffset
        }
        if (nodes[i].nodeType == 1) { // <br>
            index++
        } else {
            index += nodes[i].textContent.length
        }
    }
    while (' \n'.includes(txtjs.text.substr(start, 1))) {
        start++
    }
    while (' \n'.includes(txtjs.text.substr(end - 1, 1))) {
        end--
    }
    return start + ',' + end
}

txtjs.getRange = function(id, start, end) {
    let startContainer, startOffset, endContainer, endOffset
    let container = document.querySelector('#' + id)
    let nodes = Array.from(container.childNodes)
    let index = 0
    for (let i = 0; i < nodes.length; i++) {
        if (start < index + nodes[i].textContent.length && startOffset === void 0) {
            startContainer = nodes[i]
            startOffset = start - index
        }
        if (end <= index + nodes[i].textContent.length) {
            endContainer = nodes[i]
            endOffset = end - index
            break
        }
        if (nodes[i].nodeType == 1) { // <br>
            index++
        } else {
            index += nodes[i].textContent.length
        }
    }
    let range = document.createRange()
    range.setStart(startContainer, startOffset)
    range.setEnd(endContainer, endOffset)
    return range
}

txtjs.getSelectedNote = function() {
    let elements = Array.from(document.querySelectorAll('g.selected'))
    if (elements.length == 0) {
        return
    }
    let id = txtjs.getNoteId(elements[0])
    return Object.assign(Ox.getObjectById(txtjs.notes, id), {
        elements: elements
    })
}

txtjs.noteExists = function(note) {
    return txtjs.notes.some(function(note_) {
        return note_.position == note.position
    })
}

txtjs.onMessage = function(data, event) {
    console.log('onMessage', event, data)
    if (event == 'selectAnnotation') {
        txtjs.selectNote(data.id, false)
    } else if (event == 'addAnnotation') {
        txtjs.addNoteFromSelection()
    } else if (event == 'addAnnotations') {
        if (data.reset) {
            // fixme
        }
        data.annotations.forEach(function(note) {
            ////
            note.position = note.position.replace(':', ',')
            ////
            txtjs.renderNote(note)
            txtjs.notes.push(note)
        })
    } else if (event == 'removeAnnotation') {
        txtjs.selectNote(data.id)
        txtjs.removeNote()
    }
}

txtjs.postMessage = function(action, data) {
    console.log('postMessage', action, data)
    Ox.$parent.postMessage(action.replace('Note', 'Annotation'), data)
}

txtjs.removeNote = function() {
    let selected = txtjs.getSelectedNote()
    if (!selected) {
        return
    }
    let id = txtjs.getNoteId(selected.elements[0])
    selected.elements.forEach(function(element) {
        element.parentElement.removeChild(element)
    })
    let index = txtjs.notes.map(function(note) {
        return note.id
    }).indexOf(id)
    txtjs.notes.splice(index, 1)
    txtjs.postMessage('removeNote', {
        id: id
    })
}

txtjs.renderNote = function(note) {
    let pos = note.position.split(',').map(function(v) {
        return parseInt(v)
    })
    let ids = ['txt', 'txt-scroll']
    ids.forEach(function(id) {
        let range = txtjs.getRange(id, pos[0], pos[1])
        let rects = Array.from(range.getClientRects())
        let size = rects.reduce(function(width, rect) {
            return width + rect.width
        }, 0)
        let maxHeight = 8192
        let firstIndex = Math.floor((rects[0].top + window.pageYOffset) / maxHeight)
        let lastIndex = Math.floor((rects[rects.length - 1].top + window.pageYOffset + rects[rects.length - 1].height) / maxHeight)
        for (let index = firstIndex; index <= lastIndex; index++) {
            let g = txtjs.createSVGElement('g')
            g.classList.add('note-' + note.id)
            g.classList.add('selectable')
            if (note.editable) {
                g.classList.add('editable')
            }
            g.setAttribute('data-size', size)
            g.setAttribute('pointer-events', id == 'txt' ? 'all' : 'none')
            rects.forEach(function(rect) {
                let element = txtjs.createSVGElement('rect')
                let x = id == 'txt' ? rect.left
                    : rect.left - document.querySelector('#scroll').getBoundingClientRect().left + 8
                let y = id == 'txt' ? rect.top + window.pageYOffset - index * maxHeight
                    : rect.top + document.querySelector('#scroll').scrollTop - 16 - index * maxHeight
                element.setAttribute('x', x)
                element.setAttribute('y', y)
                element.setAttribute('width', rect.width)
                element.setAttribute('height', rect.height)
                g.appendChild(element)
            })
            let svg = document.querySelector('svg#svg-' + id + '-' + index)
            for (let i = 0; i < svg.children.length; i++) {
                let childSize = parseInt(svg.children[i].getAttribute('data-size'))
                if (size > childSize) {
                    svg.insertBefore(g, svg.children[i])
                    break
                }
            }
            if (!g.parentElement) {
                svg.appendChild(g)
            }
        }
    })
}

txtjs.renderSVG = function(id, index, width, height) {
    function mousedown(e) {
        svg.addEventListener('mouseup', mouseup)
        timeout = setTimeout(function() {
            svg.removeEventListener('mouseup', mouseup)
            e.target.classList.remove('selectable')
            e.target.setAttribute('pointer-events', 'none')
            document.addEventListener('mouseup', function() {
                e.target.classList.add('selectable')
                e.target.setAttribute('pointer-events', 'all')
            })
        }, 250)
    }
    function mouseup(e) {
        clearTimeout(timeout)
        txtjs.selectNote(txtjs.getNoteId(e.target.parentElement))
    }
    let timeout
    let svg = txtjs.createSVGElement('svg')
    svg.setAttribute('id', 'svg-' + id + '-' + index)
    svg.setAttribute('pointer-events', 'none')
    if (id == 'txt') {
        svg.style.left = 0
    } else {
        svg.style.right = '8px'
    }
    svg.style.top = index * 8192 + 'px'
    svg.style.width = width + 'px'
    svg.style.height = height + 'px'
    if (id == 'txt') {
        svg.addEventListener('mousedown', mousedown)
    }
    let parentElement = id == 'txt' ? document.body : document.querySelector('#scroll')
    parentElement.appendChild(svg)
}

txtjs.renderSVGs = function() {
    let maxHeight = 8192
    let ids = ['txt', 'txt-scroll']
    ids.forEach(function(id) {
        let rect = document.querySelector('#' + id).getBoundingClientRect()
        let lastHeight = rect.height % maxHeight
        let n = Math.ceil(rect.height / maxHeight)
        for (let i = 0; i < n; i++) {
            txtjs.renderSVG(id, i, rect.width, i < n - 1 ? maxHeight : lastHeight)
        }
    })
}

txtjs.renderText = function(text) {
    txtjs.text = text
    html = text.replace(/</g, '&lt;').replace(/>/g, '&gt;')
    html = html.replace().replace(/\r\n/g, '\n').replace(/[\r\n]/g, '<br>')
    txtjs.html = Ox.encodeHTMLEntities(text).replace(/\r\n/g, '\n').replace(/[\r\n]/g, '<br>')
    window.addEventListener('mouseup', onMouseup)
    window.addEventListener('resize', onResize)
    window.addEventListener('scroll', onScroll)
    document.addEventListener('keydown', function(e) {
        console.log(e.keyCode)
        if (e.keyCode == 8 || e.keyCode == 46) { // BACKSPACE || DELETE
            txtjs.removeNote()
        } else if (e.keyCode == 13) { // ENTER
            if (e.shiftKey) {
                txtjs.beginEdit()
            } else if (document.querySelector('g.editing')) {
                txtjs.editNote()
            } else {
                txtjs.addNoteFromSelection()
            }
        } else if (e.keyCode == 27) { // ESCAPE
            if (document.querySelector('g.editing')) {
                txtjs.cancelEdit()
            }
            if (document.querySelector('g.selected')) {
                txtjs.selectNote(null)
            }
        } else if (e.keyCode == 37) { // LEFT
            txtjs.selectNextNote(-1)
        } else if (e.keyCode == 39) { // RIGHT
            txtjs.selectNextNote(1)
        }
    })
    let style = document.createElement('style')
    style.innerText = [
        'svg { mix-blend-mode: multiply; position: absolute }',
        'g { fill: rgb(255, 255, 192); fill-opacity: 0.5 }',
        'g.selectable { cursor: pointer }',
        'g.editable { fill: rgb(255, 255, 0) }',
        'g.selected { fill: rgb(224, 240, 255) }',
        'g.editable.selected { fill: rgb(128, 192, 255) }',
        'g.editable.editing { fill: rgb(128, 255, 128) }',
        '::selection { background: rgb(192, 192, 192) }'
    ].join('\n')
    document.head.appendChild(style)
    document.body.style.backgroundColor = 'rgb(255, 255, 255)'
    document.body.style.margin = 0
    document.body.style.overflowX = 'hidden'
    let textElement = document.createElement('div')
    textElement.id = 'txt'
    textElement.style.fontFamily = 'Georgia, Palatino, DejaVu Serif, Book Antiqua, Palatino Linotype, Times New Roman, serif',
    textElement.style.fontSize = '20px'
    textElement.style.lineHeight = '30px'
    textElement.style.padding = '10% 20% 10% 10%'
    textElement.innerHTML = txtjs.html
    textElement.addEventListener('mousedown', function() {
        txtjs.selectNote(null)
    })
    document.body.appendChild(textElement)
    let scrollElement = document.createElement('div')
    scrollElement.id = 'scroll'
    scrollElement.style.bottom = '16px'
    scrollElement.style.overflow = 'hidden'
    scrollElement.style.position = 'fixed'
    scrollElement.style.right = '24px'
    scrollElement.style.width = '7%'
    scrollElement.style.top = '16px'
    document.body.appendChild(scrollElement)
    let scrollTextElement = document.createElement('div')
    scrollTextElement.id = 'txt-scroll'
    scrollTextElement.style.cursor = 'pointer'
    scrollTextElement.style.fontFamily = 'Georgia, Palatino, DejaVu Serif, Book Antiqua, Palatino Linotype, Times New Roman, serif',
    scrollTextElement.style.fontSize = '2px'
    scrollTextElement.style.lineHeight = '3px'
    scrollTextElement.style.MozUserSelect = 'none'
    scrollTextElement.style.WebkitUserSelect = 'none'
    scrollTextElement.innerHTML = txtjs.html
    scrollTextElement.addEventListener('mousedown', function(e) {
        let offset = 'offsetY' in e ? e.offsetY : e.layerY
        document.documentElement.scrollTop = offset / factor + margin - 16
    })
    scrollElement.appendChild(scrollTextElement)
    txtjs.renderSVGs()
    let factor, margin
    onResize()
    function onMouseup() {
        let note = txtjs.getNoteFromSelection()
        if (!note || txtjs.noteExists(note)) {
            txtjs.postMessage('selectText', null)
        } else {
            txtjs.postMessage('selectText', note)
        }
    }
    function onResize() {
        factor = scrollTextElement.clientHeight / textElement.clientHeight
        margin = textElement.offsetWidth * 0.1
        setTimeout(function() {
            Array.from(document.querySelectorAll('svg')).forEach(function(svg) {
                svg.parentElement.removeChild(svg)
            })
            txtjs.renderSVGs()
            txtjs.mark(txtjs.notes)
        })
    }
    function onScroll() {
        scrollElement.scrollTop = (window.pageYOffset - margin + 16) * factor
    }
}

txtjs.selectNextNote = function(direction) {
    let selected = txtjs.getSelectedNote()
    if (!selected) {
        return
    }
    let id = txtjs.getNoteId(selected.elements[0])
    let ids = txtjs.notes.sort(function(a, b) {
        return parseInt(a.position.split()[0]) - parseInt(b.position.split()[0])
    }).map(function(note) {
        return note.id
    })
    txtjs.selectNote(ids[Ox.mod(ids.indexOf(id) + direction, ids.length)])
}

txtjs.selectNote = function(id, trigger) {
    let selected = txtjs.getSelectedNote()
    if (selected) {
        selected.elements.forEach(function(element) {
            element.classList.remove('selected')
        })
    }
    if (id) {
        let editing = Array.from(document.querySelectorAll('g.editing'))
        if (editing.length && txtjs.getNoteId(editing[0]) != id) {
            editing.forEach(function(element) {
                element.classList.remove('editing')
            })
        }
        let elements = Array.from(document.querySelectorAll('g.note-' + id))
        elements.forEach(function(element) {
            element.classList.add('selected')
        })
        for (let i = 0; i < elements.length; i++) {
            if (!elements[i].parentNode.id.includes('scroll')) {
                elements[i].scrollIntoViewIfNeeded && elements[i].scrollIntoViewIfNeeded()
                break
            }
        }
    }
    if (trigger !== false) {
        txtjs.postMessage('selectNote', {id: id})
    }
}
