--- === hs.webview ===
---
--- Display web content in a window from Hammerspoon
---
--- This module uses Apple's WebKit WKWebView class to provide web content display with JavaScript injection support.  The objective is to provide a functional interface to the WKWebView and WKUserContentController classes.
---
--- This module is not intended to replace a full web browser and does have some limitations to keep in mind:
---   * Self-signed SSL certificates are not accepted unless they have first been approved and included in the users Keychain by another method, for example, opening the page first in Safari.
---   * The context-menu (right clicking or ctrl-clicking in the webview) provides some menu items which are currently unsupported -- a known example of this is any "Download..." menu item in the context menu for links and images.
---   * It is uncertain at present exactly how or where cookies and cached page data is stored or how it can be invalidated.
---     * This can be mitigated to an extent for web requests by using `hs.webview:reload(true)` and by crafting the url for `hs.webview:url({...})` as a table -- see the appropriate help entries for more information.
---
--- Any suggestions or updates to the code to address any of these or other limitations as they may become apparent are welcome at the Hammerspoon github site: https://www.github.com/Hammerspoon/hammerspoon
---

--- === hs.webview.usercontent ===
---
--- This module provides support for injecting custom JavaScript user content into your webviews and for JavaScript to post messages back to Hammerspoon.

local USERDATA_TAG = "hs.webview"

local osVersion = require"hs.host".operatingSystemVersion()
if (osVersion["major"] == 10 and osVersion["minor"] < 10) then
    hs.luaSkinLog.wf("%s is only available on OS X 10.10 or later", USERDATA_TAG)
    -- nil gets interpreted as "nothing" and thus "true" by require...
    return false
end

local module       = require(USERDATA_TAG .. ".internal")
module.usercontent = require(USERDATA_TAG .. ".usercontent")
module.toolbar     = require(USERDATA_TAG .. ".toolbar")

local objectMT     = hs.getObjectMetatable(USERDATA_TAG)

local http         = require("hs.http")

if (osVersion["major"] == 10 and osVersion["minor"] < 11) then
    local message = USERDATA_TAG .. ".datastore is only available on OS X 10.11 or later"
    module.datastore = setmetatable({}, {
        __index = function(_)
            hs.luaSkinLog.w(message)
            return nil
        end,
        __tostring = function(_) return message end,
    })
else
    module.datastore   = require(USERDATA_TAG .. ".datastore")
    objectMT.datastore = module.datastore.fromWebview
end

-- required for image support
require("hs.image")

-- private variables and methods -----------------------------------------

local deprecatedWarningsGiven = {}
local deprecatedWarningCheck = function(oldName, newName)
    if not deprecatedWarningsGiven[oldName] then
        deprecatedWarningsGiven[oldName] = true
        hs.luaSkinLog.wf("%s:%s is deprecated; use %s:%s instead", USERDATA_TAG, oldName, USERDATA_TAG, newName)
    end
end

-- Public interface ------------------------------------------------------

module.windowMasks     = ls.makeConstantsTable(module.windowMasks)
module.certificateOIDs = ls.makeConstantsTable(module.certificateOIDs)

-- allow array-like usage of object to return child webviews
objectMT.__index = function(self, _)
    if objectMT[_] then
        return objectMT[_]
    elseif type(_) == "number" then
        return self:children()[_]
    else
        return nil
    end
end

--- hs.webview.newBrowser(rect, [preferencesTable], [userContentController]) -> webviewObject
--- Constructor
--- Create a webviewObject with some presets common to an interactive web browser.
---
--- Parameters:
---  * The parameters are the same as for [hs.webview.new](#new) -- check there for more details
---    * `rect`                  - a rectangle specifying where the webviewObject should be displayed.
---    * `preferencesTable`      - an optional table which specifies special settings for the webview object.
---    * `userContentController` - an optional `hs.webview.usercontent` object to provide script injection and JavaScript messaging with Hammerspoon from the webview.
---
--- Returns:
---  * The webview object
---
--- Notes:
---  * This constructor is just a short-hand for `hs.webview.new(...):allowTextEntry(true):allowGestures(true):windowStyle(15)`, which specifies a webview with a title bar, title bar buttons (zoom, close, minimize), and allows form entry and gesture support for previous and next pages.
---
--- * See [hs.webview.new](#new) and the following for more details:
---   * [hs.webview:allowGestures](#allowGestures)
---   * [hs.webview:allowTextEntry](#allowTextEntry)
---   * [hs.webview:windowStyle](#windowStyle)
---   * [hs.webview.windowMasks](#windowMasks)
module.newBrowser = function(...)
    return module.new(...):windowStyle(1+2+4+8)
                          :allowTextEntry(true)
                          :allowGestures(true)
end

--- hs.webview:attachedToolbar([toolbar | nil]) -> webviewObject | currentValue
--- Method
--- Get or attach/detach a toolbar to/from the webview.
---
--- Parameters:
---  * `toolbar` - if an `hs.webview.toolbar` object is specified, it will be attached to the webview.  If an explicit nil is specified, the current toolbar will be removed from the webview.
---
--- Returns:
---  * if a toolbarObject or explicit nil is specified, returns the webviewObject; otherwise returns the current toolbarObject or nil, if no toolbar is attached to the webview.
---
--- Notes:
---  * this method is a convenience wrapper for the `hs.webview.toolbar.attachToolbar` function.
---
---  * If the toolbarObject is currently attached to another window when this method is called, it will be detached from the original window and attached to the webview.  If you wish to attach the same toolbar to multiple webviews, see `hs.webview.toolbar:copy`.
objectMT.attachedToolbar = module.toolbar.attachToolbar

--- hs.webview:windowStyle(mask) -> webviewObject | currentMask
--- Method
--- Get or set the window display style
---
--- Parameters:
---  * mask - if present, this mask should be a combination of values found in `hs.webview.windowMasks` describing the window style.  The mask should be provided as one of the following:
---    * integer - a number representing the style which can be created by combining values found in `hs.webview.windowMasks` with the logical or operator.
---    * string  - a single key from `hs.webview.windowMasks` which will be toggled in the current window style.
---    * table   - a list of keys from `hs.webview.windowMasks` which will be combined to make the final style by combining their values with the logical or operator.
---
--- Returns:
---  * if a mask is provided, then the webviewObject is returned; otherwise the current mask value is returned.
objectMT.windowStyle = function(self, ...)
    local arg = table.pack(...)
    local theMask = objectMT._windowStyle(self)

    if arg.n ~= 0 then
        if type(arg[1]) == "number" then
            theMask = arg[1]
        elseif type(arg[1]) == "string" then
            if module.windowMasks[arg[1]] then
                theMask = theMask ~ module.windowMasks[arg[1]]
            else
                return error("unrecognized style specified: "..arg[1])
            end
        elseif type(arg[1]) == "table" then
            theMask = 0
            for _,v in ipairs(arg[1]) do
                if module.windowMasks[v] then
                    theMask = theMask | module.windowMasks[v]
                else
                    return error("unrecognized style specified: "..v)
                end
            end
        else
            return error("invalid type: number, string, or table expected, got "..type(arg[1]))
        end
        return objectMT._windowStyle(self, theMask)
    else
        return theMask
    end
end

--- hs.webview:allowGestures([value]) -> webviewObject | current value
--- Method
--- Get or set whether or not the webview will respond to gestures from a trackpad or magic mouse.  Default is false.
---
--- Parameters:
---  * value - an optional boolean value indicating whether or not the webview should respond gestures.
---
--- Returns:
---  * If a value is provided, then this method returns the webview object; otherwise the current value
---
--- Notes:
---  * This is a shorthand method for getting or setting both `hs.webview:allowMagnificationGestures` and `hs.webview:allowNavigationGestures`.
---  * This method will set both types of gestures to true or false, if given an argument, but will only return true if *both* gesture types are currently true; if either or both gesture methods are false, then this method will return false.
objectMT.allowGestures = function(self, ...)
    local r = table.pack(...)
    if r.n ~= 0 then
        self:allowMagnificationGestures(...)
        self:allowNavigationGestures(...)
        return self
    end
    return self:allowMagnificationGestures() and self:allowNavigationGestures()
end

--- hs.webview:delete([propagate], [fadeOutTime]) -> none
--- Method
--- Destroys the webview object, optionally fading it out first (if currently visible).
---
--- Parameters:
---  * `propagate`   - an optional boolean, default false, which indicates whether or not the child windows of this webview should also be deleted.
---  * `fadeOutTime` - an optional number of seconds over which to fade out the webview object. Defaults to zero.
---
--- Returns:
---  * None
---
--- Notes:
---  * This method is automatically called during garbage collection, notably during a Hammerspoon termination or reload, with a fade time of 0.
objectMT.delete = function(self, propagate, delay)
    if type(propagate) == "number" then
        propagate, delay = nil, propagate
    end
    delay = delay or 0
    if propagate then
        for _,v in ipairs(self:children()) do
            objectMT.delete(v, propagate, delay)
        end
    end

    return objectMT._delete(self, delay)
end

--- hs.webview:frame([rect]) -> webviewObject | currentValue
--- Method
--- Get or set the frame of the webview window.
---
--- Parameters:
---  * rect - An optional rect-table containing the co-ordinates and size the webview window should be moved and set to
---
--- Returns:
---  * If an argument is provided, the webview object; otherwise the current value.
---
--- Notes:
---  * a rect-table is a table with key-value pairs specifying the new top-left coordinate on the screen of the webview window (keys `x`  and `y`) and the new size (keys `h` and `w`).  The table may be crafted by any method which includes these keys, including the use of an `hs.geometry` object.
objectMT.frame = function(obj, ...)
    local args = table.pack(...)

    if args.n == 0 then
        local topLeft = obj:topLeft()
        local size    = obj:size()
        return {
            __luaSkinType = "NSRect",
            x = topLeft.x,
            y = topLeft.y,
            h = size.h,
            w = size.w,
        }
    elseif args.n == 1 and type(args[1]) == "table" then
        obj:size(args[1])
        obj:topLeft(args[1])
        return obj
    elseif args.n > 1 then
        error("frame method expects 0 or 1 arguments", 2)
    else
        error("frame method argument must be a table", 2)
    end
end

--- hs.webview:urlParts() -> table
--- Method
--- Returns a table of keys containing the individual components of the URL for the webview.
---
--- Parameters:
---  * None
---
--- Returns:
---  * a table containing the keys for the webview's URL.  See the function `hs.http.urlParts` for a description of the possible keys returned in the table.
---
--- Notes:
---  * This method is a wrapper to the `hs.http.urlParts` function wich uses the OS X APIs, based on RFC 1808.
---  * You may also want to consider the `hs.httpserver.hsminweb.urlParts` function for a version more consistent with RFC 3986.
objectMT.urlParts = function(self)
    return http.urlParts(self)
end

--- hs.webview:asHSWindow() -> hs.window object
--- Deprecated
--- Returns an hs.window object for the webview so that you can use hs.window methods on it.
---
--- This method is identical to [hs.webview:hswindow](#hswindow).  It is included for reasons of backwards compatibility, but use of the new name is recommended for clarity.
objectMT.asHSWindow = function(self, ...)
    deprecatedWarningCheck("asHSWindow", "hswindow")
    return self:hswindow(...)
end

objectMT.__len = function(self) return #self:children() end


--- hs.webview:setLevel(theLevel) -> drawingObject
--- Deprecated
--- Deprecated; you should use [hs.webview:level](#level) instead.
---
--- Parameters:
---  * `theLevel` - the level specified as a number, which can be obtained from `hs.drawing.windowLevels`.
---
--- Returns:
---  * the webview object
---
--- Notes:
---  * see the notes for `hs.drawing.windowLevels`
objectMT.setLevel = function(self, ...)
    deprecatedWarningCheck("setLevel", "level")
    return self:level(...)
end

--- hs.webview:behaviorAsLabels(behaviorTable) -> webviewObject | currentValue
--- Method
--- Get or set the window behavior settings for the webview object using labels defined in `hs.drawing.windowBehaviors`.
---
--- Parameters:
---  * behaviorTable - an optional table of strings and/or numbers specifying the desired window behavior for the webview object.
---
--- Returns:
---  * If an argument is provided, the webview object; otherwise the current value.
---
--- Notes:
---  * Window behaviors determine how the webview object is handled by Spaces and Exposé. See `hs.drawing.windowBehaviors` for more information.
objectMT.behaviorAsLabels = function(obj, ...)
    local drawing = require"hs.drawing"
    local args = table.pack(...)

    if args.n == 0 then
        local results = {}
        local behaviorNumber = obj:behavior()

        if behaviorNumber ~= 0 then
            for i, v in pairs(drawing.windowBehaviors) do
                if type(i) == "string" then
                    if (behaviorNumber & v) > 0 then table.insert(results, i) end
                end
            end
        else
            table.insert(results, drawing.windowBehaviors[0])
        end
        return setmetatable(results, { __tostring = function(_)
            table.sort(_)
            return "{ "..table.concat(_, ", ").." }"
        end})
    elseif args.n == 1 and type(args[1]) == "table" then
        local newBehavior = 0
        for _,v in ipairs(args[1]) do
            local flag = tonumber(v) or drawing.windowBehaviors[v]
            if flag then newBehavior = newBehavior | flag end
        end
        return obj:behavior(newBehavior)
    elseif args.n > 1 then
        error("behaviorAsLabels method expects 0 or 1 arguments", 2)
    else
        error("behaviorAsLabels method argument must be a table", 2)
    end
end

--- hs.webview:asHSDrawing() -> hs.drawing object
--- Deprecated
--- Because use of this method can easily lead to a crash, useful methods from `hs.drawing` have been added to the `hs.webview` module itself.  If you believe that a useful method has been overlooked, please submit an issue.
---
--- Parameters:
---  * None
---
--- Returns:
---  * a placeholder object
objectMT.asHSDrawing = setmetatable({}, {
    __call = function(_, obj)
        if not deprecatedWarningsGiven["asHSDrawing"] then
            deprecatedWarningsGiven["asHSDrawing"] = true
            hs.luaSkinLog.wf("%s:asHSDrawing() is deprecated and should not be used.", USERDATA_TAG)
        end
        return setmetatable({}, {
            __index = function(_, func)
                if objectMT[func] then
                    deprecatedWarningCheck("asHSDrawing():" .. func, func)
                    return function (_, ...) return objectMT[func](obj, ...) end
                elseif func:match("^set") then
                    local newFunc = func:match("^set(.*)$")
                    newFunc = newFunc:sub(1,1):lower() .. newFunc:sub(2)
                    if objectMT[newFunc] then
                        deprecatedWarningCheck("asHSDrawing():" .. func, newFunc)
                        return function (_, ...) return objectMT[newFunc](obj, ...) end
                    end
                end
                hs.luaSkinLog.wf("%s:asHSDrawing() is deprecated and the method %s does not currently have a replacement.  If you believer this is an error, please submit an issue.", USERDATA_TAG, func)
                return nil
            end,
        })
    end,
})

-- Return Module Object --------------------------------------------------

return module
