use rand::prelude::*;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

pub mod TickFlag {
    pub const COLLIDED: usize = 1;
    pub const GOAL_REACHED: usize = 2;
}

pub type TickResult = usize;

pub static MAX_ACCELERATION: isize = 127;

pub static DRAG_FRACTION: (isize, isize) = (9, 10);
pub static COLLISION_FRACTION: (isize, isize) = (1, 2);
pub static MAX_COLLISION_RESOLUTIONS: usize = 5;

pub static CELL_SIZE: isize = 10_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Racer {
    pub x: isize,
    pub y: isize,
    pub vx: isize,
    pub vy: isize,
    pub radius: isize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Asteroid {
    pub x: isize,
    pub y: isize,
    pub radius: isize,
}

pub type Goal = Asteroid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct Instruction {
    pub vx: isize,
    pub vy: isize,
}

impl Instruction {
    fn valid(vx: isize, vy: isize) -> bool {
        distance_squared(vx, vy, 0, 0) <= (MAX_ACCELERATION).pow(2)
    }

    pub fn new(vx: isize, vy: isize) -> Self {
        if !Self::valid(vx, vy) {
            // use float to properly normalize here
            let float_distance = ((vx as f64).powf(2.) + (vy as f64).powf(2.)).powf(1. / 2.);

            let mut vx = ((vx as f64 / float_distance) * MAX_ACCELERATION as f64) as isize;
            let mut vy = ((vy as f64 / float_distance) * MAX_ACCELERATION as f64) as isize;

            // if we're still over, decrement both values
            if !Self::valid(vx, vy) {
                vx -= vx.signum();
                vy -= vy.signum();
            }

            return Self { vx, vy };
        }

        assert!(Self::valid(vx, vy));

        Self {
            vx,
            vy,
        }
    }

    pub fn random() -> Self {
        let mut rng = rand::rng();

        Self {
            vx: rng.random::<i64>() as isize,
            vy: rng.random::<i64>() as isize,
        }
    }

    pub fn load(path: &PathBuf) -> Vec<Instruction> {
        let contents = fs::read_to_string(path).expect("Failed reading a file!");
        let mut lines = contents.lines();

        let instruction_count = lines.next().unwrap().parse::<usize>().unwrap();

        let mut instructions = vec![];

        for _ in 0..instruction_count {
            let parts = lines
                .next()
                .expect("No more lines!")
                .split_whitespace()
                .collect::<Vec<&str>>();

            instructions.push(Instruction {
                vx: parts[0].parse::<isize>().unwrap(),
                vy: parts[1].parse::<isize>().unwrap(),
            })
        }

        instructions
    }

    pub fn save(path: &PathBuf, instructions: &Vec<Instruction>) {
        let mut file = File::create(path).expect("Failed creating a file!");

        for instruction in instructions {
            file.write_all(format!("{} {}\n", instruction.vx, instruction.vy).as_bytes())
                .expect("Failed writing to file!");
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct BoundingBox {
    pub min_x: isize,
    pub min_y: isize,
    pub max_x: isize,
    pub max_y: isize,
}

impl BoundingBox {
    pub fn width(&self) -> isize {
        self.max_x - self.min_x
    }

    pub fn height(&self) -> isize {
        self.max_y - self.min_y
    }
}

/// Squared Euclidean distance; useful for distance checks.
fn distance_squared(x1: isize, y1: isize, x2: isize, y2: isize) -> isize {
    (x1 - x2).pow(2) + (y1 - y2).pow(2)
}

/// Plain-old integer Euclidean distance.
///
/// Note: this implementation might break for larger position values, but since
/// the maps are never going to be this large, I'm not fixing it now.
pub fn euclidean_distance(x1: isize, y1: isize, x2: isize, y2: isize) -> isize {
    (distance_squared(x1, y1, x2, y2) as f64).sqrt() as isize
}

#[derive(Debug, Clone)]
pub struct Simulation {
    pub initial_racer: Racer,
    pub racer: Racer,

    pub asteroids: Vec<Asteroid>,
    pub goals: Vec<Goal>,
    pub bbox: BoundingBox,

    pub reached_goals: Vec<bool>,

    _grid: HashMap<(isize, isize), Vec<Asteroid>>,
    _cell_size: isize,
}

///
/// # Examples
/// ```
/// let map_path = PathBuf::from("../../maps/test.txt");
///
/// let mut simulation = Simulation::load(&map_path);
///
/// let mut tick_result: TickResult = 0;
///
/// println!("Running simulation until collision...");
///
/// while tick_result & TickFlag::COLLIDED == 0 {
///     tick_result = simulation.tick(Instruction::new(0, MAX_ACCELERATION));
///
///     println!("{:?}", simulation.racer);
/// }
///
/// println!("Bam!");
/// ```
///
impl Simulation {
    pub fn new(racer: Racer, asteroids: Vec<Asteroid>, goals: Vec<Goal>, bbox: BoundingBox) -> Self {
        let reached_goals = vec![false; goals.len()];

        let mut simulation = Self {
            initial_racer: racer,
            racer,
            asteroids,
            goals,
            bbox,
            reached_goals,
            _grid: HashMap::new(),
            _cell_size: CELL_SIZE,
        };

        for &asteroid in &simulation.asteroids {
            let (min_x, min_y) = simulation.coordinate_to_grid(
                asteroid.x - asteroid.radius - racer.radius,
                asteroid.y - asteroid.radius - racer.radius,
            );

            let (max_x, max_y) = simulation.coordinate_to_grid(
                asteroid.x + asteroid.radius + racer.radius,
                asteroid.y + asteroid.radius + racer.radius,
            );

            for grid_x in min_x..=max_x {
                for grid_y in min_y..=max_y {
                    simulation
                        ._grid
                        .entry((grid_x, grid_y))
                        .or_insert(vec![])
                        .push(asteroid);
                }
            }
        }

        simulation
    }

    fn coordinate_to_grid(&self, x: isize, y: isize) -> (isize, isize) {
        (x / self._cell_size, y / self._cell_size)
    }

    fn move_racer(&mut self, instruction: Instruction) {
        self.racer.vx = (self.racer.vx * DRAG_FRACTION.0) / DRAG_FRACTION.1;
        self.racer.vy = (self.racer.vy * DRAG_FRACTION.0) / DRAG_FRACTION.1;

        self.racer.vx += instruction.vx;
        self.racer.vy += instruction.vy;

        self.racer.x += self.racer.vx as isize;
        self.racer.y += self.racer.vy as isize;
    }

    fn push_from_asteroids(&mut self) -> bool {
        let grid_coordinate = self.coordinate_to_grid(self.racer.x, self.racer.y);

        match self._grid.get(&grid_coordinate) {
            None => false,
            Some(asteroids) => {
                for asteroid in asteroids {
                    // not colliding, nothing to be done
                    if euclidean_distance(self.racer.x, self.racer.y, asteroid.x, asteroid.y)
                        > self.racer.radius + asteroid.radius
                    {
                        continue;
                    }

                    // the vector to push the racer out by
                    let nx = self.racer.x - asteroid.x;
                    let ny = self.racer.y - asteroid.y;

                    // how much to push by
                    let distance =
                        euclidean_distance(self.racer.x, self.racer.y, asteroid.x, asteroid.y);
                    let push_by = distance - (self.racer.radius + asteroid.radius);

                    // the actual push
                    self.racer.x -= (nx * push_by) / distance;
                    self.racer.y -= (ny * push_by) / distance;

                    return true;
                }

                false
            }
        }
    }

    fn push_from_bounding_box(&mut self) -> bool {
        // not pretty but easy to read :)
        let mut collided = false;

        if self.racer.x - self.racer.radius < self.bbox.min_x {
            self.racer.x = self.bbox.min_x + self.racer.radius;
            collided = true;
        }
        if self.racer.x + self.racer.radius > self.bbox.max_x {
            self.racer.x = self.bbox.max_x - self.racer.radius;
            collided = true;
        }
        if self.racer.y - self.racer.radius < self.bbox.min_y {
            self.racer.y = self.bbox.min_y + self.racer.radius;
            collided = true;
        }
        if self.racer.y + self.racer.radius > self.bbox.max_y {
            self.racer.y = self.bbox.max_y - self.racer.radius;
            collided = true;
        }

        collided
    }

    fn check_goal(&mut self) -> bool {
        let mut new_goal_reached = false;

        for (i, goal) in self.goals.iter().enumerate() {
            if euclidean_distance(self.racer.x, self.racer.y, goal.x, goal.y)
                <= (self.racer.radius + goal.radius)
            {
                if !&self.reached_goals[i] {
                    new_goal_reached = true;
                }

                self.reached_goals[i] = true;
            }
        }

        new_goal_reached
    }

    fn resolve_collisions(&mut self) -> bool {
        let mut collided = false;

        for _ in 0..MAX_COLLISION_RESOLUTIONS {
            let mut collided_this_iteration = false;

            if self.push_from_asteroids() {
                collided_this_iteration = true;
                collided = true;
            }

            if self.push_from_bounding_box() {
                collided_this_iteration = true;
                collided = true;
            }

            if !collided_this_iteration {
                break;
            }
        }

        if collided {
            self.racer.vx = (self.racer.vx * COLLISION_FRACTION.0) / COLLISION_FRACTION.1;
            self.racer.vy = (self.racer.vy * COLLISION_FRACTION.0) / COLLISION_FRACTION.1;
        }

        collided
    }

    pub fn finished(&self) -> bool {
        self.reached_goals.iter().all(|v| *v)
    }

    pub fn restart(&mut self) {
        self.racer.x = self.initial_racer.x;
        self.racer.y = self.initial_racer.y;
        self.racer.vx = 0;
        self.racer.vy = 0;

        self.reached_goals.fill(false);
    }

    pub fn tick(&mut self, instruction: Instruction) -> TickResult {
        self.move_racer(instruction);
        let collided = self.resolve_collisions();
        let goal = self.check_goal();

        let mut result: TickResult = 0;

        if collided {
            result |= TickFlag::COLLIDED;
        }

        if goal {
            result |= TickFlag::GOAL_REACHED;
        }

        result
    }

    pub fn simulate(&mut self, instructions: &Vec<Instruction>) -> Vec<TickResult> {
        self.restart();

        let mut results = vec![];

        for instruction in instructions {
            results.push(self.tick(*instruction));
        }

        results
    }

    pub fn load(path: &PathBuf) -> Self {
        let binding = fs::read_to_string(path).unwrap();
        let mut lines = binding.lines();

        let mut parts_fn = || {
            lines
                .next()
                .unwrap()
                .split_whitespace()
                .collect::<Vec<&str>>()
        };

        let racer_parts = parts_fn();

        let racer = Racer {
            x: racer_parts[0].parse::<isize>().unwrap(),
            y: racer_parts[1].parse::<isize>().unwrap(),
            radius: racer_parts[2].parse::<isize>().unwrap(),
            vx: 0,
            vy: 0,
        };

        let bb_parts = parts_fn();

        let bbox = BoundingBox {
            min_x: bb_parts[0].parse::<isize>().unwrap(),
            min_y: bb_parts[1].parse::<isize>().unwrap(),
            max_x: bb_parts[2].parse::<isize>().unwrap(),
            max_y: bb_parts[3].parse::<isize>().unwrap(),
        };

        let asteroid_count = parts_fn()[0].parse::<usize>().unwrap();

        let mut asteroids = vec![];
        for _ in 0..asteroid_count {
            let asteroid_parts = parts_fn();

            asteroids.push(Asteroid {
                x: asteroid_parts[0].parse::<isize>().unwrap(),
                y: asteroid_parts[1].parse::<isize>().unwrap(),
                radius: asteroid_parts[2].parse::<isize>().unwrap(),
            });
        }

        let goal_count = parts_fn()[0].parse::<usize>().unwrap();

        let mut goals = vec![];
        for _ in 0..goal_count {
            let goal_parts = parts_fn();

            goals.push(Asteroid {
                x: goal_parts[0].parse::<isize>().unwrap(),
                y: goal_parts[1].parse::<isize>().unwrap(),
                radius: goal_parts[2].parse::<isize>().unwrap(),
            });
        }

        Self::new(racer, asteroids, goals, bbox)
    }
}