use rand::Rng;

use crate::{model::*, view, input::{self, param_or_prompt_value, prompt_two_values}};

pub fn perform_short_range_scan(galaxy: &Galaxy) {
    if galaxy.enterprise.damaged.contains_key(systems::SHORT_RANGE_SCAN) {
        view::scanners_out();
        return;
    }

    view::short_range_scan(&galaxy)
}

pub fn get_amount_and_set_shields(galaxy: &mut Galaxy, provided: Vec<String>) {

    if galaxy.enterprise.damaged.contains_key(systems::SHIELD_CONTROL) {
        view::inoperable(&systems::name_for(systems::SHIELD_CONTROL));
        return;
    }

    view::energy_available(galaxy.enterprise.total_energy);
    let value = input::param_or_prompt_value(&provided, 0, view::prompts::SHIELDS, 0, i32::MAX);
    if value.is_none() {
        view::shields_unchanged();
        return;
    }

    let value = value.unwrap() as u16;
    if value > galaxy.enterprise.total_energy {
        view::ridiculous();
        view::shields_unchanged();
        return;
    }

    galaxy.enterprise.shields = value;
    view::shields_set(value);
}

pub fn gather_dir_and_speed_then_move(galaxy: &mut Galaxy, provided: Vec<String>) {

    let course = input::param_or_prompt_value(&provided, 0, view::prompts::COURSE, 1.0, 9.0);
    if course.is_none() {
        view::bad_course_data();
        return;
    }

    let course = course.unwrap();

    let mut max_warp = 8.0;
    if galaxy.enterprise.damaged.contains_key(systems::WARP_ENGINES) {
        max_warp = 0.2;
    }

    let speed = input::param_or_prompt_value(&provided, 1, &view::prompts::warp_factor(max_warp), 0.0, 8.0);
    if speed.is_none() {
        view::bad_course_data();
        return;
    }
    
    let speed = speed.unwrap();

    if speed > max_warp {
        view::damaged_engines(max_warp, speed);
        return;
    }

    klingons_move(galaxy);
    klingons_fire(galaxy);

    if galaxy.enterprise.destroyed {
        return;
    }

    repair_systems(&mut galaxy.enterprise, speed);
    repair_or_damage_random_system(&mut galaxy.enterprise);

    move_enterprise(course, speed, galaxy);
}

fn repair_systems(enterprise: &mut Enterprise, amount: f32) {

    let keys: Vec<String> = enterprise.damaged.keys().map(|k| k.to_string()).collect();
    let mut repaired = Vec::new();
    for key in keys {
        let fully_fixed = enterprise.repair_system(&key, amount);
        if fully_fixed {
            repaired.push(systems::name_for(&key));
        }
    }

    if repaired.len() <= 0 {
        return;
    }

    view::damage_control_report();
    for name in repaired {
        view::system_repair_completed(name);
    }
}

fn repair_or_damage_random_system(enterprise: &mut Enterprise) {
    let mut rng = rand::thread_rng();

    if rng.gen::<f32>() > 0.2 {
        return;
    }

    if rng.gen::<f32>() >= 0.6 {
        if enterprise.damaged.len() == 0 {
            return;
        }

        let damaged: Vec<String> = enterprise.damaged.keys().map(|k| k.to_string()).collect();
        let system = damaged[rng.gen_range(0..damaged.len())].to_string();
        let system_name = &systems::name_for(&system);

        enterprise.repair_system(&system, rng.gen::<f32>() * 3.0 + 1.0);
        view::random_repair_report_for(system_name, false);
        return;
    }
    
    let system = systems::KEYS[rng.gen_range(0..systems::KEYS.len())].to_string();
    let system_name = &systems::name_for(&system);

    enterprise.damage_system(&system, rng.gen::<f32>() * 5.0 + 1.0);
    view::random_repair_report_for(system_name, true);
}

fn move_enterprise(course: f32, warp_speed: f32, galaxy: &mut Galaxy) {

    let ship = &mut galaxy.enterprise;

    let (mut path, mut hit_edge) = find_nav_path(ship.quadrant, ship.sector, course, warp_speed);
    for i in 0..path.len() {
        let (quadrant, sector) = path[i].to_local_quadrant_sector();
        if quadrant != ship.quadrant {
            break; // have left current quadrant, so collision checks removed. if there is a collision at the dest... /shrug?
        }
        let quadrant = &galaxy.quadrants[quadrant.as_index()];
        if quadrant.sector_status(sector) != SectorStatus::Empty {
            path = path[..i].into();
            hit_edge = false;
            if i > 0 {
                let (_, last_sector) = path[path.len() - 1].to_local_quadrant_sector();
                view::bad_nav(last_sector);
            } else {
                view::bad_nav(ship.sector);
                return;
            }
            break;
        }
    }

    if path.len() == 0 {
        if hit_edge {
            view::hit_edge(ship.quadrant, ship.sector);
        }
        return;   
    }

    let energy_cost = path.len() as u16 + 10;

    if energy_cost > ship.total_energy {
        view::insuffient_warp_energy(warp_speed);
        return
    }

    let (end_quadrant, end_sector) = path[path.len() - 1].to_local_quadrant_sector();
    if hit_edge {
        view::hit_edge(end_quadrant, end_sector);
    }
    
    if ship.quadrant != end_quadrant {
        view::enter_quadrant(end_quadrant);
        galaxy.scanned.insert(end_quadrant);
        
        if galaxy.quadrants[end_quadrant.as_index()].klingons.len() > 0 {
            view::condition_red();
            if ship.shields <= 200 {
                view::danger_shields();
            }
        }
    }

    ship.quadrant = end_quadrant;
    ship.sector = end_sector;

    let quadrant = &galaxy.quadrants[end_quadrant.as_index()];
    if quadrant.docked_at_starbase(ship.sector) {
        ship.shields = 0;
        ship.photon_torpedoes = MAX_PHOTON_TORPEDOES;
        ship.total_energy = MAX_ENERGY;
    } else {
        ship.total_energy = ship.total_energy - energy_cost;
        if ship.shields > ship.total_energy {
            view::divert_energy_from_shields();
            ship.shields = ship.total_energy;
        }
    }

    view::short_range_scan(&galaxy)
}

fn find_nav_path(start_quadrant: Pos, start_sector: Pos, course: f32, warp_speed: f32) -> (Vec<Pos>, bool) {

    let (dx, dy) = calculate_delta(course);

    let mut distance = (warp_speed * 8.0) as i8;
    if distance == 0 {
        distance = 1;
    }

    let mut last_sector = start_sector.as_galactic_sector(start_quadrant);
    let mut path = Vec::new();
    let mut hit_edge;

    loop {
        let nx = (last_sector.0 as f32 + dx) as i8;
        let ny = (last_sector.1 as f32 + dy) as i8;
        hit_edge = nx < 0 || ny < 0 || nx >= 64 || ny >= 64;
        if hit_edge {
            break;
        }
        last_sector = Pos(nx as u8, ny as u8);
        path.push(last_sector);

        distance -= 1;
        if distance == 0 {
            break;
        }
    }
  
    (path, hit_edge)
}

fn calculate_delta(course: f32) -> (f32, f32) {
    // this course delta stuff is a translation (of a translation, of a translation...) of the original basic calcs
    let dir = (course - 1.0) % 8.0;
    let (dx1, dy1) = COURSES[dir as usize];
    let (dx2, dy2) = COURSES[(dir + 1.0) as usize];
    let frac = dir - (dir as i32) as f32;

    let dx = dx1 + (dx2 - dx1) * frac;
    let dy = dy1 + (dy2 - dy1) * frac;

    (dx, dy)
}

fn klingons_move(galaxy: &mut Galaxy) {
    let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()];
    for k in 0..quadrant.klingons.len() {
        let new_sector: Pos;
        loop {
            let candidate = quadrant.find_empty_sector();
            if candidate != galaxy.enterprise.sector {
                new_sector = candidate;
                break;
            }
        }
        quadrant.klingons[k].sector = new_sector;
    }
}

fn klingons_fire(galaxy: &mut Galaxy) {
    let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()];
    if quadrant.docked_at_starbase(galaxy.enterprise.sector) {
        view::starbase_shields();
        return;
    }

    for k in 0..quadrant.klingons.len() {
        quadrant.klingons[k].fire_on(&mut galaxy.enterprise);
    }
}

pub fn run_damage_control(galaxy: &Galaxy) {
    if galaxy.enterprise.damaged.contains_key(systems::DAMAGE_CONTROL) {
        view::inoperable(&systems::name_for(systems::DAMAGE_CONTROL));
        return;
    }
    
    view::damage_control(&galaxy.enterprise);
}

pub fn try_starbase_ship_repair(galaxy: &mut Galaxy) {
    let ship = &mut galaxy.enterprise;
    let quadrant = &galaxy.quadrants[ship.quadrant.as_index()];
    if ship.damaged.len() == 0 || !quadrant.docked_at_starbase(ship.sector) {
        return;
    }

    let repair_delay = quadrant.star_base.as_ref().unwrap().repair_delay;
    let repair_time = (ship.damaged.len() as f32 * 0.1 + repair_delay).max(0.9);

    view::repair_estimate(repair_time);
    if !input::prompt_yes_no(view::prompts::REPAIR) {
        return;
    }

    ship.damaged.clear();
    galaxy.stardate += repair_time;
    view::damage_control(&ship);
}

pub fn perform_long_range_scan(galaxy: &mut Galaxy) {
    if galaxy.enterprise.damaged.contains_key(systems::LONG_RANGE_SCAN) {
        view::inoperable(&systems::name_for(systems::LONG_RANGE_SCAN));
        return;
    }

    let seen = view::long_range_scan(galaxy);
    for pos in seen {
        galaxy.scanned.insert(pos);
    }
}

pub fn access_computer(galaxy: &Galaxy, provided: Vec<String>) {
    if galaxy.enterprise.damaged.contains_key(systems::COMPUTER) {
        view::inoperable(&systems::name_for(systems::COMPUTER));
        return;
    }

    let operation : i32;
    loop {
        let entered = input::param_or_prompt_value(&provided, 0, view::prompts::COMPUTER, 0, 5);
        if entered.is_none() {
            view::computer_options();
        } else {
            operation = entered.unwrap();
            break;
        }
    } 
    
    match operation {
        0 => view::galaxy_scanned_map(galaxy),
        1 => {
            view::status_report(galaxy);
            run_damage_control(galaxy);
        },
        2 => show_klingon_direction_data(galaxy),
        3 => show_starbase_direction_data(galaxy),
        4 => direction_dist_calculator(galaxy),
        5 => view::galaxy_region_map(),
        _ => () // unreachable
    }
}

fn show_klingon_direction_data(galaxy: &Galaxy) {
    let quadrant = &galaxy.quadrants[galaxy.enterprise.quadrant.as_index()];
    if quadrant.klingons.len() == 0 {
        view::no_local_enemies();
        return;
    }

    view::klingon_report(quadrant.klingons.len() > 1);
    let origin = galaxy.enterprise.sector;
    for k in &quadrant.klingons {
        let target = k.sector;
        view::direction_distance(origin.direction(target), origin.dist(target))
    }
}

fn show_starbase_direction_data(galaxy: &Galaxy) {
    let quadrant = &galaxy.quadrants[galaxy.enterprise.quadrant.as_index()];
    match &quadrant.star_base {
        None => {
            view::no_local_starbase();
            return;
        },
        Some(s) => {
            view::starbase_report();
            let origin = galaxy.enterprise.sector;
            let target = s.sector;
            view::direction_distance(origin.direction(target), origin.dist(target))
        }
    }
}

fn direction_dist_calculator(galaxy: &Galaxy) {
    view::direction_dist_intro(&galaxy.enterprise);
    loop {
        let coords1 = prompt_two_values(view::prompts::INITIAL_COORDS, 1, 8).map(|(x, y)| Pos(x, y));
        if coords1.is_none() {
            continue;
        }
        let coords2 = prompt_two_values(view::prompts::TARGET_COORDS, 1, 8).map(|(x, y)| Pos(x, y));
        if coords2.is_none() {
            continue;
        }
        let dir = coords1.unwrap().direction(coords2.unwrap());
        let dist = coords1.unwrap().dist(coords2.unwrap());
        view::direction_distance(dir, dist);
        break;
    }
}

pub fn get_power_and_fire_phasers(galaxy: &mut Galaxy, provided: Vec<String>) {
    if galaxy.enterprise.damaged.contains_key(systems::PHASERS) {
        view::inoperable(&systems::name_for(systems::PHASERS));
        return;
    }

    let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()];
    if quadrant.klingons.len() == 0 {
        view::no_local_enemies();
        return;
    }

    let computer_damaged = galaxy.enterprise.damaged.contains_key(systems::COMPUTER);
    if computer_damaged {
        view::computer_accuracy_issue();
    }

    let available_energy = galaxy.enterprise.total_energy - galaxy.enterprise.shields;
    view::phasers_locked(available_energy);
    let mut power: f32;
    loop {
        let setting = param_or_prompt_value(&provided, 0, view::prompts::PHASERS, 0, available_energy);
        if setting.is_some() {
            power = setting.unwrap() as f32;
            break;
        }
    }

    if power == 0.0 {
        return;
    }

    galaxy.enterprise.total_energy -= power as u16;
    
    let mut rng = rand::thread_rng();
    if computer_damaged {
        power *= rng.gen::<f32>();
    }

    let per_enemy = power / quadrant.klingons.len() as f32;

    for k in &mut quadrant.klingons {
        let dist = k.sector.abs_diff(galaxy.enterprise.sector) as f32;
        let hit_strength = per_enemy / dist * (2.0 + rng.gen::<f32>());
        if hit_strength < 0.15 * k.energy {
            view::no_damage(k.sector);
        } else {
            k.energy -= hit_strength;
            view::hit_on_klingon(hit_strength, k.sector);
            if k.energy > 0.0 {
                view::klingon_remaining_energy(k.energy);
            } else {
                view::klingon_destroyed();
            }
        }
    }

    quadrant.klingons.retain(|k| k.energy > 0.0);

    klingons_fire(galaxy);
}

pub fn gather_dir_and_launch_torpedo(galaxy: &mut Galaxy, provided: Vec<String>) {
    let star_bases = galaxy.remaining_starbases();
    let ship = &mut galaxy.enterprise;

    if ship.damaged.contains_key(systems::TORPEDOES) {
        view::inoperable(&systems::name_for(systems::TORPEDOES));
        return;
    }

    if ship.photon_torpedoes == 0 {
        view::no_torpedoes_remaining();
        return;
    }

    let course = input::param_or_prompt_value(&provided, 0, view::prompts::TORPEDO_COURSE, 1.0, 9.0);
    if course.is_none() {
        view::bad_torpedo_course();
        return;
    }

    ship.photon_torpedoes -= 1;
    view::torpedo_track();

    let path = find_torpedo_path(ship.sector, course.unwrap());
    let quadrant = &mut galaxy.quadrants[ship.quadrant.as_index()];
    let mut hit = false;
    for p in path {
        view::torpedo_path(p);
        match quadrant.sector_status(p) {
            SectorStatus::Empty => continue,
            SectorStatus::Star => {
                hit = true;
                view::star_absorbed_torpedo(p);
                break;
            },
            SectorStatus::Klingon => {
                hit = true;
                quadrant.get_klingon(p).unwrap().energy = 0.0;
                quadrant.klingons.retain(|k| k.energy > 0.0);
                view::klingon_destroyed();
                break;
            },
            SectorStatus::StarBase => {
                hit = true;
                quadrant.star_base = None;
                let remaining = star_bases - 1;
                view::destroyed_starbase(remaining > 0);
                if remaining == 0 {
                    ship.destroyed = true;
                }
                break;
            }
        }
    }

    if ship.destroyed { // if you wiped out the last starbase, trigger game over
        return;
    }

    if !hit {
        view::torpedo_missed();
    }

    klingons_fire(galaxy);
}

fn find_torpedo_path(start_sector: Pos, course: f32) -> Vec<Pos> {

    let (dx, dy) = calculate_delta(course);

    let mut last_sector = start_sector;
    let mut path = Vec::new();

    loop {
        let nx = (last_sector.0 as f32 + dx) as i8;
        let ny = (last_sector.1 as f32 + dy) as i8;
        if nx < 0 || ny < 0 || nx >= 8 || ny >= 8 {
            break;
        }
        last_sector = Pos(nx as u8, ny as u8);
        path.push(last_sector);
    }
  
    path
}