use oden_js::{module, ContextRef, Error, Result, Value};
use std::cell::RefCell;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc::Sender;
use std::sync::Arc;

// TODO: Fix the dumb command structures.

#[derive(Debug)]
pub struct PrintCommand {
    pub text: String,
    pub pos: [f32; 2],
}

#[derive(Debug)]
pub struct ClearCommand {
    pub color: [f64; 4],
}

#[derive(Debug)]
pub struct CreateTextureCommand {
    pub id: u32,
    pub image: image::DynamicImage,
    pub label: Option<String>,
}

#[derive(Debug)]
pub struct SpriteCommand {
    pub src_top_left: [f32; 2],
    pub src_dims: [f32; 2],

    pub dest_top_left: [f32; 2],
    pub dest_dims: [f32; 2],
}

#[derive(Debug)]
pub struct CircleCommand {
    pub center: [f32; 2],
    pub radius: f32,
    pub stroke_width: f32,
}

#[derive(Debug)]
pub enum GraphicsCommand {
    Clear(ClearCommand),
    FillColor([f32; 4]),
    StrokeColor([f32; 4]),
    Scale([f32; 2]),
    Print(PrintCommand),
    Circle(CircleCommand),
    Sprite(SpriteCommand),
    CreateTexture(CreateTextureCommand),
    CreateWritableTexture {
        id: u32,
        width: u32,
        height: u32,
        label: Option<String>,
    },
    UseTexture(u32),
    WriteToTexture(u32),
    WriteToScreen,
    EndFrame,
}

struct GraphicsImpl {
    next_texture_id: AtomicU32,
    dimensions: (f32, f32),
    sender: Sender<GraphicsCommand>,
}

impl GraphicsImpl {
    pub fn new(sender: Sender<GraphicsCommand>) -> Self {
        GraphicsImpl {
            sender,
            dimensions: (0.0, 0.0),
            next_texture_id: AtomicU32::new(0),
        }
    }

    fn print(&self, text: String, x: f32, y: f32) -> () {
        let _ = self
            .sender
            .send(GraphicsCommand::Print(PrintCommand { text, pos: [x, y] }));
    }

    fn cls(&self, r: f64, g: f64, b: f64) -> () {
        let _ = self.sender.send(GraphicsCommand::Clear(ClearCommand {
            color: [r, g, b, 1.0],
        }));
    }

    fn color(&self, r: f32, g: f32, b: f32, a: f32) -> () {
        let _ = self.sender.send(GraphicsCommand::FillColor([r, g, b, a]));
    }

    fn stroke(&self, r: f32, g: f32, b: f32, a: f32) -> () {
        let _ = self.sender.send(GraphicsCommand::StrokeColor([r, g, b, a]));
    }

    fn scale(&self, x: f32, y: f32) -> () {
        let _ = self.sender.send(GraphicsCommand::Scale([x, y]));
    }

    fn spr(&self, x: f32, y: f32, w: f32, h: f32, u: f32, v: f32, sw: f32, sh: f32) {
        let _ = self.sender.send(GraphicsCommand::Sprite(SpriteCommand {
            src_top_left: [u, v],
            src_dims: [sw, sh],
            dest_top_left: [x, y],
            dest_dims: [w, h],
        }));
    }

    fn circle(&self, x: f32, y: f32, r: f32, s: f32) {
        let _ = self.sender.send(GraphicsCommand::Circle(CircleCommand {
            center: [x, y],
            radius: r,
            stroke_width: s,
        }));
    }

    fn create_texture(
        &self,
        context: &ContextRef,
        buffer: Value,
        label: Option<String>,
    ) -> Result<u32> {
        let bytes = buffer.get_array_buffer(context)?;
        let image = match image::load_from_memory(&bytes) {
            Ok(i) => i,
            Err(e) => return Err(Error::RustFunctionError(format!("{e}"))),
        };

        let id = self.next_texture_id.fetch_add(1, Ordering::SeqCst);
        let _ = self
            .sender
            .send(GraphicsCommand::CreateTexture(CreateTextureCommand {
                id,
                image,
                label,
            }));

        Ok(id)
    }

    fn use_texture(&self, id: u32) {
        let _ = self.sender.send(GraphicsCommand::UseTexture(id));
    }

    fn create_writable_texture(
        &self,
        width: u32,
        height: u32,
        label: Option<String>,
    ) -> Result<u32> {
        let id = self.next_texture_id.fetch_add(1, Ordering::SeqCst);
        let _ = self.sender.send(GraphicsCommand::CreateWritableTexture {
            id,
            width,
            height,
            label,
        });

        Ok(id)
    }

    fn write_to_screen(&self) {
        let _ = self.sender.send(GraphicsCommand::WriteToScreen);
    }

    fn write_to_texture(&self, id: u32) {
        let _ = self.sender.send(GraphicsCommand::WriteToTexture(id));
    }

    fn get_dimensions(&self, ctx: &ContextRef) -> Result<Value> {
        let width = ctx.new_f64(self.dimensions.0)?;
        let height = ctx.new_f64(self.dimensions.1)?;

        let mut result = ctx.new_array()?;
        result.set_index(ctx, 0, &width)?;
        result.set_index(ctx, 1, &height)?;
        Ok(result)
    }
}

pub struct GraphicsAPI {
    gfx: Arc<RefCell<GraphicsImpl>>,
}

impl GraphicsAPI {
    pub fn define(ctx: &ContextRef, sender: Sender<GraphicsCommand>) -> oden_js::Result<Self> {
        let gfx = Arc::new(RefCell::new(GraphicsImpl::new(sender)));
        let mut builder = module::native::NativeModuleBuilder::new(ctx);
        {
            let gfx = gfx.clone();
            builder.export(
                "print",
                ctx.new_fn(move |_: &ContextRef, t: String, x: f32, y: f32| {
                    gfx.borrow().print(t, x, y)
                })?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "cls",
                ctx.new_fn(move |_: &ContextRef, r: f64, g: f64, b: f64| {
                    gfx.borrow().cls(r, g, b)
                })?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "color",
                ctx.new_fn(move |_: &ContextRef, r: f32, g: f32, b: f32, a: f32| {
                    gfx.borrow().color(r, g, b, a)
                })?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "stroke",
                ctx.new_fn(move |_: &ContextRef, r: f32, g: f32, b: f32, a: f32| {
                    gfx.borrow().stroke(r, g, b, a)
                })?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "scale",
                ctx.new_fn(move |_: &ContextRef, x: f32, y: f32| gfx.borrow().scale(x, y))?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "spr",
                ctx.new_fn(
                    move |_: &ContextRef,
                          x: f32,
                          y: f32,
                          w: f32,
                          h: f32,
                          u: f32,
                          v: f32,
                          sw: f32,
                          sh: f32| gfx.borrow().spr(x, y, w, h, u, v, sw, sh),
                )?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "circle",
                ctx.new_fn(move |_: &ContextRef, x: f32, y: f32, r: f32, s: f32| {
                    gfx.borrow().circle(x, y, r, s)
                })?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "use_texture",
                ctx.new_fn(move |_: &ContextRef, id: u32| gfx.borrow().use_texture(id))?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "create_texture",
                ctx.new_fn(
                    move |c: &ContextRef, buffer: Value, label: Option<String>| {
                        gfx.borrow().create_texture(c, buffer, label)
                    },
                )?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "create_writable_texture",
                ctx.new_fn(
                    move |_: &ContextRef, width: u32, height: u32, label: Option<String>| {
                        gfx.borrow().create_writable_texture(width, height, label)
                    },
                )?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "write_to_screen",
                ctx.new_fn(move |_: &ContextRef| gfx.borrow().write_to_screen())?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "write_to_texture",
                ctx.new_fn(move |_: &ContextRef, id: u32| gfx.borrow().write_to_texture(id))?,
            )?;
        }
        {
            let gfx = gfx.clone();
            builder.export(
                "get_dimensions",
                ctx.new_fn(move |ctx: &ContextRef| gfx.borrow().get_dimensions(ctx))?,
            )?;
        }

        builder.build("graphics-core")?;
        Ok(GraphicsAPI { gfx })
    }

    pub fn set_dimensions(&mut self, dimensions: (f32, f32)) {
        self.gfx.borrow_mut().dimensions = dimensions;
    }

    pub fn end_frame(&self) {
        let _ = self.gfx.borrow().sender.send(GraphicsCommand::EndFrame);
    }
}
