use super::{
    cr_color, img_ids::Imgs, DEFAULT_NPC, ENEMY_HP_COLOR, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER,
    HP_COLOR, LOW_HP_COLOR, QUALITY_EPIC, REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR,
    TEXT_BG, TEXT_COLOR,
};
use crate::{
    hud::{get_buff_image, get_buff_info},
    i18n::Localization,
    settings::InterfaceSettings,
    ui::{fonts::Fonts, Ingameable},
};
use common::comp::{Buffs, Energy, Health, SpeechBubble, SpeechBubbleType};
use conrod_core::{
    color,
    position::Align,
    widget::{self, Image, Rectangle, Text},
    widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
};

const MAX_BUBBLE_WIDTH: f64 = 250.0;
widget_ids! {
    struct Ids {
        // Speech bubble
        speech_bubble_text,
        speech_bubble_shadow,
        speech_bubble_top_left,
        speech_bubble_top,
        speech_bubble_top_right,
        speech_bubble_left,
        speech_bubble_mid,
        speech_bubble_right,
        speech_bubble_bottom_left,
        speech_bubble_bottom,
        speech_bubble_bottom_right,
        speech_bubble_tail,
        speech_bubble_icon,

        // Name
        name_bg,
        name,

        // HP
        level,
        level_skull,
        health_bar,
        decay_bar,
        health_bar_bg,
        health_txt,
        mana_bar,
        health_bar_fg,

        // Buffs
        buffs_align,
        buffs[],
        buff_timers[],
    }
}

#[derive(Clone, Copy)]
pub struct Info<'a> {
    pub name: &'a str,
    pub health: Option<&'a Health>,
    pub buffs: &'a Buffs,
    pub energy: Option<&'a Energy>,
    pub combat_rating: f32,
}

/// Determines whether to show the healthbar
pub fn should_show_healthbar(health: &Health) -> bool {
    health.current() != health.maximum() || health.current() < health.base_max()
}
/// Determines if there is decayed health being applied
pub fn decayed_health_displayed(health: &Health) -> bool {
    (1.0 - health.maximum() as f64 / health.base_max() as f64) > 0.0
}
/// ui widget containing everything that goes over a character's head
/// (Speech bubble, Name, Level, HP/energy bars, etc.)
#[derive(WidgetCommon)]
pub struct Overhead<'a> {
    info: Option<Info<'a>>,
    bubble: Option<&'a SpeechBubble>,
    in_group: bool,
    settings: &'a InterfaceSettings,
    pulse: f32,
    i18n: &'a Localization,
    imgs: &'a Imgs,
    fonts: &'a Fonts,

    #[conrod(common_builder)]
    common: widget::CommonBuilder,
}

impl<'a> Overhead<'a> {
    #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
    pub fn new(
        info: Option<Info<'a>>,
        bubble: Option<&'a SpeechBubble>,
        in_group: bool,
        settings: &'a InterfaceSettings,
        pulse: f32,
        i18n: &'a Localization,
        imgs: &'a Imgs,
        fonts: &'a Fonts,
    ) -> Self {
        Self {
            info,
            bubble,
            in_group,
            settings,
            pulse,
            i18n,
            imgs,
            fonts,
            common: widget::CommonBuilder::default(),
        }
    }
}

pub struct State {
    ids: Ids,
}

impl<'a> Ingameable for Overhead<'a> {
    fn prim_count(&self) -> usize {
        // Number of conrod primitives contained in the overhead display. TODO maybe
        // this could be done automatically?
        // - 2 Text::new for name
        //
        // If HP Info is shown:
        // - 1 for level: either Text or Image <-- Not used currently, will be replaced
        //   by something else
        // - 3 for HP + fg + bg
        // - 1 for HP text
        // - If there's mana
        //   - 1 Rect::new for mana
        // If there are Buffs
        // - 1 Alignment Rectangle
        // - 10 + 10 Buffs and Timer Overlays (only if there is no speech bubble)
        // If there's a speech bubble
        // - 2 Text::new for speech bubble
        // - 1 Image::new for icon
        // - 10 Image::new for speech bubble (9-slice + tail)
        self.info.map_or(0, |info| {
            2 + 1
                + if self.bubble.is_none() {
                    info.buffs.kinds.len().min(10) * 2
                } else {
                    0
                }
                + if info.health.map_or(false, |h| should_show_healthbar(h)) {
                    5 + if info.energy.is_some() { 1 } else { 0 }
                } else {
                    0
                }
                + if info.health.map_or(false, |h| decayed_health_displayed(h)) {
                    1
                } else {
                    0
                }
        }) + if self.bubble.is_some() { 13 } else { 0 }
    }
}

impl<'a> Widget for Overhead<'a> {
    type Event = ();
    type State = State;
    type Style = ();

    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
        State {
            ids: Ids::new(id_gen),
        }
    }

    fn style(&self) -> Self::Style {}

    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
        let widget::UpdateArgs { id, state, ui, .. } = args;
        const BARSIZE: f64 = 2.0; // Scaling
        const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
        const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
        if let Some(Info {
            name,
            health,
            buffs,
            energy,
            combat_rating,
        }) = self.info
        {
            // Used to set healthbar colours based on hp_percentage
            let hp_percentage = health.map_or(100.0, |h| {
                h.current() as f64 / h.base_max().max(h.maximum()) as f64 * 100.0
            });
            // Compare levels to decide if a skull is shown
            let health_current = health.map_or(1.0, |h| (h.current() / 10) as f64);
            let health_max = health.map_or(1.0, |h| (h.maximum() / 10) as f64);
            let name_y = if (health_current - health_max).abs() < 1e-6 {
                MANA_BAR_Y + 20.0
            } else {
                MANA_BAR_Y + 32.0
            };
            let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
            // Show K for numbers above 10^3 and truncate them
            // Show M for numbers above 10^6 and truncate them
            let health_cur_txt = match health_current as u32 {
                0..=999 => format!("{:.0}", health_current.max(1.0)),
                1000..=999999 => format!("{:.0}K", (health_current / 1000.0).max(1.0)),
                _ => format!("{:.0}M", (health_current as f64 / 1.0e6).max(1.0)),
            };
            let health_max_txt = match health_max as u32 {
                0..=999 => format!("{:.0}", health_max.max(1.0)),
                1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
                _ => format!("{:.0}M", (health_max as f64 / 1.0e6).max(1.0)),
            };
            // Buffs
            // Alignment
            let buff_count = buffs.kinds.len().min(11);
            Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT)
                .x_y(-1.0, name_y + 60.0)
                .parent(id)
                .set(state.ids.buffs_align, ui);

            let gen = &mut ui.widget_id_generator();
            if state.ids.buffs.len() < buff_count {
                state.update(|state| state.ids.buffs.resize(buff_count, gen));
            };
            if state.ids.buff_timers.len() < buff_count {
                state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
            };

            let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; //Animation timer
            let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
            let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
            // Create Buff Widgets
            if self.bubble.is_none() {
                state
                    .ids
                    .buffs
                    .iter()
                    .copied()
                    .zip(state.ids.buff_timers.iter().copied())
                    .zip(buffs.iter_active().map(get_buff_info))
                    .enumerate()
                    .for_each(|(i, ((id, timer_id), buff))| {
                        // Limit displayed buffs
                        let max_duration = buff.data.duration;
                        let current_duration = buff.dur;
                        let duration_percentage = current_duration.map_or(1000.0, |cur| {
                            max_duration.map_or(1000.0, |max| {
                                cur.as_secs_f32() / max.as_secs_f32() * 1000.0
                            })
                        }) as u32; // Percentage to determine which frame of the timer overlay is displayed
                        let buff_img = get_buff_image(buff.kind, &self.imgs);
                        let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
                        // Sort buffs into rows of 5 slots
                        let x = i % 5;
                        let y = i / 5;
                        let buff_widget = buff_widget.bottom_left_with_margins_on(
                            state.ids.buffs_align,
                            0.0 + y as f64 * (21.0),
                            0.0 + x as f64 * (21.0),
                        );
                        buff_widget
                            .color(
                                if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) {
                                    Some(pulsating_col)
                                } else {
                                    Some(norm_col)
                                },
                            )
                            .set(id, ui);

                        Image::new(match duration_percentage as u64 {
                            875..=1000 => self.imgs.nothing, // 8/8
                            750..=874 => self.imgs.buff_0,   // 7/8
                            625..=749 => self.imgs.buff_1,   // 6/8
                            500..=624 => self.imgs.buff_2,   // 5/8
                            375..=499 => self.imgs.buff_3,   // 4/8
                            250..=374 => self.imgs.buff_4,   // 3/8
                            125..=249 => self.imgs.buff_5,   // 2/8
                            0..=124 => self.imgs.buff_6,     // 1/8
                            _ => self.imgs.nothing,
                        })
                        .w_h(20.0, 20.0)
                        .middle_of(id)
                        .set(timer_id, ui);
                    });
            }
            // Name
            Text::new(name)
                //Text::new(&format!("{} [{:?}]", name, combat_rating)) // <- Uncomment to debug combat ratings
                .font_id(self.fonts.cyri.conrod_id)
                .font_size(font_size)
                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
                .x_y(-1.0, name_y)
                .parent(id)
                .set(state.ids.name_bg, ui);
            Text::new(name)
                //Text::new(&format!("{} [{:?}]", name, combat_rating)) // <- Uncomment to debug combat ratings
                .font_id(self.fonts.cyri.conrod_id)
                .font_size(font_size)
                .color(if self.in_group {
                    GROUP_MEMBER
                /*} else if targets player { //TODO: Add a way to see if the entity is trying to attack the player, their pet(s) or a member of their group and recolour their nametag accordingly
                DEFAULT_NPC*/
                } else {
                    DEFAULT_NPC
                })
                .x_y(0.0, name_y + 1.0)
                .parent(id)
                .set(state.ids.name, ui);

            match health {
                Some(health)
                    if should_show_healthbar(health) || decayed_health_displayed(health) =>
                {
                    // Show HP Bar
                    let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
                    let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
                    let decayed_health = 1.0 - health.maximum() as f64 / health.base_max() as f64;
                    // Background
                    Image::new(if self.in_group {self.imgs.health_bar_group_bg} else {self.imgs.enemy_health_bg})
                        .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
                        .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
                        .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
                        .parent(id)
                        .set(state.ids.health_bar_bg, ui);

                    // % HP Filling
                    let size_factor = (hp_percentage / 100.0) * BARSIZE;
                    let w = if self.in_group {
                        82.0 * size_factor
                    } else {
                        73.0 * size_factor
                    };
                    let h = 6.0 * BARSIZE;
                    let x = if self.in_group {
                        (0.0 + (hp_percentage / 100.0 * 41.0 - 41.0)) * BARSIZE
                    } else {
                        (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE
                    };
                    Image::new(self.imgs.enemy_bar)
                        .w_h(w, h)
                        .x_y(x, MANA_BAR_Y + 8.0)
                        .color(if self.in_group {
                            // Different HP bar colors only for group members
                            Some(match hp_percentage {
                                x if (0.0..25.0).contains(&x) => crit_hp_color,
                                x if (25.0..50.0).contains(&x) => LOW_HP_COLOR,
                                _ => HP_COLOR,
                            })
                        } else {
                            Some(ENEMY_HP_COLOR)
                        })
                        .parent(id)
                        .set(state.ids.health_bar, ui);

                    if decayed_health > 0.0 {
                        let x_decayed = if self.in_group {
                            (0.0 - (decayed_health * 41.0 - 41.0)) * BARSIZE
                        } else {
                            (4.5 - (decayed_health * 36.45 - 36.45)) * BARSIZE
                        };

                        let decay_bar_len = decayed_health
                            * if self.in_group {
                                82.0 * BARSIZE
                            } else {
                                73.0 * BARSIZE
                            };
                        Image::new(self.imgs.enemy_bar)
                            .w_h(decay_bar_len, h)
                            .x_y(x_decayed, MANA_BAR_Y + 8.0)
                            .color(Some(QUALITY_EPIC))
                            .parent(id)
                            .set(state.ids.decay_bar, ui);
                    }
                    let mut txt = format!("{}/{}", health_cur_txt, health_max_txt);
                    if health.is_dead {
                        txt = self.i18n.get("hud.group.dead").to_string()
                    };
                    Text::new(&txt)
                        .mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
                        .font_size(10)
                        .font_id(self.fonts.cyri.conrod_id)
                        .color(TEXT_COLOR)
                        .parent(id)
                        .set(state.ids.health_txt, ui);

                    // % Mana Filling
                    if let Some(energy) = energy {
                        let energy_factor = energy.current() as f64 / energy.maximum() as f64;
                        let size_factor = energy_factor * BARSIZE;
                        let w = if self.in_group {
                            80.0 * size_factor
                        } else {
                            72.0 * size_factor
                        };
                        let x = if self.in_group {
                            ((0.0 + (energy_factor * 40.0)) - 40.0) * BARSIZE
                        } else {
                            ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE
                        };
                        Rectangle::fill_with([w, MANA_BAR_HEIGHT], STAMINA_COLOR)
                            .x_y(
                                x, MANA_BAR_Y, //-32.0,
                            )
                            .parent(id)
                            .set(state.ids.mana_bar, ui);
                    }

                    // Foreground
                    Image::new(if self.in_group {self.imgs.health_bar_group} else {self.imgs.enemy_health})
                .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
                .x_y(0.0, MANA_BAR_Y + 6.5) //-25.5)
                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
                .parent(id)
                .set(state.ids.health_bar_fg, ui);

                    let indicator_col = cr_color(combat_rating);
                    let artifact_diffculty = 122.0;

                    if combat_rating > artifact_diffculty && !self.in_group {
                        let skull_ani =
                            ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
                        Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
                            self.imgs.skull_2
                        } else {
                            self.imgs.skull
                        })
                        .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
                        .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
                        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
                        .parent(id)
                        .set(state.ids.level_skull, ui);
                    } else {
                        Image::new(if self.in_group {
                            self.imgs.nothing
                        } else {
                            self.imgs.combat_rating_ico
                        })
                        .w_h(7.0 * BARSIZE, 7.0 * BARSIZE)
                        .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 6.0)
                        .color(Some(indicator_col))
                        .parent(id)
                        .set(state.ids.level, ui);
                    }
                }
                _ => {},
            }
        }
        // Speech bubble
        if let Some(bubble) = self.bubble {
            let dark_mode = self.settings.speech_bubble_dark_mode;
            let localizer = |s: &str, i| -> String { self.i18n.get_variation(&s, i).to_string() };
            let bubble_contents: String = bubble.message(localizer);
            let (text_color, shadow_color) = bubble_color(&bubble, dark_mode);
            let mut text = Text::new(&bubble_contents)
                .color(text_color)
                .font_id(self.fonts.cyri.conrod_id)
                .font_size(18)
                .up_from(state.ids.name, 26.0)
                .x_align_to(state.ids.name, Align::Middle)
                .parent(id);

            if let Some(w) = text.get_w(ui) {
                if w > MAX_BUBBLE_WIDTH {
                    text = text.w(MAX_BUBBLE_WIDTH);
                }
            }
            Image::new(if dark_mode {
                self.imgs.dark_bubble_top_left
            } else {
                self.imgs.speech_bubble_top_left
            })
            .w_h(16.0, 16.0)
            .top_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_top_left, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_top
            } else {
                self.imgs.speech_bubble_top
            })
            .h(16.0)
            .padded_w_of(state.ids.speech_bubble_text, -4.0)
            .mid_top_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_top, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_top_right
            } else {
                self.imgs.speech_bubble_top_right
            })
            .w_h(16.0, 16.0)
            .top_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_top_right, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_left
            } else {
                self.imgs.speech_bubble_left
            })
            .w(16.0)
            .padded_h_of(state.ids.speech_bubble_text, -4.0)
            .mid_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_left, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_mid
            } else {
                self.imgs.speech_bubble_mid
            })
            .padded_wh_of(state.ids.speech_bubble_text, -4.0)
            .top_left_with_margin_on(state.ids.speech_bubble_text, -4.0)
            .parent(id)
            .set(state.ids.speech_bubble_mid, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_right
            } else {
                self.imgs.speech_bubble_right
            })
            .w(16.0)
            .padded_h_of(state.ids.speech_bubble_text, -4.0)
            .mid_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_right, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_bottom_left
            } else {
                self.imgs.speech_bubble_bottom_left
            })
            .w_h(16.0, 16.0)
            .bottom_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_bottom_left, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_bottom
            } else {
                self.imgs.speech_bubble_bottom
            })
            .h(16.0)
            .padded_w_of(state.ids.speech_bubble_text, -4.0)
            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_bottom, ui);
            Image::new(if dark_mode {
                self.imgs.dark_bubble_bottom_right
            } else {
                self.imgs.speech_bubble_bottom_right
            })
            .w_h(16.0, 16.0)
            .bottom_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
            .parent(id)
            .set(state.ids.speech_bubble_bottom_right, ui);
            let tail = Image::new(if dark_mode {
                self.imgs.dark_bubble_tail
            } else {
                self.imgs.speech_bubble_tail
            })
            .parent(id)
            .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0);

            if dark_mode {
                tail.w_h(22.0, 13.0)
            } else {
                tail.w_h(22.0, 28.0)
            }
            .set(state.ids.speech_bubble_tail, ui);

            let mut text_shadow = Text::new(&bubble_contents)
                .color(shadow_color)
                .font_id(self.fonts.cyri.conrod_id)
                .font_size(18)
                .x_relative_to(state.ids.speech_bubble_text, 1.0)
                .y_relative_to(state.ids.speech_bubble_text, -1.0)
                .parent(id);
            // Move text to front (conrod depth is lowest first; not a z-index)
            text.depth(text_shadow.get_depth() - 1.0)
                .set(state.ids.speech_bubble_text, ui);
            if let Some(w) = text_shadow.get_w(ui) {
                if w > MAX_BUBBLE_WIDTH {
                    text_shadow = text_shadow.w(MAX_BUBBLE_WIDTH);
                }
            }
            text_shadow.set(state.ids.speech_bubble_shadow, ui);
            let icon = if self.settings.speech_bubble_icon {
                bubble_icon(&bubble, &self.imgs)
            } else {
                self.imgs.nothing
            };
            Image::new(icon)
                .w_h(16.0, 16.0)
                .top_left_with_margin_on(state.ids.speech_bubble_text, -16.0)
                // TODO: Figure out whether this should be parented.
                // .parent(id)
                .set(state.ids.speech_bubble_icon, ui);
        }
    }
}

fn bubble_color(bubble: &SpeechBubble, dark_mode: bool) -> (Color, Color) {
    let light_color = match bubble.icon {
        SpeechBubbleType::Tell => TELL_COLOR,
        SpeechBubbleType::Say => SAY_COLOR,
        SpeechBubbleType::Region => REGION_COLOR,
        SpeechBubbleType::Group => GROUP_COLOR,
        SpeechBubbleType::Faction => FACTION_COLOR,
        SpeechBubbleType::World
        | SpeechBubbleType::Quest
        | SpeechBubbleType::Trade
        | SpeechBubbleType::None => TEXT_COLOR,
    };
    if dark_mode {
        (light_color, TEXT_BG)
    } else {
        (TEXT_BG, light_color)
    }
}

fn bubble_icon(sb: &SpeechBubble, imgs: &Imgs) -> conrod_core::image::Id {
    match sb.icon {
        // One for each chat mode
        SpeechBubbleType::Tell => imgs.chat_tell_small,
        SpeechBubbleType::Say => imgs.chat_say_small,
        SpeechBubbleType::Region => imgs.chat_region_small,
        SpeechBubbleType::Group => imgs.chat_group_small,
        SpeechBubbleType::Faction => imgs.chat_faction_small,
        SpeechBubbleType::World => imgs.chat_world_small,
        SpeechBubbleType::Quest => imgs.nothing, // TODO not implemented
        SpeechBubbleType::Trade => imgs.nothing, // TODO not implemented
        SpeechBubbleType::None => imgs.nothing,  // No icon (default for npcs)
    }
}
