-- Link hinting for luakit - web module.
--
-- @submodule follow_wm
-- @copyright 2016 Aidan Holm <aidanholm@gmail.com>

local select = require("select_wm")
local lousy = require("lousy")
local ui = ipc_channel("follow_wm")

local getpid = function() return luakit.web_process_id end

local evaluators = {
    click = function(element, page)
        local tag = element.tag_name
        if tag == "INPUT" or tag == "TEXTAREA" then
            local t = element.attr.type
            if t == "radio" or t == "checkbox" or t == "submit" or t == "reset" or t == "button" then
                element:click()
                return
            else
                element:focus()
                return "form-active"
            end
        end
        -- Handle <a target=_blank> indirectly; WebKit prevents opening a new
        -- window if not initiated by the user directly
        if tag == "A" and element.attr.target == "_blank" then
            ui:emit_signal("click_a_target_blank", page.id, element.href)
            return
        end
        -- Find and click the element directly in the centre of the link, with shadow DOM and anchor navigation support
        local click_func, err = page:wrap_js([=[
            try {
                if (element.childElementCount > 0) {
                    var rects = element.getClientRects();
                    if (rects && rects.length > 0) {
                        var r = rects[0];
                        var x = r.left + r.width / 2;
                        var y = r.top + r.height / 2;
                        var point_elem = document.elementFromPoint(x, y);
                        if (point_elem) {
                            var is_desc = false;
                            var curr = point_elem;
                            while (curr) {
                                if (curr === element) {
                                    is_desc = true;
                                    break;
                                }
                                curr = curr.parentNode || curr.host;
                            }
                            if (is_desc) {
                                element = point_elem;
                            }
                        }
                    }
                }
            } catch(e) {}

            var anchor = element;
            while (anchor && (!anchor.tagName || anchor.tagName.toUpperCase() !== 'A')) {
                if (anchor === document || anchor === window) {
                    anchor = null;
                    break;
                }
                anchor = anchor.parentNode || anchor.host;
            }

            if (anchor && anchor !== element) {
                var event = new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true,
                    view: window
                });
                var not_prevented = element.dispatchEvent(event);
                if (not_prevented && anchor.href && typeof anchor.click === 'function') {
                    anchor.click();
                }
            } else {
                var clickTarget = element;
                while (clickTarget && typeof clickTarget.click !== 'function') {
                    if (clickTarget === document || clickTarget === window) {
                        clickTarget = null;
                        break;
                    }
                    clickTarget = clickTarget.parentNode || clickTarget.host;
                }
                if (clickTarget) {
                    clickTarget.click();
                }
            }
        ]=], {"element"})
        if click_func then
            click_func(element)
        else
            msg.warn("wrap_js click failed: %s", err)
            element:click()
        end
    end,
    focus = function(element)
        element:focus()
        local tag = element.tag_name
        if tag == "INPUT" or tag == "TEXTAREA" then
            return "form-active"
        else
            return "root-active"
        end
    end,
    uri = function(element)
        return element.src or element.href
    end,
    desc = function(element)
        local attrs = element.attr
        return attrs.title or attrs.alt
    end,
    src = function(element)
        return element.src
    end,
    parent_href = function(element)
        return element.parent.href
    end,
}

local page_mode = {}

local function follow_hint(page, mode, hint)
    local evaluator
    if type(mode.evaluator) == "string" then
        evaluator = evaluators[mode.evaluator]
    elseif type(mode.evaluator) == "function" then
        evaluator = mode.evaluator
    else
        error("bad evaluator type '%s'", type(mode.evaluator))
    end

    local overlay_style = hint.overlay_elem.attr.style
    hint.overlay_elem.attr.style = "display: none;"
    local ret = evaluator(hint.elem, page)
    hint.overlay_elem.attr.style = overlay_style

    ui:emit_signal("follow_func", page.id, ret)
end

local function follow(page, all)
    -- Build array of hints to follow
    local hints
    if all then
        hints = select.hints(page)
    else
        local fh = select.focused_hint(page)
        if not fh then return end
        hints = { fh }
    end
    hints = lousy.util.table.filter_array(hints, function (_, hint)
        return not hint.hidden
    end)
    if #hints == 0 then return end

    -- Close hint select UI first if not persisting in follow mode
    local mode = page_mode[page]
    if mode and not mode.persist then
        select.leave(page)
        page_mode[page] = nil
    end

    -- Follow hints in idle cb to ensure select UI is closed if necessary
    luakit.idle_add(function ()
        for _, hint in pairs(hints) do
            follow_hint(page, mode, hint)
        end
    end)
end

ui:add_signal("follow", function(_, page, all)
    follow(page, all)
end)

ui:add_signal("focus", function(_, page, step)
    select.focus(page, step)
end)

ui:add_signal("enter", function(_, page, mode, ignore_case)
    page_mode[page] = mode
    select.enter(page, mode.selector, mode.stylesheet, ignore_case)

    local num_visible_hints = #(select.hints(page))
    ui:emit_signal("matches", page.id, getpid(), num_visible_hints)
end)

ui:add_signal("init_follow", function(_, page, mode, ignore_case)
    page_mode[page] = mode
    local num_elements = select.scan(page, mode.selector, ignore_case)
    ui:emit_signal("ready", page.id, getpid(), num_elements)
end)

ui:add_signal("start_follow", function(_, page, target_pid, start_idx, count, stylesheet)
    if getpid() == target_pid then
        local total_labels_needed = start_idx + count - 1
        local all_labels = select.make_labels(total_labels_needed)
        local labels = {}
        for i = 1, count do
            labels[i] = all_labels[start_idx + i - 1]
        end
        local _, num_visible_hints = select.show_hints(page, labels, stylesheet)
        ui:emit_signal("matches", page.id, getpid(), num_visible_hints)
    end
end)

ui:add_signal("changed", function(_, page, hint_pat, text_pat, text)
    local _, num_visible_hints = select.changed(page, hint_pat, text_pat, text)
    ui:emit_signal("matches", page.id, getpid(), num_visible_hints)
end)

ui:add_signal("trigger_follow", function(_, page, all)
    follow(page, all)
end)

ui:add_signal("leave", function (_, page)
    if page_mode[page] then
        page_mode[page] = nil
        select.leave(page)
    end
end)

-- vim: et:sw=4:ts=8:sts=4:tw=80
