package eu.jonahbauer.wizard.client.libgdx.actors;

import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
import com.badlogic.gdx.utils.Pools;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.util.Pair;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.Consumer;

import static com.badlogic.gdx.scenes.scene2d.actions.Actions.moveTo;

public class CardsGroup extends WidgetGroup {
    private static final float TARGET_SPACING = -50f;

    @Getter
    private final float prefWidth = 0;
    @Getter
    private final float prefHeight = CardActor.PREF_HEIGHT;

    private final TextureAtlas atlas;

    private float[] cardX;
    private float spacing;
    private float cardWidth;
    private boolean animate;

    private CardActor target;

    private boolean dragging;
    private final float touchSlop = 5;
    private CardActor dragTarget;
    private float dragStartX;

    @Setter
    @Getter
    private Card selected;

    private Consumer<CardActor> onClickListener;

    public CardsGroup(List<Card> cards, TextureAtlas atlas) {
        this.atlas = atlas;
        update(cards);

        setFillParent(true);

        this.addListener(new InputListener() {

            @Override
            public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                if (pointer != 0) return false;
                if (button != Input.Buttons.LEFT) return false;
                if (event.getTarget() instanceof CardActor card) {
                    dragTarget = card;
                    dragStartX = x;
                    return true;
                }
                return false;
            }

            @Override
            public void touchDragged(InputEvent event, float x, float y, int pointer) {
                if (pointer != 0) return;
                if (!dragging && Math.abs(x - dragStartX) > touchSlop) {
                    dragging = true;
                } else if (dragging) {
                    int index = Arrays.binarySearch(cardX, x - (cardWidth + spacing));
                    if (index < 0) {
                        index = -(index + 1);
                        if (index >= getChildren().size) return;
                    }
                    if (getChild(index) != dragTarget) {
                        float oldX = dragTarget.getX();
                        getChildren().removeValue(dragTarget, true);
                        getChildren().insert(index, dragTarget);
                        invalidate();
                        dragTarget.setHeight(1.3f * getHeight());
                        dragStartX += (dragTarget.getX() - oldX);
                    }
                }
            }

            @Override
            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                if (pointer != 0) return;
                if (button != Input.Buttons.LEFT) return;
                if (!dragging && dragTarget != null) {
                    if (onClickListener != null) {
                        onClickListener.accept(dragTarget);
                    }
                }
                dragging = false;
                dragTarget = null;
            }

            @Override
            public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
                if (pointer != -1) return;
                if (event.getTarget() instanceof CardActor card) {
                    target = card;
                }
            }

            @Override
            public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
                if (pointer != -1) return;
                if (event.getTarget() == target) {
                    target = null;
                }
            }
        });
    }

    @Override
    public void act(float delta) {
        super.act(delta);

        float offset = 0.3f * getHeight();
        float speed = 10 * offset;

        for (var child : getChildren()) {
            if (child instanceof CardActor card) {
                float height = card.getHeight();
                if ((child == dragTarget || dragTarget == null && child == target) || card.getCard() == selected) {
                    height += speed * delta;
                } else {
                    height -= speed * delta;
                }
                height = MathUtils.clamp(height, getHeight(), getHeight() + offset);
                card.setHeight(height);
            }
        }
    }

    @Override
    public void layout() {
        var children = getChildren();
        var count = children.size;

        float height = getHeight();
        float width = getWidth();
        if (cardX == null || cardX.length != count) {
            cardX = new float[count];
        }

        cardWidth = height / CardActor.ASPECT_RATIO;
        spacing = width - count * cardWidth;
        spacing /= count - 1;
        spacing = Math.min(spacing, TARGET_SPACING);

        float x = Math.max(0, (width - count * cardWidth - (count - 1) * TARGET_SPACING) / 2);
        float y = 0;

        for (int i = 0; i < children.size; i++) {
            var child = children.get(i);

            // position
            if (animate) {
                child.addAction(moveTo(x, y, AnimationTimings.HAND_LAYOUT));
            } else {
                child.setPosition(x, y);
            }

            // size
            child.setWidth(cardWidth);
            if (child != dragTarget && child != target) {
                child.setHeight(height);
            }

            cardX[i] = x;
            x += cardWidth + spacing;
        }
        animate = false;
    }

    public @NotNull Pair<List<CardActor>, List<CardActor>> update(@NotNull List<Card> cards) {
        var added = new ArrayList<>(cards);
        var removed = new ArrayList<Card>();

        for (var child : getChildren()) {
            if (child instanceof CardActor actor) {
                var card = actor.getCard();

                if (!added.remove(card)) {
                    removed.add(card);
                }
            }
        }

        var removedActors = removed.stream().map(this::remove).toList();
        var addedActors = added.stream().sorted().map(card -> new CardActor(card, atlas)).toList();

        addedActors.forEach(this::addActor);
        layout();

        return Pair.of(removedActors, addedActors);
    }

    public @Nullable CardActor remove(Card card) {
        var actor = find(card);
        if (actor == null) return null;

        // adjust actor
        actor.setY(getY() + actor.getHeight() - getHeight());
        var pos = localToStageCoordinates(Pools.get(Vector2.class).obtain().set(actor.getX(), actor.getY()));
        actor.setHeight(getHeight());
        actor.setPosition(pos.x, pos.y);
        if (target == actor) {
            target = null;
        }

        // remove actor
        animate = true;
        actor.clearActions();
        actor.remove();
        return actor;
    }

    public @Nullable CardActor find(Card card) {
        var children = getChildren();
        for (int i = 0; i < children.size; i++) {
            if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) {
                return cardActor;
            }
        }
        return null;
    }

    @Override
    public float getMinHeight() {
        return 0;
    }

    public void setOnClickListener(Consumer<CardActor> onClickListener) {
        this.onClickListener = onClickListener;
    }
}
