use crate::{ResolutionMethod, candidate::*};
use core::ops::Deref;
use routers_network::{Edge, Entry, Metadata, Network, Node};
use serde::Serialize;

use geo::Coord;

/// A route representing the parsed output from a function
/// passed through the transition graph.
#[derive(Serialize, Debug)]
pub struct RoutedPath<E, M>
where
    E: Entry,
    M: Metadata,
{
    /// The exactly-routed elements.
    ///
    /// For a map-match request, these are the values which line up with the inputs
    /// for a one-to-one match. I.e. there is a discretized point for every input point.
    pub discretized: Path<E, M>,

    /// The interpolated elements.
    ///
    /// These points are the full interpreted trip, consisting of every turn and roadway
    /// the algorithm has assumed as a part of the path taken. This is useful for visualising
    /// a trip by "recovering" lost information, or understanding subtle details such as
    /// when the route left or joined a highway.
    pub interpolated: Path<E, M>,
}

impl<E, M> RoutedPath<E, M>
where
    E: Entry,
    M: Metadata,
{
    pub fn new(collapsed_path: CollapsedPath<E>, network: &impl Network<E, M>) -> Self {
        // Collect matched candidates in order.  Virtual start/end nodes have no
        // lookup entry so flat_map quietly skips them.
        let matched: Vec<Candidate<E>> = collapsed_path
            .route
            .iter()
            .flat_map(|id| collapsed_path.candidates.candidate(id))
            .collect();

        // One PathElement per GPS input point.
        let discretized = matched
            .iter()
            .flat_map(|c| PathElement::new(*c, network))
            .collect::<Path<E, M>>();

        // The complete traversed path. Each candidate edge is interleaved
        // with the routing edges that bridge consecutive candidates.
        //
        // We iterate through all discrete elements and ensure to include the
        // edges bridging them (intermediate edges). So as to conjoin the source's
        // target and the target's source with all relevant filler, preventing seemingly
        // jumpy segment joins for much-dispersed traffic, or low-frequency positions.
        //
        // Consecutive identical ends are deduplicated so that distance-only transitions,
        // which have the same directed edge for both candidates, do not repeat the segment.
        let interpolated = {
            let mut elements = Vec::new();

            for (i, reachable) in collapsed_path.interpolated.iter().enumerate() {
                let current = &matched[i];

                // Add current candidate position
                if let Some(pe) = PathElement::new(*current, network) {
                    elements.push(pe);
                }

                if let ResolutionMethod::Standard = reachable.resolution_method {
                    // Add target of current candidate edge
                    if let Some(fat) = network.fatten(&current.edge) {
                        if let Some(pe) = PathElement::from_fat(fat, network) {
                            elements.push(pe);
                        }
                    }

                    // Add intermediate edges
                    for edge in &reachable.path {
                        if let Some(fat) = network.fatten(edge) {
                            if let Some(pe) = PathElement::from_fat(fat, network) {
                                elements.push(pe);
                            }
                            if let Some(pe) = PathElement::from_fat(fat, network) {
                                elements.push(pe);
                            }
                        }
                    }

                    // Add source of next candidate edge
                    if let Some(next) = matched.get(i + 1) {
                        if let Some(fat) = network.fatten(&next.edge) {
                            if let Some(pe) = PathElement::from_fat(fat, network) {
                                elements.push(pe);
                            }
                        }
                    }
                }
            }

            // Add the very last candidate position
            if let Some(last_candidate) = matched.last() {
                if let Some(pe) = PathElement::new(*last_candidate, network) {
                    elements.push(pe);
                }
            }

            elements.dedup_by(|a, b| a.point == b.point);

            Path { elements }
        };

        RoutedPath {
            discretized,
            interpolated,
        }
    }
}

/// A representation of a path taken.
/// Consists of an array of [PathElement]s, containing relevant information for positioning.
#[derive(Debug, Serialize)]
pub struct Path<E, M>
where
    E: Entry,
    M: Metadata,
{
    /// The elements which construct the path.
    pub elements: Vec<PathElement<E, M>>,
}

impl<E, M> FromIterator<PathElement<E, M>> for Path<E, M>
where
    E: Entry,
    M: Metadata,
{
    fn from_iter<I: IntoIterator<Item = PathElement<E, M>>>(iter: I) -> Self {
        let elements = iter.into_iter().collect::<Vec<_>>();

        Path { elements }
    }
}

impl<E, M> Deref for Path<E, M>
where
    E: Entry,
    M: Metadata,
{
    type Target = Vec<PathElement<E, M>>;

    fn deref(&self) -> &Self::Target {
        &self.elements
    }
}

/// An element within a path, consisting of the [Point] the
/// element represents within the path, as well as metadata (Meta)
/// for the path element, and the edge within the source network at
/// which the element exists.
#[derive(Debug, Serialize)]
pub struct PathElement<E, M>
where
    E: Entry,
    M: Metadata,
{
    pub point: Coord,
    pub edge: Edge<Node<E>>,

    pub metadata: M,
}

impl<E, M> PathElement<E, M>
where
    E: Entry,
    M: Metadata,
{
    pub fn new(candidate: Candidate<E>, network: &impl Network<E, M>) -> Option<Self> {
        Some(PathElement {
            point: candidate.position.0,
            edge: network.fatten(&candidate.edge)?,
            metadata: network.metadata(candidate.edge.id())?.clone(),
        })
    }

    pub fn from_fat(edge: Edge<Node<E>>, network: &impl Network<E, M>) -> Option<Self> {
        Some(PathElement {
            point: edge.source.position.0,
            metadata: network.metadata(edge.id())?.clone(),
            edge,
        })
    }
}
