#include <AK/HashMap.h>
#include <AK/JsonObject.h>
#include <AK/StringBuilder.h>
#include <LibC/SharedBuffer.h>
#include <LibC/stdio.h>
#include <LibC/stdlib.h>
#include <LibC/unistd.h>
#include <LibDraw/GraphicsBitmap.h>
#include <LibGUI/GApplication.h>
#include <LibGUI/GEvent.h>
#include <LibGUI/GPainter.h>
#include <LibGUI/GWidget.h>
#include <LibGUI/GWindow.h>
#include <LibGUI/GWindowServerConnection.h>

//#define UPDATE_COALESCING_DEBUG

static HashTable<GWindow*> all_windows;
static HashMap<int, GWindow*> reified_windows;

GWindow* GWindow::from_window_id(int window_id)
{
    auto it = reified_windows.find(window_id);
    if (it != reified_windows.end())
        return (*it).value;
    return nullptr;
}

GWindow::GWindow(CObject* parent)
    : CObject(parent)
{
    all_windows.set(this);
    m_rect_when_windowless = { 100, 400, 140, 140 };
    m_title_when_windowless = "GWindow";
}

GWindow::~GWindow()
{
    all_windows.remove(this);
    hide();
}

void GWindow::close()
{
    hide();
}

void GWindow::move_to_front()
{
    if (!m_window_id)
        return;

    GWindowServerConnection::the().send_sync<WindowServer::MoveWindowToFront>(m_window_id);
}

void GWindow::show()
{
    if (m_window_id)
        return;
    auto response = GWindowServerConnection::the().send_sync<WindowServer::CreateWindow>(
        m_rect_when_windowless,
        m_has_alpha_channel,
        m_modal,
        m_resizable,
        m_fullscreen,
        m_show_titlebar,
        m_opacity_when_windowless,
        m_background_color,
        m_base_size,
        m_size_increment,
        (i32)m_window_type,
        m_title_when_windowless);
    m_window_id = response->window_id();

    apply_icon();

    reified_windows.set(m_window_id, this);
    GApplication::the().did_create_window({});
    update();
}

void GWindow::hide()
{
    if (!m_window_id)
        return;
    reified_windows.remove(m_window_id);
    GWindowServerConnection::the().send_sync<WindowServer::DestroyWindow>(m_window_id);
    m_window_id = 0;
    m_pending_paint_event_rects.clear();
    m_back_bitmap = nullptr;
    m_front_bitmap = nullptr;

    bool app_has_visible_windows = false;
    for (auto& window : all_windows) {
        if (window->is_visible()) {
            app_has_visible_windows = true;
            break;
        }
    }
    if (!app_has_visible_windows)
        GApplication::the().did_delete_last_window({});
}

void GWindow::set_title(const StringView& title)
{
    m_title_when_windowless = title;
    if (!m_window_id)
        return;
    GWindowServerConnection::the().send_sync<WindowServer::SetWindowTitle>(m_window_id, title);
}

String GWindow::title() const
{
    if (!m_window_id)
        return m_title_when_windowless;
    return GWindowServerConnection::the().send_sync<WindowServer::GetWindowTitle>(m_window_id)->title();
}

Rect GWindow::rect() const
{
    if (!m_window_id)
        return m_rect_when_windowless;
    return GWindowServerConnection::the().send_sync<WindowServer::GetWindowRect>(m_window_id)->rect();
}

void GWindow::set_rect(const Rect& a_rect)
{
    m_rect_when_windowless = a_rect;
    if (!m_window_id) {
        if (m_main_widget)
            m_main_widget->resize(m_rect_when_windowless.size());
        return;
    }
    GWindowServerConnection::the().send_sync<WindowServer::SetWindowRect>(m_window_id, a_rect);
    if (m_back_bitmap && m_back_bitmap->size() != a_rect.size())
        m_back_bitmap = nullptr;
    if (m_front_bitmap && m_front_bitmap->size() != a_rect.size())
        m_front_bitmap = nullptr;
    if (m_main_widget)
        m_main_widget->resize(a_rect.size());
}

void GWindow::set_window_type(GWindowType window_type)
{
    m_window_type = window_type;
}

void GWindow::set_override_cursor(GStandardCursor cursor)
{
    if (!m_window_id)
        return;
    GWindowServerConnection::the().send_sync<WindowServer::SetWindowOverrideCursor>(m_window_id, (u32)cursor);
}

void GWindow::event(CEvent& event)
{
    if (event.type() == GEvent::Drop) {
        auto& drop_event = static_cast<GDropEvent&>(event);
        if (!m_main_widget)
            return;
        auto result = m_main_widget->hit_test(drop_event.position());
        auto local_event = make<GDropEvent>(result.local_position, drop_event.text(), drop_event.data_type(), drop_event.data());
        ASSERT(result.widget);
        return result.widget->dispatch_event(*local_event, this);
    }

    if (event.type() == GEvent::MouseUp || event.type() == GEvent::MouseDown || event.type() == GEvent::MouseDoubleClick || event.type() == GEvent::MouseMove || event.type() == GEvent::MouseWheel) {
        auto& mouse_event = static_cast<GMouseEvent&>(event);
        if (m_global_cursor_tracking_widget) {
            auto window_relative_rect = m_global_cursor_tracking_widget->window_relative_rect();
            Point local_point { mouse_event.x() - window_relative_rect.x(), mouse_event.y() - window_relative_rect.y() };
            auto local_event = make<GMouseEvent>((GEvent::Type)event.type(), local_point, mouse_event.buttons(), mouse_event.button(), mouse_event.modifiers(), mouse_event.wheel_delta());
            m_global_cursor_tracking_widget->dispatch_event(*local_event, this);
            return;
        }
        if (m_automatic_cursor_tracking_widget) {
            auto window_relative_rect = m_automatic_cursor_tracking_widget->window_relative_rect();
            Point local_point { mouse_event.x() - window_relative_rect.x(), mouse_event.y() - window_relative_rect.y() };
            auto local_event = make<GMouseEvent>((GEvent::Type)event.type(), local_point, mouse_event.buttons(), mouse_event.button(), mouse_event.modifiers(), mouse_event.wheel_delta());
            m_automatic_cursor_tracking_widget->dispatch_event(*local_event, this);
            if (mouse_event.buttons() == 0)
                m_automatic_cursor_tracking_widget = nullptr;
            return;
        }
        if (!m_main_widget)
            return;
        auto result = m_main_widget->hit_test(mouse_event.position());
        auto local_event = make<GMouseEvent>((GEvent::Type)event.type(), result.local_position, mouse_event.buttons(), mouse_event.button(), mouse_event.modifiers(), mouse_event.wheel_delta());
        ASSERT(result.widget);
        set_hovered_widget(result.widget);
        if (mouse_event.buttons() != 0 && !m_automatic_cursor_tracking_widget)
            m_automatic_cursor_tracking_widget = result.widget->make_weak_ptr();
        if (result.widget != m_global_cursor_tracking_widget.ptr())
            return result.widget->dispatch_event(*local_event, this);
        return;
    }

    if (event.type() == GEvent::MultiPaint) {
        if (!m_window_id)
            return;
        if (!m_main_widget)
            return;
        auto& paint_event = static_cast<GMultiPaintEvent&>(event);
        auto rects = paint_event.rects();
        ASSERT(!rects.is_empty());
        if (m_back_bitmap && m_back_bitmap->size() != paint_event.window_size()) {
            // Eagerly discard the backing store if we learn from this paint event that it needs to be bigger.
            // Otherwise we would have to wait for a resize event to tell us. This way we don't waste the
            // effort on painting into an undersized bitmap that will be thrown away anyway.
            m_back_bitmap = nullptr;
        }
        bool created_new_backing_store = !m_back_bitmap;
        if (!m_back_bitmap) {
            m_back_bitmap = create_backing_bitmap(paint_event.window_size());
        } else if (m_double_buffering_enabled) {
            bool still_has_pixels = m_back_bitmap->shared_buffer()->set_nonvolatile();
            if (!still_has_pixels) {
                m_back_bitmap = create_backing_bitmap(paint_event.window_size());
                created_new_backing_store = true;
            }
        }

        auto rect = rects.first();
        if (rect.is_empty() || created_new_backing_store) {
            rects.clear();
            rects.append({ {}, paint_event.window_size() });
        }

        for (auto& rect : rects)
            m_main_widget->dispatch_event(*make<GPaintEvent>(rect), this);

        paint_keybinds();

        if (m_double_buffering_enabled)
            flip(rects);
        else if (created_new_backing_store)
            set_current_backing_bitmap(*m_back_bitmap, true);

        if (m_window_id) {
            Vector<Rect> rects_to_send;
            for (auto& r : rects)
                rects_to_send.append(r);
            GWindowServerConnection::the().post_message(WindowServer::DidFinishPainting(m_window_id, rects_to_send));
        }
        return;
    }

    if (event.type() == GEvent::KeyUp || event.type() == GEvent::KeyDown) {
        auto keyevent = static_cast<GKeyEvent&>(event);
        if (keyevent.logo() && event.type() == GEvent::KeyUp) {
            if (m_keybind_mode) {
                m_keybind_mode = false;
            } else {
                m_keybind_mode = true;
                collect_keyboard_activation_targets();
                m_entered_keybind = "";
            }
            update();
            return;
        }

        if (m_keybind_mode) {
            if (event.type() == GEvent::KeyUp) {
                StringBuilder builder;
                builder.append(m_entered_keybind);
                builder.append(keyevent.text());
                m_entered_keybind = builder.to_string();

                auto found_widget = m_keyboard_activation_targets.find(m_entered_keybind);
                if (found_widget != m_keyboard_activation_targets.end()) {
                    m_keybind_mode = false;
                    auto event = make<GMouseEvent>(GEvent::MouseDown, Point(), 0, GMouseButton::Left, 0, 0);
                    found_widget->value->dispatch_event(*event, this);
                    event = make<GMouseEvent>(GEvent::MouseUp, Point(), 0, GMouseButton::Left, 0, 0);
                    found_widget->value->dispatch_event(*event, this);
                } else if (m_entered_keybind.length() >= m_max_keybind_length) {
                    m_keybind_mode = false;
                }
                update();
            }
        } else {
            if (m_focused_widget)
                return m_focused_widget->dispatch_event(event, this);
            if (m_main_widget)
                return m_main_widget->dispatch_event(event, this);
        }
        return;
    }

    if (event.type() == GEvent::WindowBecameActive || event.type() == GEvent::WindowBecameInactive) {
        if (event.type() == GEvent::WindowBecameInactive && m_keybind_mode) {
            m_keybind_mode = false;
            update();
        }

        m_is_active = event.type() == GEvent::WindowBecameActive;
        if (m_main_widget)
            m_main_widget->dispatch_event(event, this);
        if (m_focused_widget)
            m_focused_widget->update();
        return;
    }

    if (event.type() == GEvent::WindowCloseRequest) {
        if (on_close_request) {
            if (on_close_request() == GWindow::CloseRequestDecision::StayOpen)
                return;
        }
        close();
        return;
    }

    if (event.type() == GEvent::WindowLeft) {
        set_hovered_widget(nullptr);
        return;
    }

    if (event.type() == GEvent::Resize) {
        auto new_size = static_cast<GResizeEvent&>(event).size();
        if (m_back_bitmap && m_back_bitmap->size() != new_size)
            m_back_bitmap = nullptr;
        if (!m_pending_paint_event_rects.is_empty()) {
            m_pending_paint_event_rects.clear_with_capacity();
            m_pending_paint_event_rects.append({ {}, new_size });
        }
        m_rect_when_windowless = { {}, new_size };
        m_main_widget->set_relative_rect({ {}, new_size });
        return;
    }

    if (event.type() > GEvent::__Begin_WM_Events && event.type() < GEvent::__End_WM_Events)
        return wm_event(static_cast<GWMEvent&>(event));

    CObject::event(event);
}

void GWindow::paint_keybinds()
{
    if (!m_keybind_mode)
        return;
    GPainter painter(*m_main_widget);

    for (auto& keypair : m_keyboard_activation_targets) {
        if (!keypair.value)
            continue;
        auto& widget = *keypair.value;
        bool could_be_keybind = true;
        for (size_t i = 0; i < m_entered_keybind.length(); ++i) {
            if (keypair.key.characters()[i] != m_entered_keybind.characters()[i]) {
                could_be_keybind = false;
                break;
            }
        }

        if (could_be_keybind) {
            Rect rect { widget.x() - 5, widget.y() - 5, 4 + Font::default_font().width(keypair.key), 16 };
            Rect highlight_rect { widget.x() - 3, widget.y() - 5, 0, 16 };

            painter.fill_rect(rect, Color::WarmGray);
            painter.draw_rect(rect, Color::Black);
            painter.draw_text(rect, keypair.key.characters(), TextAlignment::Center, Color::Black);
            painter.draw_text(highlight_rect, m_entered_keybind.characters(), TextAlignment::CenterLeft, Color::MidGray);
        }
    }
}

static void collect_keyboard_activation_targets_impl(GWidget& widget, Vector<GWidget*>& targets)
{
    widget.for_each_child_widget([&](auto& child) {
        if (child.supports_keyboard_activation()) {
            targets.append(&child);
            collect_keyboard_activation_targets_impl(child, targets);
        }
        return IterationDecision::Continue;
    });
}

void GWindow::collect_keyboard_activation_targets()
{
    m_keyboard_activation_targets.clear();
    if (!m_main_widget)
        return;

    Vector<GWidget*> targets;
    collect_keyboard_activation_targets_impl(*m_main_widget, targets);

    m_max_keybind_length = ceil_div(targets.size(), ('z' - 'a'));
    size_t buffer_length = m_max_keybind_length + 1;
    char keybind_buffer[buffer_length];
    for (size_t i = 0; i < buffer_length - 1; ++i)
        keybind_buffer[i] = 'a';
    keybind_buffer[buffer_length - 1] = '\0';

    for (auto& widget : targets) {
        m_keyboard_activation_targets.set(String(keybind_buffer), widget->make_weak_ptr());
        for (size_t i = 0; i < buffer_length - 1; ++i) {
            if (keybind_buffer[i] >= 'z') {
                keybind_buffer[i] = 'a';
            } else {
                ++keybind_buffer[i];
                break;
            }
        }
    }
}

bool GWindow::is_visible() const
{
    return m_window_id != 0;
}

void GWindow::update(const Rect& a_rect)
{
    if (!m_window_id)
        return;

    for (auto& pending_rect : m_pending_paint_event_rects) {
        if (pending_rect.contains(a_rect)) {
#ifdef UPDATE_COALESCING_DEBUG
            dbgprintf("Ignoring %s since it's contained by pending rect %s\n", a_rect.to_string().characters(), pending_rect.to_string().characters());
#endif
            return;
        }
    }

    if (m_pending_paint_event_rects.is_empty()) {
        deferred_invoke([this](auto&) {
            auto rects = move(m_pending_paint_event_rects);
            if (rects.is_empty())
                return;
            Vector<Rect> rects_to_send;
            for (auto& r : rects)
                rects_to_send.append(r);
            GWindowServerConnection::the().post_message(WindowServer::InvalidateRect(m_window_id, rects_to_send));
        });
    }
    m_pending_paint_event_rects.append(a_rect);
}

void GWindow::set_main_widget(GWidget* widget)
{
    if (m_main_widget == widget)
        return;
    if (m_main_widget)
        remove_child(*m_main_widget);
    m_main_widget = widget;
    if (m_main_widget) {
        add_child(*widget);
        auto new_window_rect = rect();
        if (m_main_widget->horizontal_size_policy() == SizePolicy::Fixed)
            new_window_rect.set_width(m_main_widget->preferred_size().width());
        if (m_main_widget->vertical_size_policy() == SizePolicy::Fixed)
            new_window_rect.set_height(m_main_widget->preferred_size().height());
        set_rect(new_window_rect);
        m_main_widget->set_relative_rect({ {}, new_window_rect.size() });
        m_main_widget->set_window(this);
        if (m_main_widget->accepts_focus())
            m_main_widget->set_focus(true);
    }
    update();
}

void GWindow::set_focused_widget(GWidget* widget)
{
    if (m_focused_widget == widget)
        return;
    if (m_focused_widget) {
        CEventLoop::current().post_event(*m_focused_widget, make<GEvent>(GEvent::FocusOut));
        m_focused_widget->update();
    }
    m_focused_widget = widget ? widget->make_weak_ptr() : nullptr;
    if (m_focused_widget) {
        CEventLoop::current().post_event(*m_focused_widget, make<GEvent>(GEvent::FocusIn));
        m_focused_widget->update();
    }
}

void GWindow::set_global_cursor_tracking_widget(GWidget* widget)
{
    if (widget == m_global_cursor_tracking_widget)
        return;
    m_global_cursor_tracking_widget = widget ? widget->make_weak_ptr() : nullptr;
}

void GWindow::set_automatic_cursor_tracking_widget(GWidget* widget)
{
    if (widget == m_automatic_cursor_tracking_widget)
        return;
    m_automatic_cursor_tracking_widget = widget ? widget->make_weak_ptr() : nullptr;
}

void GWindow::set_has_alpha_channel(bool value)
{
    if (m_has_alpha_channel == value)
        return;
    m_has_alpha_channel = value;
    if (!m_window_id)
        return;

    m_pending_paint_event_rects.clear();
    m_back_bitmap = nullptr;
    m_front_bitmap = nullptr;

    GWindowServerConnection::the().send_sync<WindowServer::SetWindowHasAlphaChannel>(m_window_id, value);
    update();
}

void GWindow::set_double_buffering_enabled(bool value)
{
    ASSERT(!m_window_id);
    m_double_buffering_enabled = value;
}

void GWindow::set_opacity(float opacity)
{
    m_opacity_when_windowless = opacity;
    if (!m_window_id)
        return;
    GWindowServerConnection::the().send_sync<WindowServer::SetWindowOpacity>(m_window_id, opacity);
}

void GWindow::set_hovered_widget(GWidget* widget)
{
    if (widget == m_hovered_widget)
        return;

    if (m_hovered_widget)
        CEventLoop::current().post_event(*m_hovered_widget, make<GEvent>(GEvent::Leave));

    m_hovered_widget = widget ? widget->make_weak_ptr() : nullptr;

    if (m_hovered_widget)
        CEventLoop::current().post_event(*m_hovered_widget, make<GEvent>(GEvent::Enter));
}

void GWindow::set_current_backing_bitmap(GraphicsBitmap& bitmap, bool flush_immediately)
{
    GWindowServerConnection::the().send_sync<WindowServer::SetWindowBackingStore>(m_window_id, 32, bitmap.pitch(), bitmap.shared_buffer_id(), bitmap.has_alpha_channel(), bitmap.size(), flush_immediately);
}

void GWindow::flip(const Vector<Rect, 32>& dirty_rects)
{
    swap(m_front_bitmap, m_back_bitmap);

    set_current_backing_bitmap(*m_front_bitmap);

    if (!m_back_bitmap || m_back_bitmap->size() != m_front_bitmap->size()) {
        m_back_bitmap = create_backing_bitmap(m_front_bitmap->size());
        memcpy(m_back_bitmap->scanline(0), m_front_bitmap->scanline(0), m_front_bitmap->size_in_bytes());
        m_back_bitmap->shared_buffer()->set_volatile();
        return;
    }

    // Copy whatever was painted from the front to the back.
    Painter painter(*m_back_bitmap);
    for (auto& dirty_rect : dirty_rects)
        painter.blit(dirty_rect.location(), *m_front_bitmap, dirty_rect);

    m_back_bitmap->shared_buffer()->set_volatile();
}

NonnullRefPtr<GraphicsBitmap> GWindow::create_shared_bitmap(GraphicsBitmap::Format format, const Size& size)
{
    ASSERT(GWindowServerConnection::the().server_pid());
    ASSERT(!size.is_empty());
    size_t pitch = round_up_to_power_of_two(size.width() * sizeof(RGBA32), 16);
    size_t size_in_bytes = size.height() * pitch;
    auto shared_buffer = SharedBuffer::create_with_size(size_in_bytes);
    ASSERT(shared_buffer);
    shared_buffer->share_with(GWindowServerConnection::the().server_pid());
    return GraphicsBitmap::create_with_shared_buffer(format, *shared_buffer, size);
}

NonnullRefPtr<GraphicsBitmap> GWindow::create_backing_bitmap(const Size& size)
{
    auto format = m_has_alpha_channel ? GraphicsBitmap::Format::RGBA32 : GraphicsBitmap::Format::RGB32;
    return create_shared_bitmap(format, size);
}

void GWindow::set_modal(bool modal)
{
    ASSERT(!m_window_id);
    m_modal = modal;
}

void GWindow::wm_event(GWMEvent&)
{
}

void GWindow::set_icon(const GraphicsBitmap* icon)
{
    if (m_icon == icon)
        return;

    m_icon = create_shared_bitmap(GraphicsBitmap::Format::RGBA32, icon->size());
    {
        GPainter painter(*m_icon);
        painter.blit({ 0, 0 }, *icon, icon->rect());
    }

    apply_icon();
}

void GWindow::apply_icon()
{
    if (!m_icon)
        return;

    if (!m_window_id)
        return;

    int rc = seal_shared_buffer(m_icon->shared_buffer_id());
    ASSERT(rc == 0);

    rc = share_buffer_globally(m_icon->shared_buffer_id());
    ASSERT(rc == 0);

    static bool has_set_process_icon;
    if (!has_set_process_icon)
        set_process_icon(m_icon->shared_buffer_id());

    GWindowServerConnection::the().send_sync<WindowServer::SetWindowIconBitmap>(m_window_id, m_icon->shared_buffer_id(), m_icon->size());
}

void GWindow::start_wm_resize()
{
    GWindowServerConnection::the().post_message(WindowServer::WM_StartWindowResize(GWindowServerConnection::the().my_client_id(), m_window_id));
}

Vector<GWidget*> GWindow::focusable_widgets() const
{
    if (!m_main_widget)
        return {};

    Vector<GWidget*> collected_widgets;

    Function<void(GWidget&)> collect_focusable_widgets = [&](GWidget& widget) {
        if (widget.accepts_focus())
            collected_widgets.append(&widget);
        widget.for_each_child_widget([&](auto& child) {
            if (!child.is_visible())
                return IterationDecision::Continue;
            if (!child.is_enabled())
                return IterationDecision::Continue;
            collect_focusable_widgets(child);
            return IterationDecision::Continue;
        });
    };

    collect_focusable_widgets(const_cast<GWidget&>(*m_main_widget));
    return collected_widgets;
}

void GWindow::save_to(AK::JsonObject& json)
{
    json.set("title", title());
    json.set("visible", is_visible());
    json.set("active", is_active());
    json.set("resizable", is_resizable());
    json.set("fullscreen", is_fullscreen());
    json.set("rect", rect().to_string());
    json.set("base_size", base_size().to_string());
    json.set("size_increment", size_increment().to_string());
    CObject::save_to(json);
}

void GWindow::set_fullscreen(bool fullscreen)
{
    if (m_fullscreen == fullscreen)
        return;
    m_fullscreen = fullscreen;
    if (!m_window_id)
        return;
    GWindowServerConnection::the().send_sync<WindowServer::SetFullscreen>(m_window_id, fullscreen);
}

void GWindow::schedule_relayout()
{
    if (m_layout_pending)
        return;
    m_layout_pending = true;
    deferred_invoke([this](auto&) {
        if (main_widget())
            main_widget()->do_layout();
        update();
        m_layout_pending = false;
    });
}

void GWindow::update_all_windows(Badge<GWindowServerConnection>)
{
    for (auto* window : all_windows) {
        window->update();
    }
}
