package mindustry.editor;

import arc.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.editor.MapObjectivesCanvas.ObjectiveTilemap.ObjectiveTile.*;
import mindustry.editor.MapObjectivesDialog.*;
import mindustry.game.MapObjectives.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;

import static mindustry.Vars.*;

@SuppressWarnings("unchecked")
public class MapObjectivesCanvas extends WidgetGroup{
    public static final int
        objWidth = 5, objHeight = 2,
        bounds = 100;

    public final float unitSize = Scl.scl(48f);

    public Seq<MapObjective> objectives = new Seq<>();
    public ObjectiveTilemap tilemap;

    protected MapObjective query;

    private boolean pressed;
    private long visualPressed;
    private int queryX = -objWidth, queryY = -objHeight;

    public MapObjectivesCanvas(){
        setFillParent(true);
        addChild(tilemap = new ObjectiveTilemap());

        addCaptureListener(new InputListener(){
            @Override
            public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
                if(query != null && button == KeyCode.mouseRight){
                    stopQuery();

                    event.stop();
                    return true;
                }else{
                    return false;
                }
            }
        });

        addCaptureListener(new ElementGestureListener(){
            int pressPointer = -1;

            @Override
            public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
                if(tilemap.moving != null || tilemap.connecting != null) return;
                tilemap.x = Mathf.clamp(tilemap.x + deltaX, -bounds * unitSize + width, bounds * unitSize);
                tilemap.y = Mathf.clamp(tilemap.y + deltaY, -bounds * unitSize + height, bounds * unitSize);
            }

            @Override
            public void tap(InputEvent event, float x, float y, int count, KeyCode button){
                if(query == null) return;

                Vec2 pos = localToDescendantCoordinates(tilemap, Tmp.v1.set(x, y));
                queryX = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
                queryY = Mathf.floor((pos.y - unitSize) / unitSize);

                // In mobile, placing the query is done in a separate button.
                if(!mobile) placeQuery();
            }

            @Override
            public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
                if(pressPointer != -1) return;
                pressPointer = pointer;
                pressed = true;
                visualPressed = Time.millis() + 100;
            }

            @Override
            public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
                if(pointer == pressPointer){
                    pressPointer = -1;
                    pressed = false;
                }
            }
        });
    }

    public void clearObjectives(){
        stopQuery();
        tilemap.clearTiles();
        tilemap.x = 0f;
        tilemap.y = 0f;
    }

    protected void stopQuery(){
        if(query == null) return;
        query = null;

        Core.graphics.restoreCursor();
    }

    public void query(MapObjective obj){
        stopQuery();
        query = obj;
    }

    public void placeQuery(){
        if(isQuerying() && tilemap.createTile(queryX, queryY, query)){
            objectives.add(query);
            stopQuery();
        }
    }

    public boolean isQuerying(){
        return query != null;
    }

    public boolean isVisualPressed(){
        return pressed || visualPressed > Time.millis();
    }

    public class ObjectiveTilemap extends WidgetGroup{

        /** The connector button that is being pressed. */
        protected @Nullable Connector connecting;
        /** The current tile that is being moved. */
        protected @Nullable ObjectiveTile moving;

        public ObjectiveTilemap(){
            setTransform(false);
            setSize(getPrefWidth(), getPrefHeight());
            touchable(() -> isQuerying() ? Touchable.disabled : Touchable.childrenOnly);
        }

        @Override
        public void draw(){
            validate();
            int minX = Math.max(Mathf.floor((x - width - 1f) / unitSize), -bounds), minY = Math.max(Mathf.floor((y - height - 1f) / unitSize), -bounds),
                maxX = Math.min(Mathf.ceil((x + width + 1f) / unitSize), bounds), maxY = Math.min(Mathf.ceil((y + height + 1f) / unitSize), bounds);
            float progX = x % unitSize, progY = y % unitSize;

            Lines.stroke(3f);
            Draw.color(Pal.darkestGray, parentAlpha);

            for(int x = minX; x <= maxX; x++) Lines.line(progX + x * unitSize, minY * unitSize, progX + x * unitSize, maxY * unitSize);
            for(int y = minY; y <= maxY; y++) Lines.line(minX * unitSize, progY + y * unitSize, maxX * unitSize, progY + y * unitSize);

            if(isQuerying()){
                int tx, ty;
                if(mobile){
                    tx = queryX;
                    ty = queryY;
                }else{
                    Vec2 pos = screenToLocalCoordinates(Core.input.mouse());
                    tx = Mathf.round((pos.x - objWidth * unitSize / 2f) / unitSize);
                    ty = Mathf.floor((pos.y - unitSize) / unitSize);
                }

                Lines.stroke(4f);
                Draw.color(
                    isVisualPressed() ? Pal.metalGrayDark : validPlace(tx, ty, null) ? Pal.accent : Pal.remove,
                    parentAlpha
                );

                Lines.rect(x + tx * unitSize, y + ty * unitSize, objWidth * unitSize, objHeight * unitSize);
            }

            if(moving != null){
                int tx, ty;
                float x = this.x + (tx = Mathf.round(moving.x / unitSize)) * unitSize;
                float y = this.y + (ty = Mathf.round(moving.y / unitSize)) * unitSize;

                Draw.color(
                    validPlace(tx, ty, moving) ? Pal.accent : Pal.remove,
                    0.5f * parentAlpha
                );

                Fill.crect(x, y, objWidth * unitSize, objHeight * unitSize);
            }

            Draw.reset();
            super.draw();

            Draw.reset();
            Seq<ObjectiveTile> tiles = getChildren().as();

            Connector conTarget = null;
            if(connecting != null){
                Vec2 pos = connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY));
                if(hit(pos.x, pos.y, true) instanceof Connector con && connecting.canConnectTo(con)) conTarget = con;
            }

            boolean removing = false;
            for(var tile : tiles){
                for(var parent : tile.obj.parents){
                    var parentTile = tiles.find(t -> t.obj == parent);

                    if(parentTile == null) continue;

                    Connector
                        conFrom = parentTile.conChildren,
                        conTo = tile.conParent;

                    if(conTarget != null && (
                        (connecting.findParent && connecting == conTo && conTarget == conFrom) ||
                        (!connecting.findParent && connecting == conFrom && conTarget == conTo)
                    )){
                        removing = true;
                        continue;
                    }

                    Vec2
                        from = conFrom.localToAscendantCoordinates(this, Tmp.v1.set(conFrom.getWidth() / 2f, conFrom.getHeight() / 2f)).add(x, y),
                        to = conTo.localToAscendantCoordinates(this, Tmp.v2.set(conTo.getWidth() / 2f, conTo.getHeight() / 2f)).add(x, y);

                    drawCurve(false, from.x, from.y, to.x, to.y);
                }
            }

            if(connecting != null){
                Vec2
                    mouse = (conTarget == null
                        ? connecting.localToAscendantCoordinates(this, Tmp.v1.set(connecting.pointX, connecting.pointY))
                        : conTarget.localToAscendantCoordinates(this, Tmp.v1.set(conTarget.getWidth() / 2f, conTarget.getHeight() / 2f))
                    ).add(x, y),
                    anchor = connecting.localToAscendantCoordinates(this, Tmp.v2.set(connecting.getWidth() / 2f, connecting.getHeight() / 2f)).add(x, y);

                Vec2
                    from = connecting.findParent ? mouse : anchor,
                    to = connecting.findParent ? anchor : mouse;

                drawCurve(removing, from.x, from.y, to.x, to.y);
            }

            Draw.reset();
        }

        protected void drawCurve(boolean remove, float x1, float y1, float x2, float y2){
            Lines.stroke(4f);
            Draw.color(remove ? Pal.remove : Pal.accent, parentAlpha);

            Fill.square(x1, y1, 8f, 45f);
            Fill.square(x2, y2, 8f, 45f);

            float dist = Math.abs(x1 - x2) / 2f;
            float cx1 = x1 + dist;
            float cx2 = x2 - dist;
            Lines.curve(x1, y1, cx1, y1, cx2, y2, x2, y2, Math.max(4, (int) (Mathf.dst(x1, y1, x2, y2) / 4f)));

            float progress = (Time.time % (60 * 4)) / (60 * 4);

            float t2 = progress * progress;
            float t3 = progress * t2;
            float t1 = 1 - progress;
            float t13 = t1 * t1 * t1;
            float kx1 = t13 * x1 + 3 * progress * t1 * t1 * cx1 + 3 * t2 * t1 * cx2 + t3 * x2;
            float ky1 = t13  *y1 + 3 * progress * t1 * t1 * y1 + 3 * t2 * t1 * y2 + t3 * y2;

            Fill.circle(kx1, ky1, 6f);

            Draw.reset();
        }

        public boolean validPlace(int x, int y, @Nullable ObjectiveTile ignore){
            Tmp.r1.set(x, y, objWidth, objHeight).grow(-0.001f);

            if(!Tmp.r2.setCentered(0, 0, bounds * 2, bounds * 2).contains(Tmp.r1)){
                return false;
            }

            for(var other : children){
                if(other instanceof ObjectiveTile tile && tile != ignore && Tmp.r2.set(tile.tx, tile.ty, objWidth, objHeight).overlaps(Tmp.r1)){
                    return false;
                }
            }

            return true;
        }

        public boolean createTile(MapObjective obj){
            return createTile(obj.editorX, obj.editorY, obj);
        }

        public boolean createTile(int x, int y, MapObjective obj){
            if(!validPlace(x, y, null)) return false;

            ObjectiveTile tile = new ObjectiveTile(obj, x, y);
            tile.pack();

            addChild(tile);

            return true;
        }

        public boolean moveTile(ObjectiveTile tile, int newX, int newY){
            if(!validPlace(newX, newY, tile)) return false;

            tile.pos(newX, newY);

            return true;
        }

        public void removeTile(ObjectiveTile tile){
            if(!tile.isDescendantOf(this)) return;
            tile.remove();
        }

        public void clearTiles(){
            clearChildren();
        }

        @Override
        public float getPrefWidth(){
            return bounds * unitSize;
        }

        @Override
        public float getPrefHeight(){
            return bounds * unitSize;
        }

        public class ObjectiveTile extends Table{
            public final MapObjective obj;
            public int tx, ty;

            public final Mover mover;
            public final Connector conParent, conChildren;

            public ObjectiveTile(MapObjective obj, int x, int y){
                this.obj = obj;
                setTransform(false);
                setClip(false);

                add(conParent = new Connector(true)).size(unitSize / Scl.scl(1f), unitSize * 2 / Scl.scl(1f));
                table(Tex.whiteui, t -> {
                    float pad = (unitSize / Scl.scl(1f) - 32f) / 2f - 4f;
                    t.margin(pad);
                    t.touchable(() -> Touchable.enabled);
                    t.setColor(Pal.gray);

                    t.labelWrap(obj.typeName())
                    .style(Styles.outlineLabel)
                    .left().grow().get()
                    .setAlignment(Align.left);

                    t.row();

                    t.table(b -> {
                        b.left().defaults().size(40f);

                        b.button(Icon.pencilSmall, () -> {
                            BaseDialog dialog = new BaseDialog("@editor.objectives");
                            dialog.cont.pane(Styles.noBarPane, list -> list.top().table(e -> {
                                e.margin(0f);
                                MapObjectivesDialog.getInterpreter((Class<MapObjective>)obj.getClass()).build(
                                    e, obj.typeName(), new TypeInfo(obj.getClass()),
                                    null, null, null,
                                    () -> obj,
                                    res -> {}
                                );
                            }).width(Math.min(Core.graphics.getWidth() * 0.95f / Scl.scl(1f) - Scl.scl(20f), 700f)).fillY()).grow();

                            dialog.addCloseButton();
                            dialog.show();
                        });
                        b.button(Icon.trashSmall, () -> removeTile(this));
                    }).left().grow();
                }).growX().height(unitSize / Scl.scl(1f) * 2).get().addCaptureListener(mover = new Mover());
                add(conChildren = new Connector(false)).size(unitSize / Scl.scl(1f), unitSize / Scl.scl(1f) * 2);

                setSize(getPrefWidth(), getPrefHeight());
                pos(x, y);
            }

            public void pos(int x, int y){
                tx = obj.editorX = x;
                ty = obj.editorY = y;
                this.x = x * unitSize;
                this.y = y * unitSize;
            }

            @Override
            public float getPrefWidth(){
                return objWidth * unitSize;
            }

            @Override
            public float getPrefHeight(){
                return objHeight * unitSize;
            }

            @Override
            public boolean remove(){
                if(super.remove()){
                    obj.parents.clear();

                    var it = objectives.iterator();
                    while(it.hasNext()){
                        var next = it.next();
                        if(next == obj){
                            it.remove();
                        }else{
                            next.parents.remove(obj);
                        }
                    }

                    return true;
                }else{
                    return false;
                }
            }

            public class Mover extends InputListener{
                public int prevX, prevY;
                public float lastX, lastY;

                @Override
                public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
                    if(moving != null) return false;
                    moving = ObjectiveTile.this;
                    moving.toFront();

                    prevX = moving.tx;
                    prevY = moving.ty;

                    // Convert to world pos first because the button gets dragged too.
                    Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));
                    lastX = pos.x;
                    lastY = pos.y;
                    return true;
                }

                @Override
                public void touchDragged(InputEvent event, float x, float y, int pointer){
                    Vec2 pos = event.listenerActor.localToStageCoordinates(Tmp.v1.set(x, y));

                    moving.moveBy(pos.x - lastX, pos.y - lastY);
                    lastX = pos.x;
                    lastY = pos.y;
                }

                @Override
                public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
                    if(!moveTile(moving,
                        Mathf.round(moving.x / unitSize),
                        Mathf.round(moving.y / unitSize)
                    )) moving.pos(prevX, prevY);
                    moving = null;
                }
            }

            public class Connector extends Button{
                public float pointX, pointY;
                public final boolean findParent;

                public Connector(boolean findParent){
                    super(new ButtonStyle(){{
                        down = findParent ? Tex.buttonSideLeftDown : Tex.buttonSideRightDown;
                        up = findParent ? Tex.buttonSideLeft : Tex.buttonSideRight;
                        over = findParent ? Tex.buttonSideLeftOver : Tex.buttonSideRightOver;
                    }});

                    this.findParent = findParent;

                    clearChildren();

                    addCaptureListener(new InputListener(){
                        int conPointer = -1;

                        @Override
                        public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
                            if(conPointer != -1) return false;
                            conPointer = pointer;

                            if(connecting != null) return false;
                            connecting = Connector.this;

                            pointX = x;
                            pointY = y;
                            return true;
                        }

                        @Override
                        public void touchDragged(InputEvent event, float x, float y, int pointer){
                            if(conPointer != pointer) return;
                            pointX = x;
                            pointY = y;
                        }

                        @Override
                        public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
                            if(conPointer != pointer || connecting != Connector.this) return;
                            conPointer = -1;

                            Vec2 pos = Connector.this.localToAscendantCoordinates(ObjectiveTilemap.this, Tmp.v1.set(x, y));
                            if(ObjectiveTilemap.this.hit(pos.x, pos.y, true) instanceof Connector con && con.canConnectTo(Connector.this)){
                                if(findParent){
                                    if(!obj.parents.remove(con.tile().obj)) obj.parents.add(con.tile().obj);
                                }else{
                                    if(!con.tile().obj.parents.remove(obj)) con.tile().obj.parents.add(obj);
                                }
                            }

                            connecting = null;
                        }
                    });
                }

                public boolean canConnectTo(Connector other){
                    return
                        findParent != other.findParent &&
                        tile() != other.tile();
                }

                @Override
                public void draw(){
                    super.draw();
                    float cx = x + width / 2f;
                    float cy = y + height / 2f;

                    // these are all magic numbers tweaked until they looked good in-game, don't mind them.
                    Lines.stroke(3f, Pal.accent);
                    if(findParent){
                        Lines.line(cx, cy + 9f, cx + 9f, cy);
                        Lines.line(cx + 9f, cy, cx, cy - 9f);
                    }else{
                        Lines.square(cx, cy, 9f, 45f);
                    }
                }

                public ObjectiveTile tile(){
                    return ObjectiveTile.this;
                }

                @Override
                public boolean isPressed(){
                    return super.isPressed() || connecting == this;
                }

                @Override
                public boolean isOver(){
                    return super.isOver() && (connecting == null || connecting.canConnectTo(this));
                }
            }
        }
    }
}
