// gamenite - A graph library for board games
// Copyright (C) 2022 Dane Johnson

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! A graph library for board games
//!
//! When writing programs revolving around tabletop board games, often the graph is the preferred data
//! structure for representing the game board, especially in games that are neither linear nor grid-based,
//! i.e. _Risk_. This crate provides utilities for building and interacting with graph-based board states.
//!
//! More specifically, we provide [Board], a [HashMap] based graph with configurable labels and options.
//! With it we provide [Node], representing a location on the board, with connections to other Nodes.
//! Finally, we provide an [io] package to facilitate loading and saving these boards, and a related image to
//! a compressed file.
pub mod io;

use serde::{ Serialize, Deserialize };
use std::collections::{ HashMap, HashSet };

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
/// Configuration options for a [Board]
pub struct Config {
    /// Whether the board wraps around the horizontal edges. Games like _Risk_ make use of this
    pub horizontal_wrapping: bool,
}
impl Default for Config {
    fn default() -> Self {
        Config {
            horizontal_wrapping: false,
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(default)]
/// A space or connection on a [Board]
///
/// Nodes are [serde] [Serialize] and [Deserialize]
pub struct Node {
    /// A [0, 1] based horizontal node position
    pub x: f32,
    /// A [0, 1] based vertical node position
    pub y: f32,

    /// A human-readable identifier for this node. It need not be unique
    pub name: String,
    /// A set of edges connecting this node to other nodes on a [Board]
    pub edges: HashSet<usize>,
    /// A [HashMap] of label names to values, indicating which labels apply to this node
    pub labels: HashMap<String, String>,
}
impl Node {
    /// Constructs a new anonymous, unconnected and unlabeled node at the given xy coordinate
    /// # Panics
    /// This function panics if x and y are not in the range [0, 1]
    pub fn new(x: f32, y: f32) -> Self {
        if !(0.0..=1.0).contains(&x) || !(0.0..=1.0).contains(&y) {
            panic!("Node coordinates are not in range [0,1]");
        }
        Node {x, y, ..Default::default()}
    }
    /// Tests if the horizontal distance from this node to another is shorter by wrapping
    /// over the board edge
    /// # Examples
    /// ```
    /// use gamenite::Node;
    ///
    /// let node1 = Node::new(0.1, 0.0);
    /// let node2 = Node::new(0.2, 0.0);
    /// let node3 = Node::new(0.9, 0.0);
    /// assert!(!node1.should_wrap_horizontal(&node1));
    /// assert!(node1.should_wrap_horizontal(&node3));
    /// ```
    pub fn should_wrap_horizontal(&self, other: &Node) -> bool {
        let xs = self.horizontal_order(other).map(|n| n.x);
        xs[0] + (1.0 - xs[1]) < xs[1] - xs[0]
    }
    /// Finds the vertical position whereby a line drawn from this node to the nearest edge
    /// will have the same slope as a line drawn from the other node to the other edge at the
    /// same position
    /// # Examples
    /// ```
    /// use gamenite::Node;
    ///
    /// let node1 = Node::new(0.2, 0.3);
    /// let node2 = Node::new(0.7, 0.1);
    /// let y_mid = node1.y_mid(&node2);
    /// assert!(y_mid >= 0.21 && y_mid <= 0.23);
    /// ```
    pub fn y_mid(&self, other: &Node) -> f32 {
        let [(x1, y1), (x2, y2)] = self.horizontal_order(other).map(Node::as_coords);
        y1 - x1 * (y1 - y2) / (x1 + 1.0 - x2)
    }
    /// Gets the xy coordinates of the node a pair of [f32s](f32)
    pub fn as_coords(&self) -> (f32, f32) {
        (self.x, self.y)
    }
    /// Returns this node and another ordered by x coordinate
    /// # Examples
    /// ```
    /// use gamenite::Node;
    ///
    /// let node1 = Node::new(0.1, 0.0);
    /// let node2 = Node::new(0.2, 0.0);
    /// assert_eq!(node1.horizontal_order(&node2), [&node1, &node2]);
    /// assert_eq!(node2.horizontal_order(&node1), [&node1, &node2]);
    /// ```
    pub fn horizontal_order<'a>(&'a self, other: &'a Node) -> [&'a Node; 2] {
        if self.x < other.x {
            [self, other]
        } else {
            [other, self]
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(default)]
/// A graph of [Nodes](Node) and related information
///
/// The graph is [serde] [Serialize] and [Deserialize].
///
/// Graphs might have labels, which are arbitrarily named keys, each with one of
/// several possible values. Each [Node] then defines it's value for each label.
///
/// As an example, in the game _Risk_ each territory is assigned to a continent. Therefore
/// the board might have a label key "Continent" with values "North America", "Asia", "Australia" etc.
/// and each node would define it's "Continent" label, for example the node named "Alaska" would
/// map "Continent" to "North America".
pub struct Board {
    /// A mapping of labels to their available values
    pub labels: HashMap<String, HashSet<String>>,
    /// The [Nodes](Node) on this board, keyed by id
    pub nodes: HashMap<usize, Node>,
    /// The board configuration
    pub config: Config,
}
impl Board {
    /// Insert a [Node] into the graph, using the next available id.
    /// Returns the id this node was assigned to
    pub fn insert_node(&mut self, node: Node) -> usize {
        let id = self.next_id();
        self.nodes.insert(id, node);
        id
    }
    /// Remove a [Node] from the graph, breaking all edges referring to
    /// it and marking its id for reuse
    /// # Examples
    /// ```
    /// use gamenite::{ Board, Node };
    ///
    /// let mut board = Board::default();
    ///
    /// let id1 = board.insert_node(Node::new(0.0, 0.0));
    /// let id2 = board.insert_node(Node::new(0.0, 0.0));
    /// board.add_edge(id2, id1);
    ///
    /// board.remove_node(id1);
    /// assert!(!board.nodes.contains_key(&id1));
    /// assert!(!board.nodes[&id2].edges.contains(&id1));
    /// ```
    pub fn remove_node(&mut self, id: usize) {
        // We remove this node from the graph, then drop it from each
        // other nodes edge.
        self.nodes.remove(&id);
        for node in self.nodes.values_mut() {
            node.edges.remove(&id);
        }
    }
    /// Add a label with specified keys and values
    pub fn insert_label(&mut self, key: &str, values: &[&str]) {
        self.labels.insert(
            key.to_string(),
            values.iter().map(|s| s.to_string()).collect(),
        );
    }
    /// Remove a label key from use on this board, and remove it from any nodes
    /// # Examples
    /// ```
    /// use gamenite::{ Board, Node };
    ///
    /// let mut board = Board::default();
    /// let mut node = Node::new(0.0, 0.0);
    ///
    /// board.insert_label("Fruit", &["Apples", "Pears"]);
    /// node.labels.insert("Fruit".to_string(), "Apples".to_string());
    /// let id = board.insert_node(node);
    ///
    /// board.remove_label_key("Fruit");
    /// assert!(!board.labels.contains_key("&Fruit"));
    /// assert!(!board.nodes[&id].labels.contains_key("&Fruit"));
    /// ```
    pub fn remove_label_key(&mut self, key: &str) {
        self.labels.remove(key);
        for node in self.nodes.values_mut() {
            node.labels.remove(key);
        }
    }
    /// Removes a value from the label keyed by key
    pub fn remove_label_value(&mut self, key: &str, value: &str) {
        if let Some(l) = self.labels.get_mut(key) {
            l.remove(value);
        }
    }

    /// Adds an edge from a node to another by ids
    /// # Examples
    /// ```
    /// use gamenite::{ Board, Node };
    ///
    /// let mut board = Board::default();
    /// let id1 = board.insert_node(Node::new(0.0, 0.0));
    /// let id2 = board.insert_node(Node::new(0.0, 0.0));
    ///
    /// board.add_edge(id1, id2);
    /// assert!(board.nodes[&id1].edges.contains(&id2));
    /// assert!(!board.nodes[&id2].edges.contains(&id1));
    pub fn add_edge(&mut self, from: usize, to: usize) {
	      let node = self.nodes.get_mut(&from).expect("Could not find node");
	      node.edges.insert(to);
    }

    /// Finds the nearest node to a given position
    pub fn nearest_node(&self, x: f32, y: f32) -> Option<usize> {
        let f = |n: &Node| dist_sq((n.x, n.y), (x, y));
        let mut iter = self.nodes.iter();
        if let Some((id, node)) = iter.next() {
            let mut min = *id;
            let mut min_val = f(node);
            for (id, node) in iter {
                let val = f(node);
                if val < min_val {
                    min = *id;
                    min_val = val;
                }
            }
            Some(min)
        } else {
            None
        }
    }

    fn next_id(&self) -> usize {
        for i in 0 .. {
            if !self.nodes.contains_key(&i) {
                return i;
            }
        }
        unreachable!();
    }
}

fn dist_sq((x0, y0): (f32, f32), (x1, y1): (f32, f32)) -> f32 {
    (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
}

fn lerp(v0: f32, v1: f32, t: f32) -> f32 {
    v0 + t * (v1 - v0)
}

fn inv_lerp(v0: f32, v1: f32, a: f32) -> f32 {
    (a - v0) / (v1 - v0)
}

/// Trait to allow transformations from the (\[0,1\], \[0,1\]) coordinate space of the
/// graph to another coordinate space
/// # Examples
/// ```
/// use gamenite::{ CoordTransformer, Node };
/// use std::ops::Range;
///
/// // Convert to Cartesian coordinates
/// struct CartesianCoords { domain: Range<f32>, range: Range<f32>};
/// impl CoordTransformer<(f32, f32)> for CartesianCoords {
///     fn origin(&self) -> (f32, f32) { (self.domain.start, self.range.start) }
///     fn extremes(&self) -> (f32, f32) { (self.domain.end, self.range.end) }
/// }
///
/// // Cartesian coordinates from ([-50, 30], [-10, 100])
/// let graph = CartesianCoords { domain: -50.0..30.0, range: -10.0..100.0 };
/// let node = Node::new(0.5, 0.5);
/// assert_eq!(graph.inv_xform(node.x, node.y), (-10.0, 45.0));
/// assert_eq!(graph.xform((-10.0, 45.0)), (node.x, node.y));
/// ```
pub trait CoordTransformer<I: Into<(f32, f32)> + From<(f32, f32)>> {
    /// Returns the smallest values that a coordinate can take on
    fn origin(&self) -> I;
    /// Returns the largest values that a coordinate can take on
    fn extremes(&self) -> I;
    /// Returns the coordinate mapped into the [Board] standard (\[0, 1\], \[0, 1\]) coordinate space
    fn xform(&self, pos: I) -> (f32, f32) {
        let (sx, sy) = self.origin().into();
        let (ex, ey) = self.extremes().into();
        let (x, y) = pos.into();
        (
            inv_lerp(sx, ex, x),
            inv_lerp(sy, ey, y),
        )
    }
    /// Returns the board coordinate mapped into this space
    fn inv_xform(&self, x: f32, y: f32) -> I {
        let (sx, sy) = self.origin().into();
        let (ex, ey) = self.extremes().into();
        (lerp(sx, ex, x), lerp(sy, ey, y)).into()
    }
}
