use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use crate::value::Value;
use crate::vm::Panic;
use imbl::Vector;
use std::rc::Rc;


#[wasm_bindgen(module = "/pkg/worker.js")]
extern "C" {
    #[wasm_bindgen(catch)]
    async fn io (output: String) -> Result<JsValue, JsValue>;
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: String);
}

type Lines = Value; // expect a list of values
type Commands = Value; // expect a list of values
type Url = Value; // expect a string representing a URL
type FinalValue = Result<Value, Panic>;

fn make_json_payload(verb: &'static str, data: String) -> String {
    format!("{{\"verb\":\"{verb}\",\"data\":{data}}}")
}

#[derive(Debug, Clone, PartialEq)]
pub enum MsgOut {
    Console(Lines), 
    Commands(Commands), 
    Fetch(Url), 
    Complete(FinalValue),
    Ready
}

impl std::fmt::Display for MsgOut {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.to_json())
    }
}

impl MsgOut {
    pub fn to_json(&self) -> String {
        match self {
            MsgOut::Complete(value) => match value {
                Ok(value) => {
                    make_json_payload("Complete", serde_json::to_string(&value.show()).unwrap())
                },
                Err(_) => make_json_payload("Complete", "\"null\"".to_string())
            },
            MsgOut::Commands(commands) => {
                let commands = commands.as_list();
                let vals_json = commands.iter().map(|v| v.to_json().unwrap()).collect::<Vec<_>>().join(",");
                let vals_json = format!("[{vals_json}]");
                make_json_payload("Commands", vals_json)
            }
            MsgOut::Fetch(value) => {
                // TODO: do parsing here?
                // Right now, defer to fetch
                let url = value.to_json().unwrap();
                make_json_payload("Fetch", url)
            }
            MsgOut::Console(lines) => {
                let lines = lines.as_list();
                let json_lines = lines.iter().map(|line| line.stringify()).collect::<Vec<_>>().join("\\n");
                let json_lines = format!("\"{json_lines}\"");
                make_json_payload("Console", json_lines)
            }
            MsgOut::Ready => {
                make_json_payload("Ready", "\"null\"".to_string())
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "verb", content = "data")]
pub enum MsgIn {
    Input(String),
    Fetch(String, f64, String),
    Kill,
    Keyboard(Vec<String>),
}

impl std::fmt::Display for MsgIn {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MsgIn::Input(str) => write!(f, "Input: {str}"),
            MsgIn::Kill => write!(f, "Kill"),
            MsgIn::Fetch(url, code, text) => write!(f, "Fetch: {url} :: {code} ::\n{text}"),
            _ => todo!()
        }
    }
}

impl MsgIn {
    pub fn to_value(self) -> Value {
        match self {
            MsgIn::Input(str) => Value::string(str),
            MsgIn::Fetch(url, status_f64, string) => {
                let url = Value::string(url);
                let status = Value::Number(status_f64);
                let text = Value::string(string);
                let result_tuple = if status_f64 == 200.0 {
                    Value::tuple(vec![Value::keyword("ok".to_string()), text])
                } else {
                    Value::tuple(vec![Value::keyword("err".to_string()), status])
                };
                Value::tuple(vec![url, result_tuple])
            }
            MsgIn::Kill => Value::Nothing,
            MsgIn::Keyboard(downkeys) => {
                let mut vector = Vector::new();
                for key in downkeys {
                    vector.push_back(Value::String(Rc::new(key)));
                }
                Value::List(Box::new(vector))
            }
        }
    }
}

pub async fn do_io (msgs: Vec<MsgOut>) -> Vec<MsgIn> {
    let outbox = format!("[{}]", msgs.iter().map(|msg| msg.to_json()).collect::<Vec<_>>().join(","));
    let inbox = io (outbox).await;
    // if our request dies, make sure we return back to the event loop
    let inbox = match inbox {
        Ok(msgs) => msgs,
        Err(_) => return vec![]
    };
    let inbox = inbox.as_string().expect("response should be a string");
    // log(format!("got a message: {inbox}"));
    let inbox: Vec<MsgIn> = serde_json::from_str(inbox.as_str()).expect("response from js should be valid");
    if !inbox.is_empty() {
        log("ludus received messages".to_string());
        for msg in inbox.iter() {
            log(format!("{}", msg));
        }
    }
    inbox
}

