use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
    window, CanvasRenderingContext2d, Document, HtmlCanvasElement, KeyboardEvent, WheelEvent,
};

#[derive(Default, Clone, Copy)]
struct InputState {
    w: bool,
    a: bool,
    s: bool,
    d: bool,
}

struct Camera {
    x: f64,
    y: f64,
    ppm: f64,
}

struct SceneState {
    camera: Camera,
    speed_pps: f64,
    triangles: Vec<(&'static str, [(f64, f64); 3])>,
    need_camera_setup: bool,
}

impl SceneState {
    fn new() -> Self {
        Self {
            camera: Camera {
                x: 0.0,
                y: 0.0,
                ppm: 10.0,
            },
            speed_pps: 900.0,
            triangles: vec![
                ("#ff6b6b", [(0.0, 0.0), (3.0, 0.0), (1.5, 2.5)]),
                ("#4dabf7", [(-4.0, -2.0), (-1.0, -3.5), (-2.5, 0.5)]),
                ("#ffd43b", [(2.0, -4.0), (5.0, -4.0), (4.0, -1.0)]),
            ],
            need_camera_setup: true,
        }
    }

    fn setup_camera_if_needed(&mut self, width: f64, height: f64) {
        if !self.need_camera_setup {
            return;
        }

        if width <= 0.0 || height <= 0.0 || self.camera.ppm <= 0.0 {
            return;
        }

        self.camera.x = -width / (2.0 * self.camera.ppm);
        self.camera.y = -height / (2.0 * self.camera.ppm);
        self.need_camera_setup = false;
    }
}

fn update_key(state: &mut InputState, code: &str, is_down: bool) -> bool {
    match code {
        "KeyW" => {
            state.w = is_down;
            true
        }
        "KeyA" => {
            state.a = is_down;
            true
        }
        "KeyS" => {
            state.s = is_down;
            true
        }
        "KeyD" => {
            state.d = is_down;
            true
        }
        _ => false,
    }
}

#[wasm_bindgen]
pub fn init_board() -> Result<(), JsValue> {
    let document: Document = window().unwrap().document().unwrap();
    let canvas: HtmlCanvasElement = document
        .get_element_by_id("canvas")
        .ok_or_else(|| JsValue::from_str("Missing #canvas element"))?
        .dyn_into()?;

    let context: CanvasRenderingContext2d = canvas
        .get_context("2d")?
        .ok_or_else(|| JsValue::from_str("Missing 2d context"))?
        .dyn_into()?;

    let window = window().unwrap();
    let canvas = canvas.clone();
    let context = context.clone();

    let input = Rc::new(RefCell::new(InputState::default()));
    let scene = Rc::new(RefCell::new(SceneState::new()));
    let last_time = Rc::new(RefCell::new(
        window
            .performance()
            .ok_or_else(|| JsValue::from_str("Missing performance"))?
            .now(),
    ));

    let input_down = input.clone();
    let keydown = Closure::<dyn FnMut(KeyboardEvent)>::wrap(Box::new(move |event: KeyboardEvent| {
        let code = event.code();
        let mut state = input_down.borrow_mut();
        if update_key(&mut state, &code, true) {
            event.prevent_default();
        }
    }));
    window.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref())?;
    keydown.forget();

    let input_up = input.clone();
    let keyup = Closure::<dyn FnMut(KeyboardEvent)>::wrap(Box::new(move |event: KeyboardEvent| {
        let code = event.code();
        let mut state = input_up.borrow_mut();
        if update_key(&mut state, &code, false) {
            event.prevent_default();
        }
    }));
    window.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref())?;
    keyup.forget();

    let scene_zoom = scene.clone();
    let canvas_zoom = canvas.clone();
    let wheel = Closure::<dyn FnMut(WheelEvent)>::wrap(Box::new(move |event: WheelEvent| {
        event.prevent_default();

        let pointer_x = event.offset_x() as f64;
        let pointer_y = event.offset_y() as f64;

        let mut scene = scene_zoom.borrow_mut();
        let old_ppm = scene.camera.ppm;
        let zoom_factor = (-event.delta_y() * 0.001).exp();
        let mut new_ppm = old_ppm * zoom_factor;
        let min_ppm = 2.0;
        let max_ppm = 200.0;

        if new_ppm < min_ppm {
            new_ppm = min_ppm;
        }
        if new_ppm > max_ppm {
            new_ppm = max_ppm;
        }

        let world_x = scene.camera.x + pointer_x / old_ppm;
        let world_y = scene.camera.y + pointer_y / old_ppm;

        scene.camera.ppm = new_ppm;
        scene.camera.x = world_x - pointer_x / new_ppm;
        scene.camera.y = world_y - pointer_y / new_ppm;
    }));
    canvas_zoom.add_event_listener_with_callback("wheel", wheel.as_ref().unchecked_ref())?;
    wheel.forget();

    let draw = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
    let draw_handle = draw.clone();
    let window_handle = window.clone();
    let input_handle = input.clone();
    let scene_handle = scene.clone();
    let last_time_handle = last_time.clone();

    *draw_handle.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        let now = window_handle
            .performance()
            .map(|perf| perf.now())
            .unwrap_or(0.0);
        let mut last = last_time_handle.borrow_mut();
        let mut dt = (now - *last) / 1000.0;
        *last = now;

        if dt.is_nan() || dt.is_infinite() {
            dt = 0.0;
        }
        if dt > 0.05 {
            dt = 0.05;
        }

        let width = window_handle
            .inner_width()
            .ok()
            .and_then(|v| v.as_f64())
            .unwrap_or(0.0);
        let height = window_handle
            .inner_height()
            .ok()
            .and_then(|v| v.as_f64())
            .unwrap_or(0.0);

        canvas.set_width(width as u32);
        canvas.set_height(height as u32);

        let input = input_handle.borrow();
        let mut scene = scene_handle.borrow_mut();
        scene.setup_camera_if_needed(width, height);
        let step = (scene.speed_pps / scene.camera.ppm) * dt;

        if input.w {
            scene.camera.y -= step;
        }
        if input.s {
            scene.camera.y += step;
        }
        if input.a {
            scene.camera.x -= step;
        }
        if input.d {
            scene.camera.x += step;
        }

        context.set_fill_style(&JsValue::from_str("#0b0f17"));
        context.fill_rect(0.0, 0.0, width, height);
        context.set_line_width(1.0);
        context.set_stroke_style(&JsValue::from_str("#111827"));

        let to_screen = |(x, y): (f64, f64)| -> (f64, f64) {
            let sx = (x - scene.camera.x) * scene.camera.ppm;
            let sy = (y - scene.camera.y) * scene.camera.ppm;
            (sx, sy)
        };

        for (color, points) in scene.triangles.iter() {
            let (x0, y0) = to_screen(points[0]);
            let (x1, y1) = to_screen(points[1]);
            let (x2, y2) = to_screen(points[2]);

            context.begin_path();
            context.move_to(x0, y0);
            context.line_to(x1, y1);
            context.line_to(x2, y2);
            context.close_path();

            context.set_fill_style(&JsValue::from_str(color));
            context.fill();
            context.stroke();
        }

        let box_world_x = 15.0;
        let box_world_y = 3.0;
        let box_world_w = 6.0;
        let box_world_h = 2.0;
        let (box_x, box_y) = to_screen((box_world_x, box_world_y));
        let (box_x2, box_y2) = to_screen((box_world_x + box_world_w, box_world_y + box_world_h));
        let box_w = box_x2 - box_x;
        let box_h = box_y2 - box_y;

        context.set_fill_style(&JsValue::from_str("rgba(17, 24, 39, 0.85)"));
        context.fill_rect(box_x, box_y, box_w, box_h);
        context.set_stroke_style(&JsValue::from_str("#e5e7eb"));
        context.stroke_rect(box_x, box_y, box_w, box_h);

        let font_px = (0.8 * scene.camera.ppm).clamp(10.0, 64.0);
        context.set_font(&format!("{:.0}px sans-serif", font_px));
        context.set_text_baseline("top");
        context.set_fill_style(&JsValue::from_str("#e5e7eb"));
        let _ = context.fill_text("123\nllo-world hello-world hello-world \n ========== hello-world \n hello-world ", box_x + 6.0, box_y + 4.0);

        let _ = window_handle.request_animation_frame(
            draw.borrow().as_ref().unwrap().as_ref().unchecked_ref(),
        );
    }) as Box<dyn FnMut()>));

    window.request_animation_frame(
        draw_handle
            .borrow()
            .as_ref()
            .unwrap()
            .as_ref()
            .unchecked_ref(),
    )?;

    Ok(())
}
