package de.steamwar.cursor;

import de.steamwar.core.SWPlayer;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.entity.RFallingBlockEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.block.Action;
import org.bukkit.util.Vector;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Predicate;

@Getter
public class Cursor implements SWPlayer.Component {
    private final World WORLD = Bukkit.getWorlds().get(0);

    private final REntityServer targetServer;
    private final Player owner;

    private final AtomicBoolean isRendering = new AtomicBoolean(false);

    private RFallingBlockEntity cursorEntity;
    private final REntityServer cursorServer;
    private Location cursorLocation;
    private REntity hitEntity;

    @Setter
    private Material cursorMaterial;
    @Setter
    private List<CursorMode> allowedCursorModes;
    private final Material highlightMaterial;
    private final ClickHandler onClick;
    private final BiConsumer<Location, Optional<REntity>> onRender;


    public Cursor(REntityServer targetServer, Player owner, Material highlightMaterial, Material cursorMaterial, List<CursorMode> allowedModes, ClickHandler onClick) {
        this(targetServer, owner, highlightMaterial, cursorMaterial, allowedModes, onClick, (location, hitEntity) -> {
        });
    }

    public Cursor(REntityServer targetServer, Player owner, Material highlightMaterial, Material cursorMaterial, List<CursorMode> allowedModes, ClickHandler onClick, BiConsumer<Location, Optional<REntity>> onRender) {
        this.targetServer = targetServer;
        this.owner = owner;
        this.highlightMaterial = highlightMaterial;
        this.cursorMaterial = cursorMaterial;
        this.allowedCursorModes = allowedModes;
        this.onClick = onClick;
        this.onRender = onRender;

        cursorServer = new REntityServer();
        cursorServer.addPlayer(owner);

        SWPlayer.of(owner).setComponent(this);
    }

    public void renderDeduplicated() {
        if (!isRendering.getAndSet(true)) {
            render();
            isRendering.set(false);
        }
    }

    private void render() {
        RayTraceUtils.RRayTraceResult rayTraceResult = RayTraceUtils.traceREntity(owner, owner.getLocation(), targetServer.getEntities());
        if (rayTraceResult == null) {
            if (cursorEntity != null)
                cursorEntity.die();
            cursorEntity = null;
            cursorLocation = null;
            hitEntity = null;
            onRender.accept(null, Optional.empty());
            return;
        }

        REntity hitEntity = rayTraceResult.getHitEntity() == cursorEntity ? null : rayTraceResult.getHitEntity();


        Material activeCursorMaterial = hitEntity == null ? cursorMaterial : highlightMaterial;
        CursorMode activeCursorMode = allowedCursorModes.stream().filter((mode) -> mode.isActive.test(owner)).min(Comparator.comparingInt(a -> a.priority))
                .orElse(CursorMode.BLOCK_ALIGNED);

        Location activeCursorLocation = hitEntity == null ? activeCursorMode.positionTransform.apply(owner, rayTraceResult).toLocation(WORLD)
                : new Vector(hitEntity.getX(), hitEntity.getY(), hitEntity.getZ()).toLocation(WORLD);

        cursorLocation = activeCursorLocation;
        this.hitEntity = hitEntity;

        if (cursorEntity == null) {
            cursorEntity = new RFallingBlockEntity(cursorServer, activeCursorLocation, activeCursorMaterial);
            cursorEntity.setNoGravity(true);
        } else if (cursorEntity.getMaterial() == activeCursorMaterial) {
            cursorEntity.move(activeCursorLocation);
        } else {
            cursorEntity.die();
            cursorEntity = new RFallingBlockEntity(cursorServer, activeCursorLocation, activeCursorMaterial);
            cursorEntity.setNoGravity(true);
            if (activeCursorMaterial == highlightMaterial) {
                cursorEntity.setGlowing(true);
            }
        }
        onRender.accept(cursorLocation, Optional.ofNullable(hitEntity));
    }

    protected void handlePlayerClick(Action clickAction) {
        renderDeduplicated();
        onClick.onClick(this.cursorLocation, Optional.ofNullable(this.hitEntity), clickAction);
    }

    @Override
    public void onUnmount(SWPlayer player) {
        cursorServer.close();
    }

    @AllArgsConstructor
    public enum CursorMode {
        FREE(1, (player, rayTraceResult) -> {
            Vector pos = rayTraceResult.getHitPosition();

            BlockFace face = rayTraceResult.getHitBlockFace();
            if (face != null) {
                switch (face) {
                    case DOWN:
                        pos.setY(pos.getY() - 0.98);
                        break;
                    case EAST:
                        pos.setX(pos.getX() + 0.49);
                        break;
                    case WEST:
                        pos.setX(pos.getX() - 0.49);
                        break;
                    case NORTH:
                        pos.setZ(pos.getZ() - 0.49);
                        break;
                    case SOUTH:
                        pos.setZ(pos.getZ() + 0.49);
                        break;
                    default:
                        break;
                }

                if (face.getModY() == 0 && player.isSneaking()) {
                    pos.setY(pos.getY() - 0.49);
                }
            }

            return pos;
        }, Player::isSneaking),

        SURFACE_ALIGNED(2, (player, rayTraceResult) -> {
            Vector hitPosition = rayTraceResult.getHitPosition().clone();
            Vector pos = blockAlignedPosition(rayTraceResult);
            BlockFace face = rayTraceResult.getHitBlockFace();

            if (face != null && face != BlockFace.SELF) {
                switch (face) {
                    case UP:
                        pos.setY(hitPosition.getY());
                        break;
                    case DOWN:
                        pos.setY(hitPosition.getY() + face.getModY());
                        break;
                    case EAST:
                    case WEST:
                        pos.setX(hitPosition.getX() + face.getModX() * 0.5);
                        break;
                    case NORTH:
                    case SOUTH:
                        pos.setZ(hitPosition.getZ() + face.getModZ() * 0.5);
                        break;
                    default:
                        break;
                }
            }

            return pos;
        }, (player) -> true),

        BLOCK_ALIGNED(0, (player, rayTraceResult) -> blockAlignedPosition(rayTraceResult), (player) -> true);


        private final int priority;
        private final BiFunction<Player, RayTraceUtils.RRayTraceResult, Vector> positionTransform;
        private final Predicate<Player> isActive;

        private static Vector blockAlignedPosition(RayTraceUtils.RRayTraceResult rayTraceResult) {
            Vector pos = rayTraceResult.getHitPosition();

            BlockFace face = rayTraceResult.getHitBlockFace();
            if (face != null) {
                switch (face) {
                    case DOWN:
                        pos.setY(pos.getY() - 0.98);
                        break;
                    case EAST:
                        pos.setX(pos.getX() + 0.49);
                        break;
                    case WEST:
                        pos.setX(pos.getX() - 0.49);
                        break;
                    case NORTH:
                        pos.setZ(pos.getZ() - 0.49);
                        break;
                    case SOUTH:
                        pos.setZ(pos.getZ() + 0.49);
                        break;
                    default:
                        break;
                }
            }

            pos.setX(pos.getBlockX() + 0.5);
            if (pos.getY() - pos.getBlockY() != 0 && face == BlockFace.UP) {
                pos.setY(pos.getBlockY() + 1.0);
            } else {
                pos.setY(pos.getBlockY());
            }
            pos.setZ(pos.getBlockZ() + 0.5);
            return pos;
        }
    }

    @FunctionalInterface
    public interface ClickHandler {
        void onClick(Location location, Optional<REntity> hitEntity, Action action);
    }
}
