use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::sync::{LazyLock, Mutex};
use crate::core::{Expr, JSObjectDataPtr, Value, evaluate_expr, get_own_property, new_js_object_data, obj_set_key_value};
use crate::error::JSError;
use crate::unicode::{utf8_to_utf16, utf16_to_utf8};
static FILE_STORE: LazyLock<Mutex<HashMap<u64, File>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
static NEXT_FILE_ID: LazyLock<Mutex<u64>> = LazyLock::new(|| Mutex::new(1));
fn get_next_file_id() -> u64 {
let mut id = NEXT_FILE_ID.lock().unwrap();
let current = *id;
*id += 1;
current
}
pub(crate) fn create_tmpfile() -> Result<Value, JSError> {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let process_id = std::process::id();
let thread_id = format!("{:?}", std::thread::current().id());
let thread_hash = thread_id.chars().map(|c| c as u64).sum::<u64>();
let temp_dir = std::env::temp_dir();
let filename = temp_dir.join(format!("rust_js_tmp_{}_{}_{}.tmp", timestamp, process_id, thread_hash));
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&filename)
{
Ok(file) => {
let file_id = get_next_file_id();
FILE_STORE.lock().unwrap().insert(file_id, file);
let tmp = new_js_object_data();
obj_set_key_value(&tmp, &"__file_id".into(), Value::Number(file_id as f64))?;
obj_set_key_value(&tmp, &"__eof".into(), Value::Boolean(false))?;
obj_set_key_value(&tmp, &"puts".into(), Value::Function("tmp.puts".to_string()))?;
obj_set_key_value(&tmp, &"readAsString".into(), Value::Function("tmp.readAsString".to_string()))?;
obj_set_key_value(&tmp, &"seek".into(), Value::Function("tmp.seek".to_string()))?;
obj_set_key_value(&tmp, &"tell".into(), Value::Function("tmp.tell".to_string()))?;
obj_set_key_value(&tmp, &"putByte".into(), Value::Function("tmp.putByte".to_string()))?;
obj_set_key_value(&tmp, &"getByte".into(), Value::Function("tmp.getByte".to_string()))?;
obj_set_key_value(&tmp, &"getline".into(), Value::Function("tmp.getline".to_string()))?;
obj_set_key_value(&tmp, &"eof".into(), Value::Function("tmp.eof".to_string()))?;
obj_set_key_value(&tmp, &"close".into(), Value::Function("tmp.close".to_string()))?;
Ok(Value::Object(tmp))
}
Err(e) => Err(raise_eval_error!(format!("Failed to create temporary file: {e}"))),
}
}
pub(crate) fn handle_file_method(obj_map: &JSObjectDataPtr, method: &str, args: &[Expr], env: &JSObjectDataPtr) -> Result<Value, JSError> {
if get_own_property(obj_map, &"__file_id".into()).is_some() {
let file_id_val = get_own_property(obj_map, &"__file_id".into())
.ok_or(raise_eval_error!("Invalid file object"))?
.borrow()
.clone();
let file_id = match file_id_val {
Value::Number(n) => n as u64,
_ => {
return Err(raise_eval_error!("Invalid file object"));
}
};
let mut file_store = FILE_STORE.lock().unwrap();
let file = match file_store.get_mut(&file_id) {
Some(f) => f,
None => {
return Err(raise_eval_error!("File not found"));
}
};
match method {
"puts" => {
if args.is_empty() {
return Ok(Value::Undefined);
}
let mut to_write = String::new();
for a in args {
let av = evaluate_expr(env, a)?;
match av {
Value::String(sv) => to_write.push_str(&utf16_to_utf8(&sv)),
Value::Number(n) => to_write.push_str(&n.to_string()),
Value::Boolean(b) => to_write.push_str(&b.to_string()),
_ => {}
}
}
if file.write_all(to_write.as_bytes()).is_err() {
return Ok(Value::Number(-1.0));
}
if file.flush().is_err() {
return Ok(Value::Number(-1.0));
}
return Ok(Value::Undefined);
}
"readAsString" => {
if file.flush().is_err() {
return Ok(Value::String(utf8_to_utf16("")));
}
if file.seek(SeekFrom::Start(0)).is_err() {
return Ok(Value::String(utf8_to_utf16("")));
}
let mut contents = String::new();
if file.read_to_string(&mut contents).is_err() {
return Ok(Value::String(utf8_to_utf16("")));
}
return Ok(Value::String(utf8_to_utf16(&contents)));
}
"seek" => {
if args.len() >= 2 {
let offv = evaluate_expr(env, &args[0])?;
let whv = evaluate_expr(env, &args[1])?;
let offset = match offv {
Value::Number(n) => n as i64,
_ => 0,
};
let whence = match whv {
Value::Number(n) => n as i32,
_ => 0,
};
let seek_from = match whence {
0 => SeekFrom::Start(offset as u64), 1 => SeekFrom::Current(offset), 2 => SeekFrom::End(offset), _ => SeekFrom::Start(0),
};
match file.seek(seek_from) {
Ok(pos) => return Ok(Value::Number(pos as f64)),
Err(_) => return Ok(Value::Number(-1.0)),
}
}
return Ok(Value::Number(-1.0));
}
"tell" => match file.stream_position() {
Ok(pos) => return Ok(Value::Number(pos as f64)),
Err(_) => return Ok(Value::Number(-1.0)),
},
"putByte" => {
if !args.is_empty() {
let bv = evaluate_expr(env, &args[0])?;
let byte = match bv {
Value::Number(n) => n as u8,
_ => 0,
};
if file.write_all(&[byte]).is_err() {
return Ok(Value::Number(-1.0));
}
if file.flush().is_err() {
return Ok(Value::Number(-1.0));
}
return Ok(Value::Undefined);
}
return Ok(Value::Undefined);
}
"getByte" => {
let mut buf = [0u8; 1];
match file.read(&mut buf) {
Ok(1) => return Ok(Value::Number(buf[0] as f64)),
_ => return Ok(Value::Number(-1.0)),
}
}
"getline" => {
let mut reader = BufReader::new(&mut *file);
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => return Ok(Value::Undefined), Ok(_) => {
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
return Ok(Value::String(utf8_to_utf16(&line)));
}
Err(_) => return Ok(Value::Undefined),
}
}
"eof" => {
let mut buf = [0u8; 1];
match file.read(&mut buf) {
Ok(0) => return Ok(Value::Boolean(true)), Ok(_) => {
file.seek(SeekFrom::Current(-1))?;
return Ok(Value::Boolean(false));
}
Err(_) => return Ok(Value::Boolean(true)),
}
}
"close" => {
drop(file_store.remove(&file_id));
return Ok(Value::Undefined);
}
_ => {}
}
}
Err(raise_eval_error!(format!("File method {method} not implemented")))
}