use crate::assets::{Animation, Frame, Spritesheet};
use crate::layer::Layer;
use image::{
    GenericImageView,
    RgbaImage,
    SubImage,
};
use smithay_client_toolkit::reexports::calloop::{
        timer::{TimeoutAction, Timer},
        InsertError,
        LoopHandle,
        RegistrationToken
};
use rand::prelude::*;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use std::time::Duration;
use tracing::{debug, trace};

// default values taken from oneko.js, simple oneko implementation in JS
// https://github.com/adryd325/oneko.js/
pub const UPDATE_INTERVAL: Duration = Duration::from_millis(100);
pub const FOLLOW_DISTANCE: f64 = 48.0;
pub const SPEED: f64 = 10.0;

pub const SPRITESHEET_IMAGE: &[u8] = include_bytes!("../assets/oneko.gif");
pub const SPRITE_WIDTH: u32 = 32;
pub const SPRITE_HEIGHT: u32 = 32;

pub struct Oneko {
    animation_state: AnimationState,
    animation_ticks: u32,
    position: (u32, u32),
    spritesheet: Spritesheet
}

impl Oneko {
    pub fn new() -> Self {
        let spritesheet_image = image::load_from_memory(SPRITESHEET_IMAGE)
            .expect("failed to load spritesheet");

        Oneko {
            animation_state: AnimationState::Idle(AnimationStateIdle::Idle),
            animation_ticks: 0,
            // TODO: place in the center / random position
            // this is okay for now, not out of range of any *real* resolution
            position: (64, 64),
            spritesheet: Spritesheet::new(spritesheet_image, (SPRITE_WIDTH, SPRITE_HEIGHT))
        }
    }

    // insert in to a loop
    // similar to `calloop_wayland_source::WaylandSource` `insert` method
    pub fn insert(
        &mut self,
        loop_handle: LoopHandle<'static, Layer>
    ) -> Result<RegistrationToken, InsertError<Timer>> {
        loop_handle
            .insert_source(Timer::from_duration(UPDATE_INTERVAL), move |_event, _metadata, layer| {
                let changed = layer.update();
                if changed {
                    trace!("animation/frame has changed, requesting repaint");

                    layer.request_repaint();
                };

                return TimeoutAction::ToDuration(UPDATE_INTERVAL)
            })
    }

    /// returns true if the frame has changed
    /// will damage the buffer as necessary
    pub fn update(
        &mut self,
        pointer_position: (f64, f64),
        resolution: (u32, u32),
        surface: &WlSurface
    ) -> bool {
        let mut rng = rand::thread_rng();

        let (resolution_x, resolution_y) = resolution;
        let (cat_x, cat_y) = self.position;
        let (cat_width, cat_height) = self.spritesheet.get_size();
        let (pointer_x, pointer_y) = pointer_position;

        let distance =
            ((cat_x as f64 - pointer_x).abs().powi(2) +
            (cat_y as f64 - pointer_y).abs().powi(2)).sqrt();

        let moving = distance > FOLLOW_DISTANCE;

        let next_movement_animation = AnimationState::Moving(AnimationStateMoving::from_vec(
            (pointer_x - cat_x as f64,
            pointer_y - cat_y as f64)
        ));
        let (_, mut next_scratch_animation, touching_wall) = [
            ((0, -1), AnimationStateScratch::ScratchWallN),
            ((1, 0), AnimationStateScratch::ScratchWallE),
            ((0, 1), AnimationStateScratch::ScratchWallS),
            ((-1, 0), AnimationStateScratch::ScratchWallW)
        ]
        .into_iter()
        .map(|((x, y), anim)| {
            let pointer_dist =
                (pointer_x - cat_width as f64 / 2.0) * x as f64 +
                (pointer_y - cat_height as f64 / 2.0) * y as f64;
            let touching = match (x, y) {
                (0, -1) => cat_y <= 0,
                (1, 0) => cat_x >= resolution_x - cat_width,
                (0, 1) => cat_y >= resolution_y - cat_height,
                (-1, 0) => cat_x <= 0,
                // safety: each tuple is a valid pair of x, y from the array, we cover all cases
                _ => unreachable!()
            };
            (pointer_dist, anim, touching)
        })
        .max_by(|(a, _, _), (b, _, _)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
        .unwrap();

        if !touching_wall {
            next_scratch_animation = AnimationStateScratch::ScratchSelf;
        }

        let next_animation_state = match self.animation_state {
            AnimationState::Moving(_) if !moving => AnimationState::Idle(AnimationStateIdle::Idle),
            AnimationState::Moving(_) if moving => next_movement_animation,
            AnimationState::Idle(_) if moving => AnimationState::Alert,
            // about every 20 seconds (assuming interval is 100ms)
            AnimationState::Idle(AnimationStateIdle::Idle) if self.animation_ticks > 10 && rng.gen_bool(1.0 / 200.0) => AnimationState::Idle(
                [
                    AnimationStateIdle::Scratch(next_scratch_animation),
                    AnimationStateIdle::Tired
                ]
                .choose(&mut rng)
                .unwrap()
                .clone()
            ),
            AnimationState::Idle(AnimationStateIdle::Scratch(_)) if self.animation_ticks > 9 => AnimationState::Idle(AnimationStateIdle::Idle),
            AnimationState::Idle(AnimationStateIdle::Tired) if self.animation_ticks > 7 => AnimationState::Idle(AnimationStateIdle::Sleeping),
            AnimationState::Idle(AnimationStateIdle::Sleeping) if self.animation_ticks > 192 => AnimationState::Idle(AnimationStateIdle::Idle),
            AnimationState::Alert if self.animation_ticks > 6 => next_movement_animation,
            _ => self.animation_state
        };

        let (delta_x, delta_y) = match next_animation_state {
            AnimationState::Moving(_) => (
                ((pointer_x - cat_x as f64) / distance * SPEED) as i32,
                ((pointer_y - cat_y as f64) / distance * SPEED) as i32
            ),
            _ => (0, 0)
        };

        self.position = (
            cat_x.saturating_add_signed(delta_x).clamp(cat_width / 2, resolution_x - cat_width / 2),
            cat_y.saturating_add_signed(delta_y).clamp(cat_height / 2, resolution_y - cat_height / 2)
        );
        let (cat_x_new, cat_y_new) = self.position;

        if next_animation_state != self.animation_state {
            debug!("animation state has changed: {:?}", next_animation_state);

            self.animation_state = next_animation_state;
            self.animation_ticks = 0;
        } else {
            self.animation_ticks += 1;
        }

        let animation = get_animation(self.animation_state);

        if (animation.frames.len() > 1 && self.animation_ticks % animation.ticks_per_frame == 0)
            || (animation.frames.len() == 1 && self.animation_ticks == 0)
        {
            trace!("animation frame has changed");

            // damage the buffer, only in changed frames
            // i'm proud of this. i tested it on a compositor that cares very much about damage regions (niri) and it works perfectly
            let (cat_width_half, cat_height_half) = (cat_width / 2, cat_height / 2);
            let (x, y) = (
                (cat_x_new - cat_width_half).min(cat_x - cat_width_half),
                (cat_y_new - cat_height_half).min(cat_y - cat_height_half)
            );
            let (width, height) = (
                (cat_x_new + cat_width_half).max(cat_x + cat_width_half) - x,
                (cat_y_new + cat_height_half).max(cat_y + cat_height_half) - y
            );
            trace!("damaging buffer: x: {}, y: {}, width: {}, height: {}", x, y, width, height);
            surface.damage_buffer(
                x as i32,
                y as i32,
                width as i32,
                height as i32
            );

            return true;
        } else {
            return false;
        }
    }

    pub fn get_frame(&self) -> SubImage<&RgbaImage> {
        let animation = get_animation(self.animation_state);
        return self.spritesheet.get_animation(animation, self.animation_ticks);
    }

    // TODO (this could go in other spots too): scaling!
    // fractional? or just integer scaling? who knows
    /// handles EXCLUSIVELY the rendering of the oneko, not the damage region
    pub fn render(
        &mut self,
        canvas: &mut [u8],
        resolution: (u32, u32)
    ) {
        let (position_x, position_y) = self.position;
        let (resolution_x, resolution_y) = resolution;
        let frame = self.get_frame();
        let (frame_width, frame_height) = frame.dimensions();

        // fill with transparent color (little-endian ARGB)
        canvas.fill(0);

        for frame_pixel_x in 0..frame_width {
            for frame_pixel_y in 0..frame_height {
                let destination_x = position_x + frame_pixel_x - frame_width / 2;
                let destination_y = position_y + frame_pixel_y - frame_height / 2;

                if destination_x >= resolution_x || destination_y >= resolution_y {
                    continue;
                }

                // raw RGBA bytes
                let mut frame_pixel = frame.get_pixel(frame_pixel_x, frame_pixel_y).0;
                let destination_index = ((destination_y * resolution_x + destination_x) * 4) as usize;

                // RGBA -> BGRA (little-endian ARGB)
                frame_pixel.swap(0, 2);

                // skip transparent pixels
                // i'm honestly not even sure why i need to do this--maia_oneko.gif has a transparent background, but the "fully transparent" pixels are still colored a transparent blue?
                if frame_pixel[3] == 0 {
                    continue;
                }

                canvas[destination_index..destination_index + 4].copy_from_slice(&frame_pixel);
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum AnimationState {
    Idle(AnimationStateIdle),
    Alert,
    Moving(AnimationStateMoving)
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum AnimationStateIdle {
    Idle,
    Scratch(AnimationStateScratch),
    Tired,
    Sleeping
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum AnimationStateScratch {
    ScratchSelf,
    ScratchWallN,
    ScratchWallS,
    ScratchWallE,
    ScratchWallW
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum AnimationStateMoving {
    N,
    NE,
    E,
    SE,
    S,
    SW,
    W,
    NW
}

impl AnimationStateMoving {
    fn from_vec((x, y): (f64, f64)) -> Self {
        enum AxisDirection {
            Negative,
            Positive,
            Zero
        }

        let distance = (x * x + y * y).sqrt();

        let x_direction = match x / distance {
            ..-0.5 => AxisDirection::Negative,
            0.5.. => AxisDirection::Positive,
            // NaN is possible, catch all
            _ => AxisDirection::Zero,
        };

        let y_direction = match y / distance {
            ..-0.5 => AxisDirection::Negative,
            0.5.. => AxisDirection::Positive,
            // NaN is possible, catch all
            _ => AxisDirection::Zero,
        };

        match (x_direction, y_direction) {
            (AxisDirection::Zero, AxisDirection::Negative) => AnimationStateMoving::N,
            (AxisDirection::Positive, AxisDirection::Negative) => AnimationStateMoving::NE,
            (AxisDirection::Positive, AxisDirection::Zero) => AnimationStateMoving::E,
            (AxisDirection::Positive, AxisDirection::Positive) => AnimationStateMoving::SE,
            (AxisDirection::Zero, AxisDirection::Positive) => AnimationStateMoving::S,
            (AxisDirection::Negative, AxisDirection::Positive) => AnimationStateMoving::SW,
            (AxisDirection::Negative, AxisDirection::Zero) => AnimationStateMoving::W,
            (AxisDirection::Negative, AxisDirection::Negative) => AnimationStateMoving::NW,
            // this is possible to be evaluated (NaN, NaN), but it won't actually be used (distance is not greater than `FOLLOW_DISTANCE`)
            // just insert a bogus value so we don't panic
            (AxisDirection::Zero, AxisDirection::Zero) => AnimationStateMoving::N
        }
    }
}

fn get_animation(state: AnimationState) -> Animation {
    match state {
        AnimationState::Idle(AnimationStateIdle::Idle) => Animation {
            frames: &[Frame(3, 3)],
            ticks_per_frame: 1
        },
        AnimationState::Idle(AnimationStateIdle::Scratch(AnimationStateScratch::ScratchSelf)) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(5, 0), Frame(6, 0), Frame(7, 0)],
        },
        AnimationState::Idle(AnimationStateIdle::Scratch(AnimationStateScratch::ScratchWallN)) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(0, 0), Frame(0, 1)],
        },
        AnimationState::Idle(AnimationStateIdle::Scratch(AnimationStateScratch::ScratchWallS)) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(7, 1), Frame(6, 2)],
        },
        AnimationState::Idle(AnimationStateIdle::Scratch(AnimationStateScratch::ScratchWallE)) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(2, 2), Frame(2, 3)],
        },
        AnimationState::Idle(AnimationStateIdle::Scratch(AnimationStateScratch::ScratchWallW)) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(4, 0), Frame(4, 1)],
        },
        AnimationState::Idle(AnimationStateIdle::Tired) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(3, 2)],
        },
        AnimationState::Idle(AnimationStateIdle::Sleeping) => Animation {
            ticks_per_frame: 4,
            frames: &[Frame(2, 0), Frame(2, 1)],
        },
        AnimationState::Alert => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(7, 3)],
        },
        AnimationState::Moving(AnimationStateMoving::N) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(1, 2), Frame(1, 3)],
        },
        AnimationState::Moving(AnimationStateMoving::NE) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(0, 2), Frame(0, 3)],
        },
        AnimationState::Moving(AnimationStateMoving::E) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(3, 0), Frame(3, 1)],
        },
        AnimationState::Moving(AnimationStateMoving::SE) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(5, 1), Frame(5, 2)],
        },
        AnimationState::Moving(AnimationStateMoving::S) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(6, 3), Frame(7, 2)],
        },
        AnimationState::Moving(AnimationStateMoving::SW) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(5, 3), Frame(6, 1)],
        },
        AnimationState::Moving(AnimationStateMoving::W) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(4, 2), Frame(4, 3)],
        },
        AnimationState::Moving(AnimationStateMoving::NW) => Animation {
            ticks_per_frame: 1,
            frames: &[Frame(1, 0), Frame(1, 1)],
        }
    }
}
