use js_sys::Uint8Array;
use serde::Serialize;
use std::cell::UnsafeCell;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;

use yrs::block::ClientID;
use yrs::sync::{Awareness as YAwareness, AwarenessUpdate, Timestamp};
use yrs::updates::decoder::Decode;
use yrs::updates::encoder::Encode;

use crate::doc::YDoc;
use crate::js::{Callback, Js};

#[wasm_bindgen]
pub struct Awareness {
    inner: UnsafeCell<YAwareness>,
}

#[wasm_bindgen]
impl Awareness {
    #[wasm_bindgen(constructor)]
    pub fn new(doc: YDoc) -> Awareness {
        let inner = UnsafeCell::new(YAwareness::with_clock(doc.0.clone(), JsClock));
        Awareness { inner }
    }

    fn inner(&self) -> &YAwareness {
        unsafe { &*self.inner.get() }
    }

    fn inner_mut(&self) -> &mut YAwareness {
        unsafe { &mut *self.inner.get() }
    }

    #[wasm_bindgen(getter, js_name = doc)]
    pub fn doc(&self) -> YDoc {
        YDoc(self.inner().doc().clone())
    }

    #[wasm_bindgen(getter, js_name = meta)]
    pub fn meta(&self) -> crate::Result<js_sys::Map> {
        #[derive(Serialize)]
        struct ClientStateMeta {
            clock: u32,
            last_updated: u64,
        }
        let result = js_sys::Map::new();
        for (client_id, state) in self.inner().iter() {
            let info = ClientStateMeta {
                clock: state.clock,
                last_updated: state.last_updated,
            };
            let info = crate::js::to_js(&info).map_err(|e| JsValue::from_str(&e.to_string()))?;
            result.set(&JsValue::from_f64(client_id.get() as f64), &info);
        }
        Ok(result)
    }

    #[wasm_bindgen(js_name = destroy)]
    pub fn destroy(&self) {
        self.inner_mut().clean_local_state();
    }

    #[wasm_bindgen(js_name = getLocalState)]
    pub fn local_state(&self) -> crate::Result<JsValue> {
        match self.inner().local_state_raw() {
            None => Ok(JsValue::NULL),
            Some(js) => js_sys::JSON::parse(js.as_ref()),
        }
    }

    #[wasm_bindgen(js_name = setLocalState)]
    pub fn set_local_state(&self, state: JsValue) -> crate::Result<()> {
        let inner = self.inner_mut();
        if state.is_null() {
            inner.clean_local_state();
        } else {
            let json = js_sys::JSON::stringify(&state)?.as_string().unwrap();
            inner.set_local_state_raw(json);
        }
        Ok(())
    }

    #[wasm_bindgen(js_name = setLocalStateField)]
    pub fn set_field(&self, key: &str, value: JsValue) -> crate::Result<()> {
        let state = self.local_state()?;
        js_sys::Reflect::set(&state, &JsValue::from_str(key), &value)?;
        self.set_local_state(state)
    }

    #[wasm_bindgen(js_name = getStates)]
    pub fn states(&self) -> crate::Result<js_sys::Map> {
        let result = js_sys::Map::new();
        for (client_id, state) in self.inner().iter() {
            if let Some(data) = &state.data {
                let state = js_sys::JSON::parse(data.as_ref())?;
                result.set(&JsValue::from_f64(client_id.get() as f64), &state);
            }
        }
        Ok(result)
    }

    #[wasm_bindgen(js_name = on)]
    pub fn on(&self, event: &str, callback: js_sys::Function) -> crate::Result<()> {
        let abi = callback.subscription_key();
        let inner = self.inner_mut();
        match event {
            "update" => inner.on_update_with(abi, move |_, e, origin| {
                let json = crate::js::to_js(e.summary()).unwrap();
                let origin = match origin {
                    None => JsValue::UNDEFINED,
                    Some(origin) => Js::from(origin).into(),
                };
                callback.call2(&JsValue::NULL, &json, &origin).unwrap();
            }),
            "change" => inner.on_change_with(abi, move |_, e, origin| {
                let json = crate::js::to_js(e.summary()).unwrap();
                let origin = match origin {
                    None => JsValue::UNDEFINED,
                    Some(origin) => Js::from(origin).into(),
                };
                callback.call2(&JsValue::NULL, &json, &origin).unwrap();
            }),
            unknown => return Err(JsValue::from_str(&format!("Unknown event: {}", unknown))),
        }
        Ok(())
    }

    #[wasm_bindgen(js_name = off)]
    pub fn off(&self, event: &str, callback: js_sys::Function) -> crate::Result<bool> {
        let abi = callback.subscription_key();
        let inner = self.inner_mut();
        match event {
            "update" => Ok(inner.unobserve_update(abi)),
            "change" => Ok(inner.unobserve_change(abi)),
            unknown => return Err(JsValue::from_str(&format!("Unknown event: {}", unknown))),
        }
    }
}

#[wasm_bindgen(js_name = removeAwarenessStates)]
pub fn remove_states(awareness: &mut Awareness, clients: Vec<u64>) -> crate::Result<()> {
    let inner = awareness.inner_mut();
    for client_id in clients {
        inner.remove_state(ClientID::new(client_id));
    }
    Ok(())
}

#[wasm_bindgen(js_name = encodeAwarenessUpdate)]
pub fn encode_update(awareness: &Awareness, clients: JsValue) -> crate::Result<Uint8Array> {
    let inner = awareness.inner();
    let res = if clients.is_null() || clients.is_undefined() {
        inner.update()
    } else {
        let client_ids: Vec<u64> = serde_wasm_bindgen::from_value(clients)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        inner.update_with_clients(client_ids.into_iter().map(ClientID::new))
    };

    let update = res.map_err(|e| JsValue::from_str(&e.to_string()))?;
    let bytes = update.encode_v1();
    Ok(Uint8Array::from(bytes.as_slice()))
}

#[wasm_bindgen(js_name = modifyAwarenessUpdate)]
pub fn modify_update(update: Uint8Array, modify: js_sys::Function) -> crate::Result<Uint8Array> {
    let mut update = AwarenessUpdate::decode_v1(&update.to_vec())
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    for (client_id, state) in update.clients.iter_mut() {
        let js = js_sys::JSON::parse(&state.json)?;
        let new_state = modify.call2(&JsValue::NULL, &js, &JsValue::from(client_id.get()))?;
        let json: String = js_sys::JSON::stringify(&new_state)?.into();
        state.json = json.into();
    }
    Ok(Uint8Array::from(update.encode_v1().as_slice()))
}

#[wasm_bindgen(js_name = applyAwarenessUpdate)]
pub fn apply_update(
    awareness: &mut Awareness,
    update: Uint8Array,
    _origin: JsValue, //TODO: use origin in Awareness::apply_update
) -> crate::Result<()> {
    let inner = awareness.inner_mut();
    let update = AwarenessUpdate::decode_v1(&update.to_vec())
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    inner
        .apply_update(update)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    Ok(())
}

pub struct JsClock;

impl yrs::sync::time::Clock for JsClock {
    fn now(&self) -> Timestamp {
        js_sys::Date::now() as u64
    }
}
