local awful = require("awful")
local gears = require("gears")
local naughty = require("naughty")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local capi = { awesome = awesome, client = client }
local ruled = capi.awesome.version ~= "v4.3" and require("ruled") or nil
local pairs = pairs

local Scratchpad = { mt = {} }

--- Called when the turn off animation has ended
local function on_animate_turn_off_end(self, tag)
    -- When toggling off a scratchpad that's present on multiple tags
    -- depsite still being unminizmied on the other tags it will become invisible
    -- as it's position could be outside the screen from the animation
    self.client:geometry({
        x = self.geometry.x + self.client.screen.geometry.x,
        y = self.geometry.y + self.client.screen.geometry.y,
        width = self.geometry.width,
        height = self.geometry.height,
    })

    helpers.client.turn_off(self.client, tag)

    self.turning_off = false

    self:emit_signal("turn_off", self.client)
end

--- The turn off animation
local function animate_turn_off(self, anim, axis)
    self.screen_on_toggled_scratchpad = self.client.screen
    self.tag_on_toggled_scratchpad = self.screen_on_toggled_scratchpad.selected_tag

    if self.client.floating == false then
        -- Save the client geometry before floating it
        local non_floating_x = self.client.x
        local non_floating_y = self.client.y
        local non_floating_width = self.client.width
        local non_floating_height = self.client.height

        -- Can't animate non floating clients
        self.client.floating = true

        -- Set the client geometry back to what it was before floating it
        self.client:geometry({
            x = non_floating_x,
            y = non_floating_y,
            width = non_floating_width,
            height = non_floating_height,
        })
    end

    if axis == "x" then
        anim.pos = self.client.x
    else
        anim.pos = self.client.y
    end

    anim:set(anim:initial())
end

-- Handles changing tag mid animation
local function abort_if_tag_was_switched(self)
    -- Check for the following scenerio:
    -- Toggle on scratchpad at tag 1
    -- Toggle on scratchpad at tag 2
    -- Toggle off scratchpad at tag 1
    -- Switch to tag 2
    -- Outcome: The client will remain on tag 1 and will instead be removed from tag 2
    if (self.turning_off) and (self.screen_on_toggled_scratchpad and
        self.screen_on_toggled_scratchpad.selected_tag) ~= self.tag_on_toggled_scratchpad
    then
        if self.rubato.x then
            self.rubato.x:abort()
        end
        if self.rubato.y then
            self.rubato.y:abort()
        end
        on_animate_turn_off_end(self, self.tag_on_toggled_scratchpad)
        self.screen_on_toggled_scratchpad.selected_tag = nil
        self.tag_on_toggled_scratchpad = nil
    end
end

--- The turn on animation
local function animate_turn_on(self, anim, axis)
    -- Check for the following scenerio:
    -- Toggle on scratchpad at tag 1
    -- Toggle on scratchpad at tag 2
    -- The animation will instantly end
    -- as the timer pos is already at the on position
    -- from toggling on the scratchpad at tag 1
    if axis == "x" and anim.pos == self.geometry.x then
        anim.pos = anim:initial()
    else
        if anim.pos == self.geometry.y then
            anim.pos = anim:initial()
        end
    end

    if axis == "x" then
        anim:set(self.geometry.x)
    else
        anim:set(self.geometry.y)
    end
end

--- Creates a new scratchpad object based on the argument
--
-- @param args A table of possible arguments
-- @return The new scratchpad object
function Scratchpad:new(args)
    args = args or {}
    if args.awestore then
        naughty.notify({
            title = "Bling Error",
            text = "Awestore is no longer supported! Please take a look at the scratchpad documentation and use rubato for animations instead.",
        })
    end

    args.rubato = args.rubato or {}

    local ret = gears.object{}
    gears.table.crush(ret, Scratchpad)
    gears.table.crush(ret, args)

    if ret.rubato.x then
        ret.rubato.x:subscribe(function(pos)
            if ret.client and ret.client.valid then
                ret.client.x = pos
            end
            abort_if_tag_was_switched(ret)
        end)

        ret.rubato.x.ended:subscribe(function()
            if ((ret.rubato.y and ret.rubato.y.state == false) or (ret.rubato.y == nil)) and ret.turning_off == true then
                on_animate_turn_off_end(ret)
            end
        end)
    end
    if ret.rubato.y then
        ret.rubato.y:subscribe(function(pos)
            if ret.client and ret.client.valid then
                ret.client.y = pos
            end
            abort_if_tag_was_switched(ret)
        end)

        ret.rubato.y.ended:subscribe(function()
            if ((ret.rubato.x and ret.rubato.x.state == false) or (ret.rubato.x == nil)) and ret.turning_off == true then
                on_animate_turn_off_end(ret)
            end
        end)
    end

    return ret
end

--- Find all clients that satisfy the the rule
--
-- @return A list of all clients that satisfy the rule
function Scratchpad:find()
    return helpers.client.find(self.rule)
end

--- Applies the objects scratchpad properties to a given client
--
-- @param c A client to which to apply the properties
function Scratchpad:apply(c)
    if not c or not c.valid then
        return
    end
    c.floating = self.floating
    c.sticky = self.sticky
    c.fullscreen = false
    c.maximized = false
    c:geometry({
        x = self.geometry.x + awful.screen.focused().geometry.x,
        y = self.geometry.y + awful.screen.focused().geometry.y,
        width = self.geometry.width,
        height = self.geometry.height,
    })

    if self.autoclose then
        c:connect_signal("unfocus", function(c1)
            c1.sticky = false -- client won't turn off if sticky
            helpers.client.turn_off(c1)
        end)
    end
end

--- Turns the scratchpad on
function Scratchpad:turn_on()
    self.client = self:find()[1]

    local anim_x = self.rubato.x
    local anim_y = self.rubato.y

    local in_anim = false
    if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then
        in_anim = true
    end

    if self.client and not in_anim and self.client.first_tag and self.client.first_tag.selected then
        self.client:raise()
        capi.client.focus = self.client
        return
    end
    if self.client and not in_anim then
        -- if a client was found, turn it on
        if self.reapply then
            self:apply(self.client)
        end
        -- c.sticky was set to false in turn_off so it has to be reapplied anyway
        self.client.sticky = self.sticky

        if anim_x then
            animate_turn_on(self, anim_x, "x")
        end
        if anim_y then
            animate_turn_on(self, anim_y, "y")
        end

        helpers.client.turn_on(self.client)
        self:emit_signal("turn_on", self.client)

        return
    end
    if not self.client then
        -- if no client was found, spawn one, find the corresponding window,
        --  apply the properties only once (until the next closing)
        local pid = awful.spawn.with_shell(self.command)
        if capi.awesome.version ~= "v4.3" then
            ruled.client.append_rule({
                id = "scratchpad",
                rule = self.rule,
                properties = {
                    -- If a scratchpad is opened it should spawn at the current tag
                    -- the same way it will behave if the client was already open
                    tag = awful.screen.focused().selected_tag,
                    switch_to_tags = false,
                    -- Hide the client until the gemoetry rules are applied
                    hidden = true,
                    minimized = true,
                },
                callback = function(c)
                    -- For a reason I can't quite get the gemotery rules will fail to apply unless we use this timer
                    gears.timer({
                        timeout = 0.15,
                        autostart = true,
                        single_shot = true,
                        callback = function()
                            self.client = c

                            self:apply(c)
                            c.hidden = false
                            c.minimized = false
                            -- Some clients fail to gain focus
                            c:activate({})

                            if anim_x then
                                animate_turn_on(self, anim_x, "x")
                            end
                            if anim_y then
                                animate_turn_on(self, anim_y, "y")
                            end

                            self:emit_signal("inital_apply", c)

                            -- Discord spawns 2 windows, so keep the rule until the 2nd window shows
                            if c.name ~= "Discord Updater" then
                                ruled.client.remove_rule("scratchpad")
                            end
                            -- In a case Discord is killed before the second window spawns
                            c:connect_signal("request::unmanage", function()
                                ruled.client.remove_rule("scratchpad")
                            end)
                        end,
                    })
                end,
            })
        else
            local function inital_apply(c1)
                if helpers.client.is_child_of(c1, pid) then
                    self.client = c1

                    self:apply(c1)
                    if anim_x then
                        animate_turn_on(self, anim_x, "x")
                    end
                    if anim_y then
                        animate_turn_on(self, anim_y, "y")
                    end
                    self:emit_signal("inital_apply", c1)
                    client.disconnect_signal("manage", inital_apply)
                end
            end
            client.connect_signal("manage", inital_apply)
        end
    end
end

--- Turns the scratchpad off
function Scratchpad:turn_off()
    self.client = self:find()[1]

    -- Get the tweens
    local anim_x = self.rubato.x
    local anim_y = self.rubato.y

    local in_anim = false
    if (anim_x and anim_x.state == true) or (anim_y and anim_y.state == true) then
        in_anim = true
    end

    if self.client and not in_anim then
        if anim_x then
            self.turning_off = true
            animate_turn_off(self, anim_x, "x")
        end
        if anim_y then
            self.turning_off = true
            animate_turn_off(self, anim_y, "y")
        end

        if not anim_x and not anim_y then
            helpers.client.turn_off(self.client)
            self:emit_signal("turn_off", self.client)
        end
    end
end

--- Turns the scratchpad off if it is focused otherwise it raises the scratchpad
function Scratchpad:toggle()
    local is_turn_off = false
    local c = self:find()[1]
    if self.dont_focus_before_close then
        if c then
            if c.sticky and #c:tags() > 0 then
                is_turn_off = true
            else
                local current_tag = c.screen.selected_tag
                for k, tag in pairs(c:tags()) do
                    if tag == current_tag then
                        is_turn_off = true
                        break
                    else
                        is_turn_off = false
                    end
                end
            end
        end
    else
        is_turn_off = capi.client.focus
            and awful.rules.match(capi.client.focus, self.rule)
    end

    if is_turn_off then
        self:turn_off()
    else
        self:turn_on()
    end
end

--- Make the module callable without putting a `:new` at the end of it
--
-- @param args A table of possible arguments
-- @return The new scratchpad object
function Scratchpad.mt:__call(...)
    return Scratchpad:new(...)
end

return setmetatable(Scratchpad, Scratchpad.mt)