// Copyright(c) 2025 3NSoft Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

use std::{rc::Rc, time::Duration};

use deno_core::{error::JsError, extension, op2, FsModuleLoader, JsBuffer, JsRuntime};
pub use deno_core::error::{CoreError, };
use ipc_duplex::{IpcClientEvent, SignalToSend};
use tokio::{sync::mpsc::{error::SendTimeoutError, Receiver, Sender}, time::timeout};

// Following tutorial https://deno.com/blog/roll-your-own-javascript-runtime (covered part 1, to extensions! macro)

pub async fn make_and_run_deno_with_main_js(
  file_path: &String, tx: Sender<SignalToSend>, rx: Receiver<IpcClientEvent>
) -> Result<(), CoreError> {

  unsafe {
    STATIC_RX = Some(rx);
    STATIC_TX = Some(tx);
  };

  let main_module = deno_core::resolve_path(
    file_path,
    &std::env::current_dir()?
  ).unwrap();

  let (mut js_runtime, w3n_mod_result) = new_deno_with_w3n().await?;

  let main_mod_id = js_runtime.load_main_es_module(&main_module).await?;
  let result = js_runtime.mod_evaluate(main_mod_id);

  js_runtime.run_event_loop(Default::default()).await?;

  w3n_mod_result.await?;
  result.await
}

async fn new_deno_with_w3n() -> Result<(JsRuntime, impl Future<Output = Result<(), CoreError>>), CoreError> {

  let mut js_runtime = JsRuntime::new(
    deno_core::RuntimeOptions {
      module_loader: Some(Rc::new(FsModuleLoader)),
      extensions: vec![
        ipc_ext::init()
      ],
      ..Default::default()
    }
  );

  let w3n_mod_id = js_runtime
    .load_side_es_module_from_code(
      &deno_core::ModuleSpecifier::parse("w3n:preload.js")?,
      include_str!("../js/w3n.js")
    ).await?;
  let w3n_mod_result = js_runtime.mod_evaluate(w3n_mod_id);

  Ok((js_runtime, w3n_mod_result))
}

static mut STATIC_RX: Option<Receiver<IpcClientEvent>> = None;
static mut STATIC_TX: Option<Sender<SignalToSend>> = None;

extension!(
  ipc_ext,
  ops = [
    op_ipc_send,
    op_ipc_next,
    op_ipc_exit
  ]
);

#[op2(async)]
#[buffer]
async fn op_ipc_send(
  #[buffer(detach)] bytes: JsBuffer,
  timeout_millis: Option<u32>
) -> Result<Vec<u8>, JsError> {
  unsafe {
    match (&raw const STATIC_TX).as_ref().expect("this was statically set to non-null") {
      Some(tx) => match timeout_millis {
        Some(timeout_millis) => match tx.send_timeout(
          SignalToSend::Data(bytes.to_vec()), Duration::from_millis(timeout_millis as u64)
        ).await {
          Ok(_) => Ok(vec![SEND_SIGNAL_BYTE]),
          Err(SendTimeoutError::Timeout(_)) => Ok(vec![TIMEOUT_SIGNAL_BYTE]),
          Err(SendTimeoutError::Closed(_)) => Ok(vec![ERROR_SIGNAL_BYTE])
        },
        None => match tx.send(SignalToSend::Data(bytes.to_vec())).await {
          Ok(_) => Ok(vec![SEND_SIGNAL_BYTE]),
          Err(err) => Ok([vec![ERROR_SIGNAL_BYTE], err.to_string().into_bytes()].concat())
        }
      },
      None => Ok(vec![IPC_NOT_SET_SIGNAL_BYTE])
    }
  }
}

const CONNECTED_SIGNAL_BYTE: u8 = 1;
const DATA_SIGNAL_BYTE: u8 = 2;
const ERROR_SIGNAL_BYTE: u8 = 3;
const EXIT_SIGNAL_BYTE: u8 = 4;
const IPC_NOT_SET_SIGNAL_BYTE: u8 = 5;
const SEND_SIGNAL_BYTE: u8 = 6;
const TIMEOUT_SIGNAL_BYTE: u8 = 7;

#[op2(async)]
#[buffer]
async fn op_ipc_next(
  timeout_millis: Option<u32>
) -> Result<Vec<u8>, JsError> {
  unsafe {
    match (&raw mut STATIC_RX).as_mut().expect("this was statically set to non-null") {
      Some(rx) => match timeout_millis {
        Some(timeout_millis) => match timeout(
          Duration::from_millis(timeout_millis as u64), rx.recv()
        ).await {
          Ok(Some(IpcClientEvent::Connected)) => Ok(vec![CONNECTED_SIGNAL_BYTE]),
          Ok(Some(IpcClientEvent::Data(data))) => Ok([&[DATA_SIGNAL_BYTE], &data[..]].concat()),
          Ok(Some(IpcClientEvent::Error(err))) => Ok(
            [vec![ERROR_SIGNAL_BYTE], err.to_string().into_bytes()].concat()
          ),
          Ok(Some(IpcClientEvent::Exit)) => Ok(vec![EXIT_SIGNAL_BYTE]),
          Ok(None) => Ok(vec![IPC_NOT_SET_SIGNAL_BYTE]),
          Err(_) => Ok(vec![TIMEOUT_SIGNAL_BYTE])
        },
        None => match rx.recv().await {
          Some(IpcClientEvent::Connected) => Ok(vec![CONNECTED_SIGNAL_BYTE]),
          Some(IpcClientEvent::Data(data)) => Ok([&[DATA_SIGNAL_BYTE], &data[..]].concat()),
          Some(IpcClientEvent::Error(err)) => Ok([vec![ERROR_SIGNAL_BYTE], err.to_string().into_bytes()].concat()),
          Some(IpcClientEvent::Exit) => Ok(vec![EXIT_SIGNAL_BYTE]),
          None => Ok(vec![IPC_NOT_SET_SIGNAL_BYTE])
        }
      },
      None => Ok(vec![IPC_NOT_SET_SIGNAL_BYTE])
    }
  }
}

#[op2(async)]
async fn op_ipc_exit(
  timeout_millis: Option<u32>
) -> Result<(), JsError> {
  unsafe {
    match (&raw const STATIC_TX).as_ref().expect("this was statically set to non-null") {
      Some(tx) => match timeout_millis {
        Some(timeout_millis) => match tx.send_timeout(
          SignalToSend::Exit, Duration::from_millis(timeout_millis as u64)
        ).await {
          _ => Ok(())
        },
        None =>  match tx.send(SignalToSend::Exit).await {
          _ => Ok(())
        }
      },
      _ => Ok(())
    }
  }
}
