package com.example.components;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.shared.Registration;

import java.util.UUID;

/**
 * A single cell inside a {@link GridStackLayout}. Position/size are written as
 * {@code gs-*} attributes, which is how gridstack.js reads a widget's initial
 * placement when {@code makeWidget} converts it client-side (see grid-stack.ts).
 * <p>
 * Dragging starts from the grip handle only, never from the item body, so the
 * components inside an item stay interactive (see
 * {@link GridStackLayout#setDragHandle(String)}). A close button sits next to
 * the grip (see {@link #setClosable(boolean)}); removing the item server-side
 * is enough to unregister the widget client-side, since grid-stack.ts observes
 * its own childList.
 */
public class GridStackItem extends Div {

    /** Selector gridstack is told to treat as the drag handle — see
     *  {@link GridStackLayout}'s {@code handle} option. */
    public static final String DRAG_HANDLE_CLASS = "dialect-drag-handle";

    /** Marker class for the close button; {@code styles.css} keys both its own
     *  styling and the grip's offset off it. */
    public static final String CLOSE_BUTTON_CLASS = "dialect-close-button";

    private final Div content = new Div();
    private final Div dragHandle = new Div();
    private final Div closeButton = new Div();
    private boolean closable = true;

    public GridStackItem(Component... content) {
        this(UUID.randomUUID().toString(), 0, 0, 4, 3, content);
    }

    public GridStackItem(String id, int x, int y, int w, int h, Component... content) {
        addClassName("grid-stack-item");
        getElement().setAttribute("gs-id", id);
        getElement().setAttribute("gs-x", String.valueOf(x));
        getElement().setAttribute("gs-y", String.valueOf(y));
        getElement().setAttribute("gs-w", String.valueOf(w));
        getElement().setAttribute("gs-h", String.valueOf(h));

        this.content.addClassName("grid-stack-item-content");
        // Attach the wrapper div directly through the element API — calling
        // the overridden add(Component...) below would route it through
        // `content.add(...)`, making the wrapper try to add itself as its
        // own child.
        getElement().appendChild(this.content.getElement());
        this.content.add(content);

        // Sibling of the content wrapper, not a child of it: the wrapper is
        // `overflow: auto`, so a handle inside it would scroll out of view.
        dragHandle.addClassName(DRAG_HANDLE_CLASS);
        dragHandle.add(Fa.DRAG.create());
        String label = getTranslation("gridstack.dragHandle");
        dragHandle.getElement().setAttribute("title", label);
        dragHandle.getElement().setAttribute("aria-label", label);
        getElement().appendChild(dragHandle.getElement());

        // Sibling of the content wrapper for the same reason as the grip, and
        // outside the drag-handle selector so clicking it never starts a drag.
        closeButton.addClassName(CLOSE_BUTTON_CLASS);
        closeButton.add(Fa.CLOSE.create());
        String closeLabel = getTranslation("gridstack.close");
        closeButton.getElement().setAttribute("title", closeLabel);
        closeButton.getElement().setAttribute("aria-label", closeLabel);
        closeButton.getElement().setAttribute("role", "button");
        closeButton.getElement().setAttribute("tabindex", "0");
        closeButton.getElement().addEventListener("click", e -> close(true));
        closeButton.getElement().addEventListener("keydown", e -> close(true))
                .setFilter("event.key === 'Enter' || event.key === ' '");
        getElement().appendChild(closeButton.getElement());
    }

    /**
     * Detaches this item from its layout and notifies
     * {@link #addCloseListener(ComponentEventListener) close listeners}. Called
     * by the close button, and usable server-side to close an item
     * programmatically.
     */
    public void close() {
        close(false);
    }

    private void close(boolean fromClient) {
        // Detach first so listeners observe the layout they are about to see —
        // getLayout() on the grid no longer counts this item.
        getElement().removeFromParent();
        fireEvent(new CloseEvent(this, fromClient));
    }

    /** Shows or hides the close button (shown by default). */
    public GridStackItem setClosable(boolean closable) {
        if (closable == this.closable) {
            return this;
        }
        this.closable = closable;
        if (closable) {
            getElement().appendChild(closeButton.getElement());
        } else {
            closeButton.getElement().removeFromParent();
        }
        return this;
    }

    public boolean isClosable() {
        return closable;
    }

    /** The close button element. Exposed for restyling, the same way
     *  {@link #getDragHandle()} is. */
    public Div getCloseButton() {
        return closeButton;
    }

    public Registration addCloseListener(ComponentEventListener<CloseEvent> listener) {
        return addListener(CloseEvent.class, listener);
    }

    /** Fired after the item has been removed from its {@link GridStackLayout}. */
    public static class CloseEvent extends ComponentEvent<GridStackItem> {
        CloseEvent(GridStackItem source, boolean fromClient) {
            super(source, fromClient);
        }
    }

    public void add(Component... components) {
        content.add(components);
    }

    public void removeAll() {
        content.removeAll();
    }

    public String getItemId() {
        return getElement().getAttribute("gs-id");
    }

    public GridStackItem setResizable(boolean resizable) {
        setBooleanAttribute("gs-no-resize", !resizable);
        return this;
    }

    /** Also hides the grip handle when {@code false} — the {@code gs-no-move}
     *  attribute written here is what {@code styles.css} keys the handle's
     *  visibility off. */
    public GridStackItem setMovable(boolean movable) {
        setBooleanAttribute("gs-no-move", !movable);
        return this;
    }

    /** The element gridstack drags the item by. Exposed so callers can restyle
     *  or reposition it; removing it from the DOM makes the item immovable. */
    public Div getDragHandle() {
        return dragHandle;
    }

    /** Writes an explicit "true" (gridstack reads this via its own
     *  {@code Utils.toBool}) or removes the attribute — avoids relying on
     *  Vaadin's HTML5 empty-string boolean-attribute convention, which
     *  gridstack's own boolean parsing does not document matching. */
    private void setBooleanAttribute(String name, boolean value) {
        if (value) {
            getElement().setAttribute(name, "true");
        } else {
            getElement().removeAttribute(name);
        }
    }

    public GridStackItem setMinSize(int minWidth, int minHeight) {
        getElement().setAttribute("gs-min-w", String.valueOf(minWidth));
        getElement().setAttribute("gs-min-h", String.valueOf(minHeight));
        return this;
    }

    public GridStackItem setPosition(int x, int y, int w, int h) {
        getElement().setAttribute("gs-x", String.valueOf(x));
        getElement().setAttribute("gs-y", String.valueOf(y));
        getElement().setAttribute("gs-w", String.valueOf(w));
        getElement().setAttribute("gs-h", String.valueOf(h));
        return this;
    }

    /** Snapshot of a widget's grid placement, as reported by the client after a
     *  drag/resize. */
    public record Position(String id, int x, int y, int w, int h) {
    }
}
