//! Custom format to store graph with embedding and additional decorations.
//!
//! To use the format create an [`Embedding`] and call [`Embedding::write`]
//!
//!
//! The format consists of 3 initial lines denoting the number of: _vertices_, _edges_,
//! and _triangles_ respectively.
//! These first lines are followed by the vertices which have 2 coordinates, an
//! additional time coordinate, and an optional field value.
//! Then the edges with the indices of the start and end vertices follow, with the type of the edge
//! marked by an [`EdgeKind`] 0 or 1.
//! Then the triangles with the indices of the three vertices in clockwise order, the label of the
//! triangle itself, and an optional field value of the triangle.
//!
//! Note that all the elements are space-separated.
//! An example with a field on the vertices:
//! ```csv
//! 3
//! 3
//! 1
//! 0.0 0.0 3.125
//! 1.0 0.0 2.334
//! 0.0 1.0 6.234
//! 0 1 0
//! 1 2 1
//! 2 0 1
//! 0 1 2 0
//! ```

use std::{collections::HashMap, fmt::Display, io::Write};

use itertools::iproduct;

use crate::triangulation::{
    causal_triangulation2d::CausalTriangulation2D,
    causal_triangulation2d_edge::CausalTriangulation2D as CausalTriangulation2DEdge,
};

use super::tutte::{
    construct_tutte_embedding_with_graph_with_path,
    construct_tutte_embedding_with_graph_with_path_edgerepr,
};

/// Struct storing all simplices of the embedding
#[derive(Debug, Clone)]
#[allow(missing_docs)]
pub struct Embedding {
    vertices: Vec<Vertex>,
    edges: Vec<Edge>,
    triangles: Vec<Triangle>,
}

impl Embedding {
    /// Creates new embedding from a `triangulation`
    ///
    /// This embedding is a cut-open periodic embedding in the flat plane. `num_patches` specifies
    /// how may periodic copies are used.
    pub fn new_edgerepr(triangulation: &CausalTriangulation2DEdge, num_patches: usize) -> Self {
        let graph = triangulation.vertex_graph();
        let path = triangulation.identify_timelike_loop(0);

        let coordinates = construct_tutte_embedding_with_graph_with_path_edgerepr(
            triangulation,
            &graph,
            0,
            &path,
        );

        Self::new_with_coordinates_with_path_edgereps(
            triangulation,
            &coordinates,
            num_patches,
            &path,
        )
    }

    /// Creates new embedding from base toroidal `coordinates` for a `triangulation`
    ///
    /// Uses `path` as timelike loop along which to cut open the torus.
    pub fn new_with_coordinates_with_path_edgereps(
        triangulation: &CausalTriangulation2DEdge,
        coordinates: &[(f64, f64)],
        patches: usize,
        path: &[usize],
    ) -> Self {
        let nvertex = triangulation.num_vertices();
        let vertices: Vec<Vertex> = (0..patches)
            .flat_map(|i| {
                triangulation
                    .vertex_times
                    .iter()
                    .copied()
                    .zip(coordinates.iter().copied())
                    .map(move |(time, coordinate)| Vertex {
                        coordinate: shift_by_patchindex(coordinate, i),
                        time,
                    })
            })
            .collect();

        // Determine the triangles in the vertical boundary with previous and next index
        let mut vertical_bound = HashMap::with_capacity(path.len());
        for i in (0..path.len()).rev() {
            let current = path[i];
            let next = path[(path.len() + i - 1) % path.len()];
            let prev = path[(i + 1) % path.len()];

            let prev_index = triangulation.triangles[current]
                .get_neighbours()
                .iter()
                .position(|&nbr| nbr == prev)
                .expect("Previous path element must be neighbour for valid paths");
            let next_index = triangulation.triangles[current]
                .get_neighbours()
                .iter()
                .position(|&nbr| nbr == next)
                .expect("Next path element must be neighbour for valid paths");
            vertical_bound.insert(current, (prev_index, next_index));
        }

        // Determine the triangles in the horizontal boundary with previous and next index
        let mut horizontal_bound = HashMap::new();
        let triangle_origin = *path.last().expect("Path should not be empty");
        let mut prev = triangle_origin;
        let mut current = triangulation.get_right_neighbour(prev);
        loop {
            let next = triangulation.get_right_neighbour(current);

            let prev_index = triangulation.triangles[current]
                .get_neighbours()
                .iter()
                .position(|&nbr| nbr == prev)
                .expect("Previous path element must be neighbour for valid paths");
            let next_index = triangulation.triangles[current]
                .get_neighbours()
                .iter()
                .position(|&nbr| nbr == next)
                .expect("Next path element must be neighbour for valid paths");
            horizontal_bound.insert(current, (prev_index, next_index));

            prev = current;
            current = next;
            if prev == triangle_origin {
                break;
            }
        }

        let mut edges = Vec::with_capacity(triangulation.num_triangles() * 3 / 2 * patches);
        let mut triangles = Vec::with_capacity(triangulation.num_triangles() * patches);
        for (ipatch, (i, triangle)) in
            iproduct!(0..patches, triangulation.triangles.iter().enumerate())
        {
            let mut shifts = [(0, 0); 3];
            if let Some((prev, next)) = vertical_bound.get(&i) {
                shifts[(prev + 1) % 3].0 = -1;
                shifts[(next + 2) % 3].0 = -1;
            }
            if let Some((prev, next)) = horizontal_bound.get(&i) {
                shifts[(prev + 2) % 3].1 = -1;
                shifts[(next + 1) % 3].1 = -1;
            }
            let patch = usize_to_patch(ipatch);
            let ishift: [usize; 3] =
                shifts.map(|shift| patch_to_usize((patch.0 + shift.0, patch.1 + shift.1)));

            let v0 = triangle.vertices[0] + nvertex * ishift[0];
            let v1 = triangle.vertices[1] + nvertex * ishift[1];
            let v2 = triangle.vertices[2] + nvertex * ishift[2];

            if v0 < v1 && v0 < vertices.len() && v1 < vertices.len() {
                edges.push(Edge {
                    vertices: (v0, v1),
                    kind: EdgeKind::Timelike,
                });
            }
            if v1 < v2 && v1 < vertices.len() && v2 < vertices.len() {
                edges.push(Edge {
                    vertices: (v1, v2),
                    kind: EdgeKind::Spacelike,
                });
            }
            if v2 < v0 && v0 < vertices.len() && v2 < vertices.len() {
                edges.push(Edge {
                    vertices: (v2, v0),
                    kind: EdgeKind::Timelike,
                });
            }

            if v0 < vertices.len() && v1 < vertices.len() && v2 < vertices.len() {
                triangles.push(Triangle {
                    vertices: (v0, v1, v2),
                });
            }
        }

        Self {
            vertices,
            edges,
            triangles,
        }
    }

    /// Creates new embedding from a `triangulation`
    ///
    /// This embedding is a cut-open periodic embedding in the flat plane. `num_patches` specifies
    /// how may periodic copies are used.
    pub fn new(triangulation: &CausalTriangulation2D, num_patches: usize) -> Self {
        let graph = triangulation.vertex_graph();
        let path = triangulation.identify_timelike_loop(0);

        let coordinates =
            construct_tutte_embedding_with_graph_with_path(triangulation, &graph, 0, &path);

        Self::new_with_coordinates_with_path(triangulation, &coordinates, num_patches, &path)
    }

    /// Creates new embedding from base toroidal `coordinates` for a `triangulation`
    ///
    /// Uses `path` as timelike loop along which to cut open the torus.
    pub fn new_with_coordinates_with_path(
        triangulation: &CausalTriangulation2D,
        coordinates: &[(f64, f64)],
        patches: usize,
        path: &[usize],
    ) -> Self {
        let nvertex = triangulation.num_vertices();
        let vertices: Vec<Vertex> = (0..patches)
            .flat_map(|i| {
                triangulation
                    .vertex_times
                    .iter()
                    .copied()
                    .zip(coordinates.iter().copied())
                    .map(move |(time, coordinate)| Vertex {
                        coordinate: shift_by_patchindex(coordinate, i),
                        time,
                    })
            })
            .collect();

        // Determine the triangles in the vertical boundary with previous and next index
        let mut vertical_bound = HashMap::with_capacity(path.len());
        for i in (0..path.len()).rev() {
            let current = path[i];
            let next = path[(path.len() + i - 1) % path.len()];
            let prev = path[(i + 1) % path.len()];

            let prev_index = triangulation.triangles[current]
                .neighbours
                .iter()
                .position(|&nbr| nbr == prev)
                .expect("Previous path element must be neighbour for valid paths");
            let next_index = triangulation.triangles[current]
                .neighbours
                .iter()
                .position(|&nbr| nbr == next)
                .expect("Next path element must be neighbour for valid paths");
            vertical_bound.insert(current, (prev_index, next_index));
        }

        // Determine the triangles in the horizontal boundary with previous and next index
        let mut horizontal_bound = HashMap::new();
        let triangle_origin = *path.last().expect("Path should not be empty");
        let mut prev = triangle_origin;
        let mut current = triangulation.get_right_neighbour(prev);
        loop {
            let next = triangulation.get_right_neighbour(current);

            let prev_index = triangulation.triangles[current]
                .neighbours
                .iter()
                .position(|&nbr| nbr == prev)
                .expect("Previous path element must be neighbour for valid paths");
            let next_index = triangulation.triangles[current]
                .neighbours
                .iter()
                .position(|&nbr| nbr == next)
                .expect("Next path element must be neighbour for valid paths");
            horizontal_bound.insert(current, (prev_index, next_index));

            prev = current;
            current = next;
            if prev == triangle_origin {
                break;
            }
        }

        let mut edges = Vec::with_capacity(triangulation.num_triangles() * 3 / 2 * patches);
        let mut triangles = Vec::with_capacity(triangulation.num_triangles() * patches);
        for (ipatch, (i, triangle)) in
            iproduct!(0..patches, triangulation.triangles.iter().enumerate())
        {
            let mut shifts = [(0, 0); 3];
            if let Some((prev, next)) = vertical_bound.get(&i) {
                shifts[(prev + 1) % 3].0 = -1;
                shifts[(next + 2) % 3].0 = -1;
            }
            if let Some((prev, next)) = horizontal_bound.get(&i) {
                shifts[(prev + 2) % 3].1 = -1;
                shifts[(next + 1) % 3].1 = -1;
            }
            let patch = usize_to_patch(ipatch);
            let ishift: [usize; 3] =
                shifts.map(|shift| patch_to_usize((patch.0 + shift.0, patch.1 + shift.1)));

            let v0 = triangle.vertices[0] + nvertex * ishift[0];
            let v1 = triangle.vertices[1] + nvertex * ishift[1];
            let v2 = triangle.vertices[2] + nvertex * ishift[2];

            if v0 < v1 && v0 < vertices.len() && v1 < vertices.len() {
                edges.push(Edge {
                    vertices: (v0, v1),
                    kind: EdgeKind::Timelike,
                });
            }
            if v1 < v2 && v1 < vertices.len() && v2 < vertices.len() {
                edges.push(Edge {
                    vertices: (v1, v2),
                    kind: EdgeKind::Spacelike,
                });
            }
            if v2 < v0 && v0 < vertices.len() && v2 < vertices.len() {
                edges.push(Edge {
                    vertices: (v2, v0),
                    kind: EdgeKind::Timelike,
                });
            }

            if v0 < vertices.len() && v1 < vertices.len() && v2 < vertices.len() {
                triangles.push(Triangle {
                    vertices: (v0, v1, v2),
                });
            }
        }

        Self {
            vertices,
            edges,
            triangles,
        }
    }
}

fn shift_by_patchindex(coordinate: (f64, f64), index: usize) -> (f64, f64) {
    shift_by_patch(coordinate, usize_to_patch(index))
}

fn shift_by_patch(coordinate: (f64, f64), patch: (i8, i8)) -> (f64, f64) {
    (coordinate.0 + patch.0 as f64, coordinate.1 + patch.1 as f64)
}

/// Strange mapping of patch to number looking like:
/// 23 21 20 22 24
/// 11  7  6  8 18
///  9  1  0  4 16
/// 10  3  2  5 17
/// 15 13 12 14 19
/// This is done is such a way that the closer squares appear earlier in their
/// completeness.
fn patch_to_usize(patch: (i8, i8)) -> usize {
    let ux = i8usize_central_mapping(patch.0);
    let ut = i8usize_central_mapping(patch.1);

    if ut < ux {
        ux * ux + ut
    } else {
        ut * (ut + 1) + ux
    }
}

/// Map of `i8` to `usize` in such a way that:
/// (0, -1, 1, -2, 2, 3, ...) -> (0, 1, 2, 3, 4, 5, 6, ...)
fn i8usize_central_mapping(int: i8) -> usize {
    let abs_int = int.unsigned_abs();
    if int.is_negative() {
        2 * (abs_int as usize) - 1
    } else {
        2 * (abs_int as usize)
    }
}

/// Strange mapping of index to 2d lattice point, looking like:
/// 23 21 20 22 24
/// 11  7  6  8 18
///  9  1  0  4 16
/// 10  3  2  5 17
/// 15 13 12 14 19
/// This is done is such a way that the closer squares appear earlier in ordering
fn usize_to_patch(index: usize) -> (i8, i8) {
    let isqrt = (index as f64).sqrt() as usize;
    let (ut, ux) = if index - isqrt * isqrt < isqrt {
        (index - isqrt * isqrt, isqrt)
    } else {
        (isqrt, index - isqrt * (isqrt + 1))
    };

    (usizei8_central_mapping(ux), usizei8_central_mapping(ut))
}

/// Map of `usize` to `i8` in such a way that:
/// (0, 1, 2, 3, 4, 5, 6, ...) -> (0, -1, 1, -2, 2, 3, ...)
fn usizei8_central_mapping(uint: usize) -> i8 {
    if uint % 2 == 0 {
        (uint / 2) as i8
    } else {
        -(((uint + 1) / 2) as i8)
    }
}

/// Struct storing the [`Embedding`] along with a field on the vertices
#[derive(Debug, Clone)]
pub struct EmbeddingField<T> {
    /// Underlying embedding
    pub embedding: Embedding,
    /// Field values on the vertices
    ///
    /// Should have exactly as many elements as the vertices of the embedding
    pub field: Vec<T>,
}

/// Struct storing the [`Embedding`] along with a field on the triangles
#[derive(Debug, Clone)]
pub struct EmbeddingDualField<T> {
    /// Underlying embedding
    pub embedding: Embedding,
    /// Field values on the triangles
    ///
    /// Should have exactly as many elements as the triangles of the embedding
    pub field: Vec<T>,
}

/// Vertex of triangulation in 2D embedding with associated time coordinate
#[derive(Debug, Clone)]
struct Vertex {
    /// 2D vertex coordinate
    pub coordinate: (f64, f64),
    /// Time associated with vertex in CDT triangulatio
    pub time: u16,
}

/// Edge of triangulation in 2D embedding of spacelike or timelike type
#[derive(Debug, Clone)]
struct Edge {
    /// Label of vertices
    vertices: (usize, usize),
    /// Type of edge: spacelike or timelike
    kind: EdgeKind,
}

/// Struct representing a triangle in the embedding
/// Designed to hold the vertex labels of the triangle in counterclockwise order
#[derive(Debug, Clone)]
struct Triangle {
    /// Labels of the vertices in counterclockwise order
    pub vertices: (usize, usize, usize),
}

/// Type of edge in CDT triangulation: spacelike or timelike
#[derive(Debug, Clone, Copy)]
enum EdgeKind {
    /// Spacelike means the edge connects two vertices with equal time
    Spacelike = 0,
    /// Timelike means the edge connects two vertices with times differing by exactly 1
    Timelike = 1,
}

impl Display for EdgeKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EdgeKind::Spacelike => write!(f, "0"),
            EdgeKind::Timelike => write!(f, "1"),
        }
    }
}

impl Embedding {
    /// Write the embedding to a text buffer to export the embedding to a file for example.
    ///
    /// For the used format read the module docs [`embedding`](super::embedding)
    pub fn write<W: Write>(&self, buffer: &mut W) -> std::io::Result<()> {
        writeln!(buffer, "{}", self.vertices.len())?;
        writeln!(buffer, "{}", self.edges.len())?;
        writeln!(buffer, "{}", self.triangles.len())?;

        for vertex in self.vertices.iter() {
            writeln!(
                buffer,
                "{} {} {}",
                vertex.coordinate.0, vertex.coordinate.1, vertex.time
            )?;
        }

        for edge in self.edges.iter() {
            writeln!(
                buffer,
                "{} {} {}",
                edge.vertices.0, edge.vertices.1, edge.kind
            )?;
        }

        for (label, triangle) in self.triangles.iter().enumerate() {
            writeln!(
                buffer,
                "{} {} {} {}",
                triangle.vertices.0, triangle.vertices.1, triangle.vertices.2, label
            )?;
        }

        Ok(())
    }
}

impl<T: Display> EmbeddingField<T> {
    /// Write the embedding to a text buffer to export the embedding to a file for example.
    ///
    /// For the used format read the module docs [`embedding`](super::embedding)
    pub fn write<W: Write>(&self, buffer: &mut W) -> std::io::Result<()> {
        writeln!(buffer, "{}", self.embedding.vertices.len())?;
        writeln!(buffer, "{}", self.embedding.edges.len())?;
        writeln!(buffer, "{}", self.embedding.triangles.len())?;

        for (vertex, value) in self.embedding.vertices.iter().zip(self.field.iter()) {
            writeln!(
                buffer,
                "{} {} {} {}",
                vertex.coordinate.0, vertex.coordinate.1, vertex.time, value
            )?;
        }

        for edge in self.embedding.edges.iter() {
            writeln!(
                buffer,
                "{} {} {}",
                edge.vertices.0, edge.vertices.1, edge.kind
            )?;
        }

        for (label, triangle) in self.embedding.triangles.iter().enumerate() {
            writeln!(
                buffer,
                "{} {} {} {}",
                triangle.vertices.0, triangle.vertices.1, triangle.vertices.2, label
            )?;
        }

        Ok(())
    }
}

impl<T: Display> EmbeddingDualField<T> {
    /// Write the embedding to a text buffer to export the embedding to a file for example.
    ///
    /// For the used format read the module docs [`embedding`](super::embedding)
    pub fn write<W: Write>(&self, buffer: &mut W) -> std::io::Result<()> {
        writeln!(buffer, "{}", self.embedding.vertices.len())?;
        writeln!(buffer, "{}", self.embedding.edges.len())?;
        writeln!(buffer, "{}", self.embedding.triangles.len())?;

        for vertex in self.embedding.vertices.iter() {
            writeln!(
                buffer,
                "{} {} {}",
                vertex.coordinate.0, vertex.coordinate.1, vertex.time
            )?;
        }

        for edge in self.embedding.edges.iter() {
            writeln!(
                buffer,
                "{} {} {}",
                edge.vertices.0, edge.vertices.1, edge.kind
            )?;
        }

        for (label, triangle) in self.embedding.triangles.iter().enumerate() {
            let value = self
                .field
                .get(label)
                .expect("Not all triangle field values were set");
            writeln!(
                buffer,
                "{} {} {} {} {}",
                triangle.vertices.0, triangle.vertices.1, triangle.vertices.2, label, value
            )?;
        }

        Ok(())
    }
}
