use crate::color::{get_style_alpha, set_alpha};
use crate::component::arrow::Arrow;
use crate::component::grid::Grid;
use crate::egui::{Align2, Context, Vec2, Window};
use crate::fade_in::{fade_in, fade_in_manual};
use crate::governor::Governor;
use crate::slide::Slide;
use crate::window::WindowPosition;
use eframe::egui::style::Margin;
use eframe::egui::{Color32, Frame, Ui};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

/// Conway's Game of Life, etc.
pub struct Automata {
    /// Persistent state of the grid.
    life: Grid,
    /// State of the transitions.
    state: AutomataState,
    /// For knowing when to advance the simulation.
    governor: Governor,
}

enum AutomataState {
    /// Random colors on the grid, but no simulation.
    Static {
        /// When started fading in "alive" arrow.
        alive: Option<f64>,
        /// When started fading in "alive" arrow.
        dead: Option<f64>,
        /// When rules (each) started fading in.
        rules: [Option<f64>; 3],
    },
    /// Simulation.
    Life {
        /// Whether we inverted the color and turned on fading yet.
        rule_changed: bool,
        /// Conway iteration counter.
        iterations: usize,
    },
}

impl Default for Automata {
    fn default() -> Self {
        let mut life = Grid::default();
        // Unique identifier for the grid.
        life.name = "life";

        randomize_grid(&mut life);

        Self {
            life,
            state: AutomataState::Static {
                alive: None,
                dead: None,
                rules: [None; 3],
            },
            governor: Governor::default(),
        }
    }
}

impl Slide for Automata {
    fn transition(&mut self, ctx: &Context) -> bool {
        match &mut self.state {
            AutomataState::Static { alive, dead, rules } => {
                // Enable arrows/text one by one.
                if alive.is_none() {
                    *alive = Some(ctx.input().time);
                } else if dead.is_none() {
                    *dead = Some(ctx.input().time);
                } else if rules.iter().any(|f| f.is_none()) {
                    // Fade in the rules one by one.
                    for fade_in in rules {
                        if fade_in.is_none() {
                            *fade_in = Some(ctx.input().time);
                            break;
                        }
                    }
                } else {
                    self.state = AutomataState::Life {
                        rule_changed: false,
                        iterations: 0,
                    };
                }
            }
            AutomataState::Life { rule_changed, .. } => {
                if !*rule_changed {
                    *rule_changed = true;
                } else {
                    // Done with the slide.
                    return true;
                }
            }
        }
        false
    }

    fn show(&mut self, ui: &mut Ui) {
        Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("Cellular Automata");
            });
        });

        // Need to continuously animate the grid, or at least poll the governor.
        ui.ctx().request_repaint();

        // If [`None`], don't show rules. If [`Some`], fade in rules as if they started fading in
        // at this time.
        let mut rules_fade: [Option<f64>; 3] = [None; 3];

        // How much grid goes left and rules go right.
        const HORIZONTAL_OFFSET: f32 = 250.0;

        match &mut self.state {
            AutomataState::Static { rules, .. } => {
                if let Some(first_rule_fade_start) = rules[0] {
                    let elapsed = ui.ctx().input().time - first_rule_fade_start;
                    self.life.position = WindowPosition::FromCenter(Vec2::new(
                        (elapsed as f32 * -400.0).max(-HORIZONTAL_OFFSET),
                        0.0,
                    ));
                    rules_fade = *rules;
                }
            }
            AutomataState::Life {
                rule_changed,
                iterations,
            } => {
                // Iterate grid ~5 times a second.
                if self.governor.ready(ui.ctx().input().time, 0.2) {
                    // Schedule some special events at some iteration counts.
                    let expanding = *iterations >= 10;
                    match *iterations {
                        20 => {
                            for y in 4..self.life.size_cells - 4 {
                                // Line on the left.
                                self.life.set_fill(2, y, Color32::BLACK);

                                // Line with gaps on the right.
                                if y < self.life.size_cells - 11 && y % 8 != 0 {
                                    self.life
                                        .set_fill(self.life.size_cells - 2, y, Color32::BLACK);
                                }
                            }
                        }
                        30 => {
                            // Draw another set of lines to create a cool pattern.
                            for x in 4..self.life.size_cells - 4 {
                                // Solid line at the bottom.
                                self.life
                                    .set_fill(x, self.life.size_cells - 2, Color32::BLACK);
                                // Line with gaps at the top.
                                if x % 6 != 0 {
                                    self.life.set_fill(x, 2, Color32::BLACK);
                                }
                            }
                        }
                        _ => {}
                    }

                    // Boolean to int cast yields 1 if true, 0 otherwise.
                    let expansion = (expanding && self.life.size_cells < 64) as usize;

                    // Implement rule changes.
                    let (background_color, alive_color, stroke_color, fade) = if *rule_changed {
                        (Color32::BLACK, Color32::WHITE, Color32::TRANSPARENT, true)
                    } else {
                        (Color32::TRANSPARENT, Color32::BLACK, Color32::GRAY, false)
                    };
                    self.life.background = background_color;
                    self.life.stroke = stroke_color;
                    self.life = conways_game_of_life(&self.life, expansion, alive_color, fade);
                    *iterations += 1;
                }

                // Kludge to render rules with full opacity.
                rules_fade = [Some(0.0); 3];
            }
        }

        if let Some(first_rule_fade_start) = rules_fade[0] {
            // Give some time for the grid to slide left.
            const RULES_DELAY: f64 = 0.3;
            fade_in(ui, first_rule_fade_start + RULES_DELAY, |ui| {
                Window::new("conway_rules")
                    .frame(Frame::window(&ui.style()))
                    .anchor(Align2::CENTER_CENTER, Vec2::new(HORIZONTAL_OFFSET, 0.0))
                    .title_bar(false)
                    .resizable(false)
                    .show(ui.ctx(), |ui| {
                        let rules = [
                            "Cells with fewer than two neighbors die",
                            "Cells with more than three neighbors die",
                            "Cells with three neighbors become alive",
                        ];
                        for (i, (rule, fade_start)) in rules.into_iter().zip(rules_fade).enumerate()
                        {
                            // Infinity is a kludge to make it invisible by still take up space.
                            fade_in(
                                ui,
                                fade_start.unwrap_or(f64::INFINITY)
                                    + if i == 0 { RULES_DELAY } else { 0.0 },
                                |ui| {
                                    ui.label(format!(" ⏵ {}", rule));
                                },
                            );
                        }
                    });
            });
        }

        // Hack to fade in stuff along with the slide.
        let inherent_alpha = get_style_alpha(ui.style());
        self.life.alpha = inherent_alpha;

        self.life.show_with_overlays(ui.ctx(), |ui, to_screen| {
            if let &AutomataState::Static { alive, dead, .. } = &self.state {
                if let Some(alive) = alive {
                    fade_in_manual(ui, alive, |ui, alpha| {
                        let color = set_alpha(Color32::GREEN, alpha);
                        Arrow {
                            origin: to_screen * self.life.center(4, 2),
                            tip: to_screen * self.life.center(7, 7),
                            stroke: color,
                            label: "Alive".into(),
                            ..Arrow::default()
                        }
                        .show(ui);
                    });
                }
                if let Some(dead) = dead {
                    fade_in_manual(ui, dead, |ui, alpha| {
                        let color = set_alpha(Color32::DARK_GREEN, alpha);
                        Arrow {
                            origin: to_screen * self.life.center(11, 13),
                            tip: to_screen * self.life.center(10, 10),
                            stroke: color,
                            label: "Dead".into(),
                            ..Arrow::default()
                        }
                        .show(ui);
                    });
                }
            }
        });
    }
}

/// Run one iteration of Conway's Game of Life on the grid, producing a new grid.
///
/// A non-zero expansion parameter expands the grid on each side by that many squares.
pub fn conways_game_of_life(
    grid: &Grid,
    expansion: usize,
    alive_color: Color32,
    fade: bool,
) -> Grid {
    let mut new = grid.clone();
    new.clear_fill();

    // Allocate more room on left, right, top and bottom.
    new.size_cells += expansion * 2;

    for cy in 0..grid.size_cells {
        for cx in 0..grid.size_cells {
            let mut neighbors = 0;
            for dy in -1..=1 {
                for dx in -1..=1 {
                    let (x, y) = match (cx.checked_add_signed(dx), cy.checked_add_signed(dy)) {
                        (Some(x), Some(y)) if x < grid.size_cells && y < grid.size_cells => (x, y),
                        _ => continue,
                    };
                    if x == cx && y == cy {
                        // Don't consider self.
                        continue;
                    }
                    if grid.fill(x, y).is_opaque() {
                        neighbors += 1;
                    }
                }
            }

            let was_alive = grid.fill(cx, cy).is_opaque();

            let alive = match (was_alive, neighbors) {
                (false, 3) => true,
                (false, _) => false,
                (true, 2) | (true, 3) => true,
                (true, _) => false,
            };

            // Write back to the middle of the expanded area to avoid shift.
            new.set_fill(
                cx + expansion,
                cy + expansion,
                if alive {
                    alive_color
                } else if !fade {
                    Color32::TRANSPARENT
                } else {
                    // Fade out gradually.
                    let previous_alpha = grid.fill(cx, cy).a();
                    set_alpha(alive_color, previous_alpha as f32 / (255.0 * 2.0))
                },
            );
        }
    }

    new
}

// Sets a deterministic subset of the grid (approximately 30% of it) to black.
pub fn randomize_grid(grid: &mut Grid) {
    // Seeded such that we get the same pattern every time, which is important since
    // we draw arrows to preset coordinates.
    let mut rng = ChaCha8Rng::seed_from_u64(123456789876543216);

    // Randomize grid.
    for y in 0..grid.size_cells {
        for x in 0..grid.size_cells {
            if rng.gen_bool(0.3) {
                grid.set_fill(x, y, Color32::BLACK);
            }
        }
    }
}