package com.crashinvaders.texturepackergui.controllers.ninepatcheditor;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
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.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.crashinvaders.common.MutableInt;

public class PatchGrid extends WidgetGroup {
    protected static final float EXTRA_TOUCH_SPACE = 16f;
    protected static final float FILL_COLOR_ALPHA = 0.25f;
    private static final Color tmpColor = new Color();
    private static final Vector2 tmpVec2 = new Vector2();

    protected final Array<PatchLine> patchLines = new Array<>();
    protected final Color primaryColor;
    protected final GridValues values;
    protected final Drawable whiteDrawable;
    protected final Drawable gridNodeDrawable;
    protected final PatchLine left, right, top, bottom;
    protected final LineDragListener lineDragListener;
    protected final ModelChangeListener modelListener;

    protected float pixelSize = 1f;
    private int imageWidth, imageHeight;
    private boolean disabled;

    public PatchGrid(Skin skin, Color primaryColor, GridValues values) {
        this.primaryColor = new Color(primaryColor);
        this.values = values;
        setTouchable(Touchable.enabled);

        whiteDrawable = skin.getDrawable("white");
        gridNodeDrawable = skin.getDrawable("custom/nine-patch-gird-node");

        left = new PatchLine(whiteDrawable, primaryColor);
        right = new PatchLine(whiteDrawable, primaryColor);
        top = new PatchLine(whiteDrawable, primaryColor);
        bottom = new PatchLine(whiteDrawable, primaryColor);
        patchLines.addAll(left, right, top, bottom);

        addActor(left);
        addActor(right);
        addActor(top);
        addActor(bottom);

        lineDragListener = new LineDragListener();
        addListener(lineDragListener);

        values.addListener(modelListener = new ModelChangeListener());
    }

    public GridValues getValues() {
        return values;
    }

    public void setImageSize(int width, int height) {
        imageWidth = width;
        imageHeight = height;
    }

    public void setPixelSize(float pixelSize) {
        this.pixelSize = pixelSize;
    }

    public void setDisabled(boolean disabled) {
        this.disabled = disabled;
        setTouchable(disabled ? Touchable.disabled : Touchable.enabled);

        for (int i = 0; i < patchLines.size; i++) {
            patchLines.get(i).setDisabled(disabled);
        }

        // Forcefully remove hover from all lines
        updateLinesHover(-1f, -1f);
    }

    @Override
    public void layout() {
        updateLinesFromModel();

        // Manually update hover state
        {
            int screenX = Gdx.input.getX();
            int screenY = Gdx.input.getY();
            Vector2 localCoord = screenToLocalCoordinates(tmpVec2.set(screenX, screenY));
            updateLinesHover(localCoord.x, localCoord.y);
        }
    }

    @Override
    public void draw(Batch batch, float parentAlpha) {
        if (!disabled && lineDragListener.isHovered() && !lineDragListener.isDragging()) {
            int screenX = Gdx.input.getX();
            int screenY = Gdx.input.getY();
            Vector2 localCoord = screenToLocalCoordinates(tmpVec2.set(screenX, screenY));
            updateLinesHover(localCoord.x, localCoord.y);
        }

        if (!disabled) drawAreaGraphics(batch, parentAlpha);

        super.draw(batch, parentAlpha);

        drawGridNodes(batch, parentAlpha);
    }

    protected void drawAreaGraphics(Batch batch, float parentAlpha) {
        batch.setColor(tmpColor.set(primaryColor.r, primaryColor.g, primaryColor.b, FILL_COLOR_ALPHA)
                .mul(getColor())
                .mul(1f, 1f, 1f, parentAlpha));
        if (!disabled) {
            // Fill both height and width rectangles
            whiteDrawable.draw(batch, getX(), getY() + bottom.getY(), getWidth(), top.getY() - bottom.getY());
            whiteDrawable.draw(batch, getX() + left.getX(), getY(), right.getX() - left.getX(), getHeight());
        } else {
            // Fill only central rectangle
            whiteDrawable.draw(batch, getX() + left.getX(), getY() + bottom.getY(), right.getX() - left.getX(), top.getY() - bottom.getY());
        }
    }

    protected void drawGridNodes(Batch batch, float parentAlpha) {
        if (disabled) return;

        batch.setColor(tmpColor.set(getColor()).mul(1f, 1f, 1f, parentAlpha));

        float width = gridNodeDrawable.getMinWidth();
        float height = gridNodeDrawable.getMinHeight();
        float halfWidth = width * 0.5f;
        float halfHeight = height * 0.5f;
        gridNodeDrawable.draw(batch, getX() + left.getX() - halfWidth, getY() + bottom.getY() - halfHeight, width, height);
        gridNodeDrawable.draw(batch, getX() + right.getX() - halfWidth, getY() + bottom.getY() - halfHeight, width, height);
        gridNodeDrawable.draw(batch, getX() + left.getX() - halfWidth, getY() + top.getY() - halfHeight, width, height);
        gridNodeDrawable.draw(batch, getX() + right.getX() - halfWidth, getY() + top.getY() - halfHeight, width, height);
    }

    @Override
    public Actor hit(float x, float y, boolean touchable) {
        if (touchable && getTouchable() == Touchable.disabled) return null;
        Vector2 point = tmpVec2;
        Actor[] childrenArray = getChildren().items;
        for (int i = getChildren().size - 1; i >= 0; i--) {
            Actor child = childrenArray[i];
            if (!child.isVisible()) continue;
            child.parentToLocalCoordinates(point.set(x, y));
            Actor hit = child.hit(point.x, point.y, touchable);
            if (hit != null) return hit;
        }

        // Extend regular touch area by EXTRA_TOUCH_SPACE (to allow line snap beyond PatchGrid borders)
        if (touchable && this.getTouchable() != Touchable.enabled) return null;
        return x >= -EXTRA_TOUCH_SPACE &&
                x < getWidth() + EXTRA_TOUCH_SPACE *2f &&
                y >= -EXTRA_TOUCH_SPACE &&
                y < getHeight() + EXTRA_TOUCH_SPACE *2f ?
                this : null;
    }

    protected void validateLinePositions() {
        if (top.dragging && top.getY() < bottom.getY() + pixelSize) {
            top.setY(bottom.getY() + pixelSize);
        }
        if (bottom.dragging && bottom.getY() > top.getY() - pixelSize) {
            bottom.setY(top.getY() - pixelSize);
        }
        if (right.dragging && right.getX() < left.getX() + pixelSize) {
            right.setX(left.getX() + pixelSize);
        }
        if (left.dragging && left.getX() > right.getX() - pixelSize) {
            left.setX(right.getX() - pixelSize);
        }

        left.setPosition(MathUtils.clamp(MathUtils.round(left.getX()/pixelSize)*pixelSize, 0f, getWidth()), 0f);
        right.setPosition(MathUtils.clamp(MathUtils.round(right.getX()/pixelSize)*pixelSize, 0f, getWidth()), 0f);
        top.setPosition(0f, MathUtils.clamp(MathUtils.round(top.getY()/pixelSize)*pixelSize, 0f, getHeight()));
        bottom.setPosition(0f, MathUtils.clamp(MathUtils.round(bottom.getY()/pixelSize)*pixelSize, 0f, getHeight()));
    }

    private void updateLinesHover(float x, float y) {
        for (int i = 0; i < patchLines.size; i++) {
            patchLines.get(i).updateHover(x, y);
        }
        // Do not allow to select both vertical lines at the same time.
        if (left.hovered && right.hovered) {
            float leftDistance = Math.abs(left.getX(Align.center) - x);
            float rightDistance = Math.abs(right.getX(Align.center) - x);
            if (leftDistance < rightDistance) {
                right.hovered = false;
                right.updateVisualState();
            } else {
                left.hovered = false;
                left.updateVisualState();
            }
        }
        // Do not allow to select both horizontal lines at the same time.
        if (top.hovered && bottom.hovered) {
            float topDistance = Math.abs(top.getY(Align.center) - y);
            float bottomDistance = Math.abs(bottom.getY(Align.center) - y);
            if (topDistance < bottomDistance) {
                bottom.hovered = false;
                bottom.updateVisualState();
            } else {
                top.hovered = false;
                top.updateVisualState();
            }
        }
    }

    private void updateLinesFromModel() {
        left.setBounds(values.left.get() * pixelSize, 0f, 0f, getHeight());
        right.setBounds(getWidth() - values.right.get() * pixelSize, 0f, 0f, getHeight());
        top.setBounds(0f, getHeight() - values.top.get() * pixelSize, getWidth(), 0f);
        bottom.setBounds(0f, values.bottom.get() * pixelSize, getWidth(), 0f);
        validateLinePositions();
    }

    private void updateModelFromLines() {
        modelListener.ignoreModelChanges = true;
        values.left.set(MathUtils.round(left.getX()/pixelSize));
        values.right.set(MathUtils.round((getWidth() - right.getX())/pixelSize));
        values.top.set(MathUtils.round((getHeight() - top.getY())/pixelSize));
        values.bottom.set(MathUtils.round(bottom.getY()/pixelSize));
        modelListener.ignoreModelChanges = false;
    }

    private class ModelChangeListener implements GridValues.ChangeListener {
        boolean ignoreModelChanges;

        @Override
        public void onValuesChanged(GridValues values) {
            if (!ignoreModelChanges) {
                updateLinesFromModel();
            }
        }
    }

    private class LineDragListener extends InputListener {
        private final Array<PatchLine> draggingLines = new Array<>();
        private boolean hovered;
        private boolean ignoreNextExitEvent;

        public boolean isHovered() {
            return hovered;
        }

        public boolean isDragging() {
            return draggingLines.size > 0;
        }

        @Override
        public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
            if (button==0) {
                for (PatchLine patchLine : patchLines) {
                    if (patchLine.hovered) {
                        draggingLines.add(patchLine);
                        patchLine.startDragging(x, y);
                    }
                }
                if (draggingLines.size > 0) {
                    event.handle();
                    event.stop();
                    return true;
                }
            }
            return false;
        }

        @Override
        public void touchDragged(InputEvent event, float x, float y, int pointer) {
            if (draggingLines.size > 0) {
                for (int i = 0; i < draggingLines.size; i++) {
                    draggingLines.get(i).drag(x, y);
                }
                validateLinePositions();
            }
        }

        @Override
        public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
            if (draggingLines.size > 0) {
                for (PatchLine patchLine : draggingLines) {
                    patchLine.endDragging(x, y);
                }
                draggingLines.clear();
                validateLinePositions();
                updateModelFromLines();
            }

            // Stage fires "exit" event upon touchUp() even if pointer is still over the actor.
            // This is simple workaround.
            if (hovered) ignoreNextExitEvent = true;
        }

        @Override
        public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
            hovered = true;
        }

        @Override
        public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
            if (ignoreNextExitEvent) {
                ignoreNextExitEvent = false;
                return;
            }

            hovered = false;
            updateLinesHover(x, y);
        }
    }

    protected static class PatchLine extends Actor {
        private static final Rectangle tmpRect = new Rectangle();

        protected final Drawable drawable;
        private final Color primaryColor;
        private float thickness = 3f;

        /** Determines whether line is horizontal or vertical */
        protected boolean horizontal;

        protected boolean disabled;
        protected boolean hovered;

        public PatchLine(Drawable drawable, Color primaryColor) {
            this.drawable = drawable;
            this.primaryColor = primaryColor;
            setTouchable(Touchable.disabled);

            updateVisualState();
        }

        public boolean isDisabled() {
            return disabled;
        }

        public void setDisabled(boolean disabled) {
            this.disabled = disabled;

            updateVisualState();
        }

        @Override
        public void draw(Batch batch, float parentAlpha) {
            batch.setColor(tmpColor.set(getColor()).mul(1f,1f,1f,parentAlpha));
            if (horizontal) {
                // Horizontal line
                drawable.draw(batch, getX()-thickness*0.5f, getY()-thickness*0.5f, getWidth()+thickness, thickness);
            } else {
                // Vertical line
                drawable.draw(batch, getX()-thickness*0.5f, getY()-thickness*0.5f, thickness, getHeight()+thickness);
            }
        }

        @Override
        protected void sizeChanged() {
            horizontal = getWidth() > getHeight();
            updateVisualState();
        }

        /** Position in parent's coordinates */
        public void updateHover(float x, float y) {
            if (disabled) return;

            boolean hit = checkHit(x, y);
            if (hovered != hit) {
                hovered = hit;
                updateVisualState();
            }
        }

        /** Position in parent's coordinates */
        public boolean checkHit(float x, float y) {
            if (disabled) return false;

            return tmpRect.set(
                    getX() - EXTRA_TOUCH_SPACE,
                    getY() - EXTRA_TOUCH_SPACE,
                    getWidth() + EXTRA_TOUCH_SPACE *2f,
                    getHeight() + EXTRA_TOUCH_SPACE *2f)
                    .contains(x, y);
        }

        private void updateVisualState() {
            if (disabled) {
                this.setColor(primaryColor);
                thickness = 2f;
            } else if (hovered || dragging) {
                this.setColor(Color.WHITE);
                thickness = 4f;
            } else {
                this.setColor(primaryColor);
                thickness = 4f;
            }
        }

        //region Dragging
        protected float dragOffsetX, dragOffsetY;
        protected boolean dragging = false;

        public void startDragging(float x, float y) {
            dragOffsetX = getX() - x;
            dragOffsetY = getY() - y;
            dragging = true;

            updateVisualState();
        }

        public void drag(float x, float y) {
            setPosition(x + dragOffsetX, y + dragOffsetY);
        }

        public void endDragging(float x, float y) {
            dragging = false;

            updateVisualState();
        }
        //endregion
    }
}