#![allow(
    clippy::cast_sign_loss,
    clippy::cast_possible_wrap,
    clippy::cast_possible_truncation,
    clippy::enum_glob_use
)]

use std::{fmt, process};

use clap::Parser;
use image::{ImageBuffer, Rgb};
use rand::{
    distr::{Distribution, StandardUniform},
    Rng,
};
use serde::Deserialize;

// i'm putting my foot down
const OUTPUT_DIGITS: usize = 5;

fn main() {
    let Args {
        width,
        height,
        output_prefix,
        steps,
    } = Args::parse();

    let mut rng = rand::rng();

    let mut game = Game::new_random(width, height, &mut rng);
    let mut last = game.clone();
    for i in 0..steps {
        let GameStep { old, new } = game.step(last, &mut rng);
        game = new;
        last = old;

        let path = format!("{output_prefix}{i:0OUTPUT_DIGITS$}.png");
        // TODO: reuse `ImageBuffer`
        if let Err(e) = ImageBuffer::from(&game).save(path) {
            eprintln!("error saving image: {e}");
            process::exit(1);
        }
    }
}

#[derive(Debug, clap::Parser)]
#[command(version, about, long_about = None)]
struct Args {
    /// Width of the output images (pixel-widths)
    width: u16,
    /// Width of the output images (pixel-widths)
    height: u16,
    /// Prefix for image paths; ex: /tmp/example- -> /tmp/example-000.png ...
    #[arg(short, long)]
    output_prefix: String,
    /// How many steps to run the automata
    #[arg(short, long)]
    steps: u64,
}

#[derive(Debug, Clone)]
struct Game {
    width: i64,
    height: i64,
    field: Vec<Type>,
}

#[derive(Debug, Clone)]
struct GameStep {
    old: Game,
    new: Game,
}

impl Game {
    fn new_random(width: u16, height: u16, rng: &mut impl Rng) -> Self {
        let (width, height) = (i64::from(width), i64::from(height));
        Self {
            width,
            height,
            field: (0..(width * height)).map(|_| rng.random()).collect(),
        }
    }

    fn in_bounds(&self, x: i64, y: i64) -> bool {
        (0..self.width).contains(&x) && (0..self.height).contains(&y)
    }

    fn at(&self, x: i64, y: i64) -> Option<Type> {
        if self.in_bounds(x, y) {
            self.field.get((x + y * self.width) as usize).copied()
        } else {
            None
        }
    }

    fn set(&mut self, x: i64, y: i64, to: Type) {
        if self.in_bounds(x, y) {
            self.field[(x + y * self.width) as usize] = to;
        }
    }

    // let's see if we can get away with not swapping the field
    // nah it's easy
    fn step(self, mut next: Self, rng: &mut impl Rng) -> GameStep {
        for x in 0..self.width {
            for y in 0..self.height {
                let dx = rng.random_range(-1..=1);
                let dy = rng.random_range(-1..=1);
                let (ox, oy) = (x + dx, y + dy);

                if (x, y) == (ox, oy) {
                    continue;
                }

                let Some(other) = self.at(ox, oy) else {
                    continue;
                };
                let here = self.at(x, y).unwrap();
                let won = rng.random::<f32>() <= here.win_chance(other);
                if won {
                    next.set(ox, oy, here);
                } else {
                    next.set(x, y, other);
                }
            }
        }
        GameStep {
            old: self,
            new: next,
        }
    }
}

impl From<&Game> for ImageBuffer<Rgb<u8>, Vec<u8>> {
    fn from(val: &Game) -> Self {
        ImageBuffer::from_fn(val.width as u32, val.height as u32, |x, y| {
            val.at(i64::from(x), i64::from(y)).unwrap().color()
        })
    }
}

#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone, Copy)]
enum Type {
    Normal,
    Fire,
    Water,
    Electric,
    Grass,
    Ice,
    Fighting,
    Poison,
    Ground,
    Flying,
    Psychic,
    Bug,
    Rock,
    Ghost,
    Dragon,
    Dark,
    Steel,
    Fairy,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Efficacy {
    Strong,
    Neutral,
    Weak,
    Immune,
}

impl Type {
    const UNIVERSE: [Type; 18] = [
        Self::Normal,
        Self::Fire,
        Self::Water,
        Self::Electric,
        Self::Grass,
        Self::Ice,
        Self::Fighting,
        Self::Poison,
        Self::Ground,
        Self::Flying,
        Self::Psychic,
        Self::Bug,
        Self::Rock,
        Self::Ghost,
        Self::Dragon,
        Self::Dark,
        Self::Steel,
        Self::Fairy,
    ];

    // https://gist.github.com/apaleslimghost/0d25ec801ca4fc43317bcff298af43c3
    fn color(self) -> Rgb<u8> {
        use Type::*;
        match self {
            Normal => Rgb([0xA8, 0xA7, 0x7A]),
            Fire => Rgb([0xEE, 0x81, 0x30]),
            Water => Rgb([0x63, 0x90, 0xF0]),
            Electric => Rgb([0xF7, 0xD0, 0x2C]),
            Grass => Rgb([0x7A, 0xC7, 0x4C]),
            Ice => Rgb([0x96, 0xD9, 0xD6]),
            Fighting => Rgb([0xC2, 0x2E, 0x28]),
            Poison => Rgb([0xA3, 0x3E, 0xA1]),
            Ground => Rgb([0xE2, 0xBF, 0x65]),
            Flying => Rgb([0xA9, 0x8F, 0xF3]),
            Psychic => Rgb([0xF9, 0x55, 0x87]),
            Bug => Rgb([0xA6, 0xB9, 0x1A]),
            Rock => Rgb([0xB6, 0xA1, 0x36]),
            Ghost => Rgb([0x73, 0x57, 0x97]),
            Dragon => Rgb([0x6F, 0x35, 0xFC]),
            Dark => Rgb([0x70, 0x57, 0x46]),
            Steel => Rgb([0xB7, 0xB7, 0xCE]),
            Fairy => Rgb([0xD6, 0x85, 0xAD]),
        }
    }

    // nothing is immune to itself, phew
    fn win_chance(self, other: Type) -> f32 {
        use Efficacy::{Immune, Neutral, Strong, Weak};
        let self_eff = self.efficacy(other);
        let def_eff = other.efficacy(self);
        // if you are twice as effective, you should win twice as often: 2/3 vs 1/3
        // if you are four times as effective, you should win four times as often: 4/5 vs 1/5
        match (self_eff, def_eff) {
            (Strong, Strong) | (Neutral, Neutral) | (Weak, Weak) | (Immune, Immune) => 0.5,
            (Immune, _) => 1.0,
            (_, Immune) => 0.0,
            (Efficacy::Strong, Efficacy::Neutral) | (Efficacy::Neutral, Efficacy::Weak) => 2. / 3.,
            (Efficacy::Neutral, Efficacy::Strong) | (Efficacy::Weak, Efficacy::Neutral) => 1. / 3.,
            (Efficacy::Strong, Efficacy::Weak) => 4. / 5.,
            (Efficacy::Weak, Efficacy::Strong) => 1. / 5.,
        }
    }

    // fudge it, i'm hard-coding
    // https://github.com/filipekiss/pokemon-type-chart/blob/master/types.json
    // great lint normally but not here
    #[allow(clippy::match_same_arms)]
    fn efficacy(self, defender: Type) -> Efficacy {
        use Efficacy::*;
        use Type::*;
        match (self, defender) {
            (Normal, Ghost) => Immune,
            (Normal, Rock | Steel) => Weak,
            (Normal, _) => Neutral,
            (Fire, Fire | Water | Rock | Dragon) => Weak,
            (Fire, Grass | Ice | Bug | Steel) => Strong,
            (Fire, _) => Neutral,
            (Water, Water | Grass | Dragon) => Weak,
            (Water, Fire | Ground | Rock) => Strong,
            (Water, _) => Neutral,
            (Electric, Ground) => Immune,
            (Electric, Electric | Grass | Dragon) => Weak,
            (Electric, Water | Flying) => Strong,
            (Electric, _) => Neutral,
            (Grass, Fire | Grass | Poison | Flying | Bug | Dragon | Steel) => Weak,
            (Grass, Water | Ground | Rock) => Strong,
            (Grass, _) => Neutral,
            (Ice, Fire | Water | Ice | Steel) => Weak,
            (Ice, Grass | Ground | Flying | Dragon) => Strong,
            (Ice, _) => Neutral,
            (Fighting, Ghost) => Immune,
            (Fighting, Poison | Flying | Psychic | Bug | Fairy) => Weak,
            (Fighting, Normal | Ice | Rock | Dark | Steel) => Strong,
            (Fighting, _) => Neutral,
            (Poison, Steel) => Immune,
            (Poison, Poison | Ground | Rock | Ghost) => Weak,
            (Poison, Grass | Fairy) => Strong,
            (Poison, _) => Neutral,
            (Ground, Flying) => Immune,
            (Ground, Grass | Bug) => Weak,
            (Ground, Fire | Electric | Poison | Rock | Steel) => Strong,
            (Ground, _) => Neutral,
            (Flying, Electric | Rock | Steel) => Weak,
            (Flying, Grass | Fighting | Bug) => Strong,
            (Flying, _) => Neutral,
            (Psychic, Dark) => Immune,
            (Psychic, Psychic | Steel) => Weak,
            (Psychic, Fighting | Poison) => Strong,
            (Psychic, _) => Neutral,
            (Bug, Fire | Fighting | Poison | Flying | Ghost | Steel | Fairy) => Weak,
            (Bug, Grass | Psychic | Dark) => Strong,
            (Bug, _) => Neutral,
            (Rock, Fighting | Ground | Steel) => Weak,
            (Rock, Fire | Ice | Flying | Bug) => Strong,
            (Rock, _) => Neutral,
            (Ghost, Normal) => Immune,
            (Ghost, Dark) => Weak,
            (Ghost, Psychic | Ghost) => Strong,
            (Ghost, _) => Neutral,
            (Dragon, Fairy) => Immune,
            (Dragon, Steel) => Weak,
            (Dragon, Dragon) => Strong,
            (Dragon, _) => Neutral,
            (Dark, Fighting | Dark | Fairy) => Weak,
            (Dark, Psychic | Ghost) => Strong,
            (Dark, _) => Neutral,
            (Steel, Fire | Water | Electric | Steel) => Weak,
            (Steel, Ice | Rock | Fairy) => Strong,
            (Steel, _) => Neutral,
            (Fairy, Fire | Poison | Steel) => Weak,
            (Fairy, Fighting | Dragon | Dark) => Strong,
            (Fairy, _) => Neutral,
        }
    }
}

impl Distribution<Type> for StandardUniform {
    fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Type {
        Type::UNIVERSE[rng.random_range(0..(Type::UNIVERSE.len()))]
    }
}

impl fmt::Display for Type {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self:?}")
    }
}