use super::input_query::*;
use super::level_query::*;
use super::player_event::*;
use super::points_counter::*;
use super::time_tracker::*;
use euclid::default::Vector3D;
use std::f64::consts::PI;

const STEER_SLOW_DOWN: f64 = STEER_WIDTH_SCALE * 3.0 / 5.0;
const STEER_MAX_SPEED: f64 = STEER_WIDTH_SCALE * 2.0 / 5.0;
const STEER_WIDTH_SCALE: f64 = 500.0;

const JUMP_INIT: f64 = JUMP_HEIGHT_SCALE * 3.0;
const JUMP_GRAVITY: f64 = JUMP_HEIGHT_SCALE * 6.0;
const JUMP_HEIGHT_SCALE: f64 = 100.0;

const FORWARD_JUMP_BOOST: f64 = FORWARD_DEPTH_SCALE * 2.0;
const FORWARD_STEER_BOOST: f64 = FORWARD_DEPTH_SCALE * 3.0;
const FORWARD_MAX_SPEED: f64 = FORWARD_DEPTH_SCALE * 10.0;
const FORWARD_SLOW_DOWN_LINEAR: f64 = FORWARD_DEPTH_SCALE * 0.2;
const FORWARD_SLOW_DOWN_SQUARE: f64 = 0.5;
const FORWARD_DEPTH_SCALE: f64 = 100.0;

const BACKING_REAL_DELAY: f64 = 0.5;
const BACKING_SIMULATED_DELAY: f64 = 5.0;

#[derive(Debug, Clone, Copy)]
struct PointInTime {
    position_vec: Vector3D<f64>,
    time_sec: f64,
}

#[derive(Debug, Clone, Copy)]
struct BackingData {
    begin: PointInTime,
    end: PointInTime,
}

#[derive(Debug)]
pub struct Player {
    position_vec: Vector3D<f64>,
    speed_vec: Vector3D<f64>,
    time_tracker: TimeTracker,
    is_jumping: bool,
    backing_data: Option<BackingData>,
    points_counter: PointsCounter,
}

impl Player {
    pub fn new() -> Player {
        Player {
            position_vec: Vector3D::default(),
            speed_vec: Vector3D::default(),
            time_tracker: TimeTracker::new(),
            is_jumping: false,
            backing_data: None,
            points_counter: PointsCounter::new(),
        }
    }

    pub fn tick(
        &mut self,
        delta: f64,
        level: &impl LevelQuery,
        input: &mut impl InputQuery,
    ) -> Vec<PlayerEvent> {
        self.time_tracker.add(delta);
        let mut events = self.handle_steer(delta, input);
        events.append(&mut self.handle_jump(delta, input));
        events.append(&mut self.handle_forward(delta));
        events.append(&mut self.handle_fall(level));
        self.points_counter.process(delta, self.dist());
        events
    }

    fn handle_fall(&mut self, level: &impl LevelQuery) -> Vec<PlayerEvent> {
        match self.backing_data {
            Some(backing_data) => self.update_fall(backing_data),
            None => {
                if !self.check_valid_position(level) {
                    self.begin_fall(level)
                } else {
                    Vec::new()
                }
            }
        }
    }

    fn update_fall(&mut self, backing_data: BackingData) -> Vec<PlayerEvent> {
        let now_sec = self.time_tracker.now_sec();
        let end_sec = backing_data.end.time_sec;
        if now_sec >= end_sec {
            return self.end_fall(backing_data);
        }
        let begin_sec = if backing_data.begin.time_sec < now_sec {
            backing_data.begin.time_sec
        } else {
            now_sec
        };
        if end_sec <= begin_sec {
            return self.end_fall(backing_data);
        }
        let alpha = (now_sec - begin_sec) / (end_sec - begin_sec);
        self.position_vec = backing_data
            .begin
            .position_vec
            .lerp(backing_data.end.position_vec, alpha);
        Vec::new()
    }

    fn begin_fall(&mut self, level: &impl LevelQuery) -> Vec<PlayerEvent> {
        let offset = BACKING_SIMULATED_DELAY * self.speed_vec.z / FORWARD_DEPTH_SCALE;
        let backing_col = self.level_col(level.width());
        let backing_row = self.level_row() - (offset as isize);
        if let Some(start_spot) = level.find_start_spot(backing_col, backing_row) {
            let now_sec = self.time_tracker.now_sec();
            let begin = PointInTime {
                position_vec: self.position_vec,
                time_sec: now_sec,
            };
            let end = PointInTime {
                position_vec: Self::from_level_coord(start_spot.0, start_spot.1, level.width()),
                time_sec: now_sec + BACKING_REAL_DELAY,
            };
            self.backing_data = Some(BackingData { begin, end });
        };

        vec![PlayerEvent::Fall]
    }

    fn end_fall(&mut self, backing_data: BackingData) -> Vec<PlayerEvent> {
        self.position_vec = backing_data.end.position_vec;
        self.speed_vec = Vector3D::new(0.0, 0.0, 0.0);
        self.backing_data = None;
        vec![PlayerEvent::Start]
    }

    fn export_vector3(v: Vector3D<f64>) -> Vector3D<f32> {
        Vector3D::<f32>::new(
            (-v.x * 2.0 * PI / STEER_WIDTH_SCALE) as f32,
            (v.y / JUMP_HEIGHT_SCALE) as f32,
            (((v.z / FORWARD_DEPTH_SCALE) + 0.5).fract() - 0.5) as f32,
        )
    }

    pub fn position(&self) -> Vector3D<f32> {
        Self::export_vector3(self.position_vec)
    }

    pub fn speed(&self) -> Vector3D<f32> {
        Self::export_vector3(self.speed_vec)
    }

    pub fn level_col(&self, width: usize) -> isize {
        ((self.position_vec.x * width as f64 / STEER_WIDTH_SCALE).round() as isize)
            % (width as isize)
    }

    pub fn level_row(&self) -> isize {
        (self.dist()).round() as isize
    }

    pub fn dist(&self) -> f64 {
        self.position_vec.z / FORWARD_DEPTH_SCALE
    }

    pub fn from_level_coord(col: isize, row: isize, width: usize) -> Vector3D<f64> {
        let mut real_col = col;
        while real_col < 0 {
            real_col += width as isize;
        }
        real_col = real_col % (width as isize);
        let real_row = std::cmp::max(row, 0);
        Vector3D::new(
            real_col as f64 * STEER_WIDTH_SCALE / width as f64,
            0.0,
            real_row as f64 * FORWARD_DEPTH_SCALE,
        )
    }

    fn check_valid_position(&self, level: &impl LevelQuery) -> bool {
        let item = level.item(self.level_col(level.width()), self.level_row());
        !(item < 0 && self.position_vec.y <= 0.0)
    }

    fn handle_steer(&mut self, delta: f64, input: &mut impl InputQuery) -> Vec<PlayerEvent> {
        if let Some(_) = self.backing_data {
            return Vec::new();
        }

        let steer = input.pop_steer();
        self.speed_vec.x += steer;

        if self.speed_vec.x > STEER_MAX_SPEED {
            self.speed_vec.x = STEER_MAX_SPEED;
        }
        if self.speed_vec.x < -STEER_MAX_SPEED {
            self.speed_vec.x = -STEER_MAX_SPEED;
        }

        if self.speed_vec.x > 0.0 {
            self.speed_vec.x -= delta * STEER_SLOW_DOWN;
            if self.speed_vec.x < 0.0 {
                self.speed_vec.x = 0.0;
            }
            self.speed_vec.z += delta * (self.speed_vec.x / STEER_MAX_SPEED) * FORWARD_STEER_BOOST;
        }
        if self.speed_vec.x < 0.0 {
            self.speed_vec.x += delta * STEER_SLOW_DOWN;
            if self.speed_vec.x > 0.0 {
                self.speed_vec.x = 0.0;
            }
            self.speed_vec.z -= delta * (self.speed_vec.x / STEER_MAX_SPEED) * FORWARD_STEER_BOOST;
        }

        self.position_vec.x += self.speed_vec.x * (delta);

        while self.position_vec.x >= STEER_WIDTH_SCALE {
            self.position_vec.x -= STEER_WIDTH_SCALE;
        }
        while self.position_vec.x < 0.0 {
            self.position_vec.x += STEER_WIDTH_SCALE;
        }
        Vec::new()
    }

    fn handle_jump(&mut self, delta: f64, input: &mut impl InputQuery) -> Vec<PlayerEvent> {
        if let Some(_) = self.backing_data {
            return Vec::new();
        }

        let jump = input.pop_jump();
        if self.is_jumping {
            self.speed_vec.y -= delta * JUMP_GRAVITY;
            self.position_vec.y += delta * self.speed_vec.y;

            if self.position_vec.y < 0.0 {
                self.speed_vec.y = 0.0;
                self.position_vec.y = 0.0;
                self.is_jumping = false;
                return vec![PlayerEvent::Land];
            }

            if self.speed_vec.y > 0.0 {
                self.speed_vec.z += delta * FORWARD_JUMP_BOOST;
            }
        } else if jump {
            self.is_jumping = true;
            self.speed_vec.y = JUMP_INIT;
            self.position_vec.y = 0.0;
            return vec![PlayerEvent::Jump];
        }
        Vec::new()
    }

    fn handle_forward(&mut self, delta: f64) -> Vec<PlayerEvent> {
        if let Some(_) = self.backing_data {
            return Vec::new();
        }

        self.speed_vec.z -= delta * FORWARD_SLOW_DOWN_LINEAR;
        self.speed_vec.z -= delta * FORWARD_SLOW_DOWN_SQUARE * self.speed_vec.z;
        if self.speed_vec.z > FORWARD_MAX_SPEED {
            self.speed_vec.z = FORWARD_MAX_SPEED;
        }
        if self.speed_vec.z < 0.0 {
            self.speed_vec.z = 0.0;
        }
        self.position_vec.z += delta * self.speed_vec.z;
        if self.position_vec.z < 0.0 {
            self.position_vec.z = 0.0;
        }
        Vec::new()
    }

    pub fn score(&self) -> i32 {
        self.points_counter.score()
    }

    pub fn best_row(&self) -> isize {
        self.points_counter.best_row()
    }
}
