
class ElementTagger {
    constructor() {
        this.isTagged = false;
        this.tagList = {};

        const baseMnemonics = [
            'a', 's', 'd', 'f', 'g',
            'q', 'w', 'e', 'r', 't',
            'z', 'x', 'c', 'v', 'b',
            'y', 'u', 'i', 'o', 'p',
            'h', 'j', 'k', 'l',
            'n', 'm'
        ];

        let additionalMnemonics = [];

        // Upper + lower
        baseMnemonics.forEach( (upperLetter) => {
            upperLetter = upperLetter.toUpperCase();

            // Upper + lower
            baseMnemonics.forEach( (lowerLetter) => {
                additionalMnemonics.push("" + upperLetter + lowerLetter);
            });

            // Upper + upper
            baseMnemonics.forEach( (letter) => {
                additionalMnemonics.push("" + upperLetter + letter.toUpperCase());
            });
        });

        let mnemonics = baseMnemonics.concat(additionalMnemonics);

        mnemonics.forEach( (letter) => {
            this.tagList[letter] = undefined;
        });
    }

    tagElement(elem) {
        if (!this.isBoundedByViewport(elem)) {
            return;
        }

        let keys = Object.keys(this.tagList);
        for (let key_i in keys) {
            let key = keys[key_i];
            if (this.tagList[key] === undefined) {
                if ( this.addOverlay(elem, key) ) {
                    this.tagList[key] = elem;
                }

                break;
            }
        }
    }

    isBoundedByViewport(elem) {
        const viewport = window.visualViewport;
        const rect = elem.getClientRects()[0];

        if (rect == undefined) {
            return false;
        }

        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        const elemTop = (scrollTop + rect.top);
        if (elemTop > viewport.pageTop && elemTop < (viewport.pageTop + viewport.height)) {
            return true;
        }

        return false;
    }

    addOverlay(parentElem, mnemonic) {
        var rects = parentElem.getClientRects();
        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;

        if (parentElem._overlayElem == undefined) {
            var rect = undefined;
            for (let i = 0; i < rects.length; i++) {
                if (rects[i] !== undefined) {
                    rect = rects[i];
                    break;
                }
            }

            if (rect === undefined) {
                return false;
            }

            let elem = document.createElement('div');
            elem.style.position = 'absolute';
            elem.style.border = '1px solid blue';
            elem.style.borderRadius = '5px';
            elem.style.background = 'rgba(0, 0, 255, 0.8)';
            elem.style.color = 'white';
            elem.style.font = '12px Helvetica bold';
            elem.style.padding = '2px';


            elem.innerText = "[" + mnemonic + "]";
            elem.style.top = (rect.top + scrollTop) + 'px';
            elem.style.left = (rect.left + scrollLeft) + 'px';
            document.body.appendChild(elem);

            parentElem._overlayElem = elem;

            return true;
        }

        return false;
    }

    removeOverlay(elem) {
        let overlayElem = elem._overlayElem;
        if (overlayElem != undefined) {
            document.body.removeChild(overlayElem);
            elem._overlayElem = undefined;
        }
    }

    tagDocument() {
        // Anchors
        var anchors = document.getElementsByTagName("a");
        for (let i = 0; i < anchors.length; i++) {
            this.tagElement(anchors[i]);
        }

        // Buttons
        var buttons = document.getElementsByTagName("button");
        for (let i = 0; i < buttons.length; i++) {
            this.tagElement(buttons[i]);
        }

        this.isTagged = true;
    }

    untagDocument() {
        const keys = Object.keys(this.tagList);
        for (let key_i in keys) {
            let elem = this.tagList[keys[key_i]];
            if (elem !== undefined) {
                this.removeOverlay(elem);
                this.tagList[keys[key_i]] = undefined;
            }
        }

        this.isTagged = false;
    }

    clickLinkWithTag(tag) {
        if (this.tagList[tag] !== undefined) {
            this.tagList[tag].click();
        }
    }
}

class Scroller
{
    constructor() {
        this.modelPositionY = 0;
        this.modelPositionX = 0;

        this.modelPositionXVel = 0;
        this.modelPositionYVel = 0;
    }

    clampHorizontal(xOffset) {
        return Math.max(
            0, Math.min(
                xOffset, document.documentElement.scrollWidth - window.visualViewport.width
            )
        );
    }

    clampVertical(yOffset) {
        return Math.max(
            0, Math.min(
                yOffset, document.documentElement.scrollHeight - window.visualViewport.height
            )
        );
    }

    initializeModelPosition() {
        if (this.timerIdentifier == undefined) {
            this.startTimer();

            this.modelPositionY = window.scrollY;
            this.modelPositionX = window.scrollX;
        }
    }

    scrollBy(xOffset, yOffset) {
        this.initializeModelPosition();

        this.modelPositionY += yOffset;
        this.modelPositionY = this.clampVertical(this.modelPositionY);

        this.modelPositionX += xOffset;
        this.modelPositionX = this.clampHorizontal(this.modelPositionX);
    }

    setScrollVelocity(xVelocity, yVelocity) {
        this.initializeModelPosition();

        this.modelPositionXVel = xVelocity;
        this.modelPositionYVel = yVelocity;

        if (this.timerIdentifier == undefined) {
            this.startTimer();
        }
    }

    startTimer() {
        this.timerIdentifier = setInterval(this.updateTimer, (30 / 1000), this);
    }

    stopTimer() {
        clearInterval(this.timerIdentifier);
        this.timerIdentifier = undefined;
    }

    updateTimer(self) {
        self.modelPositionX = self.clampHorizontal(self.modelPositionX + self.modelPositionXVel);
        self.modelPositionY = self.clampVertical(self.modelPositionY + self.modelPositionYVel);

        const accelerationFactor = 0.05;
        let positionOffsetY = (self.modelPositionY - window.scrollY) * accelerationFactor;
        let positionOffsetX = (self.modelPositionX - window.scrollX) * accelerationFactor;
        window.scrollBy(positionOffsetX, positionOffsetY);

        var atRest = Math.abs(positionOffsetX) <= 1.0 && Math.abs(positionOffsetY) <= 1.0;
        var noVelocity = self.modelPositionXVel == 0 && self.modelPositionYVel == 0;
        if (atRest && noVelocity) {
            self.stopTimer();
        }
    }
}

const ScrollDirection = {
    Up:    'k',
    Down:  'j',
    Left:  'h',
    Right: 'l',
};

(function() {
    let tagger = new ElementTagger();
    let scroller = new Scroller();

    var tagAccum = [];
    document.addEventListener('keypress', (event) => {
        if (document.activeElement !== document.body) {
            return;
        }

        const keyName = String.fromCharCode(event.charCode);
        if (keyName == undefined) {
            return;
        }

        // Tagger
        if (tagger.isTagged) {
            // Uppercase
            if (tagAccum.length > 0 || keyName.toUpperCase() == keyName) {
                tagAccum.push(keyName);
            }

            if (tagAccum.length == 2) {
                tagger.clickLinkWithTag(tagAccum.join(''));
                tagger.untagDocument();
                tagAccum = [];
            } else if (tagAccum.length == 0) {
                tagger.clickLinkWithTag(keyName);
                tagger.untagDocument();
            }
        } else if (event.shiftKey) {
            // Otherwise, interpret as a single character command

            let lowerKey = keyName.toLowerCase();
            let scrollAmount = event.shiftKey ? window.visualViewport.height : 30;

            switch (lowerKey) {
                case ScrollDirection.Down:
                    scroller.scrollBy(0, scrollAmount);
                    break;
                case ScrollDirection.Up:
                    scroller.scrollBy(0, -scrollAmount);
                    break;
                case ScrollDirection.Left:
                    scroller.scrollBy(-scrollAmount, 0);
                    break;
                case ScrollDirection.Right:
                    scroller.scrollBy(scrollAmount, 0);
                    break;
            }
        }
    });

    document.addEventListener('keydown', (event) => {
        if (document.activeElement !== document.body) {
            return;
        }

        if (event.key == " ") {
            if (!tagger.isTagged) {
                tagger.tagDocument();
            }
            event.preventDefault();
        } else {
            let lowerKey = event.key.toLowerCase();
            let velocity = 5;

            switch (lowerKey) {
                case ScrollDirection.Down:
                    scroller.setScrollVelocity(0, velocity);
                    break;
                case ScrollDirection.Up:
                    scroller.setScrollVelocity(0, -velocity);
                    break;
                case ScrollDirection.Left:
                    scroller.setScrollVelocity(-velocity, 0);
                    break;
                case ScrollDirection.Right:
                    scroller.setScrollVelocity(velocity, 0);
                    break;
            }
        }
    });

    document.addEventListener('keyup', (event) => {
        if (document.activeElement !== document.body) {
            return;
        }

        if (event.key == " " && tagger.isTagged) {
            tagger.untagDocument();
            event.preventDefault();
        } else if (event.key == "Backspace") {
            if (!event.shiftKey) {
                window.history.back();
            } else {
                window.history.forward();
            }
        } else {
            scroller.setScrollVelocity(0, 0);
        }
    });
})();

