use rand::{
    distributions::{Distribution, Standard},
    Rng,
};

/// A Robot *aka droid* is represented here.
/// Each robot must have a unique id.
pub struct Robot {
    pub id: u32,
    pub o: Orientation,
    pub p: Position,
    pub i: Vec<Instruction>,
}

impl Robot {
    /// Create new `Robot` with given id, `Orientation`, `Position` and instructions.
    pub fn new(id: u32, o: Orientation, p: Position, i: Vec<Instruction>) -> Robot {
        Robot { id, o, p, i }
    }
    /// Create new random `Robot`.
    pub fn new_random(id: u32, posx_max: i32, posy_max: i32) -> Result<Robot, String> {
        let mut rng = rand::thread_rng();
        let x = rng.gen_range(2, posx_max);
        let y = rng.gen_range(2, posy_max);
        let inst = gen_random_instructions();
        let instructions: Vec<Instruction> = instructions_from_string(inst)?;
        let o: Orientation = rand::random();
        Ok(Robot {
            id,
            o,
            p: Position { x, y },
            i: instructions,
        })
    }
    /// Apply given instruction to a `Robot`.
    pub fn execute_instruction(&mut self) {
        match self.i.pop() {
            Some(instruction) => match instruction {
                Instruction::L => self.o = turn_left(&self.o),
                Instruction::R => self.o = turn_right(&self.o),
                Instruction::F => match self.o {
                    Orientation::N => self.p.y += 1,
                    Orientation::E => self.p.x += 1,
                    Orientation::S => self.p.y -= 1,
                    Orientation::W => self.p.x -= 1,
                },
            },
            None => (),
        }
    }
}

/// Enum to store all possible orientations.
#[derive(Debug)]
pub enum Orientation {
    N,
    E,
    S,
    W,
}

fn turn_left(o: &Orientation) -> Orientation {
    match o {
        Orientation::N => Orientation::W,
        Orientation::E => Orientation::N,
        Orientation::S => Orientation::E,
        Orientation::W => Orientation::S,
    }
}
fn turn_right(o: &Orientation) -> Orientation {
    match o {
        Orientation::N => Orientation::E,
        Orientation::E => Orientation::S,
        Orientation::S => Orientation::W,
        Orientation::W => Orientation::N,
    }
}

/// Enum to store all possible instructions.
#[derive(Debug, Eq, PartialEq)]
pub enum Instruction {
    L,
    R,
    F,
}

pub fn instructions_from_string(s: String) -> Result<Vec<Instruction>, String> {
    let mut v: Vec<Instruction> = Vec::new();
    for c in s.chars() {
        match c {
            'L' => v.push(Instruction::L),
            'R' => v.push(Instruction::R),
            'F' => v.push(Instruction::F),
            _ => return Err(String::from("Not an instruction.")),
        }
    }
    Ok(v)
}

impl Distribution<Orientation> for Standard {
    /// Generating random orientation.
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Orientation {
        match rng.gen_range(0, 3) {
            0 => Orientation::N,
            1 => Orientation::E,
            2 => Orientation::S,
            _ => Orientation::W,
        }
    }
}

/// Struct to store robot position.
#[derive(PartialEq, Eq, Hash)]
pub struct Position {
    pub x: i32,
    pub y: i32,
}

/// Generate random instructions.
pub fn gen_random_instructions() -> String {
    let mut rng = rand::thread_rng();
    let n = rng.gen_range(5, 10);
    let mut instructions = String::with_capacity(n);
    const CHARSET: &[u8] = b"LRF";
    for _ in 0..n {
        let l = rng.gen_range(0, CHARSET.len());
        instructions.push(CHARSET[l] as char);
    }

    instructions
}

/// Check if a robot is piouff.
pub fn is_piouff(r: &Robot) -> bool {
    r.i.len() == 0
}

/// Print robots id, position and instructions.
pub fn print_robots(robot_pool: &Vec<Robot>) -> String {
    let mut res = format!("Robots [\n");
    for r in robot_pool {
        res = format!(
            "{}{{ id = {}, x = {}; y = {}; orientation: {}, instructions: {:?}, }},\n",
            res,
            r.id,
            r.p.x,
            r.p.y,
            match r.o {
                Orientation::N => "Norts",
                Orientation::S => "South",
                Orientation::E => "East",
                Orientation::W => "West",
            },
            r.i
        );
    }
    res = format!("{}]\n", res);
    res
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::any::type_name;

    fn type_of<T>(_: T) -> &'static str {
        type_name::<T>()
    }

    #[test]
    fn test_rand_orientation() {
        let o: Orientation = rand::random();
        assert_eq!(type_of(o), "dancing_droid::robot::Orientation");
    }

    #[test]
    fn test_new_robot() {
        let r: Robot = Robot::new(
            0,
            Orientation::N,
            Position { x: 1, y: 2 },
            vec![Instruction::L, Instruction::F, Instruction::R],
        );
        assert_eq!(r.id, 0);
        assert!(matches!(r.o, Orientation::N));
        assert_eq!(r.p.x, 1);
        assert_eq!(r.p.y, 2);
        assert_eq!(r.i[0], Instruction::L);
        assert_eq!(r.i[1], Instruction::F);
        assert_eq!(r.i[2], Instruction::R);
    }

    #[test]
    fn test_execute_instruction() {
        let mut r: Robot = Robot::new(
            0,
            Orientation::N,
            Position { x: 1, y: 2 },
            vec![
                Instruction::R,
                Instruction::F,
                Instruction::L,
                Instruction::F,
            ],
        );
        let mut hash = std::collections::HashMap::new();
        //hash.insert(&r.p, &r.id); // first insert while initializing.
        hash.remove(&r.p); // remove before execute_instruction().
        r.execute_instruction();
        hash.insert(&r.p, &r.id); // second insert after moving.
        assert_eq!(r.p.x, 1);
        assert_eq!(r.p.y, 3);
        r.execute_instruction();
        r.execute_instruction();
        assert_eq!(r.p.x, 0);
        assert_eq!(r.p.y, 3);
    }

    #[test]
    fn test_piouf() {
        let mut r: Robot = Robot::new(
            0,
            Orientation::N,
            Position { x: 1, y: 2 },
            vec![
                Instruction::R,
                Instruction::F,
                Instruction::L,
                Instruction::F,
            ],
        );
        r.i.pop();
        r.i.pop();
        r.i.pop();
        r.i.pop();
        assert!(is_piouff(&r));
    }
}