/*
 * Copyright © Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 or 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <miral/bounce_keys.h>

#include <mir/events/input_event.h>
#include <mir/events/keyboard_event.h>
#include <mir/input/composite_event_filter.h>
#include <mir/input/event_filter.h>
#include <mir/input/mousekeys_keymap.h>
#include <mir/log.h>
#include <mir/server.h>
#include <mir/synchronised.h>
#include <mir/time/types.h>
#include <miral/live_config.h>

#include <chrono>
#include <optional>

namespace mi = mir::input;

struct miral::BounceKeys::Self
{
    struct Config
    {
        explicit Config(bool enabled_by_default) :
            enabled{enabled_by_default}
        {
        }

        bool enabled;
        std::chrono::milliseconds delay{200};
        std::function<void(const MirKeyboardEvent*)> on_press_rejected{[](auto) {}};
    };

    struct BounceKeysFilter : public mi::EventFilter
    {
        struct RuntimeState
        {
            mi::XkbSymkey last_key;
            mir::time::Timestamp last_key_time;
        };

        explicit BounceKeysFilter(std::shared_ptr<mir::Synchronised<Config>> const& config) :
            config{config}
        {
        }

        bool handle(MirEvent const& event) override
        {
            if(event.type() != mir_event_type_input)
                return false;

            auto const* input_event = event.to_input();
            if(input_event->input_type() != mir_input_event_type_key)
                return false;

            auto const* key_event = input_event->to_keyboard();

            // Only consume "down" events
            auto const action = key_event->action();
            if (action != mir_keyboard_action_down)
                return false;

            auto const keysym = mir_keyboard_event_keysym(key_event);
            auto const keytime =
                mir::time::Timestamp{std::chrono::nanoseconds{mir_input_event_get_event_time(input_event)}};

            auto config_ = config->lock();

            if (state)
            {
                auto rejected = false;
                if (keysym == state->last_key && keytime - state->last_key_time < config_->delay)
                {
                    config_->on_press_rejected(key_event);
                    rejected = true;;
                }

                state->last_key = keysym;
                state->last_key_time = keytime;
                return rejected;
            }
            else
            {
                state.emplace(keysym, keytime);
            }

            return true;
        }

        std::optional<RuntimeState> state;
        std::shared_ptr<mir::Synchronised<Config>> const config;
    };

    void enable()
    {
        if (auto cef = composite_event_filter.lock())
        {
            bounce_keys_filter = std::make_shared<Self::BounceKeysFilter>(config);
            cef->prepend(bounce_keys_filter);
        }
    }

    explicit Self(bool enabled_by_default) :
        config{std::make_shared<mir::Synchronised<Config>>(Config{enabled_by_default})}
    {
    }

    std::shared_ptr<mir::Synchronised<Config>> const config;
    std::shared_ptr<BounceKeysFilter> bounce_keys_filter;

    std::weak_ptr<mi::CompositeEventFilter> composite_event_filter;
};

auto miral::BounceKeys::enabled() -> BounceKeys
{
    return BounceKeys{std::make_shared<Self>(true)};
}

auto miral::BounceKeys::disabled() -> BounceKeys
{
    return BounceKeys{std::make_shared<Self>(false)};
}

miral::BounceKeys::BounceKeys(std::shared_ptr<Self> self) :
    self{std::move(self)}
{
}

miral::BounceKeys::BounceKeys(live_config::Store& config_store) :
    self{std::make_shared<Self>(false)}
{
    config_store.add_bool_attribute(
        {"bounce_keys", "enable"},
        "Enable or disable bounce keys",
        [this](live_config::Key const&, std::optional<bool> val) {
            if(val)
            {
                if(*val)
                    this->enable();
                else
                    this->disable();
            }
        });

    config_store.add_int_attribute(
        {"bounce_keys", "delay"},
        "How much time in milliseconds must pass between keypresses to not be rejected.",
        [this](live_config::Key const& key, std::optional<int> val)
        {
            if (val)
            {
                if (*val >= 0)
                {
                    this->delay(std::chrono::milliseconds{*val});
                }
                else
                {
                    mir::log_warning(
                        "Config value %s does not support negative values. Ignoring the supplied value (%d)...",
                        key.to_string().c_str(),
                        *val);
                }
            }
        });
}

void miral::BounceKeys::operator()(mir::Server& server)
{
    server.add_init_callback(
        [&server, this]
        {
            self->composite_event_filter = server.the_composite_event_filter();

            if (self->config->lock()->enabled)
                self->enable();
        });
}

miral::BounceKeys& miral::BounceKeys::delay(std::chrono::milliseconds delay)
{
    if(delay < std::chrono::milliseconds{0})
        mir::log_warning("Bounce keys delay set to a negative value. Clamping to zero.");

    self->config->lock()->delay = std::max(delay, std::chrono::milliseconds{0});
    return *this;
}

miral::BounceKeys& miral::BounceKeys::on_press_rejected(std::function<void(const MirKeyboardEvent*)>&& on_press_rejected)
{
    self->config->lock()->on_press_rejected = std::move(on_press_rejected);
    return *this;
}

miral::BounceKeys& miral::BounceKeys::enable()
{
    auto const config = self->config->lock();
    if (config->enabled)
        return *this;
    config->enabled = true;

    self->enable();

    return *this;
}

miral::BounceKeys& miral::BounceKeys::disable()
{
    auto const config = self->config->lock();
    if (!config->enabled)
        return *this;
    config->enabled = false;

    self->bounce_keys_filter.reset();

    return *this;
}
