<?php

namespace Graphp\Algorithms\ShortestPath;

use Graphp\Algorithms\BaseVertex;
use Fhaculty\Graph\Walk;
use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Edge\Base as Edge;
use Fhaculty\Graph\Set\Edges;
use Fhaculty\Graph\Set\Vertices;

/**
 * Abstract base class for shortest path algorithms
 *
 * This abstract base class provides the base interface for working with
 * single-source shortest paths (SSSP).
 *
 * The shortest path problem is the problem of finding a path between two
 * vertices such that the sum of the weights of its constituent edges is
 * minimized. The weight of the shortest path is referred to as distance.
 *
 *    A--[10]-------------B---E<--F
 *     \                 /
 *      \--[4]--C--[2]--D
 *
 * In the above pictured graph, the distance (weight of the shortest path)
 * between A and C is 4, and the shortest path between A and B is "A->C->D->B"
 * with a distance (total weight) of 6.
 *
 * In graph theory, it is usually assumed that a path to an unreachable vertex
 * has infinite distance. In the above pictured graph, there's no way path
 * from A to F, i.e. vertex F is unreachable from vertex A because of the
 * directed edge "E <- F" pointing in the opposite direction. This library
 * considers this an Exception instead. So if you're asking for the distance
 * between A and F, you'll receive an OutOfBoundsException instead.
 *
 * In graph theory, it is usually assumed that each vertex has a (pseudo-)path
 * to itself with a distance of 0. In order to produce reliable, consistent
 * results, this library considers this (pseudo-)path to be non-existant, i.e.
 * there's NO "magic" path between A and A. So if you're asking for the distance
 * between A and A, you'll receive an OutOfBoundsException instead. This allows
 * us to check hether there's a real path between A and A (cycle via other
 * vertices) as well as working with loop edges.
 *
 * @link http://en.wikipedia.org/wiki/Shortest_path_problem
 * @link http://en.wikipedia.org/wiki/Tree_%28data_structure%29
 * @see ShortestPath\Dijkstra
 * @see ShortestPath\MooreBellmanFord which also supports negative Edge weights
 * @see ShortestPath\BreadthFirst with does not consider Edge weights, but only the number of hops
 */
abstract class Base extends BaseVertex
{
    /**
     * get walk (path) from start vertex to given end vertex
     *
     * @param  Vertex    $endVertex
     * @return Walk
     * @throws OutOfBoundsException if there's no path to the given end vertex
     * @uses self::getEdgesTo()
     * @uses Walk::factoryFromEdges()
     */
    public function getWalkTo(Vertex $endVertex)
    {
        return Walk::factoryFromEdges($this->getEdgesTo($endVertex), $this->vertex);
    }

    /**
     * get array of edges (path) from start vertex to given end vertex
     *
     * @param  Vertex    $endVertex
     * @throws OutOfBoundsException if there's no path to the given end vertex
     * @return Edges
     * @uses self::getEdges()
     * @uses self::getEdgesToInternal()
     */
    public function getEdgesTo(Vertex $endVertex)
    {
        return $this->getEdgesToInternal($endVertex, $this->getEdges());
    }

    /**
     * get array of edges (path) from start vertex to given end vertex
     *
     * @param  Vertex       $endVertex
     * @param  Edges|Edge[] $edges     set or array of all input edges to operate on
     * @throws OutOfBoundsException if there's no path to the given vertex
     * @return Edges
     * @uses self::getEdges() if no edges were given
     */
    protected function getEdgesToInternal(Vertex $endVertex, $edges)
    {
        $currentVertex = $endVertex;
        $path = array();
        do {
            $pre = NULL;
            // check all edges to search for edge that points TO current vertex
            foreach ($edges as $edge) {
                try {
                    // get start point of this edge (fails if current vertex is not its end point)
                    $pre = $edge->getVertexFromTo($currentVertex);
                    $path []= $edge;
                    $currentVertex = $pre;
                    break;
                } catch (InvalidArgumentException $ignore) {
                } // ignore: this edge does not point TO current vertex
            }
            if ($pre === NULL) {
                throw new OutOfBoundsException('No edge leading to vertex');
            }
        } while ($currentVertex !== $this->vertex);

        return new Edges(array_reverse($path));
    }

    /**
     * get sum of weight of given edges
     *
     * @param  Edges $edges
     * @return float
     * @uses Edge::getWeight()
     */
    private function sumEdges(Edges $edges)
    {
        $sum = 0;
        foreach ($edges as $edge) {
            $sum += $edge->getWeight();
        }

        return $sum;
    }

    /**
     * get set of all Vertices the given start vertex has a path to
     *
     * @return Vertices
     * @uses self::getDistanceMap()
     */
    public function getVertices()
    {
        $vertices = array();
        $map = $this->getDistanceMap();
        foreach ($this->vertex->getGraph()->getVertices()->getMap() as $vid => $vertex) {
            if (isset($map[$vid])) {
                $vertices[$vid] = $vertex;
            }
        }

        return new Vertices($vertices);
    }

    /**
     * checks whether there's a path from this start vertex to given end vertex
     *
     * @param  Vertex  $endVertex
     * @return boolean
     * @uses self::getEdgesTo()
     */
    public function hasVertex(Vertex $vertex)
    {
        try {
            $this->getEdgesTo($vertex);
        }
        catch (OutOfBoundsException $e) {
            return false;
        }
        return true;
    }

    /**
     * get map of vertex IDs to distance
     *
     * @return float[]
     * @uses self::getEdges()
     * @uses self::getEdgesToInternal()
     * @uses self::sumEdges()
     */
    public function getDistanceMap()
    {
        $edges = $this->getEdges();
        $ret = array();
        foreach ($this->vertex->getGraph()->getVertices()->getMap() as $vid => $vertex) {
            try {
                $ret[$vid] = $this->sumEdges($this->getEdgesToInternal($vertex, $edges));
            } catch (OutOfBoundsException $ignore) {
            } // ignore vertices that can not be reached
        }

        return $ret;
    }

    /**
     * get distance (sum of weights) between start vertex and given end vertex
     *
     * @param  Vertex    $endVertex
     * @return float
     * @throws OutOfBoundsException if there's no path to the given end vertex
     * @uses self::getEdgesTo()
     * @uses self::sumEdges()
     */
    public function getDistance(Vertex $endVertex)
    {
        return $this->sumEdges($this->getEdgesTo($endVertex));
    }

    /**
     * create new resulting graph with only edges on shortest path
     *
     * The resulting Graph will always represent a tree with the start vertex
     * being the root vertex.
     *
     * For example considering the following input Graph with equal weights on
     * each edge:
     *
     *     A----->F
     *    / \     ^
     *   /   \   /
     *  /     \ /
     *  |      E
     *  |       \
     *  |        \
     *  B--->C<---D
     *
     * The resulting shortest path tree Graph will look like this:
     *
     *     A----->F
     *    / \
     *   /   \
     *  /     \
     *  |      E
     *  |       \
     *  |        \
     *  B--->C    D
     *
     * Or by just arranging the Vertices slightly different:
     *
     *          A
     *         /|\
     *        / | \
     *       B  E  \->F
     *      /   |
     *  C<-/    D
     *
     * @return Graph
     * @uses self::getEdges()
     * @uses Graph::createGraphCloneEdges()
     */
    public function createGraph()
    {
        return $this->vertex->getGraph()->createGraphCloneEdges($this->getEdges());
    }

    /**
     * get cheapest edges (lowest weight) for given map of vertex predecessors
     *
     * @param  Vertex[] $predecessor
     * @return Edges
     * @uses Graph::getVertices()
     * @uses Vertex::getEdgesTo()
     * @uses Edges::getEdgeOrder()
     */
    protected function getEdgesCheapestPredecesor(array $predecessor)
    {
        $vertices = $this->vertex->getGraph()->getVertices()->getMap();

        $edges = array();
        foreach ($vertices as $vid => $vertex) {
            if (isset($predecessor[$vid])) {
                // get predecor
                $predecesVertex = $predecessor[$vid];

                // get cheapest edge
                $edges []= $predecesVertex->getEdgesTo($vertex)->getEdgeOrder(Edges::ORDER_WEIGHT);
            }
        }

        return new Edges($edges);
    }

    /**
     * get all edges on shortest path for this vertex
     *
     * @return Edges
     */
    abstract public function getEdges();
}
