use oden_js::{module::native::NativeModuleBuilder, ContextRef, ValueResult};
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;

type Job = Box<dyn FnOnce() + Send + 'static>;

struct ThreadPoolWorker {
    _thread: thread::JoinHandle<()>,
}

impl ThreadPoolWorker {
    fn new(queue: Arc<Mutex<Receiver<Job>>>) -> Self {
        let thread = thread::spawn(move || loop {
            let r = {
                let locked = queue.lock();
                locked.expect("Should not be orphaning the lock").recv()
            };
            if let Ok(item) = r {
                item();
            } else {
                break;
            }
        });
        ThreadPoolWorker { _thread: thread }
    }
}

struct ThreadPool {
    _workers: Vec<ThreadPoolWorker>,
    sender: Sender<Job>,
}

impl ThreadPool {
    fn new(size: usize) -> Self {
        let (sender, receiver) = channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = vec![];
        for _ in 0..size {
            workers.push(ThreadPoolWorker::new(receiver.clone()));
        }

        ThreadPool {
            _workers: workers,
            sender,
        }
    }

    fn execute(&self, job: Job) {
        let _ = self.sender.send(job);
    }
}

struct IoImpl {
    thread_pool: ThreadPool,
}

impl IoImpl {
    fn new() -> Self {
        IoImpl {
            thread_pool: ThreadPool::new(4),
        }
    }

    fn load(&self, context: &ContextRef, path: &str) -> ValueResult {
        let (value, promise) = context.new_promise()?;

        let path = path.to_string();
        self.thread_pool.execute(Box::new(move || {
            let path = path;

            let result = resolve_path(&path, &[]).and_then(|p| std::fs::read(p));
            promise.resolve(move |ctx: &ContextRef| match result {
                Ok(v) => ctx.new_array_buffer(v),
                Err(err) => Err(err.into()),
            });
        }));

        Ok(value)
    }

    fn load_string(&self, context: &ContextRef, path: &str) -> ValueResult {
        let (value, promise) = context.new_promise()?;

        let path = path.to_string();
        self.thread_pool.execute(Box::new(move || {
            let path = path;

            let result = resolve_path(&path, &[]).and_then(|p| std::fs::read(p));
            promise.resolve(move |ctx: &ContextRef| {
                let result = result?;
                let string = std::str::from_utf8(&result)?;
                ctx.new_string(string)
            });
        }));

        Ok(value)
    }
}

pub struct IoAPI {}

impl IoAPI {
    pub fn define(ctx: &ContextRef) -> oden_js::Result<Self> {
        let io = Arc::new(IoImpl::new());
        let mut builder = NativeModuleBuilder::new(ctx);
        {
            let io = io.clone();
            builder.export(
                "load",
                ctx.new_fn(move |ctx: &ContextRef, p: String| io.load(ctx, &p))?,
            )?;
        }
        {
            let io = io.clone();
            builder.export(
                "load_string",
                ctx.new_fn(move |ctx: &ContextRef, p: String| io.load_string(ctx, &p))?,
            )?;
        }
        builder.build("io-core")?;
        Ok(IoAPI {})
    }
}

fn append_ext(ext: impl AsRef<OsStr>, path: PathBuf) -> PathBuf {
    let mut os_string: OsString = path.into();
    os_string.push(".");
    os_string.push(ext.as_ref());
    os_string.into()
}

/// Resolve a path to an existing file.
pub fn resolve_path(input: &str, extensions: &[&str]) -> std::io::Result<PathBuf> {
    let mut path = PathBuf::with_capacity(input.len());
    for segment in input.split('/') {
        if segment == "." {
            continue;
        } else if segment == ".." {
            if path.parent().is_none() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("{input} contains too many '..'; directory underflow detected"),
                ));
            }

            path.pop();
        } else if segment.contains(":") || segment.contains('\\') {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("{input} contains invalid characters"),
            ));
        } else {
            path.push(segment);
        }
    }

    if path.has_root() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("{input} is absolute"),
        ));
    }
    if path.file_name().is_none() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("{input} refers to a directory"),
        ));
    }

    for prefix in ["game", "src"] {
        let mut actual = PathBuf::from(prefix);
        actual.push(path.clone());
        //eprintln!("Trying '{}'...", actual.to_string_lossy());
        match actual.try_exists() {
            Ok(true) => return Ok(actual),
            _ => (),
        }
        for extension in extensions.iter() {
            let actual = append_ext(extension, actual.clone());
            //eprintln!("Trying '{}'...", actual.to_string_lossy());
            match actual.try_exists() {
                Ok(true) => return Ok(actual),
                _ => (),
            }
        }
    }

    return Err(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        format!("{input} not found or not accessible"),
    ));
}
