package net.runelite.client.plugins.microbot.api;

import net.runelite.api.WorldView;
import net.runelite.api.coords.WorldPoint;
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel;
import net.runelite.client.plugins.microbot.util.player.Rs2Player;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public abstract class AbstractEntityQueryable<
        Q extends IEntityQueryable<Q, E>,
        E extends IEntity
        >
        implements IEntityQueryable<Q, E> {

    protected Stream<E> source;

    protected AbstractEntityQueryable() {
        this.source = initialSource();
    }

    protected abstract Stream<E> initialSource();

    @SuppressWarnings("unchecked")
    @Override
    public Q fromWorldView() {
        var worldView = new Rs2PlayerModel().getWorldView();
        if (worldView == null) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source
                .filter(o -> o.getWorldView() != null && o.getWorldView().getId() == worldView.getId());

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q where(Predicate<E> predicate) {
        source = source.filter(predicate);
        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q within(int distance) {
        WorldPoint playerLoc = new Rs2PlayerModel().getWorldLocation();
        if (playerLoc == null) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source
                .filter(o -> o.getWorldLocation().distanceTo(playerLoc) <= distance);

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q within(WorldPoint anchor, int distance) {
        if (anchor == null) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source
                .filter(o -> o.getWorldLocation().distanceTo(anchor) <= distance);

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q withName(String name) {
        if (name == null) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source.filter(x -> {
            String n = x.getName();
            return n != null && n.equalsIgnoreCase(name);
        });

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q withNameContains(String substring) {
        if (substring == null || substring.isEmpty()) {
            this.source = Stream.empty();
            return (Q) this;
        }

        String needle = substring.toLowerCase();
        this.source = this.source.filter(x -> {
            String n = x.getName();
            return n != null && n.toLowerCase().contains(needle);
        });

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q withNames(String... names) {
        if (names == null || names.length == 0) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source.filter(x -> {
            String n = x.getName();
            if (n == null) return false;
            return Arrays.stream(names).anyMatch(n::equalsIgnoreCase);
        });

        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q withId(int id) {
        this.source = this.source.filter(x -> x.getId() == id);
        return (Q) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Q withIds(int... ids) {
        if (ids == null || ids.length == 0) {
            this.source = Stream.empty();
            return (Q) this;
        }

        this.source = this.source.filter(x -> {
            int entityId = x.getId();
            for (int id : ids) {
                if (entityId == id) return true;
            }
            return false;
        });

        return (Q) this;
    }

    @Override
    public E first() {
        return source.findFirst().orElse(null);
    }

    @Override
    public E firstReachable() {
        return source.filter(IEntity::isReachable).findFirst().orElse(null);
    }

    @Override
    public E nearest() {
        return nearest(Integer.MAX_VALUE);
    }

    @Override
    public E nearestReachable() {
        return nearestReachable(Integer.MAX_VALUE);
    }

    @Override
    public E nearestReachable(int maxDistance) {
        try {
            var player = new Rs2PlayerModel();
            WorldPoint playerLoc = player.getWorldLocation();
            WorldView worldView = player.getWorldView();
            if (playerLoc == null || worldView == null) {
                return null;
            }

            return source
                    .filter(IEntity::isReachable)
                    .map(entity -> {
                        WorldPoint loc = entity.getWorldLocation();
                        int distance = (loc != null) ? loc.distanceTo(playerLoc) : Integer.MAX_VALUE;
                        return new EntityDistance<>(entity, distance);
                    })
                    .filter(pair -> pair.distance <= maxDistance)
                    .min(Comparator.comparingInt(pair -> pair.distance))
                    .map(pair -> pair.entity)
                    .orElse(null);
        } catch (RuntimeException e) {
            return returnNullIfInterrupted(e);
        }
    }

    @Override
    public E nearest(int maxDistance) {
        try {
            var player = new Rs2PlayerModel();
            WorldPoint playerLoc = player.getWorldLocation();
            WorldView worldView = player.getWorldView();
            if (playerLoc == null || worldView == null) {
                return null;
            }

            return nearest(playerLoc, maxDistance);
        } catch (RuntimeException e) {
            return returnNullIfInterrupted(e);
        }
    }

    @Override
    public E nearest(WorldPoint anchor, int maxDistance) {
        if (anchor == null) {
            return null;
        }

        try {
            return source
                    .map(entity -> {
                        WorldPoint loc = entity.getWorldLocation();
                        int distance = (loc != null) ? loc.distanceTo(anchor) : Integer.MAX_VALUE;
                        return new EntityDistance<>(entity, distance);
                    })
                    .filter(pair -> pair.distance <= maxDistance)
                    .min(Comparator.comparingInt(pair -> pair.distance))
                    .map(pair -> pair.entity)
                    .orElse(null);
        } catch (RuntimeException e) {
            return returnNullIfInterrupted(e);
        }
    }

    private E returnNullIfInterrupted(RuntimeException e) {
        if (Thread.currentThread().isInterrupted() || e.getCause() instanceof InterruptedException) {
            return null;
        }
        throw e;
    }

    @Override
    public List<E> toList() {
        return source.collect(Collectors.toList());
    }

    @Override
    public int count() {
        return (int) source.count();
    }

    public E firstOnClientThread() {
        return Microbot.getClientThread().invoke(() -> first());
    }

    public E nearestOnClientThread() {
        return Microbot.getClientThread().invoke(() -> nearest());
    }

    public E nearestOnClientThread(int maxDistance) {
        return Microbot.getClientThread().invoke(() -> nearest(maxDistance));
    }

    public E nearestOnClientThread(WorldPoint anchor, int maxDistance) {
        return Microbot.getClientThread().invoke(() -> nearest(anchor, maxDistance));
    }

    public List<E> toListOnClientThread() {
        return Microbot.getClientThread().invoke(() -> toList());
    }

    public boolean interact() {
        E entity = nearestReachable();
        if (entity == null) return false;

        return entity.click();
    }

    public boolean interact(String action) {
        E entity = nearestReachable();
        if (entity == null) return false;
        return entity.click(action);
    }

    public boolean interact(String action, int maxDistance) {
        E entity = nearestReachable(maxDistance);
        if (entity == null) return false;
        return entity.click(action);
    }

    public boolean interact(int id) {
        E entity = this.withId(id).nearestReachable();
        if (entity == null) return false;
        return entity.click();
    }

    public boolean interact(int id, String action) {
        E entity = this.withId(id).nearestReachable();
        if (entity == null) return false;
        return entity.click(action);
    }

    public boolean interact(int id, String action, int maxDistance) {
        E entity = this.withId(id).nearestReachable(maxDistance);
        if (entity == null) return false;
        return entity.click(action);
    }
}



class EntityDistance<E> {
    final E entity;
    final int distance;

    EntityDistance(E entity, int distance) {
        this.entity = entity;
        this.distance = distance;
    }
}