/* This is the basic linkfollowing script.
 *
 * TODO:
 * Some pages mess around a lot with the zIndex which
 *  lets some hints in the background.
 * Some positions are not calculated correctly (mostly
 *  because of uber-fancy-designed-webpages. Basic HTML and CSS
 *  works good
 * Still some links can't be followed/unexpected things
 *  happen. Blame some freaky webdesigners ;)
 */

// Globals
uzbldivid = 'uzbl_link_hints';

uzbl.follow = function() {
    // Export
    uzbl.follow.charset = arguments[0];

    if (arguments[2] == 0 || arguments[2] == 'click') {
        uzbl.follow.mode = 'click';
    } else if (arguments[2] == 1 || arguments[2] == 'newwindow') {
        uzbl.follow.mode = 'newwindow';
    } else if (arguments[2] == 'returnuri') {
        uzbl.follow.mode = 'returnuri';
    }

    var keypress = arguments[1];
    return arguments.callee.followLinks(keypress);
}

uzbl.follow.isFrame = function(el) {
    return (el.tagName == "FRAME" || el.tagName == "IFRAME");
}

// find the document that the given element belongs to
uzbl.follow.getDocument = function(el) {
    if (this.isFrame(el))
        return el.contentDocument;

    var doc = el;
    while (doc.parentNode !== null)
        doc = doc.parentNode;
    return doc;
}

// find all documents in the display, searching frames recursively
uzbl.follow.documents = function() {
    return this.windows().map(function(w) { return w.document; }).filter(function(d) { return d != null; });
}

// find all windows in the display, searching for frames recursively
uzbl.follow.windows = function(w) {
    w = (typeof w == 'undefined') ? window.top : w;

    var wins = [w];
    var frames = w.frames;
    for(var i = 0; i < frames.length; i++)
        wins = wins.concat(uzbl.follow.windows(frames[i]));
    return wins;
}

// search all frames for elements matching the given CSS selector
uzbl.follow.query = function(selector) {
    var res = [];
    this.documents().forEach(function (doc) {
        var set = doc.body.querySelectorAll(selector);
        // convert the NodeList to an Array
        set = Array.prototype.slice.call(set);
        res = res.concat(set);
    });
    return res;
}

// Calculate element position to draw the hint
uzbl.follow.elementPosition = function(el) {
    // el.getBoundingClientRect is another way to do this, but when a link is
    // line-wrapped we want our hint at the left end of the link, not its
    // bounding rectangle
    var up     = el.offsetTop;
    var left   = el.offsetLeft;
    var width  = el.offsetWidth;
    var height = el.offsetHeight;

    while (el.offsetParent) {
        el = el.offsetParent;
        up += el.offsetTop;
        left += el.offsetLeft;
    }

    return [up, left, width, height];
}

// Calculate if an element is on the viewport.
uzbl.follow.elementInViewport = function(el) {
    offset = uzbl.follow.elementPosition(el);
    var up     = offset[0];
    var left   = offset[1];
    var width  = offset[2];
    var height = offset[3];
    return  up   < window.pageYOffset + window.innerHeight &&
            left < window.pageXOffset + window.innerWidth &&
            (up + height)  > window.pageYOffset &&
            (left + width) > window.pageXOffset;
}

// Removes all hints/leftovers that might be generated
// by this script in the given document.
uzbl.follow.removeHints = function(doc) {
    var elements = doc.getElementById(uzbldivid);
    if (elements)
        elements.parentNode.removeChild(elements);

    // this returns a live NodeList, which is super-annoying when we to try
    // to remove the class.
    var followTextMatches = doc.getElementsByClassName('uzbl-follow-text-match');
    var matches = [];
    for(var i = 0; i < followTextMatches.length; ++i)
        matches.push(followTextMatches[i]);

    for(var i = 0; i < matches.length; ++i)
        matches[i].classList.remove('uzbl-follow-text-match');
}

// Clears all hints in every document
uzbl.follow.clearHints = function() {
    this.documents().forEach(uzbl.follow.removeHints);
}

// Generate a hint for an element with the given label
// Here you can play around with the style of the hints!
uzbl.follow.generateHint = function(doc, el, label, top, left) {
    var hint = doc.createElement('span');
    hint.innerText = label;
    hint.style.position = 'absolute';
    hint.style.top  = top  + 'px';
    hint.style.left = left + 'px';
    return hint;
}

// this is pointlessly duplicated in uzbl.formfiller
uzbl.follow.textInputTypes = [
  'text', 'password', 'search', 'email', 'url', 'number', 'range', 'color',
  'date', 'month', 'week', 'time', 'datetime', 'datetime-local'
];

// this is pointlessly duplicated in uzbl.formfiller
uzbl.follow.inputTypeIsText = function(type) {
    return uzbl.follow.textInputTypes.indexOf(type) >= 0;
}

// Here we choose what to do with an element that's been selected.
// On text form elements we focus and select the content. On other
// elements we simulate a mouse click.
uzbl.follow.clickElem = function(item) {
    if(!item) return;

    if (item instanceof HTMLInputElement && uzbl.follow.inputTypeIsText(item.type)) {
        item.focus();
        item.select();
        return "XXXFORM_ACTIVEXXX";
    } else if (item instanceof HTMLTextAreaElement || item instanceof HTMLSelectElement) {
        item.focus();
        if(typeof item.select != 'undefined')
            item.select();
        return "XXXFORM_ACTIVEXXX";
    }

    // simulate a mouseclick to activate the element
    var mouseEvent = document.createEvent("MouseEvent");
    mouseEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    item.dispatchEvent(mouseEvent);
    return "XXXRESET_MODEXXX";
}

// Draw all hints for all elements passed.
uzbl.follow.reDrawHints = function(elements, len) {
    // we have to calculate element positions before we modify the DOM
    // otherwise the elementPosition call slows way down.
    var positions = elements.map(uzbl.follow.elementPosition);

    this.documents().forEach(function(doc) {
        uzbl.follow.removeHints(doc);
        if (!doc.body) return;
        doc.hintdiv = doc.createElement('div');
        doc.hintdiv.id = uzbldivid;
        if(uzbl.follow.mode == 'newwindow')
          doc.hintdiv.className = "new-window";
        doc.body.appendChild(doc.hintdiv);
    });

    elements.forEach(function(el, i) {
        var label = uzbl.follow.intToLabel(i, len);
        var pos   = positions[i];

        try {
            var doc   = uzbl.follow.getDocument(el);
            var h = uzbl.follow.generateHint(doc, el, label, pos[0], pos[1]);
            doc.hintdiv.appendChild(h);
        } catch (err) {
            // Unable to attach label -> shrug it off and continue
        }
    });
}

// pass: number of keys
// returns: key length
uzbl.follow.labelLength = function(n) {
    var oldn = n;
    var keylen = 0;
    if(n < 2) return 1;
    n -= 1; // Our highest key will be n-1
    while(n) {
        keylen += 1;
        n = Math.floor(n / uzbl.follow.charset.length);
    }
    return keylen;
}

// converts an integer 'n' to a string of length 'len' composed of
// characters selected from uzbl.follow.charset.
uzbl.follow.intToLabel = function(n, len) {
    var label = '';
    do {
        label = uzbl.follow.charset.charAt(n % uzbl.follow.charset.length) + label;
        n = Math.floor(n / uzbl.follow.charset.length);
    } while(n);

    for (var x = label.length; x < len; x++) {
        label = uzbl.follow.charset.charAt(0) + label;
    }

    return label;
}

// pass: label
// returns: number
uzbl.follow.labelToInt = function(label) {
    var n = 0;
    for(var i = 0; i < label.length; ++i) {
        n *= uzbl.follow.charset.length;
        n += uzbl.follow.charset.indexOf(label[i]);
    }
    return n;
}

uzbl.follow.findMatchingHintId = function(elems, str) {
    var linknr = this.labelToInt(str);

    var len = this.labelLength(elems.length);

    if (str.length == len && linknr < elems.length && linknr >= 0) {
        // an element has been selected!
        var el = elems[linknr];
        return [el];
    }

    return elems.filter(function(el, i) {
        // return elements whose labels begin with the given str
        var label = uzbl.follow.intToLabel(i, len);
        return label.slice(0, str.length) == str;
    });
}

uzbl.follow.getInterestingElements = function() {
    var followable  = 'a, area, textarea, select, input:not([type=hidden]), button, *[onclick]';
    var uri         = 'a, area, frame, iframe';
    //var focusable   = 'a, area, textarea, select, input:not([type=hidden]), button, frame, iframe, applet, object';
    //var desc        = '*[title], img[alt], applet[alt], area[alt], input[alt]';
    //var image       = 'img, input[type=image]';

    if(uzbl.follow.mode == 'newwindow' || uzbl.follow.mode == 'returnuri')
        var elems = this.query(uri);
    else
        var elems = this.query(followable);

    return elems.filter(uzbl.follow.elementInViewport);
}

uzbl.follow.elementSelected = function(el) {
    // clear all of our hints
    this.clearHints();

    if (uzbl.follow.mode == 'returnuri') {
        var uri = el.src || el.href;
        return "XXXRETURNED_URIXXX" + uri
    } else if (uzbl.follow.mode == 'newwindow') {
        // we're opening a new window using the URL attached to this element
        var uri = el.src || el.href;
        if(uri.match(/javascript:/)) return;
        return "XXXNEW_WINDOWXXX " + uri;
    } else {
        // we're just going to click the element
        return this.clickElem(el);
    }
}

uzbl.follow.followTextContent = function(str) {
    str = str.toUpperCase();

    var matching = [];

    var elems = uzbl.follow.getInterestingElements();
    elems.forEach(function(el) {
      // do a case-insensitive match on element content
      if(el.textContent.toUpperCase().match(str)) {
        el.classList.add('uzbl-follow-text-match');
        matching.push(el);
      } else {
        el.classList.remove('uzbl-follow-text-match');
      }
    });

    if(matching.length == 1)
        return uzbl.follow.elementSelected(matching[0]);
}

uzbl.follow.followLinks = function(str) {
    var elems    = uzbl.follow.getInterestingElements();
    var leftover = uzbl.follow.findMatchingHintId(elems, str);

    if(leftover.length == 1)
        return uzbl.follow.elementSelected(leftover[0]);
    else {
        var len = this.labelLength(elems.length) - str.length;
        this.reDrawHints(leftover, len);
    }
}

uzbl.follow.followSelection = function(mode) {
    var selection = window.getSelection()
    if (!selection)
        return

    var node = selection.anchorNode
    if (!node)
        return

    var el = node.parentElement
    if (!el)
        return

    this.elementSelected(el)
}
