package ca.recrown.islandsurvivalcraft.utilities.pathfinding;

import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Queue;

import org.apache.commons.lang.NullArgumentException;

import ca.recrown.islandsurvivalcraft.utilities.datatypes.Point2;

public class DepthFirstSearch {
    private Queue<DFSNode> queue;
    private CoordinateValidatable coordValidatable;
    private DFSNode startNode;
    private DFSNode endNode;
    private HashSet<DFSNode> checkedNodes;
    private int maxNodesSearched = -1;
    private DFSNode foundNode;

    public DepthFirstSearch() {
        checkedNodes = new HashSet<>();
        queue = new PriorityQueue<>();
    }

    public DepthFirstSearch(int maxNodesSearched) {
        this();
        this.maxNodesSearched = maxNodesSearched;
    }

    public DepthFirstSearch(int searchRadius, CoordinateValidatable validatable) {
        this(searchRadius);
        setValidatable(validatable);
    }

    public DepthFirstSearch(CoordinateValidatable validatable) {
        this();
        setValidatable(validatable);
    }

    public void setValidatable(CoordinateValidatable validatable) {
        this.coordValidatable = validatable;
    }

    public void setStartPosition(int x, int y) {
        startNode = new DFSNode(null, x, y, endNode);
    }

    public void setStartPosition(Point2 start) {
        startNode = new DFSNode(null, start, endNode);
    }

    public void setEndPosition(int x, int y) {
        endNode = new DFSNode(null, x, y, endNode);
    }

    public void setEndPosition(Point2 goal) {
        endNode = new DFSNode(null, goal, endNode);
    }

    public DFSNode getFoundNode() {
        return foundNode;
    }

    public void deleteTree() {
        startNode = null;
        endNode = null;
    }

    public boolean buildPathToEndNode() {
        if (coordValidatable == null)
            throw new IllegalStateException("Need to set a validator.");
        if (startNode == null)
            throw new IllegalStateException("Need to set the starting position.");

        return findTarget(new CoordinateTargetValidatable() {

            @Override
            public boolean isCoordinateTarget(int x, int y) {
                return endNode.getX() == x && endNode.getY() == y;
            }
        });
    }

    /**
     * Finds a coordinate determined by a target validator. If an end node is set,
     * it will try going in that direction first.
     * 
     * @param targetValidator
     * @return If it succeeded in finding an end node.
     */
    public boolean findTarget(CoordinateTargetValidatable targetValidator) {
        if (coordValidatable == null)
            throw new IllegalStateException("Need to set a validator.");
        if (targetValidator == null)
            throw new NullArgumentException("targetValidator");
        if (startNode == null)
            throw new IllegalStateException("Need to set the starting position.");

        queue.clear();
        checkedNodes.clear();


        DFSNode begin = startNode;
        if (!coordValidatable.validate(begin.x, begin.y)) return false;
        if (targetValidator.isCoordinateTarget(begin.x, begin.y)) return true;
        checkedNodes.add(begin);
        queue.add(begin);
        while (!queue.isEmpty()) {
            DFSNode n = queue.poll();
            if (maxNodesSearched != -1 && checkedNodes.size() > maxNodesSearched) return false;
            n.child[0] = new DFSNode(n, n.getX() + 1, n.getY(), endNode);
            n.child[1] = new DFSNode(n, n.getX() - 1, n.getY(), endNode);
            n.child[2] = new DFSNode(n, n.getX(), n.getY() + 1, endNode);
            n.child[3] = new DFSNode(n, n.getX(), n.getY() - 1, endNode);
            for (int c = 0; c < n.child.length; c++) {
                if (checkedNodes.add(n.child[c]) && coordValidatable.validate(n.child[c].x, n.child[c].y)) {
                    if (targetValidator.isCoordinateTarget(n.child[c].x, n.child[c].y)) {
                        foundNode = n.child[c];
                        return true;
                    }
                    queue.add(n.child[c]);
                }
            }
        }
        return false;
    }

    public int getSearchedCount() {
        return checkedNodes.size();
    }
}