use std::cell::RefCell;
use std::rc::Rc;

use js_sys::{Array, Object, Reflect, Uint8Array};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};

const HASH_MARKER: &str = "\nreport-source-sha256:";
const PROTOCOL_VERSION: u16 = 4;
const WORKER_URL: &str = "ocs-parse-worker.js?v=4";

#[derive(serde::Deserialize)]
struct EntityRuntimeFields {
    handle: u64,
    linetype_handle: Option<u64>,
    color_book_handle: Option<u64>,
    face_visual_style_handle: Option<u64>,
    edge_visual_style_handle: Option<u64>,
    material_flags: u8,
    material_handle: Option<u64>,
    shadow_flags: u8,
    plotstyle_flags: u8,
    plotstyle_handle: Option<u64>,
    entity_mode: Option<u8>,
    has_ds_data: bool,
}

pub(super) async fn parse_document(
    name: &str,
    bytes: &[u8],
    recovery_mode: bool,
    initial_error: &str,
) -> Result<(acadrust::ReadOutcome, Option<String>), super::OpenLoadError> {
    let options = WorkerOptions::new();
    options.set_type(WorkerType::Module);
    let worker = Worker::new_with_options(WORKER_URL, &options)
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;

    let (sender, receiver) = iced::futures::channel::oneshot::channel::<
        Result<(acadrust::ReadOutcome, Option<String>), super::OpenLoadError>,
    >();
    let sender = Rc::new(RefCell::new(Some(sender)));
    let message_sender = sender.clone();
    let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
        let data = event.data();
        let ok = Reflect::get(&data, &JsValue::from_str("ok"))
            .ok()
            .and_then(|value| value.as_bool())
            .unwrap_or(false);
        let result = if ok {
            Reflect::get(&data, &JsValue::from_str("data"))
                .map_err(|error| super::OpenLoadError::from(js_error(error)))
                .and_then(|value| {
                    let bytes = Uint8Array::new(&value).to_vec();
                    let payload: (
                        u16,
                        Result<
                            acadrust::ReadOutcome,
                            (String, Option<acadrust::ReadStats>),
                        >,
                        Option<String>,
                        bool,
                        Vec<EntityRuntimeFields>,
                    ) = bincode::deserialize(&bytes)
                        .map_err(|error| super::OpenLoadError::from(error.to_string()))?;
                    if payload.0 != PROTOCOL_VERSION {
                        return Err(super::OpenLoadError::from(format!(
                            "parser worker protocol mismatch: expected {}, received {}",
                            PROTOCOL_VERSION, payload.0
                        )));
                    }
                    match payload.1 {
                        Ok(mut outcome) => {
                            restore_entity_runtime_fields(
                                &mut outcome.document,
                                payload.4,
                            );
                            Ok((outcome, payload.2))
                        }
                        Err((message, read_stats)) => Err(super::OpenLoadError {
                            message,
                            source_sha256: payload.2,
                            read_stats,
                            recovery_available: payload.3,
                        }),
                    }
                })
        } else {
            let message = Reflect::get(&data, &JsValue::from_str("error"))
                .ok()
                .and_then(|value| value.as_string())
                .unwrap_or_else(|| "CAD parser worker failed".to_string());
            Err(decode_worker_error(message))
        };
        if let Some(sender) = message_sender.borrow_mut().take() {
            let _ = sender.send(result);
        }
    });
    worker.set_onmessage(Some(on_message.as_ref().unchecked_ref()));

    let error_sender = sender;
    let on_error = Closure::<dyn FnMut(ErrorEvent)>::new(move |event: ErrorEvent| {
        if let Some(sender) = error_sender.borrow_mut().take() {
            let _ = sender.send(Err(super::OpenLoadError::from(event.message())));
        }
    });
    worker.set_onerror(Some(on_error.as_ref().unchecked_ref()));

    let payload = Object::new();
    Reflect::set(
        &payload,
        &JsValue::from_str("name"),
        &JsValue::from_str(name),
    )
    .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    Reflect::set(
        &payload,
        &JsValue::from_str("recoveryMode"),
        &JsValue::from_bool(recovery_mode),
    )
    .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    Reflect::set(
        &payload,
        &JsValue::from_str("initialError"),
        &JsValue::from_str(initial_error),
    )
    .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    let input = Uint8Array::from(bytes);
    Reflect::set(&payload, &JsValue::from_str("bytes"), &input.buffer())
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    let transfer = Array::new();
    transfer.push(&input.buffer());
    worker
        .post_message_with_transfer(&payload, &transfer)
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;

    let result = receiver
        .await
        .map_err(|_| super::OpenLoadError::from("CAD parser worker closed without a result"))?;
    worker.terminate();
    result
}

fn restore_entity_runtime_fields(
    document: &mut acadrust::CadDocument,
    fields: Vec<EntityRuntimeFields>,
) {
    let handle = |value: Option<u64>| value.map(acadrust::Handle::new);
    for fields in fields {
        let Some(entity) = document.get_entity_mut(acadrust::Handle::new(fields.handle)) else {
            continue;
        };
        let common = entity.common_mut();
        common.linetype_handle = handle(fields.linetype_handle);
        common.color_book_handle = handle(fields.color_book_handle);
        common.face_visual_style_handle = handle(fields.face_visual_style_handle);
        common.edge_visual_style_handle = handle(fields.edge_visual_style_handle);
        common.material_flags = fields.material_flags;
        common.material_handle = handle(fields.material_handle);
        common.shadow_flags = fields.shadow_flags;
        common.plotstyle_flags = fields.plotstyle_flags;
        common.plotstyle_handle = handle(fields.plotstyle_handle);
        common.entity_mode = fields.entity_mode;
        common.has_ds_data = fields.has_ds_data;
    }
}

pub(super) async fn sha256_document(bytes: &[u8]) -> Result<String, super::OpenLoadError> {
    let options = WorkerOptions::new();
    options.set_type(WorkerType::Module);
    let worker = Worker::new_with_options(WORKER_URL, &options)
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;

    let (sender, receiver) = iced::futures::channel::oneshot::channel::<
        Result<String, super::OpenLoadError>,
    >();
    let sender = Rc::new(RefCell::new(Some(sender)));
    let message_sender = sender.clone();
    let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
        let data = event.data();
        let result = Reflect::get(&data, &JsValue::from_str("digest"))
            .map_err(|error| super::OpenLoadError::from(js_error(error)))
            .and_then(|value| {
                value
                    .as_string()
                    .ok_or_else(|| super::OpenLoadError::from("hash worker returned no digest"))
            });
        if let Some(sender) = message_sender.borrow_mut().take() {
            let _ = sender.send(result);
        }
    });
    worker.set_onmessage(Some(on_message.as_ref().unchecked_ref()));

    let error_sender = sender;
    let on_error = Closure::<dyn FnMut(ErrorEvent)>::new(move |event: ErrorEvent| {
        if let Some(sender) = error_sender.borrow_mut().take() {
            let _ = sender.send(Err(super::OpenLoadError::from(event.message())));
        }
    });
    worker.set_onerror(Some(on_error.as_ref().unchecked_ref()));

    let payload = Object::new();
    Reflect::set(
        &payload,
        &JsValue::from_str("action"),
        &JsValue::from_str("hash"),
    )
    .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    let input = Uint8Array::from(bytes);
    Reflect::set(&payload, &JsValue::from_str("bytes"), &input.buffer())
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;
    let transfer = Array::new();
    transfer.push(&input.buffer());
    worker
        .post_message_with_transfer(&payload, &transfer)
        .map_err(|error| super::OpenLoadError::from(js_error(error)))?;

    let result = receiver
        .await
        .map_err(|_| super::OpenLoadError::from("hash worker closed without a result"))?;
    worker.terminate();
    result
}

fn decode_worker_error(message: String) -> super::OpenLoadError {
    let Some((message, digest)) = message.rsplit_once(HASH_MARKER) else {
        return super::OpenLoadError::from(message);
    };
    let digest = digest.trim_start().get(..64).unwrap_or_default();
    let source_sha256 = (digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()))
        .then(|| digest.to_ascii_lowercase());
    super::OpenLoadError::new(message.to_string(), source_sha256)
}

fn js_error(value: JsValue) -> String {
    value
        .as_string()
        .unwrap_or_else(|| format!("browser worker error: {value:?}"))
}
