use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use ratatui::{
    buffer::Buffer,
    layout::{Flex, Layout, Rect},
    style::{Color, Style},
    text::Text,
    widgets::Widget,
    Frame,
};

/// delay the start of the animation so it doesn't start immediately
const DELAY: usize = 120;
/// higher means more pixels per frame are modified in the animation
const DRIP_SPEED: usize = 500;
/// delay the start of the text animation so it doesn't start immediately after the initial delay
const TEXT_DELAY: usize = 180;

/// Destroy mode activated by pressing `d`
pub fn destroy(frame: &mut Frame<'_>) {
    let frame_count = frame.count().saturating_sub(DELAY);
    if frame_count == 0 {
        return;
    }

    let area = frame.area();
    let buf = frame.buffer_mut();

    drip(frame_count, area, buf);
    text(frame_count, area, buf);
}

/// Move a bunch of random pixels down one row.
///
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
/// do this, but it works well enough for this demo.
#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)]
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
    // a seeded rng as we have to move the same random pixels each frame
    let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
    let ramp_frames = 450;
    let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
    let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
    let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
    for _ in 0..pixel_count {
        let src_x = rng.gen_range(0..area.width);
        let src_y = rng.gen_range(1..area.height - 2);
        let src = buf[(src_x, src_y)].clone();
        // 1% of the time, move a blank or pixel (10:1) to the top line of the screen
        if rng.gen_ratio(1, 100) {
            let dest_x = rng
                .gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
                .clamp(area.left(), area.right() - 1);
            let dest_y = area.top() + 1;

            let dest = &mut buf[(dest_x, dest_y)];
            // copy the cell to the new location about 1/10 of the time blank out the cell the rest
            // of the time. This has the effect of gradually removing the pixels from the screen.
            if rng.gen_ratio(1, 10) {
                *dest = src;
            } else {
                dest.reset();
            }
        } else {
            // move the pixel down one row
            let dest_x = src_x;
            let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
            // copy the cell to the new location
            buf[(dest_x, dest_y)] = src;
        }
    }
}

/// draw some text fading in and out from black to red and back
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
    let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
    if sub_frame == 0 {
        return;
    }

    let logo = indoc::indoc! {"
        ██████      ████    ██████    ████    ██████  ██    ██  ██
        ██    ██  ██    ██    ██    ██    ██    ██    ██    ██  ██
        ██████    ████████    ██    ████████    ██    ██    ██  ██
        ██  ██    ██    ██    ██    ██    ██    ██    ██    ██  ██
        ██    ██  ██    ██    ██    ██    ██    ██      ████    ██
    "};
    let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
    let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);

    let mask_buf = &mut Buffer::empty(area);
    logo_text.render(area, mask_buf);

    let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);

    for row in area.rows() {
        for col in row.columns() {
            let cell = &mut buf[(col.x, col.y)];
            let mask_cell = &mut mask_buf[(col.x, col.y)];
            cell.set_symbol(mask_cell.symbol());

            // blend the mask cell color with the cell color
            let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
            let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));

            let color = blend(mask_color, cell_color, percentage);
            cell.set_style(Style::new().fg(color));
        }
    }
}

fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
    let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
        return mask_color;
    };
    let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
        return mask_color;
    };

    let remain = 1.0 - percentage;

    let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
    let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
    let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);

    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    Color::Rgb(red as u8, green as u8, blue as u8)
}

/// a centered rect of the given size
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
    let horizontal = Layout::horizontal([width]).flex(Flex::Center);
    let vertical = Layout::vertical([height]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}
