use super::scene_helper::*;
use babalcore::*;
use gdnative::api::GridMap;
use gdnative::api::Performance;
use gdnative::prelude::*;

#[derive(NativeClass)]
#[inherit(Spatial)]
pub struct Tunnel {
    data: Option<Data>,
    template: Option<Ref<PackedScene, ThreadLocal>>,
    grid_scale: Vector3,
    level_row: isize,
    level_col: isize,
    player_position: Vector3,
    player_best_row: isize,
    fall_state: bool,
    boost_state: bool,
    now_sec: f64,
    far_view: isize,
    time_since_last_adjust_far_view: f64,
}

struct Data {
    level: Level,
    rail_resource_name: String,
}

const LENGTH_MARGIN: usize = 100;
const LEVEL_LENGTH: usize = 10000;
const SLAB_SIZE: f32 = 1.0;
const RAIL_OFFSET: isize = 2;
const RAIL_FAR_VIEW_INIT: isize = 25;
const RAIL_FAR_VIEW_MAX: isize = 45;
const RAIL_FAR_VIEW_MIN: isize = 15;
const ADJUST_FAR_VIEW_DELAY: f64 = 0.5;
const FPS_TARGET: f64 = 55.0;
const SLAB_RATIO: f64 = 3.0;

#[methods]
impl Tunnel {
    fn new(_owner: &Spatial) -> Self {
        Tunnel {
            data: None,
            template: None,
            grid_scale: Vector3::new(1.0, 1.0, 1.0),
            level_row: 0,
            level_col: 0,
            player_position: Vector3::default(),
            player_best_row: 0,
            fall_state: false,
            boost_state: false,
            now_sec: 0.0,
            far_view: RAIL_FAR_VIEW_INIT,
            time_since_last_adjust_far_view: 0.0,
        }
    }

    #[export]
    fn _ready(&self, _owner: &Spatial) {
        godot_print!("Tunnel is ready.");
    }

    #[export]
    fn _process(&mut self, owner: &Spatial, delta: f64) {
        self.now_sec += delta;
        self.adjust_far_view(owner, delta);
        self.sync(owner);
    }

    #[export]
    fn setup(
        &mut self,
        owner: &Spatial,
        rail_resource_name: GodotString,
        skill: GodotString,
        seed: u64,
    ) {
        let skill = Skill::from_str(skill.to_string().as_str());
        godot_print!("Tunnel setup {}x{}.", skill.width(), LEVEL_LENGTH);
        let mut level = Level::new(seed, skill);
        Self::generate_missing(&mut level, 0);
        let data = Data {
            level,
            rail_resource_name: rail_resource_name.to_string(),
        };
        self.template = load_scene(data.rail_resource_name.as_str());
        match &self.template {
            Some(_) => godot_print!(
                "Loaded template rail scene \"{}\".",
                data.rail_resource_name
            ),
            None => godot_print!(
                "Unable to load rail scene \"{}\", please check name.",
                data.rail_resource_name
            ),
        };
        self.data = Some(data);

        self.remove_all_children(owner);
        self.add_all_rails(owner);
        match self.get_grid_scale(owner) {
            Ok(mut scale) => {
                scale.z = Self::calc_scale_z(skill.width());
                match self.set_grid_scale(owner, scale) {
                    Ok(_) => {}
                    Err(e) => godot_print!("unable to set grid scale: {}", e),
                }
                self.grid_scale = scale;
            }
            Err(e) => godot_print!("unable to get grid scale: {}", e),
        }
        self.sync(owner);
    }

    fn calc_scale_z(width: usize) -> f32 {
        (2.0 * std::f64::consts::PI * SLAB_RATIO as f64 / width as f64) as f32
    }

    fn generate_missing(level: &mut Level, row: isize) {
        while level.last() < row + (LENGTH_MARGIN as isize) {
            level.generate(LENGTH_MARGIN);
        }
    }

    #[export]
    fn remove_last_child(&mut self, owner: &Spatial) -> bool {
        let num_children = owner.get_child_count();
        if num_children <= 0 {
            return false;
        }

        let last_child = owner.get_child(num_children - 1);
        if let Some(node) = last_child {
            owner.remove_child(node);
            unsafe {
                node.assume_safe().queue_free();
            }
        }

        return num_children == owner.get_child_count() + 1;
    }

    #[export]
    fn remove_all_children(&mut self, owner: &Spatial) -> usize {
        let mut removed: usize = 0;
        while self.remove_last_child(owner) {
            removed += 1;
        }
        removed
    }

    fn prepare_rail_grid(&self, grid: &Spatial, n: usize) {
        let w: f32 = SLAB_SIZE * std::f32::consts::PI * 2f32 / n as f32;
        let mut translation = grid.translation();
        translation.x = -w / 2f32;
        grid.set_translation(translation);
        let mut scale = grid.scale();
        scale.x = w;
        grid.set_scale(scale);
    }

    fn prepare_rail(&self, rail: &Spatial, i: usize, n: usize) {
        let key_str = format!("rail_{}", i);
        rail.set_name(&key_str);
        let angle: f64 = i as f64 * 360f64 / n as f64;
        rail.rotate_z(angle * std::f64::consts::PI / 180f64);

        let num_children = rail.get_child_count();
        if num_children != 1 {
            godot_print!("Unexpected number of children for rail: {}.", num_children);
            return;
        }

        let child = rail.get_child(0);
        if let Some(grid) = child {
            let grid = unsafe { grid.assume_safe() };
            if let Some(grid) = grid.cast::<Spatial>() {
                self.prepare_rail_grid(&grid, n);
            }
        }
    }

    #[export]
    fn add_one_rail(&mut self, owner: &Spatial, i: usize, n: usize) -> bool {
        match &self.template {
            Some(template) => match instanciate_scene::<Spatial>(template) {
                Ok(spatial) => {
                    self.prepare_rail(&spatial, i, n);
                    owner.add_child(spatial.into_shared(), false);
                    true
                }
                Err(err) => {
                    godot_print!("Could not instanciate rail scene: \"{:?}\".", err);
                    false
                }
            },
            None => false,
        }
    }

    #[export]
    fn add_all_rails(&mut self, owner: &Spatial) -> usize {
        match &self.data {
            Some(data) => {
                let mut added: usize = 0;
                let width = data.level.width();
                if width == 0 {
                    return 0;
                }
                for i in 0..width {
                    if !self.add_one_rail(owner, i, width) {
                        break;
                    }
                    added += 1;
                }
                added
            }
            None => 0,
        }
    }

    fn get_grid_scale(&self, owner: &Spatial) -> Result<Vector3, String> {
        let num_children = owner.get_child_count() as usize;
        if num_children == 0 {
            return Err(format!("Please setup tunnel"));
        }
        match owner.get_child(0) {
            Some(rail) => {
                let rail = unsafe { rail.assume_safe() };
                match rail.cast::<Spatial>() {
                    Some(rail) => match rail.get_child(0) {
                        Some(grid) => {
                            let grid = unsafe { grid.assume_safe() };
                            match grid.cast::<GridMap>() {
                                Some(grid) => return Ok(grid.scale()),
                                None => Err(format!("Unable to cast child of rail to GridMap.")),
                            }
                        }
                        None => Err(format!("rail has no child")),
                    },
                    None => Err(format!("rail is not spatial")),
                }
            }
            None => Err(format!("tunnel has no child")),
        }
    }

    fn set_grid_scale(&self, owner: &Spatial, scale: Vector3) -> Result<(), String> {
        let num_children = owner.get_child_count() as usize;
        if num_children == 0 {
            return Err(format!("Please setup tunnel"));
        }
        for i in 0..(num_children as isize) {
            let child = owner.get_child(i as i64);
            if let Some(rail) = child {
                let rail = unsafe { rail.assume_safe() };
                match rail.cast::<Spatial>() {
                    Some(rail) => match rail.get_child(0) {
                        Some(grid) => {
                            let grid = unsafe { grid.assume_safe() };
                            match grid.cast::<GridMap>() {
                                Some(grid) => {
                                    let mut translation = grid.translation();
                                    translation.z = -scale.z / 2.0;
                                    grid.set_translation(translation);
                                    grid.set_scale(scale);
                                }
                                None => {
                                    return Err(format!("Unable to cast child of rail to GridMap."))
                                }
                            }
                        }
                        None => return Err(format!("rail has no child")),
                    },
                    None => return Err(format!("rail is not spatial")),
                }
            }
        }
        return Ok(());
    }

    fn sync_rail_grid(&mut self, rail: &GridMap, col: isize) -> bool {
        //godot_print!("sync {:?} {}", rail, i);
        rail.clear();
        match &mut self.data {
            Some(data) => {
                data.level.set_now_sec(self.now_sec);
                let start = -RAIL_OFFSET;
                let end = std::cmp::min(
                    start + data.level.len() as isize,
                    RAIL_OFFSET + self.far_view,
                );
                for row in start..end {
                    let mut kind =
                        data.level
                            .slab_kind(col, row + self.level_row, self.boost_state);
                    if self.level_col == col
                        && self.player_position.y == 0.0
                        && row == 0
                        && kind != SlabKind::Void
                    {
                        kind = SlabKind::HighLight;
                    }
                    if self.fall_state
                        && row + self.level_row == self.player_best_row
                        && self.player_best_row > 0
                        && kind != SlabKind::Void
                    {
                        kind = SlabKind::HighLight;
                    }

                    let item = kind.as_item();
                    if item >= 0 {
                        let orientation = 0;
                        rail.set_cell_item(0, 0, (RAIL_OFFSET - 2 - row) as i64, item, orientation);
                    }
                }
                true
            }
            None => false,
        }
    }

    fn adjust_far_view(&mut self, _owner: &Spatial, delta: f64) {
        self.time_since_last_adjust_far_view += delta;
        if self.time_since_last_adjust_far_view < ADJUST_FAR_VIEW_DELAY {
            return;
        }
        self.time_since_last_adjust_far_view = 0.0;
        let perf = Performance::godot_singleton();
        let fps = perf.get_monitor(Performance::TIME_FPS);
        if fps < FPS_TARGET && self.far_view > RAIL_FAR_VIEW_MIN {
            self.far_view -= 1;
            godot_print!("Decreasing far view to {}", self.far_view);
        }
        if fps > FPS_TARGET && self.far_view < RAIL_FAR_VIEW_MAX {
            self.far_view += 1;
            godot_print!("Increasing far view to {}", self.far_view);
        }
    }

    #[export]
    fn sync(&mut self, owner: &Spatial) -> bool {
        match &mut self.data {
            Some(data) => {
                Self::generate_missing(&mut data.level, self.level_row);
                let num_children = owner.get_child_count() as usize;
                let width = data.level.width();
                if num_children == 0 {
                    godot_print!("Please setup tunnel");
                    return false;
                }
                if num_children != (data.level.width()) {
                    godot_print!("Wrong number of rails: {} != {}.", num_children, width);
                    return false;
                }

                for i in 0..(num_children as isize) {
                    let child = owner.get_child(i as i64);
                    if let Some(rail) = child {
                        let rail = unsafe { rail.assume_safe() };
                        match rail.cast::<Spatial>() {
                            Some(rail) => {
                                let child = rail.get_child(0);
                                if let Some(grid) = child {
                                    let grid = unsafe { grid.assume_safe() };
                                    match grid.cast::<GridMap>() {
                                        Some(grid) => {
                                            self.sync_rail_grid(&grid, i);
                                        }
                                        None => {
                                            godot_print!(
                                                "Unable to cast child of rail to GridMap.",
                                            );
                                        }
                                    }
                                }
                            }
                            None => {
                                godot_print!("Unable to cast child of rail to Spatial.",);
                            }
                        }
                    }
                }
                true
            }
            None => false,
        }
    }

    #[export]
    fn update_player_position(&mut self, owner: &Spatial, pos: Vector3) {
        let mut rotation = owner.rotation();
        rotation.z = pos.x;
        owner.set_rotation(rotation);

        let mut translation = owner.translation();
        translation.z = pos.z * self.grid_scale.z;
        owner.set_translation(translation);

        self.player_position = pos;
    }

    #[export]
    fn set_fall_state(&mut self, _owner: &Spatial, fall_state: bool) {
        self.fall_state = fall_state;
    }

    #[export]
    fn fall_state(&mut self, _owner: &Spatial) -> bool {
        self.fall_state
    }

    #[export]
    fn set_boost_state(&mut self, _owner: &Spatial, boost_state: bool) {
        self.boost_state = boost_state;
    }

    #[export]
    fn boost_state(&mut self, _owner: &Spatial) -> bool {
        self.boost_state
    }

    #[export]
    fn set_level_row(&mut self, _owner: &Spatial, level_row: isize) {
        self.level_row = level_row;
        if self.player_best_row < level_row {
            self.player_best_row = level_row;
        }
    }

    #[export]
    fn set_level_col(&mut self, _owner: &Spatial, level_col: isize) {
        self.level_col = level_col;
    }

    #[export]
    pub fn width(&self, _owner: &Spatial) -> usize {
        match &self.data {
            Some(data) => data.level.width(),
            None => 0,
        }
    }

    #[export]
    pub fn len(&self, _owner: &Spatial) -> usize {
        match &self.data {
            Some(data) => data.level.len(),
            None => 0,
        }
    }

    #[export]
    pub fn first(&self, _owner: &Spatial) -> isize {
        match &self.data {
            Some(data) => data.level.first(),
            None => 0,
        }
    }

    #[export]
    pub fn last(&self, _owner: &Spatial) -> isize {
        match &self.data {
            Some(data) => data.level.last(),
            None => 0,
        }
    }

    #[export]
    pub fn item(&self, _owner: &Spatial, col: isize, row: isize, boost: bool) -> i64 {
        match &self.data {
            Some(data) => data.level.item(col, row, boost),
            None => 0,
        }
    }

    #[export]
    pub fn find_start_spot(
        &self,
        _owner: &Spatial,
        col: isize,
        row: isize,
    ) -> Option<(isize, isize)> {
        match &self.data {
            Some(data) => data.level.find_start_spot(col, row),
            None => None,
        }
    }

    #[export]
    pub fn skill(&self, _owner: &Spatial) -> GodotString {
        let skill = match &self.data {
            Some(data) => data.level.skill(),
            None => Skill::default(),
        };
        GodotString::from_str(skill.as_str())
    }
}
