#include "app_core/canvas_hotkey.h"
#include "test_harness.h"

#include <cmath>

namespace {

class FakeCanvasHotkeyServices final : public pp::app::CanvasHotkeyServices {
public:
    pp::foundation::Status execute_tool(const pp::app::CanvasToolPlan& plan) override
    {
        tool_calls += 1;
        last_tool_mode = plan.mode;
        return pp::foundation::Status::success();
    }

    pp::foundation::Status execute_history(const pp::app::HistoryUiPlan& plan) override
    {
        history_calls += 1;
        last_history_operation = plan.operation;
        return pp::foundation::Status::success();
    }

    void save_document(pp::app::DocumentSaveIntent intent) override
    {
        save_calls += 1;
        last_save_intent = intent;
    }

    void toggle_ui() override { toggle_ui_calls += 1; }

    void adjust_brush_size(float delta) override
    {
        brush_adjust_calls += 1;
        last_brush_delta = delta;
    }

    void show_cursor() override { show_cursor_calls += 1; }

    int tool_calls = 0;
    int history_calls = 0;
    int save_calls = 0;
    int toggle_ui_calls = 0;
    int brush_adjust_calls = 0;
    int show_cursor_calls = 0;
    pp::app::CanvasToolMode last_tool_mode = pp::app::CanvasToolMode::draw;
    pp::app::HistoryUiOperation last_history_operation = pp::app::HistoryUiOperation::undo;
    pp::app::DocumentSaveIntent last_save_intent = pp::app::DocumentSaveIntent::save;
    float last_brush_delta = 0.0F;
};

pp::app::CanvasHotkeyState default_state() noexcept
{
    pp::app::CanvasHotkeyState state;
    state.undo_count = 2;
    state.redo_count = 1;
    return state;
}

void e_key_toggles_erase_and_draw(pp::tests::Harness& harness)
{
    const auto down = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_down,
        pp::app::CanvasHotkeyKey::e,
        default_state());
    const auto up = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::e,
        default_state());

    PP_EXPECT(harness, down);
    PP_EXPECT(harness, up);
    if (down) {
        PP_EXPECT(harness, down.value().action == pp::app::CanvasHotkeyAction::select_tool);
        PP_EXPECT(harness, down.value().tool.mode == pp::app::CanvasToolMode::erase);
        PP_EXPECT(harness, !down.value().no_op);
    }
    if (up) {
        PP_EXPECT(harness, up.value().action == pp::app::CanvasHotkeyAction::select_tool);
        PP_EXPECT(harness, up.value().tool.mode == pp::app::CanvasToolMode::draw);
        PP_EXPECT(harness, !up.value().no_op);
    }
}

void ctrl_z_and_shift_z_plan_history(pp::tests::Harness& harness)
{
    auto state = default_state();
    state.ctrl_down = true;
    const auto undo = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::z,
        state);
    state.shift_down = true;
    const auto redo = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::z,
        state);

    PP_EXPECT(harness, undo);
    PP_EXPECT(harness, redo);
    if (undo) {
        PP_EXPECT(harness, undo.value().action == pp::app::CanvasHotkeyAction::history);
        PP_EXPECT(harness, undo.value().history.operation == pp::app::HistoryUiOperation::undo);
        PP_EXPECT(harness, undo.value().history.invokes_undo);
    }
    if (redo) {
        PP_EXPECT(harness, redo.value().action == pp::app::CanvasHotkeyAction::history);
        PP_EXPECT(harness, redo.value().history.operation == pp::app::HistoryUiOperation::redo);
        PP_EXPECT(harness, redo.value().history.invokes_redo);
    }
}

void save_hotkeys_plan_document_intents(pp::tests::Harness& harness)
{
    auto state = default_state();
    state.ctrl_down = true;
    const auto save = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::s,
        state);
    state.shift_down = true;
    const auto save_version = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::s,
        state);

    PP_EXPECT(harness, save);
    PP_EXPECT(harness, save_version);
    if (save) {
        PP_EXPECT(harness, save.value().action == pp::app::CanvasHotkeyAction::save_document);
        PP_EXPECT(harness, save.value().save_intent == pp::app::DocumentSaveIntent::save);
    }
    if (save_version) {
        PP_EXPECT(harness, save_version.value().action == pp::app::CanvasHotkeyAction::save_document);
        PP_EXPECT(
            harness,
            save_version.value().save_intent == pp::app::DocumentSaveIntent::save_dirty_version);
    }
}

void app_and_brush_hotkeys_are_planned(pp::tests::Harness& harness)
{
    auto state = default_state();
    state.mouse_focused = true;
    const auto alt = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_down,
        pp::app::CanvasHotkeyKey::alt,
        state);
    const auto tab = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::tab,
        state);
    const auto smaller = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::bracket_left,
        state);
    const auto larger = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::bracket_right,
        state);

    PP_EXPECT(harness, alt);
    PP_EXPECT(harness, tab);
    PP_EXPECT(harness, smaller);
    PP_EXPECT(harness, larger);
    if (alt) {
        PP_EXPECT(harness, alt.value().action == pp::app::CanvasHotkeyAction::show_cursor);
    }
    if (tab) {
        PP_EXPECT(harness, tab.value().action == pp::app::CanvasHotkeyAction::toggle_ui);
    }
    if (smaller) {
        PP_EXPECT(harness, smaller.value().brush_size_delta < 0.0F);
    }
    if (larger) {
        PP_EXPECT(harness, larger.value().brush_size_delta > 0.0F);
    }
}

void touch_and_android_back_plan_undo(pp::tests::Harness& harness)
{
    auto state = default_state();
    const auto android = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_down,
        pp::app::CanvasHotkeyKey::android_back,
        state);
    state.touch_finger_count = 2;
    const auto tap = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::touch_tap,
        pp::app::CanvasHotkeyKey::other,
        state);

    PP_EXPECT(harness, android);
    PP_EXPECT(harness, tap);
    if (android) {
        PP_EXPECT(harness, android.value().action == pp::app::CanvasHotkeyAction::history);
        PP_EXPECT(harness, android.value().history.operation == pp::app::HistoryUiOperation::undo);
    }
    if (tap) {
        PP_EXPECT(harness, tap.value().action == pp::app::CanvasHotkeyAction::history);
        PP_EXPECT(harness, tap.value().history.operation == pp::app::HistoryUiOperation::undo);
    }
}

void planner_preserves_no_ops_and_rejects_bad_counts(pp::tests::Harness& harness)
{
    auto state = default_state();
    const auto no_ctrl_z = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::z,
        state);
    state.undo_count = -1;
    const auto bad_undo = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_down,
        pp::app::CanvasHotkeyKey::android_back,
        state);

    PP_EXPECT(harness, no_ctrl_z);
    if (no_ctrl_z) {
        PP_EXPECT(harness, no_ctrl_z.value().action == pp::app::CanvasHotkeyAction::none);
        PP_EXPECT(harness, no_ctrl_z.value().no_op);
    }
    PP_EXPECT(harness, !bad_undo);
    if (!bad_undo) {
        PP_EXPECT(harness, bad_undo.status().code == pp::foundation::StatusCode::out_of_range);
    }
}

void executor_dispatches_actions(pp::tests::Harness& harness)
{
    FakeCanvasHotkeyServices services;
    auto state = default_state();
    state.ctrl_down = true;

    const auto save = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::s,
        state);
    const auto undo = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::z,
        state);
    const auto erase = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_down,
        pp::app::CanvasHotkeyKey::e,
        state);
    const auto brush = pp::app::plan_canvas_hotkey(
        pp::app::CanvasHotkeyEvent::key_up,
        pp::app::CanvasHotkeyKey::bracket_right,
        state);

    PP_EXPECT(harness, save && undo && erase && brush);
    if (save && undo && erase && brush) {
        PP_EXPECT(harness, pp::app::execute_canvas_hotkey_plan(save.value(), services).ok());
        PP_EXPECT(harness, pp::app::execute_canvas_hotkey_plan(undo.value(), services).ok());
        PP_EXPECT(harness, pp::app::execute_canvas_hotkey_plan(erase.value(), services).ok());
        PP_EXPECT(harness, pp::app::execute_canvas_hotkey_plan(brush.value(), services).ok());
    }

    PP_EXPECT(harness, services.save_calls == 1);
    PP_EXPECT(harness, services.history_calls == 1);
    PP_EXPECT(harness, services.tool_calls == 1);
    PP_EXPECT(harness, services.brush_adjust_calls == 1);
    PP_EXPECT(harness, services.last_save_intent == pp::app::DocumentSaveIntent::save);
    PP_EXPECT(harness, services.last_history_operation == pp::app::HistoryUiOperation::undo);
    PP_EXPECT(harness, services.last_tool_mode == pp::app::CanvasToolMode::erase);
    PP_EXPECT(harness, std::fabs(services.last_brush_delta - 0.05F) < 0.001F);
}

void executor_rejects_malformed_brush_adjust(pp::tests::Harness& harness)
{
    FakeCanvasHotkeyServices services;
    pp::app::CanvasHotkeyPlan malformed;
    malformed.action = pp::app::CanvasHotkeyAction::adjust_brush_size;
    malformed.no_op = false;

    const auto status = pp::app::execute_canvas_hotkey_plan(malformed, services);
    PP_EXPECT(harness, !status.ok());
    PP_EXPECT(harness, status.code == pp::foundation::StatusCode::invalid_argument);
    PP_EXPECT(harness, services.brush_adjust_calls == 0);
}

} // namespace

int main()
{
    pp::tests::Harness harness;
    harness.run("e key toggles erase and draw", e_key_toggles_erase_and_draw);
    harness.run("ctrl z and shift z plan history", ctrl_z_and_shift_z_plan_history);
    harness.run("save hotkeys plan document intents", save_hotkeys_plan_document_intents);
    harness.run("app and brush hotkeys are planned", app_and_brush_hotkeys_are_planned);
    harness.run("touch and android back plan undo", touch_and_android_back_plan_undo);
    harness.run("planner preserves no ops and rejects bad counts", planner_preserves_no_ops_and_rejects_bad_counts);
    harness.run("executor dispatches actions", executor_dispatches_actions);
    harness.run("executor rejects malformed brush adjust", executor_rejects_malformed_brush_adjust);
    return harness.finish();
}
