mod links;
pub(crate) use links::{render_lines_with_links, LinkAlign};

use super::ui_frame::UiFrame;
use super::{
    button::Button,
    clickable_list::{ClickableList, ClickableListItem},
    constants::*,
    gif_map::GifMap,
    hover_text_line::HoverTextLine,
    hover_text_span::HoverTextSpan,
    traits::UiStyled,
    ui_callback::UiCallback,
    utils::format_satoshi,
};
use crate::core::skill::{Skill, MAX_SKILL, MIN_SKILL};
use crate::core::types::TeamBonus;
use crate::core::{
    ChargeUnit, Honour, Planet, PlanetUpgradeTarget, ScoutReport, Shield, Shooter,
    SpaceshipComponent, Upgrade, UpgradeableElement,
};
use crate::ui::utils::format_au;
use crate::ui::{ui_key, PopupMessage};
use crate::{
    core::{
        constants::*,
        player::Player,
        position::{GamePosition, GamePositionUtils, NUM_GAME_POSITIONS},
        resources::Resource,
        skill::{GameSkill, Rated, SKILL_NAMES},
        spaceship_upgrades::SpaceshipUpgradeTarget,
        team::Team,
        types::TeamLocation,
        world::World,
    },
    image::{player::PLAYER_IMAGE_WIDTH, spaceship::SPACESHIP_IMAGE_WIDTH},
    types::*,
};
use anyhow::anyhow;
use itertools::Itertools;
use ratatui::crossterm::event::KeyCode;
use ratatui::{
    prelude::*,
    text::Span,
    widgets::{Block, BorderType, Borders, Clear, Paragraph},
};
use std::collections::{HashMap, HashSet};
use std::iter::zip;
use std::sync::LazyLock;
use strum::Display;

const SPACES: &str = "                    ";

pub static UP_ARROW_SPAN: LazyLock<Span<'static>> =
    LazyLock::new(|| Span::styled("↑", MAX_SKILL.style()));
pub static UP_RIGHT_ARROW_SPAN: LazyLock<Span<'static>> =
    LazyLock::new(|| Span::styled("↗", UiStyle::OK));
pub static DOWN_ARROW_SPAN: LazyLock<Span<'static>> =
    LazyLock::new(|| Span::styled("↓", UiStyle::ERROR));
pub static DOWN_RIGHT_ARROW_SPAN: LazyLock<Span<'static>> =
    LazyLock::new(|| Span::styled("↘", UiStyle::WARNING));

pub static SWITCH_ARROW_SPAN: LazyLock<Span<'static>> =
    LazyLock::new(|| Span::styled("⇆", Style::default().fg(Color::Yellow)));

#[derive(Debug, Default, Display, Clone, Copy, PartialEq)]
pub enum PlayerWidgetView {
    #[default]
    Skills,
    Stats,
}

pub fn default_block() -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
}

pub fn thick_block() -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Thick)
}

pub fn selectable_list<'a>(options: Vec<(String, Style)>) -> ClickableList<'a> {
    let items: Vec<ClickableListItem> = options
        .iter()
        .map(|content| ClickableListItem::new(Span::styled(format!(" {}", content.0), content.1)))
        .collect();

    ClickableList::new(items)
}

pub fn render_navigable_list<Id: Copy>(
    frame: &mut UiFrame,
    area: Rect,
    title: &str,
    options: &[(Id, String, Style)],
    on_click: impl Fn(Id) -> UiCallback,
) {
    frame.render_widget(Clear, area);
    let rows = Layout::vertical([Constraint::Length(1)].repeat(options.len()))
        .split(area.inner(Margin::new(2, 1)));
    for (idx, (id, text, style)) in options.iter().enumerate() {
        frame.render_interactive_widget(
            Button::no_box(
                Span::styled(text.clone(), *style).into_left_aligned_line(),
                on_click(*id),
            ),
            rows[idx],
        );
    }
    frame.render_widget(default_block().title(title.to_string()), area);
}

pub fn go_to_planet_button<'a>(world: &World, planet_id: PlanetId) -> AppResult<Button<'a>> {
    let planet_name = world.planets.get_or_err(&planet_id)?.name.as_str();
    Ok(Button::new(
        format!("Go to planet: {planet_name}"),
        UiCallback::GoToPlanet { planet_id },
    )
    .hover_text(format!("Go to planet {planet_name}"))
    .hotkey(ui_key::GO_TO_PLANET))
}

pub fn go_to_own_space_cove_button<'a>() -> AppResult<Button<'a>> {
    Ok(
        Button::new("Go to space cove", UiCallback::GoToOwnSpaceCove)
            .hover_text("Go to space cove panel".to_string())
            .hotkey(ui_key::GO_TO_SPACE_COVE),
    )
}

pub fn teleport_button<'a>(world: &World, planet_id: PlanetId) -> AppResult<Button<'a>> {
    let own_team = world.get_own_team()?;
    let planet = world.planets.get_or_err(&planet_id)?;

    let rum_cost = own_team.teleport_rum_cost(planet_id);
    let button_label = if rum_cost > 0 {
        format!("Teleport (-{} Rum)", rum_cost)
    } else {
        "Teleport".to_string()
    };

    let mut teleport_button = Button::new(button_label, UiCallback::TravelToPlanet { planet_id })
        .hover_text(format!(
            "Travel instantaneously to {}{}",
            planet.name,
            if rum_cost == 0 {
                String::new()
            } else {
                format!(" for {} Rum", rum_cost)
            }
        ))
        .hotkey(ui_key::TRAVEL);

    if let Err(e) = own_team.can_travel_to_planet(planet, TELEPORT_TRAVEL_DURATION) {
        teleport_button.disable(Some(e.to_string()));
    }

    Ok(teleport_button)
}

pub fn travel_or_teleport_button<'a>(
    world: &World,
    planet_id: PlanetId,
    from: PlanetId,
) -> AppResult<Button<'a>> {
    let own_team = world.get_own_team()?;
    let planet = world.planets.get_or_err(&planet_id)?;

    if own_team.can_teleport_to(planet).is_ok() {
        return teleport_button(world, planet_id);
    }

    let duration = world.travel_duration_to_planet(own_team.id, planet_id)?;
    let formatted_duration = duration.formatted();

    let hover_text = format!(
        "Travel to {}: Distance {} - Duration {} - Fuel {}",
        planet.name,
        format_au(world.distance_between_planets(from, planet_id)? as f32 / AU as f32),
        formatted_duration,
        world.fuel_consumption_to_planet(own_team.id, planet_id)?
    );

    let mut button = Button::new(
        format!("Travel ({formatted_duration})"),
        UiCallback::TravelToPlanet { planet_id },
    )
    .hotkey(ui_key::TRAVEL)
    .hover_text(hover_text);

    if let Err(e) = own_team.can_travel_to_planet(planet, duration) {
        button.disable(Some(e.to_string()));
    }

    Ok(button)
}

pub fn go_to_team_home_planet_button<'a>(world: &World, team_id: &TeamId) -> AppResult<Button<'a>> {
    let team = world.teams.get_or_err(team_id)?;
    let planet_name = &world.planets.get_or_err(&team.home_planet_id)?.name;
    Ok(Button::new(
        format!("Home planet {planet_name}"),
        UiCallback::GoToHomePlanet { team_id: team.id },
    )
    .hover_text(format!("Go to team home planet {planet_name}",))
    .hotkey(ui_key::GO_TO_HOME_PLANET))
}

pub fn go_to_team_current_planet_button<'a>(
    world: &World,
    team_id: &TeamId,
) -> AppResult<Button<'a>> {
    let team = world.teams.get_or_err(team_id)?;
    let go_to_team_current_planet_button = match team.current_location {
        TeamLocation::OnPlanet { planet_id } => Button::new(
            format!("On planet {}", world.planets.get_or_err(&planet_id)?.name),
            UiCallback::GoToCurrentTeamPlanet { team_id: team.id },
        )
        .hover_text(format!(
            "Go to planet {}",
            world.planets.get_or_err(&planet_id)?.name
        ))
        .hotkey(ui_key::ON_PLANET),

        TeamLocation::Travelling {
            from: _from,
            to,
            started,
            duration,
            ..
        } => {
            let to = world.planets.get_or_err(&to)?.name.to_string();
            let text = if started + duration > world.last_tick_short_interval + 3 * SECONDS {
                format!("Travelling to {to}")
            } else {
                "Landing".to_string()
            };

            Button::new(text, UiCallback::None).disabled(Some(format!("Travelling to planet {to}")))
        }
        TeamLocation::Exploring {
            around,
            started,
            duration,
        } => {
            let around_planet = world.planets.get_or_err(&around)?.name.to_string();
            let text = if started + duration > world.last_tick_short_interval + 3 * SECONDS {
                format!("Around {around_planet}")
            } else {
                "Landing".to_string()
            };
            let countdown = if started + duration > world.last_tick_short_interval {
                (started + duration - world.last_tick_short_interval).formatted()
            } else {
                (0 as Tick).formatted()
            };
            Button::new(format!("{text} {countdown}"), UiCallback::None)
                .disabled(Some(format!("Exploring around planet {around_planet}")))
        }
        TeamLocation::OnSpaceAdventure { .. } => {
            return Err(anyhow!("Team is on a space adventure"))
        }
    };

    Ok(go_to_team_current_planet_button)
}

pub fn drink_button<'a>(world: &World, player_id: &PlayerId) -> AppResult<Button<'a>> {
    let player = world.players.get_or_err(player_id)?;
    let can_drink = player.can_drink(&world.teams);

    let mut button = Button::new(
        "Drink! (-1 Rum)",
        UiCallback::Drink {
            player_id: *player_id,
        },
    )
    .hotkey(ui_key::player::DRINK)
    .hover_text(
        "Drink a liter of rum, increasing morale and drunkenness. Drink too much and the pirate could get wasted!",
    );

    if let Err(err) = can_drink {
        button.disable(Some(err.to_string()));
    }

    Ok(button)
}

pub fn gold_button<'a>(world: &World, player_id: &PlayerId) -> AppResult<Button<'a>> {
    let player = world.players.get_or_err(player_id)?;
    let can_receive_gold = player.can_receive_gold(&world.teams);

    let mut button = Button::new(
        "Give gold! (-1 Gold)",
        UiCallback::GiveGold {
            player_id: *player_id,
        },
    )
    .hotkey(ui_key::player::GIVE_GOLD)
    .hover_text("Give a piece of gold, the right way to cheer any pirate!");

    if let Err(err) = can_receive_gold {
        button.disable(Some(err.to_string()));
    }

    Ok(button)
}

pub fn render_challenge_button(
    world: &World,
    team: &Team,
    hotkey: bool,
    frame: &mut UiFrame,
    area: Rect,
) -> AppResult<()> {
    let own_team = world.get_own_team()?;
    let can_challenge = if team.peer_id.is_some() {
        own_team.can_challenge_network_team(team)
    } else if let Err(e) = own_team.can_challenge_local_team(team) {
        Err(e)
    } else {
        // If other team is local, reject challenge if team is too tired
        let average_tiredness = team.average_tiredness(world);
        if average_tiredness > MAX_AVG_TIREDNESS_PER_CHALLENGED_GAME {
            Err(anyhow!("{} is too tired", team.name))
        } else {
            Ok(())
        }
    };

    // If we received a challenge from that team, display the accept/decline buttons
    if let Some(challenge) = own_team.received_challenges.get(&team.id) {
        let c_split = Layout::horizontal([
            Constraint::Min(10),
            Constraint::Length(6),
            Constraint::Length(6),
        ])
        .split(area);

        let mut accept_button = Button::new(
            format!("{:6^}", UiText::YES),
            UiCallback::AcceptChallenge {
                challenge: challenge.clone(),
            },
        )
        .block(default_block().border_style(UiStyle::OK))
        .hover_text(format!(
            "Accept the challenge from {} and start a game.",
            team.name
        ));
        if own_team.current_game.is_some() {
            accept_button.disable(Some(format!("{} is already playing", own_team.name)));
        } else if let Err(e) = own_team.can_accept_network_challenge(team) {
            accept_button.disable(Some(e.to_string()));
        }

        let decline_button = Button::new(
            format!("{:6^}", UiText::NO),
            UiCallback::DeclineChallenge {
                challenge: challenge.clone(),
            },
        )
        .block(default_block().border_style(UiStyle::ERROR))
        .hover_text(format!("Decline the challenge from {}.", team.name));

        frame.render_widget(
            Paragraph::new("Challenged!")
                .centered()
                .block(default_block()),
            c_split[0],
        );

        frame.render_interactive_widget(accept_button, c_split[1]);
        frame.render_interactive_widget(decline_button, c_split[2]);
        return Ok(());
    }

    let challenge_button = if let Some(game_id) = team.current_game {
        // FIXME: The game is not necessarily part of the world if it's a network game.
        let game_text = if let Ok(game) = world.games.get_or_err(&game_id) {
            if let Some(action) = game.action_results.last() {
                format!(
                    "{} {:>3}-{:<3} {}",
                    game.home_team_in_game.name,
                    action.home_score,
                    action.away_score,
                    game.away_team_in_game.name,
                )
            } else {
                format!(
                    "{}   0-0   {}",
                    game.home_team_in_game.name, game.away_team_in_game.name,
                )
            }
        } else {
            "Local game".to_string()
        };
        let mut b = Button::new(
            format!("Game: {game_text}"),
            UiCallback::GoToGame {
                game_id,
                from_popup: false,
            },
        )
        .hover_text("Go to team's game")
        .hotkey(ui_key::GO_TO_GAME);
        if world.games.get_or_err(&game_id).is_err() {
            b.disable(Some("Game is not visible"));
        }
        b
    } else {
        let mut button = Button::new("Challenge", UiCallback::ChallengeTeam { team_id: team.id })
            .hover_text(format!("Challenge {} to a game", team.name));

        if hotkey {
            button = button.hotkey(ui_key::game::CHALLENGE_TEAM)
        }

        if let Err(err) = can_challenge {
            button.disable(Some(err.to_string()));
        } else {
            button = if team.peer_id.is_some() {
                button.block(default_block().border_style(UiStyle::NETWORK))
            } else {
                button.block(default_block().border_style(UiStyle::OK))
            };
        }
        button
    };
    frame.render_interactive_widget(challenge_button, area);

    Ok(())
}

pub fn trade_resource_button<'a>(
    world: &World,
    resource: Resource,
    amount: i32,
    unit_cost: u32,
    hotkey: Option<KeyCode>,
    box_style: Style,
) -> AppResult<Button<'a>> {
    let mut button = Button::new(
        format!("{amount:^+}"),
        UiCallback::TradeResource {
            resource,
            amount,
            unit_cost,
        },
    )
    .block(default_block().border_style(box_style));

    let can_trade_resource = if amount == 0 {
        Ok(())
    } else if amount > 0 {
        world
            .get_own_team()?
            .can_buy_resource(resource, amount as u32, unit_cost)
    } else {
        world
            .get_own_team()?
            .can_sell_resource(resource, amount.unsigned_abs())
    };

    if let Err(e) = can_trade_resource {
        button.disable(Some(e.to_string()));
    }

    if amount == 0 {
        button = button.set_text("");
        let disabled_text: Option<&str> = None;
        button.disable(disabled_text);
    }

    let mut button = button.hover_text(format!(
        "{} {} {} for {}.",
        if amount > 0 { "Buy" } else { "Sell" },
        amount.abs(),
        resource,
        format_satoshi(amount.unsigned_abs() * unit_cost),
    ));
    if let Some(key) = hotkey {
        button = button.hotkey(key);
    }

    Ok(button)
}

pub fn explore_button<'a>(world: &World, team: &Team) -> AppResult<Button<'a>> {
    let duration = EXPLORATION_DURATION;
    let mut button = Button::new(
        format!("Explore ({})", duration.formatted()),
        UiCallback::ExploreAroundPlanet { duration },
    )
    .hotkey(ui_key::EXPLORE);

    match team.current_location {
        TeamLocation::OnPlanet { planet_id } => {
            let planet = world.planets.get_or_err(&planet_id)?;
            let needed_fuel = (duration as f32 * team.spaceship_fuel_consumption_per_tick()) as u32;
            button = button.hover_text(
                format!(
                    "Explore the space around {} on autopilot (need {} t of fuel). Hope to find resources, free pirates or more...",
                    planet.name,
                    needed_fuel
                ),
            );

            if let Err(msg) = team.can_explore_around_planet(planet, duration) {
                button.disable(Some(msg.to_string()));
            }
        }
        TeamLocation::Travelling {
            from: _from, to, ..
        } => {
            button = button.hover_text(
                "Explore the space on autopilot. Hope to find resources, free pirates or more..."
                    .to_string(),
            );
            let to = world.planets.get_or_err(&to)?.name.to_string();
            button.disable(Some(format!("Travelling to planet {to}")));
        }
        TeamLocation::Exploring { around, .. } => {
            button = button.hover_text(
                "Explore the space on autopilot. Hope to find resources, free pirates or more..."
                    .to_string(),
            );
            let around_planet = world.planets.get_or_err(&around)?.name.to_string();
            button.disable(Some(format!("Exploring around planet {around_planet}")));
        }
        TeamLocation::OnSpaceAdventure { .. } => {
            return Err(anyhow!("Team is on a space adventure"))
        }
    };

    Ok(button)
}

pub fn space_adventure_button<'a>(world: &World, team: &Team) -> AppResult<Button<'a>> {
    let average_tiredness = team.average_tiredness(world);

    let popup_message = PopupMessage::ConfirmSpaceAdventure {
        has_shooter: team.spaceship.has_shooters(),
        average_tiredness,
        timestamp: Tick::now(),
    };
    let mut button = Button::new("Space Adventure", UiCallback::PushUiPopup { popup_message })
        .hotkey(ui_key::SPACE_ADVENTURE)
        .block(default_block().border_style(UiStyle::WARNING));

    match team.current_location {
        TeamLocation::OnPlanet { planet_id } => {
            let planet = world.planets.get_or_err(&planet_id)?;
            button = button.hover_text(format!(
                "Start a space adventure around {} to collect resources and more...",
                planet.name,
            ));

            if let Err(msg) = team.can_start_space_adventure(average_tiredness) {
                button.disable(Some(msg.to_string()));
            }
        }
        TeamLocation::Travelling {
            from: _from, to, ..
        } => {
            button = button.hover_text(
                "Start a space adventure to manually collect resources and more...".to_string(),
            );
            let to = world.planets.get_or_err(&to)?.name.to_string();
            button.disable(Some(format!("Travelling to planet {to}")));
        }
        TeamLocation::Exploring { around, .. } => {
            button = button.hover_text(
                "Start a space adventure to manually collect resources and more...".to_string(),
            );
            let around_planet = world.planets.get_or_err(&around)?.name.to_string();
            button.disable(Some(format!("Exploring around planet {around_planet}")));
        }
        TeamLocation::OnSpaceAdventure { .. } => {
            return Err(anyhow!("Already on a space adventure"))
        }
    };

    Ok(button)
}

pub fn get_storage_lengths(
    resources: &ResourceMap,
    storage_capacity: u32,
    bars_length: usize,
) -> Vec<usize> {
    let gold = resources.value(&Resource::GOLD);
    let scraps = resources.value(&Resource::SCRAPS);
    let rum = resources.value(&Resource::RUM);

    // Calculate temptative length
    let mut gold_length = ((Resource::GOLD.to_storing_space() * gold) as f32
        / storage_capacity as f32
        * bars_length as f32)
        .round() as usize;
    let mut scraps_length = ((Resource::SCRAPS.to_storing_space() * scraps) as f32
        / storage_capacity as f32
        * bars_length as f32)
        .round() as usize;
    let mut rum_length = ((Resource::RUM.to_storing_space() * rum) as f32 / storage_capacity as f32
        * bars_length as f32)
        .round() as usize;

    // If the quantity is larger than 0, we should display it with at least 1 bar.
    if gold > 0 {
        gold_length = gold_length.max(1);
    }
    if scraps > 0 {
        scraps_length = scraps_length.max(1);
    }
    if rum > 0 {
        rum_length = rum_length.max(1);
    }

    // free_bars can be negative because of the previous rule.
    let mut free_bars: isize =
        bars_length as isize - (gold_length + scraps_length + rum_length) as isize;

    // If free_bars is negative, remove enough bars from the largest length.
    if free_bars < 0 {
        if gold_length > scraps_length && gold_length > rum_length {
            gold_length -= (-free_bars) as usize;
        } else if rum_length > scraps_length {
            rum_length -= (-free_bars) as usize;
        } else {
            scraps_length -= (-free_bars) as usize;
        }
        free_bars = 0;
    } else if free_bars > 0 {
        // Round up to eliminate free bars when storage is full
        let free_space = storage_capacity - resources.used_storage_capacity();
        if free_space == 0 {
            if gold_length >= scraps_length && gold_length >= rum_length {
                gold_length += free_bars as usize;
            } else if rum_length >= scraps_length {
                rum_length += free_bars as usize;
            } else {
                scraps_length += free_bars as usize;
            }
            free_bars = 0
        }
    }

    vec![gold_length, scraps_length, rum_length, free_bars as usize]
}

pub fn get_storage_spans(
    resources: &'_ ResourceMap,
    storage_capacity: u32,
    bars_length: usize,
) -> Vec<Span<'_>> {
    if let [gold_length, scraps_length, rum_length, free_bars] =
        get_storage_lengths(resources, storage_capacity, bars_length)[..4]
    {
        vec![
            Span::raw("Cargo  ".to_string()),
            Span::styled("▰".repeat(gold_length), Resource::GOLD.style()),
            Span::styled("▰".repeat(scraps_length), Resource::SCRAPS.style()),
            Span::styled("▰".repeat(rum_length), Resource::RUM.style()),
            Span::raw("▱".repeat(free_bars)),
            Span::raw(format!(
                " {:>04}/{:<04} ",
                resources.used_storage_capacity(),
                storage_capacity
            )),
        ]
    } else {
        vec![Span::default()]
    }
}

pub fn get_crew_spans<'a>(crew_size: usize, crew_capacity: usize) -> Vec<Span<'a>> {
    let bars_length = crew_capacity;
    let crew_length = crew_size;

    let crew_bars = format!(
        "{}{}",
        "▰".repeat(crew_length),
        "▱".repeat(bars_length.saturating_sub(crew_length)),
    );

    let crew_style = match crew_length {
        x if x < MIN_PLAYERS_PER_GAME => UiStyle::ERROR,
        x if x < crew_capacity => UiStyle::WARNING,
        _ => UiStyle::OK,
    };

    vec![
        Span::raw("Crew   ".to_string()),
        Span::styled(crew_bars, crew_style),
        Span::raw(format!(" {crew_size}/{crew_capacity}  ")),
    ]
}

pub fn get_energy_spans<'a>(average_tiredness: Skill) -> Vec<Span<'a>> {
    let tiredness_length = (average_tiredness / MAX_SKILL * BARS_LENGTH as f32).round() as usize;
    let energy_string = format!(
        "{}{}",
        "▰".repeat(BARS_LENGTH.saturating_sub(tiredness_length)),
        "▱".repeat(tiredness_length),
    );
    let energy_style = ((MAX_SKILL - average_tiredness) / MAX_SKILL * GREEN_STYLE_SKILL).style();

    vec![
        Span::raw("Energy ".to_string()),
        Span::styled(energy_string, energy_style),
    ]
}

pub fn get_durability_spans<'a>(
    value: u32,
    max_value: u32,
    shield_value: u32,
    max_shield_value: u32,
    bars_length: usize,
) -> Vec<Span<'a>> {
    let value_bars_length = (max_value as f32 / (max_value + max_shield_value) as f32
        * bars_length as f32)
        .round() as usize;

    let mut shield_bars_length = (max_shield_value as f32 / (max_value + max_shield_value) as f32
        * bars_length as f32)
        .round() as usize;

    // In case we rounded up twice.
    if value_bars_length + shield_bars_length > bars_length {
        shield_bars_length -= 1;
    }

    let value_length =
        (value as f32 / max_value as f32 * value_bars_length as f32).round() as usize;
    let shield_value_length = (shield_value as f32 / max_shield_value as f32
        * shield_bars_length as f32)
        .round() as usize;

    let value_bars = "▰".repeat(value_length);
    let empty_bars = "▱".repeat(value_bars_length.saturating_sub(value_length));
    let shield_bars = "▰".repeat(shield_value_length);
    let shield_empty_bars = "▱".repeat(shield_bars_length.saturating_sub(shield_value_length));
    let value_style = (GREEN_STYLE_SKILL * (value as f32 / max_value as f32))
        .bound()
        .style();

    vec![
        Span::raw("Hull   "),
        Span::styled(value_bars, value_style),
        Span::styled(empty_bars, value_style),
        Span::styled(shield_bars, UiStyle::SHIELD),
        Span::styled(shield_empty_bars, UiStyle::SHIELD),
        Span::raw(format!(
            " {value}/{max_value}{}",
            if max_shield_value > 0 {
                format!("+{shield_value}/{max_shield_value}")
            } else {
                String::new()
            }
        )),
    ]
}

pub fn get_charge_spans<'a>(
    value: u32,
    max_value: u32,
    is_recharging: bool,
    bars_length: usize,
) -> Vec<Span<'a>> {
    let length = (value as f32 / max_value as f32 * bars_length as f32).round() as usize;
    let bars = format!("{}{}", "▰".repeat(length), "▱".repeat(bars_length - length),);

    let style = if is_recharging {
        MIN_SKILL.style()
    } else {
        (GREEN_STYLE_SKILL * (value as f32 / max_value as f32))
            .bound()
            .style()
    };

    vec![
        Span::raw(if is_recharging {
            format!("{:>6} ", "Reload",)
        } else {
            format!("{:>6} ", "Charge",)
        }),
        Span::styled(bars, style),
        Span::raw(format!(" {value}/{max_value}")),
    ]
}

pub fn get_fuel_spans<'a>(fuel: u32, fuel_capacity: u32, bars_length: usize) -> Vec<Span<'a>> {
    let fuel_length = (fuel.min(fuel_capacity) as f32 / fuel_capacity as f32 * bars_length as f32)
        .round() as usize;
    let fuel_bars = format!(
        "{}{}",
        "▰".repeat(fuel_length),
        "▱".repeat(bars_length.saturating_sub(fuel_length)),
    );

    let fuel_style = (GREEN_STYLE_SKILL * (fuel as f32 / fuel_capacity as f32))
        .bound()
        .style();

    vec![
        Span::raw("Tank   ".to_string()),
        Span::styled(fuel_bars, fuel_style),
        Span::raw(format!(" {fuel}/{fuel_capacity}")),
    ]
}

pub fn drunkenness_description(drunkenness: Skill) -> &'static str {
    match drunkenness {
        x if x < MIN_SKILL => "completely drunk",
        x if x == MIN_SKILL => "completely sober",
        x if x < 4.0 => "dignifiedly tipsy",
        x if x < 8.0 => "respectably buzzed",
        x if x < 12.0 => "pretty merry",
        x if x < 16.0 => "undignifiedly inebriated",
        _ => "sloshed",
    }
}

pub fn render_spaceship_description(
    team: &Team,
    world: &World,
    team_rating: Skill,
    full_info: bool,
    with_average_energy: bool,
    gif_map: &mut GifMap,
    tick: usize,
    frame: &mut UiFrame,
    area: Rect,
) {
    let spaceship_split = Layout::horizontal([
        Constraint::Length(SPACESHIP_IMAGE_WIDTH as u16 + 2),
        Constraint::Min(1),
    ])
    .split(area.inner(Margin {
        horizontal: 1,
        vertical: 1,
    }));

    if let Ok(lines) = gif_map.on_planet_spaceship_lines(&team.spaceship, tick) {
        let paragraph = Paragraph::new(lines);
        frame.render_widget(
            paragraph.centered(),
            spaceship_split[0].inner(Margin {
                horizontal: 1,
                vertical: 1,
            }),
        );
    }

    if full_info {
        let speed_bonus = TeamBonus::SpaceshipSpeed
            .current_team_bonus(&team.id, &world.teams, &world.players)
            .unwrap_or(1.0);
        let weapon_bonus = TeamBonus::Weapons
            .current_team_bonus(&team.id, &world.teams, &world.players)
            .unwrap_or(1.0);
        let widget = Paragraph::new(vec![
            Line::default(),
            Line::from(get_crew_spans(
                team.player_ids.len(),
                team.spaceship.crew_capacity() as usize,
            )),
            Line::from(get_durability_spans(
                team.spaceship.current_durability(),
                team.spaceship.max_durability(),
                team.spaceship.shield_max_durability() as u32,
                team.spaceship.shield_max_durability() as u32,
                BARS_LENGTH,
            )),
            Line::from(get_charge_spans(
                team.spaceship.max_charge(),
                team.spaceship.max_charge(),
                false,
                BARS_LENGTH,
            )),
            Line::from(get_fuel_spans(
                team.fuel(),
                team.spaceship.fuel_capacity(),
                BARS_LENGTH,
            )),
            Line::from(get_storage_spans(
                &team.resources,
                team.spaceship.storage_capacity(),
                BARS_LENGTH,
            )),
            Line::default(),
            Line::from(format!(
                "Speed {:.3} AU/h",
                team.spaceship_speed() * speed_bonus * HOURS as f32 / AU as f32
            )),
            Line::from(format!(
                "Shooters {}x{}",
                (team.spaceship.damage() * team.spaceship.fire_rate() * weapon_bonus) as u8,
                team.spaceship.shooting_points()
            )),
            Line::from(format!(
                "Consumption {:.2} t/h  Max distance {:<5.3} AU ",
                team.spaceship_fuel_consumption_per_tick() * HOURS as f32,
                team.spaceship.max_distance(team.fuel()) / AU as f32,
            )),
            Line::from(format!(
                "Distance travelled {}",
                format_au(team.total_travelled as f32 / AU as f32)
            )),
        ]);

        frame.render_widget(
            widget,
            spaceship_split[1].inner(Margin {
                horizontal: 0,
                vertical: 1,
            }),
        );
    } else {
        let area = spaceship_split[1].inner(Margin {
            horizontal: 0,
            vertical: 0,
        });

        let split = Layout::vertical([
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Length(8),
            Constraint::Length(3),
        ])
        .split(area);

        let rating_span =  HoverTextSpan::new(
            Span::raw(format!(
                "Rating {}  ",
                team_rating.stars()
            )),
            format!("The rating is an indicator of the overall basketball proficiency of the crew. (current value {})", team_rating.value()),
        );
        frame.render_interactive_widget(rating_span, split[1]);

        let reputation_span =
        HoverTextSpan::new(
            Span::raw(format!(
                "Reputation {}  ",
                team.reputation.stars()
            )),
            format!("Reputation indicates how much the team is respected in the galaxy. It affects hiring costs. (current value {})", team.reputation.value()),
        );
        frame.render_interactive_widget(reputation_span, split[2]);

        let game_record = format!(
            "Local Elo {:.0}{}",
            team.local_game_rating.rating,
            if team.peer_id.is_some() || team.id == world.own_team_id {
                format!("  Network Elo {:.0}", team.network_game_rating.rating)
            } else {
                String::new()
            }
        );

        let mut lines = vec![
            HoverTextLine::from(format!("Treasury {}", format_satoshi(team.balance()))),
            HoverTextLine::from(game_record),
            HoverTextLine::from(get_crew_spans(
                team.player_ids.len(),
                team.spaceship.crew_capacity() as usize,
            )),
        ];

        if with_average_energy {
            let average_tiredness = team.average_tiredness(world);
            lines.push(HoverTextLine::from(get_energy_spans(average_tiredness)));
        }

        if team.creation_time != Tick::default() {
            let creation_date = team.creation_time.formatted_as_date();
            lines.push(HoverTextLine::from(format!(
                "Roaming the galaxy since {creation_date}"
            )));
        } else {
            lines.push(HoverTextLine::default());
        }

        if !team.honours.is_empty() {
            lines.append(&mut honour_lines(&team.honours));
        }

        let lines_split = Layout::vertical([1].repeat(lines.len())).split(split[3]);
        for (line, &l_split) in zip(lines, lines_split.iter()) {
            frame.render_interactive_widget(line, l_split);
        }

        if let Some(cove) = team.space_cove.as_ref() {
            frame.render_interactive_widget(
                Button::new(
                    format!("Go to space cove {}", cove.name),
                    UiCallback::GoToSpaceCove { team_id: team.id },
                )
                .hotkey(ui_key::GO_TO_SPACE_COVE),
                split[4].inner(Margin::new(2, 0)),
            );
        }
    }

    // Render main block
    let block = default_block().title(format!("Spaceship - {}", team.spaceship.name));
    frame.render_widget(block, area);
}

fn honour_lines<'a>(team_honours: &HashSet<Honour>) -> Vec<HoverTextLine<'a>> {
    let honour_color = |honour| match honour {
        Honour::Defiant => (Color::Blue, Color::Yellow),
        Honour::Galactic => (Color::White, Color::Blue),
        Honour::Maximalist => (Color::Yellow, Color::DarkGray),
        Honour::MultiKulti => (Color::Red, Color::LightCyan),
        Honour::Pirate => (Color::Black, Color::Red),
        Honour::Traveller => (Color::Blue, Color::LightMagenta),
        Honour::Veteran => (Color::DarkGray, Color::White),
    };

    let mut top_spans = vec![];
    let mut spans = vec![];
    let mut btm_spans = vec![];

    let title = "Honours ";
    top_spans.push(HoverTextSpan::new(Span::raw(" ".repeat(title.len())), ""));
    spans.push(HoverTextSpan::new(Span::raw(title), ""));
    btm_spans.push(HoverTextSpan::new(Span::raw(" ".repeat(title.len())), ""));

    for &honour in team_honours.iter().sorted_by_key(|h| h.to_string()) {
        let (fg, bg) = honour_color(honour);
        top_spans.push(HoverTextSpan::new(
            Span::styled("▄ ▄", Style::default().fg(bg)),
            honour.description().to_string(),
        ));
        spans.push(HoverTextSpan::new(
            Span::styled(
                format!(" {} ", honour.symbol()),
                Style::default().bg(bg).fg(fg).bold(),
            ),
            honour.description().to_string(),
        ));
        btm_spans.push(HoverTextSpan::new(
            Span::styled(" ▀ ", Style::default().fg(bg)),
            honour.description().to_string(),
        ));
    }
    vec![
        HoverTextLine::from(top_spans),
        HoverTextLine::from(spans),
        HoverTextLine::from(btm_spans),
    ]
}

// FIXME: unify the following two functions.
// It's unclear how to get custom description at the moment for the spaceship upgrades, maybe delegate it to the upgrade target?
pub fn render_available_upgrades<U: UpgradeableElement>(
    pending_upgrade: Option<Upgrade<U>>,
    possible_upgrade: Option<Upgrade<U>>,
    world: &World,
    own_team: &Team,
    frame: &mut UiFrame,
    area: Rect,
) -> AppResult<()> {
    if let Some(upgrade) = pending_upgrade {
        let countdown = (upgrade.started + upgrade.duration)
            .saturating_sub(world.last_tick_short_interval)
            .formatted();

        frame.render_widget(
            Paragraph::new(vec![
                Line::from(Span::styled(
                    format!("Building {}", upgrade.target),
                    UiStyle::HEADER.bold(),
                )),
                Line::from(countdown),
            ])
            .centered(),
            area,
        );
    } else if let Some(upgrade) = possible_upgrade {
        {
            let mut lines = vec![Line::from(Span::styled(
                format!("{} upgrade cost", upgrade.target),
                UiStyle::HEADER.bold(),
            ))];
            lines.append(&mut upgrade_resources_lines(&upgrade.target, own_team));
            frame.render_widget(Paragraph::new(lines).centered(), area);
        }
    } else {
        frame.render_widget(Paragraph::new("Nothing left to build").centered(), area);
    }

    Ok(())
}

pub fn render_available_spaceship_upgrades(
    pending_upgrade: Option<Upgrade<SpaceshipUpgradeTarget>>,
    possible_upgrade: Option<Upgrade<SpaceshipUpgradeTarget>>,
    world: &World,
    own_team: &Team,
    frame: &mut UiFrame,
    area: Rect,
) {
    if let Some(upgrade) = pending_upgrade {
        let header = match upgrade.target {
            SpaceshipUpgradeTarget::Repairs { .. } => "Repairing spaceship".to_string(),
            target => format!("Upgrading {target}"),
        };

        let countdown = (upgrade.started + upgrade.duration)
            .saturating_sub(world.last_tick_short_interval)
            .formatted();

        frame.render_widget(
            Paragraph::new(vec![
                Line::from(vec![Span::styled(header, UiStyle::HEADER.bold())]),
                Line::from(countdown),
            ])
            .centered(),
            area,
        );
    } else if let Some(upgrade) = possible_upgrade {
        {
            let mut lines =
                vec![Line::from(Span::styled("Upgrade cost", UiStyle::HEADER.bold())).centered()];
            lines.append(&mut upgrade_resources_lines(&upgrade.target, own_team));
            frame.render_widget(Paragraph::new(lines).centered(), area);
        }
    }
}

pub fn render_build_asteroid_upgrade_button(
    asteroid: &Planet,
    possible_upgrade: Option<Upgrade<PlanetUpgradeTarget>>,
    own_team: &Team,
    frame: &mut UiFrame,
    area: Rect,
) {
    if let Some(pending_upgrade) = asteroid.pending_upgrade {
        let build_button = Button::new(
            format!("Building {}", pending_upgrade.target),
            UiCallback::None,
        )
        .hover_text(format!(
            "Building {} on {}",
            pending_upgrade.target, asteroid.name
        ))
        .disabled(Some(format!("Already building {}", pending_upgrade.target)));

        frame.render_interactive_widget(build_button, area);
    } else if let Some(upgrade) = possible_upgrade {
        let on_click = if upgrade.target == PlanetUpgradeTarget::SpaceCove {
            UiCallback::PushUiPopup {
                popup_message: PopupMessage::BuildSpaceCove {
                    asteroid_name: asteroid.name.clone(),
                    asteroid_id: asteroid.id,
                    timestamp: Tick::now(),
                },
            }
        } else {
            UiCallback::SetAsteroidPendingUpgrade {
                asteroid_id: asteroid.id,
                upgrade,
            }
        };

        let mut build_button = Button::new(
            format!(
                "Build {} ({})",
                upgrade.target,
                upgrade.duration.formatted()
            ),
            on_click,
        )
        .hotkey(ui_key::BUILD_ASTEROID_UPGRADE)
        .hover_text(upgrade.target.description());

        if upgrade.target == PlanetUpgradeTarget::SpaceCove {
            build_button = build_button.block(default_block().border_style(UiStyle::WARNING));
        }

        let can_upgrade_asteroid = own_team.can_upgrade_asteroid(asteroid, &upgrade);
        if let Err(e) = can_upgrade_asteroid.as_ref() {
            build_button.disable(Some(e.to_string()));
        }

        frame.render_interactive_widget(build_button, area);
    } else {
        let build_button = Button::new(
            format!("Nothing left to build on {}", asteroid.name),
            UiCallback::None,
        )
        .disabled(None::<String>);
        frame.render_interactive_widget(build_button, area);
    }
}

pub fn render_spaceship_upgrade(
    team: &Team,
    upgrade_target: SpaceshipUpgradeTarget,
    in_shipyard: bool,
    gif_map: &mut GifMap,
    tick: usize,
    frame: &mut UiFrame,
    area: Rect,
) {
    let spaceship_split = Layout::horizontal([
        Constraint::Length(SPACESHIP_IMAGE_WIDTH as u16 + 2),
        Constraint::Min(1),
    ])
    .split(area.inner(Margin {
        horizontal: 1,
        vertical: 1,
    }));
    let mut upgraded_ship = team.spaceship.clone();

    match upgrade_target {
        SpaceshipUpgradeTarget::ChargeUnit { component } => upgraded_ship.charge_unit = component,
        SpaceshipUpgradeTarget::Hull { component } => upgraded_ship.hull = component,
        SpaceshipUpgradeTarget::Engine { component } => upgraded_ship.engine = component,
        SpaceshipUpgradeTarget::Shield { component } => upgraded_ship.shield = component,
        SpaceshipUpgradeTarget::Storage { component } => upgraded_ship.storage = component,
        SpaceshipUpgradeTarget::Shooter { component } => upgraded_ship.shooter = component,
        SpaceshipUpgradeTarget::Repairs { .. } => upgraded_ship.reset_durability(),
    }

    if in_shipyard {
        if let Ok(lines) = gif_map.in_shipyard_spaceship_lines(&upgraded_ship, tick) {
            let paragraph = Paragraph::new(lines);
            frame.render_widget(
                paragraph.centered(),
                spaceship_split[0].inner(Margin::new(1, 1)),
            );
        }
    } else if matches!(upgrade_target, SpaceshipUpgradeTarget::Shooter { .. })
        && upgraded_ship.fire_rate() > 0.0
    {
        if let Ok(lines) = gif_map.shooting_spaceship_lines(&upgraded_ship, tick) {
            let paragraph = Paragraph::new(lines);
            frame.render_widget(
                paragraph.centered(),
                spaceship_split[0].inner(Margin::new(1, 1)),
            );
        }
    } else if matches!(upgrade_target, SpaceshipUpgradeTarget::Shield { .. }) {
        if let Ok(lines) = gif_map.with_shield_spaceship_lines(&upgraded_ship, tick) {
            let paragraph = Paragraph::new(lines);
            frame.render_widget(
                paragraph.centered(),
                spaceship_split[0].inner(Margin::new(1, 1)),
            );
        }
    } else if let Ok(lines) = gif_map.on_planet_spaceship_lines(&upgraded_ship, tick) {
        let paragraph = Paragraph::new(lines);
        frame.render_widget(
            paragraph.centered(),
            spaceship_split[0].inner(Margin {
                horizontal: 1,
                vertical: 1,
            }),
        );
    }

    let storage_units = 0;
    let spaceship_info = Paragraph::new(vec![
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:.3}",
                "Max speed",
                team.spaceship.speed(storage_units) * HOURS as f32 / AU as f32
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{:.3}",
                    upgraded_ship.speed(storage_units) * HOURS as f32 / AU as f32
                ),
                if upgraded_ship.speed(storage_units) > team.spaceship.speed(storage_units) {
                    UiStyle::OK
                } else if upgraded_ship.speed(storage_units) < team.spaceship.speed(storage_units) {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
            Span::raw(" AU/h"),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:<5}",
                "Max crew",
                team.spaceship.crew_capacity()
            )),
            Span::raw(" --> "),
            Span::styled(
                format!("{}", upgraded_ship.crew_capacity()),
                if upgraded_ship.crew_capacity() > team.spaceship.crew_capacity() {
                    UiStyle::OK
                } else if upgraded_ship.crew_capacity() < team.spaceship.crew_capacity() {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:<5}",
                "Tank",
                team.spaceship.fuel_capacity()
            )),
            Span::raw(" --> "),
            Span::styled(
                format!("{}", upgraded_ship.fuel_capacity()),
                if upgraded_ship.fuel_capacity() > team.spaceship.fuel_capacity() {
                    UiStyle::OK
                } else if upgraded_ship.fuel_capacity() < team.spaceship.fuel_capacity() {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
            Span::raw(" t"),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:<5}",
                "Max charge",
                team.spaceship.max_charge()
            )),
            Span::raw(" --> "),
            Span::styled(
                format!("{}", upgraded_ship.max_charge()),
                if upgraded_ship.max_charge() > team.spaceship.max_charge() {
                    UiStyle::OK
                } else if upgraded_ship.max_charge() < team.spaceship.max_charge() {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
            Span::raw(" MW"),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:<5}",
                "Stiva",
                team.spaceship.storage_capacity(),
            )),
            Span::raw(" --> "),
            Span::styled(
                format!("{}", upgraded_ship.storage_capacity()),
                if upgraded_ship.storage_capacity() > team.spaceship.storage_capacity() {
                    UiStyle::OK
                } else if upgraded_ship.storage_capacity() < team.spaceship.storage_capacity() {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:>02}/{:<02}",
                "Durability",
                team.spaceship.current_durability(),
                team.spaceship.max_durability(),
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{:>2}/{:<2}",
                    upgraded_ship.max_durability(),
                    upgraded_ship.max_durability()
                ),
                if upgraded_ship.max_durability() > team.spaceship.max_durability() {
                    UiStyle::OK
                } else if upgraded_ship.max_durability() < team.spaceship.max_durability() {
                    UiStyle::ERROR
                } else if upgraded_ship.current_durability() > team.spaceship.current_durability() {
                    UiStyle::OK
                } else if upgraded_ship.current_durability() < team.spaceship.current_durability() {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:5}",
                "Shield",
                format!(
                    "{}/{:.0}%",
                    team.spaceship.shield_max_durability(),
                    (1.0 - team.spaceship.shield_damage_reduction()) * 100.0
                )
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{}/{:.0}%",
                    upgraded_ship.shield_max_durability(),
                    (1.0 - upgraded_ship.shield_damage_reduction()) * 100.0
                ),
                if upgraded_ship.shield_max_durability() > team.spaceship.shield_max_durability() {
                    UiStyle::OK
                } else if upgraded_ship.shield_max_durability()
                    < team.spaceship.shield_max_durability()
                {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:5}",
                "Shooters",
                format!(
                    "{}x{}",
                    (team.spaceship.damage() * team.spaceship.fire_rate()) as u8,
                    team.spaceship.shooting_points()
                )
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{}x{}",
                    (upgraded_ship.damage() * upgraded_ship.fire_rate()) as u8,
                    upgraded_ship.shooting_points()
                ),
                if (upgraded_ship.damage()
                    * upgraded_ship.fire_rate()
                    * upgraded_ship.shooting_points() as f32)
                    > (team.spaceship.damage()
                        * team.spaceship.fire_rate()
                        * team.spaceship.shooting_points() as f32)
                {
                    UiStyle::OK
                } else if (upgraded_ship.damage()
                    * upgraded_ship.fire_rate()
                    * upgraded_ship.shooting_points() as f32)
                    < (team.spaceship.damage()
                        * team.spaceship.fire_rate()
                        * team.spaceship.shooting_points() as f32)
                {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:.3}",
                "Consumption",
                team.spaceship.fuel_consumption_per_tick(storage_units) * HOURS as f32
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{:.3}",
                    upgraded_ship.fuel_consumption_per_tick(storage_units) * HOURS as f32
                ),
                if upgraded_ship.fuel_consumption_per_tick(storage_units)
                    < team.spaceship.fuel_consumption_per_tick(storage_units)
                {
                    UiStyle::OK
                } else if upgraded_ship.fuel_consumption_per_tick(storage_units)
                    > team.spaceship.fuel_consumption_per_tick(storage_units)
                {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
            Span::raw(" t/h"),
        ]),
        Line::from(vec![
            Span::raw(format!(
                "{:<12} {:.3}",
                "Max distance",
                team.spaceship.max_distance(team.spaceship.fuel_capacity()) / AU as f32
            )),
            Span::raw(" --> "),
            Span::styled(
                format!(
                    "{:.3}",
                    upgraded_ship.max_distance(upgraded_ship.fuel_capacity()) / AU as f32
                ),
                if upgraded_ship.max_distance(upgraded_ship.fuel_capacity())
                    > team.spaceship.max_distance(team.spaceship.fuel_capacity())
                {
                    UiStyle::OK
                } else if upgraded_ship.max_distance(upgraded_ship.fuel_capacity())
                    < team.spaceship.max_distance(team.spaceship.fuel_capacity())
                {
                    UiStyle::ERROR
                } else {
                    UiStyle::DEFAULT
                },
            ),
            Span::raw(" AU"),
        ]),
    ]);

    frame.render_widget(
        spaceship_info,
        spaceship_split[1].inner(Margin {
            horizontal: 0,
            vertical: 2,
        }),
    );
}

pub fn render_player_description(
    player: &Player,
    players_scouting: &HashMap<PlayerId, ScoutReport>,
    view: PlayerWidgetView,
    gif_map: &mut GifMap,
    tick: usize,
    world: &World,
    frame: &mut UiFrame,
    area: Rect,
) {
    let default_report = ScoutReport::default();
    let report = players_scouting.get(&player.id).unwrap_or(&default_report);

    let h_split = Layout::horizontal([
        Constraint::Length(PLAYER_IMAGE_WIDTH as u16 + 4),
        Constraint::Min(2),
    ])
    .split(area);

    let header_body_img = Layout::vertical([Constraint::Length(2), Constraint::Min(2)]).split(
        h_split[0].inner(Margin {
            horizontal: 2,
            vertical: 1,
        }),
    );

    let header_body_stats = Layout::vertical([
        Constraint::Length(2),  //margin
        Constraint::Length(1),  //header
        Constraint::Length(1),  //header
        Constraint::Length(1),  //header
        Constraint::Length(1),  //header
        Constraint::Length(1),  //margin
        Constraint::Length(20), //skills/stats
    ])
    .split(h_split[1]);

    if let Ok(lines) = gif_map.player_frame_lines(player, tick) {
        let paragraph = Paragraph::new(lines);
        frame.render_widget(paragraph, header_body_img[1]);
    }

    let trait_span = if report.is_special_trait_scouted() {
        player.special_trait.map_or_else(
            || Span::default(),
            |t| Span::styled(format!("{t}"), t.style()),
        )
    } else {
        Span::default()
    };

    let drunkenness = player.current_drunkenness(world);
    let line = HoverTextLine::from(vec![
        HoverTextSpan::new(
            Span::raw(format!(
                "Reputation {}  ",
                player.reputation.stars()
            )),
            format!("Reputation indicates how much the player is respected in the galaxy. It affects special bonuses. (current value {})", player.reputation.value()),
        ),
        HoverTextSpan::new(
            trait_span,
            player.special_trait.map_or_else(String::new, |t| t.description(player)),
        ),
    ]);
    frame.render_interactive_widget(line, header_body_stats[1]);

    let morale = player.current_morale(world);
    let morale_length = (morale / MAX_SKILL * BARS_LENGTH as f32).round() as usize;
    let morale_string = format!(
        "{}{}",
        "▰".repeat(morale_length),
        "▱".repeat(BARS_LENGTH.saturating_sub(morale_length)),
    );
    let morale_style = (morale / MAX_SKILL * GREEN_STYLE_SKILL).style();

    frame.render_interactive_widget(
        HoverTextLine::from(vec![
            HoverTextSpan::new(
                Span::raw("Morale ".to_string()),
                format!(
                    "When morale is low, pirates may decide to leave the team! (current value {morale:.2})"
                ),
            ),
            HoverTextSpan::new(
                Span::styled(morale_string, morale_style),
                ""
            ),
        ]),
        header_body_stats[2],
    );

    if drunkenness < 0.0 {
        let drunkenness_length =
            (drunkenness / DRUNKENNESS_ON_GETTING_DRUNK * BARS_LENGTH as f32).round() as usize;
        let drunkenness_string = format!(
            "{}{}",
            "▰".repeat(drunkenness_length),
            "▱".repeat(BARS_LENGTH.saturating_sub(drunkenness_length)),
        );

        frame.render_interactive_widget(
        HoverTextLine::from(vec![
            HoverTextSpan::new(
                Span::raw("Drunk  ".to_string()),
                format!("{} is drunk! While {} {} getting sober, tiredness is not recovered (current value {:.2})", player.info.full_name(), player.info.pronouns.as_subject().to_lowercase(), player.info.pronouns.to_be(), -drunkenness),
            ),
            HoverTextSpan::new(Span::styled( drunkenness_string, UiStyle::DRUNK),"", 
           ),
        ]),
        header_body_stats[3],
    );
    } else {
        let tiredness = player.current_tiredness(world);
        let tiredness_length = (tiredness / MAX_SKILL * BARS_LENGTH as f32).round() as usize;
        let energy_string = format!(
            "{}{}",
            "▰".repeat(BARS_LENGTH.saturating_sub(tiredness_length)),
            "▱".repeat(tiredness_length),
        );
        let energy_style = ((MAX_SKILL - tiredness) / MAX_SKILL * GREEN_STYLE_SKILL).style();

        frame.render_interactive_widget(
        HoverTextLine::from(vec![
            HoverTextSpan::new(
                Span::raw("Energy ".to_string()),
                format!("Energy affects player's performance in a game. When the energy goes to 0, the player is exhausted and will fail most game actions. (current value {:.2})", (MAX_SKILL-tiredness)),
            ),
            HoverTextSpan::new(Span::styled( energy_string, energy_style),"", 
           ),
        ]),
        header_body_stats[3],
    );
    }

    frame.render_widget(
        Paragraph::new(format!(
            "{} yo, {} cm, {} kg, {}",
            player.info.age as u8,
            player.info.height as u8,
            player.info.weight as u8,
            player.info.population,
        )),
        header_body_stats[4],
    );

    match view {
        PlayerWidgetView::Skills => frame.render_widget(
            Paragraph::new(format_player_skills(player, report)),
            header_body_stats[6],
        ),
        PlayerWidgetView::Stats => frame.render_widget(
            Paragraph::new(format_player_stats(player)),
            header_body_stats[6],
        ),
    }

    // Render main block
    let block = default_block().title(format!(
        "{} {} {}",
        player.info.first_name,
        player.info.last_name,
        player.stars()
    ));
    frame.render_widget(block, area);
}

pub fn upgrade_resources_lines<'a, U: UpgradeableElement>(
    upgrade: &'a U,
    team: &'a Team,
) -> Vec<Line<'a>> {
    let mut lines = vec![];
    for &(resource, amount) in upgrade.upgrade_cost().iter() {
        if amount == 0 {
            continue;
        }
        let have = team.resources.value(&resource);
        if amount > have {
            lines.push(Line::from(vec![
                Span::styled(format!("{:<7} ", resource.to_string()), resource.style()),
                Span::styled(format!("{have:>5}/{amount:<5} ❌"), UiStyle::ERROR),
            ]));
        } else {
            lines.push(Line::from(vec![
                Span::styled(format!("{:<7} ", resource.to_string()), resource.style()),
                Span::styled(format!("{amount:^11} ✅"), UiStyle::OK),
            ]));
        }
    }

    lines
}

fn improvement_indicator<'a>(skill: f32, previous: f32) -> Span<'a> {
    let delta = (skill - previous) / TREND_WINDOW_DAYS;

    match delta {
        d if d >= TREND_STRONG_UP => UP_ARROW_SPAN.clone(),
        d if d >= TREND_WEAK_UP => UP_RIGHT_ARROW_SPAN.clone(),
        d if d <= -TREND_STRONG_DOWN => DOWN_ARROW_SPAN.clone(),
        d if d <= -TREND_WEAK_DOWN => DOWN_RIGHT_ARROW_SPAN.clone(),
        _ => {
            // If previous was at MAX_SKILL and skill < previous, display down-right arrow,
            // so there is a visual indicator that the stat dropped (which at 20 can happen easily).
            if previous == MAX_SKILL && skill.value() < previous.value() {
                DOWN_RIGHT_ARROW_SPAN.clone()
            } else {
                Span::raw(" ")
            }
        }
    }
}

fn scouted_player_skill_spans<'a>(
    player: &Player,
    report: &ScoutReport,
    i: usize,
) -> Vec<Span<'a>> {
    const PADDING: usize = 3;
    let skill = player.skill_at_index(i);

    if report.scouting() == MAX_SKILL {
        return vec![
            Span::styled(
                format!("{:10} {:02} ", SKILL_NAMES[i], skill.value()),
                skill.style(),
            ),
            improvement_indicator(skill, player.previous_skills[i]),
            Span::raw(&SPACES[..PADDING]),
        ];
    }

    if report.is_skill_scouted(i) {
        return vec![
            Span::styled(
                format!("{:10} {:02}", SKILL_NAMES[i], skill.value()),
                skill.style(),
            ),
            Span::raw(&SPACES[..PADDING + 2]),
        ];
    }

    vec![
        Span::styled(format!("{:10} --", SKILL_NAMES[i]), UiStyle::DEFAULT),
        Span::raw(&SPACES[..PADDING + 2]),
    ]
}

fn skill_summary_span<'a>(name: &'a str) -> Span<'a> {
    let padding = MAX_NAME_LENGTH.saturating_sub(name.len()) + 5;
    Span::styled(
        format!("{name} {}", &SPACES[..padding]),
        UiStyle::HEADER.bold(),
    )
}

fn scouted_player_role_spans<'a>(player: &Player, report: &ScoutReport, i: usize) -> Vec<Span<'a>> {
    const PADDING: usize = 8;

    let value = player.game_position_fitness[i];
    let role = (i as GamePosition).as_role().to_string();

    if report.scouting() == MAX_SKILL {
        return vec![
            Span::styled(
                format!("{:2} {} ", role, value.stars()),
                value.rating().style(),
            ),
            improvement_indicator(value, player.previous_game_position_fitness[i]),
            Span::raw(&SPACES[..PADDING]),
        ];
    }

    // Stronger roles are visualized first
    if report.is_role_scouted(value) {
        return vec![
            Span::styled(
                format!("{:2} {} ", role, value.stars()),
                value.rating().style(),
            ),
            Span::raw(&SPACES[..PADDING + 1]),
        ];
    }

    vec![Span::raw(&SPACES[..PADDING + 10])]
}

fn format_player_skills<'a>(player: &'a Player, report: &ScoutReport) -> Vec<Line<'a>> {
    let mut text = vec![];
    let sorted_roles = (0..NUM_GAME_POSITIONS as usize)
        .sorted_by(|&a, &b| {
            player.game_position_fitness[b].total_cmp(&player.game_position_fitness[a])
        })
        .collect_vec();

    let mut spans = scouted_player_role_spans(player, report, sorted_roles[0]);
    spans.push(skill_summary_span("Athletics"));
    text.push(Line::from(spans));

    for i in 1..sorted_roles.len() {
        let mut spans = scouted_player_role_spans(player, report, sorted_roles[i]);
        spans.extend(scouted_player_skill_spans(player, report, i));
        text.push(Line::from(spans));
    }

    text.push(Line::default());
    text.push(Line::from(vec![
        skill_summary_span("Offense"),
        skill_summary_span("Defense"),
    ]));
    for i in 0..4 {
        let mut spans = scouted_player_skill_spans(player, report, i + 4);
        spans.extend(scouted_player_skill_spans(player, report, i + 8));
        text.push(Line::from(spans));
    }

    text.push(Line::default());
    text.push(Line::from(vec![
        skill_summary_span("Technical"),
        skill_summary_span("Mental"),
    ]));

    for i in 0..4 {
        let mut spans = scouted_player_skill_spans(player, report, i + 12);
        spans.extend(scouted_player_skill_spans(player, report, i + 16));
        text.push(Line::from(spans));
    }

    text
}

fn format_player_stats(player: &'_ Player) -> Vec<Line<'_>> {
    let stats = &player.historical_stats;
    let mut text = vec![];

    let games_played = stats.games.iter().sum::<u16>().max(1) as f32;

    text.push(Line::from(format!(
        "{:<12} W{}/L{}",
        "Games", stats.games[0], stats.games[1]
    )));

    text.push(Line::from(format!(
        "{:<12} W{}/L{}/D{}",
        "Brawls", stats.brawls[0], stats.brawls[1], stats.brawls[2]
    )));

    text.push(Line::default());
    text.push(Line::from(Span::styled(
        format!("{:<12} {:^9} {:>9}", "Stat", "Total", "Per game"),
        UiStyle::HEADER.bold(),
    )));

    let seconds_played = stats.seconds_played as Tick * SECONDS;
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9}",
        "Play time",
        if seconds_played >= DAYS {
            seconds_played.formatted_up_to_hours()
        } else {
            seconds_played.formatted()
        },
        ((seconds_played as f32 / games_played) as Tick).formatted()
    )));

    text.push(Line::from(format!(
        "{:<12} {:>+9} {:>+9.1}",
        "Plus/Minus",
        stats.plus_minus,
        stats.plus_minus as f32 / games_played
    )));

    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9}",
        "2 points",
        format!("{}/{}", stats.made_2pt, stats.attempted_2pt),
        format!(
            "{:3.1}/{:3.1}",
            stats.made_2pt as f32 / games_played,
            stats.attempted_2pt as f32 / games_played
        ),
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9}",
        "3 points",
        format!("{}/{}", stats.made_3pt, stats.attempted_3pt),
        format!(
            "{:3.1}/{:3.1}",
            stats.made_3pt as f32 / games_played,
            stats.attempted_3pt as f32 / games_played
        ),
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Points",
        stats.points,
        stats.points as f32 / games_played
    )));

    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Def Rebounds",
        stats.defensive_rebounds,
        stats.defensive_rebounds as f32 / games_played
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Off Rebounds",
        stats.offensive_rebounds,
        stats.offensive_rebounds as f32 / games_played
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Assists",
        stats.assists,
        stats.assists as f32 / games_played
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Steals",
        stats.steals,
        stats.steals as f32 / games_played
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Blocks",
        stats.blocks,
        stats.blocks as f32 / games_played
    )));
    text.push(Line::from(format!(
        "{:<12} {:>9} {:>9.1}",
        "Turnovers",
        stats.turnovers,
        stats.turnovers as f32 / games_played
    )));

    text
}

pub fn spaceship_upgrade_target_description_lines<'a>(
    upgrade_target: SpaceshipUpgradeTarget,
) -> Vec<Line<'a>> {
    match upgrade_target {
        SpaceshipUpgradeTarget::ChargeUnit { component } => {
            let mut lines = spaceship_component_description_lines(component);
            lines.append(&mut charge_unit_extra_description_lines(component));
            lines
        }
        SpaceshipUpgradeTarget::Hull { component } => {
            spaceship_component_description_lines(component)
        }
        SpaceshipUpgradeTarget::Engine { component } => {
            spaceship_component_description_lines(component)
        }
        SpaceshipUpgradeTarget::Shield { component } => {
            let mut lines = spaceship_component_description_lines(component);
            lines.append(&mut shield_extra_description_lines(component));
            lines
        }
        SpaceshipUpgradeTarget::Shooter { component } => {
            let mut lines = spaceship_component_description_lines(component);
            lines.append(&mut shooter_extra_description_lines(component));
            lines
        }
        SpaceshipUpgradeTarget::Storage { component } => {
            spaceship_component_description_lines(component)
        }
        SpaceshipUpgradeTarget::Repairs { .. } => vec![],
    }
}

fn charge_unit_extra_description_lines<'a>(component: ChargeUnit) -> Vec<Line<'a>> {
    let previous_component = component.previous();
    let mut lines = Vec::new();

    let max_charge = component.max_charge()
        - previous_component
            .map(|c| c.max_charge())
            .unwrap_or_default();
    if max_charge != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Max charge "),
            Span::styled(
                format!("{max_charge:<+.0}"),
                if max_charge > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    lines
}

fn shield_extra_description_lines<'a>(component: Shield) -> Vec<Line<'a>> {
    let previous_component = component.previous();
    let mut lines = Vec::new();

    let absorbtion = previous_component
        .map(|c| c.damage_reduction())
        .unwrap_or_default()
        - component.damage_reduction();
    if absorbtion != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Damage absorbtion "),
            Span::styled(
                format!("{:<+.0}%", (absorbtion * 100.0).round()),
                if absorbtion > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let shield_durability = component.max_durability()
        - previous_component
            .map(|c| c.max_durability())
            .unwrap_or_default();
    if shield_durability != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Shield durability "),
            Span::styled(
                format!("{shield_durability:<+.0}"),
                if shield_durability > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    lines
}

fn shooter_extra_description_lines<'a>(component: Shooter) -> Vec<Line<'a>> {
    let previous_component = component.previous();
    let mut lines = Vec::new();

    let damage = component.damage() - previous_component.map(|c| c.damage()).unwrap_or_default();
    if damage != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Damage "),
            Span::styled(
                format!("{damage:+.0}"),
                if damage > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let fire_rate = component.fire_rate()
        - previous_component
            .map(|c| c.fire_rate())
            .unwrap_or_default();
    if fire_rate != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Fire rate "),
            Span::styled(
                format!("{fire_rate:<+.0}"),
                if fire_rate > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let shooting_points = component.shooting_points().cast_signed()
        - previous_component
            .map(|c| c.shooting_points().cast_signed())
            .unwrap_or_default();
    if shooting_points != 0 {
        lines.push(Line::from(vec![
            Span::raw("Bullets "),
            Span::styled(
                format!("{shooting_points:+}"),
                if shooting_points > 0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    lines
}

fn spaceship_component_description_lines<'a, C: SpaceshipComponent>(component: C) -> Vec<Line<'a>> {
    let previous_component = component.previous();
    let mut lines = Vec::new();

    let crew = component.crew_capacity().cast_signed()
        - previous_component
            .map(|c| c.crew_capacity().cast_signed())
            .unwrap_or_default();
    if crew != 0 {
        lines.push(Line::from(vec![
            Span::raw("Crew "),
            Span::styled(
                format!("{crew:+}"),
                if crew > 0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let storage = component.storage_capacity().cast_signed()
        - previous_component
            .map(|c| c.storage_capacity().cast_signed())
            .unwrap_or_default();
    if storage != 0 {
        lines.push(Line::from(vec![
            Span::raw("Cargo "),
            Span::styled(
                format!("{storage:+}"),
                if storage > 0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let tank = component.fuel_capacity().cast_signed()
        - previous_component
            .map(|c| c.fuel_capacity().cast_signed())
            .unwrap_or_default();
    if tank != 0 {
        lines.push(Line::from(vec![
            Span::raw("Tank "),
            Span::styled(
                format!("{tank:+}"),
                if tank > 0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let consumption = component.fuel_consumption_per_tick()
        - previous_component
            .map(|c| c.fuel_consumption_per_tick())
            .unwrap_or_default();
    if consumption != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Consumption "),
            Span::styled(
                format!("{:<+.0}%", (consumption * 100.0).round()),
                if consumption < 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let speed = component.speed() - previous_component.map(|c| c.speed()).unwrap_or_default();
    if speed != 0.0 {
        lines.push(Line::from(vec![
            Span::raw("Speed "),
            Span::styled(
                format!("{:<+.0}%", (speed * 100.0).round()),
                if speed > 0.0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    let durability = component.durability().cast_signed()
        - previous_component
            .map(|c| c.durability().cast_signed())
            .unwrap_or_default();
    if durability != 0 {
        lines.push(Line::from(vec![
            Span::raw("Durability "),
            Span::styled(
                format!("{durability:+}"),
                if durability > 0 {
                    UiStyle::OK
                } else {
                    UiStyle::ERROR
                },
            ),
        ]));
    }

    lines
}

#[cfg(test)]
mod tests {
    use super::{AppResult, BARS_LENGTH};
    use crate::{
        core::{resources::Resource, spaceship::SpaceshipPrefab, team::Team},
        ui::renders::get_storage_lengths,
    };

    #[test]
    fn test_storage_spans() -> AppResult<()> {
        let mut team = Team::random(None);
        team.spaceship = SpaceshipPrefab::Bresci.spaceship();

        let bars_length = BARS_LENGTH;
        if let [gold_length, scraps_length, rum_length, free_bars] = get_storage_lengths(
            &team.resources,
            team.spaceship.storage_capacity(),
            bars_length,
        )[..4]
        {
            println!("{:?}", team.resources);
            println!(
                "gold={} scraps={} rum={} free={} storage={}/{}",
                gold_length,
                scraps_length,
                rum_length,
                free_bars,
                team.used_storage_capacity(),
                team.storage_capacity()
            );
            assert_eq!(gold_length, 0);
            assert_eq!(scraps_length, 0);
            assert_eq!(rum_length, 0);
            assert_eq!(free_bars, bars_length);
            assert_eq!(
                gold_length + scraps_length + rum_length + free_bars,
                bars_length
            );
        } else {
            panic!("Failed to calculate resource length");
        }

        team.add_resource(Resource::SCRAPS, 178)?;
        team.add_resource(Resource::RUM, 11)?;

        if let [gold_length, scraps_length, rum_length, free_bars] = get_storage_lengths(
            &team.resources,
            team.spaceship.storage_capacity(),
            bars_length,
        )[..4]
        {
            println!("{:?}", team.resources);
            println!(
                "gold={} scraps={} rum={} free={} storage={}/{}",
                gold_length,
                scraps_length,
                rum_length,
                free_bars,
                team.used_storage_capacity(),
                team.storage_capacity()
            );
            assert_eq!(gold_length, 0);
            assert_eq!(scraps_length, 21);
            assert_eq!(rum_length, 1);
            assert_eq!(free_bars, 3);
            assert_eq!(
                gold_length + scraps_length + rum_length + free_bars,
                bars_length
            );
        } else {
            panic!("Failed to calculate resource length");
        }
        team.add_resource(Resource::SCRAPS, 24)?;
        team.add_resource(Resource::GOLD, 1)?;

        if let [gold_length, scraps_length, rum_length, free_bars] = get_storage_lengths(
            &team.resources,
            team.spaceship.storage_capacity(),
            bars_length,
        )[..4]
        {
            println!("{:?}", team.resources);
            println!(
                "gold={} scraps={} rum={} free={} storage={}/{}",
                gold_length,
                scraps_length,
                rum_length,
                free_bars,
                team.used_storage_capacity(),
                team.storage_capacity()
            );
            assert_eq!(gold_length, 1);
            assert_eq!(scraps_length, 23);
            assert_eq!(rum_length, 1);
            assert_eq!(free_bars, 0);
            assert_eq!(
                gold_length + scraps_length + rum_length + free_bars,
                bars_length
            );
        } else {
            panic!("Failed to calculate resource length");
        }

        Ok(())
    }
}
