use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use oden_js::{
    module::loader::{ModuleLoader, ModuleSource},
    Atom, Context, ContextRef, RejectedPromiseTracker, Result, Runtime, Value,
};
use sourcemap::SourceMap;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Instant;
use tracy_client::span;
use winit::event::*;

pub mod graphics;
mod input;
mod io;
mod time;

use graphics::{ClearCommand, GraphicsCommand, PrintCommand};

mod typescript;
use typescript::transpile_to_javascript;

struct Loader {
    watcher: RecommendedWatcher,
    source_map: HashMap<Atom, SourceMap>,
}

impl Loader {
    pub fn new(reload_trigger: Sender<()>) -> Loader {
        Loader {
            watcher: notify::recommended_watcher(move |_| {
                let _ = reload_trigger.send(());
            })
            .expect("Unable to create watcher"),
            source_map: HashMap::new(),
        }
    }
}

impl ModuleLoader for Loader {
    fn load<'a>(&mut self, context: &'a ContextRef, name: &str) -> Result<ModuleSource<'a>> {
        eprintln!("Loading {name}...");
        let path = io::resolve_path(name, &["ts"])?;
        let contents = std::fs::read_to_string(&path)?;
        let contents = if path.extension().and_then(OsStr::to_str) == Some("ts") {
            let (c, sm) = transpile_to_javascript(name, contents)?;
            let a = context.new_atom(name)?;
            self.source_map.insert(a, sm);
            c
        } else {
            contents
        };

        let _ = self.watcher.watch(&path, RecursiveMode::NonRecursive);
        Ok(ModuleSource::JavaScript(contents))
    }

    fn support_source_map(&self) -> bool {
        true
    }

    fn map_source(
        &self,
        context: &ContextRef,
        file: &oden_js::AtomRef,
        line: i32,
    ) -> Result<(Atom, i32)> {
        let file = file.dup(&context);

        // Yuck.
        if let Ok(line) = line.try_into() {
            if let Some(sm) = self.source_map.get(&file) {
                if let Some(token) = sm.lookup_token(line, 0) {
                    if let Ok(src_line) = token.get_src_line().try_into() {
                        return Ok((file, src_line));
                    }
                }
            }
        }

        eprintln!("Not mapped");
        Ok((file, line))
    }
}

struct RejectedPromiseHandler {
    error_lines: Sender<String>,
}

impl RejectedPromiseTracker for RejectedPromiseHandler {
    fn on_rejected_promise(
        &self,
        ctx: &ContextRef,
        _promise: &oden_js::ValueRef,
        reason: &oden_js::ValueRef,
        is_handled: bool,
    ) {
        if !is_handled {
            let reason_str = reason.to_string(ctx).expect(
                "Unhandled rejected promise: reason unknown: unable to convert reason to string",
            );

            let reason_str = format!("Unhandled rejected promise:\n{reason_str}");
            for line in reason_str.lines().map(|l| l.to_owned()) {
                let _ = self.error_lines.send(line);
            }

            let stack = reason
                .get_property(ctx, "stack")
                .and_then(|stack| stack.to_string(ctx))
                .unwrap_or_else(|_| String::new());
            for line in stack.lines().map(|l| l.to_owned()) {
                let _ = self.error_lines.send(line);
            }
        }
    }
}

pub struct ScriptContext {
    context: Context,
    update: Value,
    draw: Value,
    suspend: Value,

    state: Value,

    gfx: graphics::GraphicsAPI,
    gfx_receive: Receiver<graphics::GraphicsCommand>,

    time: time::TimeAPI,
    input: input::InputAPI,

    error_lines: Vec<String>,
    promise_error_reciever: Receiver<String>,
}

impl ScriptContext {
    pub fn new(suspend_state: Option<Vec<u8>>, reload_trigger: Sender<()>) -> Result<Self> {
        let (promise_error_send, promise_error_recv) = channel();

        let mut runtime = Runtime::new();
        runtime.set_module_loader(Loader::new(reload_trigger));
        runtime.set_rejected_promise_tracker(RejectedPromiseHandler {
            error_lines: promise_error_send,
        });

        let mut context = Context::new(runtime);
        context.add_intrinsic_bigfloat();
        context.add_intrinsic_bigdecimal();
        context.add_intrinsic_operators();

        let (gfx_send, gfx_receive) = channel();

        let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone())?;
        let _io = io::IoAPI::define(&context)?;
        let time = time::TimeAPI::define(&context)?;
        let input = input::InputAPI::define(&context)?;

        let module = context.import_module("./main.ts", "")?;

        let init = module.get_export(&context, "init")?;
        let suspend = module.get_export(&context, "suspend")?;
        let resume = module.get_export(&context, "resume")?;
        let update = module.get_export(&context, "update")?;
        let draw = module.get_export(&context, "draw")?;

        let mut state = init.call(&context, &[])?;
        if let Some(buffer) = suspend_state {
            if !resume.is_undefined() {
                let serialized_state = context.deserialize(&buffer)?;
                state = resume.call(&context, &[&serialized_state, &state])?;
            }
        }

        Ok(ScriptContext {
            context,

            suspend,
            update,
            draw,

            state,

            gfx,
            gfx_receive,

            time,
            input,

            error_lines: Vec::new(),
            promise_error_reciever: promise_error_recv,
        })
    }

    /// Allow the script to save its state before we destroy it and re-create
    /// it.
    pub fn suspend(&self) -> Option<Vec<u8>> {
        let _span = span!("script suspend");
        if !self.suspend.is_undefined() {
            match self.suspend.call(&self.context, &[&self.state]) {
                Ok(suspend_state) => Some(
                    suspend_state
                        .serialize(&self.context)
                        .expect("Unable to serialize state"),
                ),
                Err(e) => {
                    eprintln!("WARNING: Error during suspend, state will not be saved: {e}");
                    None
                }
            }
        } else {
            None
        }
    }

    // TODO: The script could really be on a background thread you know.
    //       We would want a bi-directional gate for frames to not let the
    //       game thread go to fast probably? And to discard whole frames &c.

    pub fn input(&mut self, event: &WindowEvent) -> bool {
        match event {
            WindowEvent::KeyboardInput { input, .. } => self.input.handle_keyboard_input(input),
            _ => false,
        }
    }

    pub fn update(&mut self, dimensions: (f32, f32)) {
        let _span = span!("script update");

        self.gfx.set_dimensions(dimensions);

        if self.error_lines.len() > 0 {
            return; // Don't bother, nothing.
        }

        // Do we update the frame time before of after async completion?
        // Hmmmmm.
        self.time.set_frame_time(Instant::now());

        // Tell the runtime to process all pending "jobs". This includes
        // promise completions.
        {
            let _span = span!("process jobs");
            self.handle_result(self.context.process_all_jobs());

            // Check to make sure we don't have any rejected promises.
            while let Ok(line) = self.promise_error_reciever.try_recv() {
                self.error_lines.push(line);
            }
        }

        // Now run the update function.
        if self.error_lines.len() == 0 {
            let _span = span!("javascript update");
            let old_state = &self.state;
            self.state = match self.update.call(&self.context, &[old_state]) {
                Ok(v) => v,
                Err(e) => {
                    self.handle_error(e);
                    self.context.null()
                }
            }
        }
    }

    pub fn render(&mut self) -> Vec<graphics::GraphicsCommand> {
        let _span = span!("script render");
        if self.error_lines.len() > 0 {
            // TODO: Use font 0 for a fallback.
            // TODO: Scale!! Remember you're using a font at size 8 or something.
            let mut commands = vec![
                GraphicsCommand::Clear(ClearCommand {
                    color: [0.0, 0.0, 1.0, 1.0],
                }),
                GraphicsCommand::Scale([4.0, 4.0]),
                GraphicsCommand::Print(PrintCommand {
                    text: "FATAL SCRIPT ERROR".to_owned(),
                    pos: [8.0, 8.0],
                }),
            ];

            let mut y = 20.0;
            for line in &self.error_lines {
                commands.push(GraphicsCommand::Print(PrintCommand {
                    text: line.clone(),
                    pos: [6.0, y],
                }));
                y += 8.0;
            }

            commands
        } else {
            self.handle_result(self.draw.call(&self.context, &[&self.state]));
            self.gfx.end_frame();

            let mut commands = Vec::new();
            loop {
                match self.gfx_receive.recv().unwrap() {
                    GraphicsCommand::EndFrame => break,
                    other => commands.push(other),
                }
            }
            commands
        }
    }

    fn handle_result<T>(&mut self, result: Result<T>) {
        if let Err(e) = result {
            self.handle_error(e);
        }
    }

    fn handle_error(&mut self, error: oden_js::Error) {
        self.error_lines = error.to_string().lines().map(|l| l.to_owned()).collect();
    }
}
