package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;

import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.metadata.other.FallingBlockMeta;
import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.Tag;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public class Tetromino {
    private final Shape shape;
    private final StatelessGame instance;
    private Pos position;
    private int[][] shapeArray;
    private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK;
    private final UUID uuid;
    private final static Tag<String> uuidTag = Tag.String("uuid");

    public enum Shape {
        I,
        J,
        L,
        O,
        S,
        T,
        Z
    }

    public Tetromino(StatelessGame instance, Shape shape) {
        this.instance = instance;
        this.shape = shape;
        this.uuid = UUID.randomUUID();

        switch (this.shape) {
            case I -> shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}};
            case J -> shapeArray = new int[][]{{1,0,0}, {1,1,1}, {0,0,0}};
            case L -> shapeArray = new int[][]{{0,0,1}, {1,1,1}, {0,0,0}};
            case O -> shapeArray = new int[][]{{1,1}, {1,1}};
            case S -> shapeArray = new int[][]{{0,1,1}, {1,1,0}, {0,0,0}};
            case T -> shapeArray = new int[][]{{0,1,0}, {1,1,1}, {0,0,0}};
            case Z -> shapeArray = new int[][]{{1,1,0}, {0,1,1}, {0,0,0}};
        }
    }

    public static EntityType getGhostEntityType() {
        return ghostEntityType;
    }

    public void setPosition(Pos newPosition) {
        this.position = newPosition;
    }

    public boolean rotate(boolean clockwise) {
        int[][] newShapeArray = this.getTurnedShapeArray(clockwise);
        return checkCollisionAndMove(this.position, newShapeArray);
    }

    public boolean moveDown() {
        Pos newPosition = this.position.sub(0, 1, 0);
        return checkCollisionAndMove(newPosition, this.shapeArray);
    }

    public boolean moveLeft() {
        Pos newPosition = this.position.sub(1, 0, 0);
        return checkCollisionAndMove(newPosition, this.shapeArray);
    }

    public boolean moveRight() {
        Pos newPosition = this.position.add(1, 0, 0);
        return checkCollisionAndMove(newPosition, this.shapeArray);
    }

    public void draw() {
        this.draw(true);
    }

    public void draw(boolean withGhost) {
        if(withGhost) {
            Pos ghostPos = this.position;
            while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
                ghostPos = ghostPos.sub(0, 1, 0);
            }
            Pos positionChange = this.position.sub(ghostPos);
            getBlockPositions().forEach(pos -> {
                Entity ghostBlock = new Entity(ghostEntityType);
                ((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock());
                ghostBlock.setNoGravity(true);
                ghostBlock.setGlowing(true);
                ghostBlock.setTag(uuidTag, this.uuid.toString());
                ghostBlock.setInstance(this.instance, pos.sub(positionChange).add(0.5, 0, 0.5));
            });
        }

        getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
    }

    public void drawAsEntities() {
        getBlockPositions().forEach(pos -> {
            Entity ghostBlock = new Entity(ghostEntityType);
            ((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getColoredBlock());
            ghostBlock.setNoGravity(true);
            ghostBlock.setTag(uuidTag, this.uuid.toString());
            ghostBlock.setInstance(this.instance, pos.add(0.5, 0, 0.5));
        });
    }

    public void remove() {
        this.removeOwnEntities();
        this.getBlockPositions().forEach(pos -> this.instance.setBlock(pos, Block.AIR));
    }

    public Block getColoredBlock() {
        Block returnBlock;
        switch (this.shape) {
            case I -> returnBlock = Block.LIGHT_BLUE_CONCRETE;
            case J -> returnBlock = Block.BLUE_CONCRETE;
            case L -> returnBlock = Block.ORANGE_CONCRETE;
            case O -> returnBlock = Block.YELLOW_CONCRETE;
            case S -> returnBlock = Block.GREEN_CONCRETE;
            case T -> returnBlock = Block.PURPLE_CONCRETE;
            case Z -> returnBlock = Block.RED_CONCRETE;
            default -> returnBlock = Block.WHITE_CONCRETE;
        }
        return returnBlock;
    }

    public void removeOwnEntities() {
        this.instance.getEntities().stream()
            .filter(entity -> {
                String tagValue = entity.getTag(uuidTag);
                if(tagValue == null) return false;
                return entity.getTag(uuidTag).equals(this.uuid.toString());
            })
            .forEach(Entity::remove);
    }

    public double getXChange() {
        switch (this.shape) {
            case O, I -> {
                return 0;
            }
            case null, default -> {
                return 0.5;
            }
        }
    }


    public Shape getShape() {
        return this.shape;
    }

    private Block getGhostBlock() {
        Block returnBlock;
        switch (this.shape) {
            case I -> returnBlock = Block.LIGHT_BLUE_STAINED_GLASS;
            case J -> returnBlock = Block.BLUE_STAINED_GLASS;
            case L -> returnBlock = Block.ORANGE_STAINED_GLASS;
            case O -> returnBlock = Block.YELLOW_STAINED_GLASS;
            case S -> returnBlock = Block.GREEN_STAINED_GLASS;
            case T -> returnBlock = Block.PURPLE_STAINED_GLASS;
            case Z -> returnBlock = Block.RED_STAINED_GLASS;
            default -> returnBlock = Block.WHITE_STAINED_GLASS;
        }
        return returnBlock;
    }

    private int[][] getTurnedShapeArray(boolean clockwise) {
        int iterations = 1;
        if(!clockwise) iterations = 3;

        int arrayLength = this.shapeArray.length;
        int[][] startArray = Arrays.stream(this.shapeArray).map(int[]::clone).toArray(int[][]::new);
        int[][] returnArray = new int[arrayLength][arrayLength];

        for(int k=0; k<iterations; k++) {
            for(int i=0; i<arrayLength; i++) {
                for(int j=0; j<arrayLength; j++) {
                    returnArray[i][arrayLength-1-j] = startArray[j][i];
                }
            }
            startArray = Arrays.stream(returnArray).map(int[]::clone).toArray(int[][]::new);
        }

        return returnArray;
    }

    private boolean isPartOfTetromino(Pos position) {
        return this.getBlockPositions().stream()
            .anyMatch(pos -> pos.equals(position));
    }

    private List<Pos> getBlockPositions() {
        return this.getBlockPositions(this.position, this.shapeArray);
    }

    private List<Pos> getBlockPositions(Pos position, int[][] shapeArray) {
        List<Pos> returnList = new ArrayList<>();
        if(position == null) return returnList;

        int arrayLength = shapeArray.length;

        for(int x=0; x<arrayLength; x++) {
            for(int y=0; y<arrayLength; y++) {
                if(shapeArray[arrayLength-1-y][x] == 1) {
                    switch (this.shape) {
                        case I -> returnList.add(position.add(x-1, y-2, 0));
                        case O -> returnList.add(position.add(x, y, 0));
                        default -> returnList.add(position.add(x-1, y-1, 0));
                    }
                }
            }
        }

        return returnList;
    }

    private boolean checkCollision(Pos newPosition, int[][] newShapeArray) {
        List<Pos> newBlockPositions = getBlockPositions(newPosition, newShapeArray);

        for(Pos pos : newBlockPositions) {
            if(isPartOfTetromino(pos)) continue;
            if(this.instance.getBlock(pos) == this.getGhostBlock()) continue;
            if(this.instance.getBlock(pos) != Block.AIR) return true;
        }

        return false;
    }

    private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) {
        if(!checkCollision(newPosition, newShapeArray)) {
            this.remove();
            this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new);
            this.setPosition(newPosition);
            this.draw();
            return true;
        }
        return false;
    }
}
