/*
 * meli
 *
 * Copyright 2017-2020 Manos Pitsidianakis
 *
 * This file is part of meli.
 *
 * meli is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * meli 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with meli. If not, see <http://www.gnu.org/licenses/>.
 */

/*! Simple blocking job control.
 */
use crate::types::ThreadEvent;
use crossbeam::{
    channel::{bounded, unbounded, Sender},
    select,
};
use melib::async_workers::{Work, WorkContext};
use melib::datetime::{self, UnixTimestamp};
use melib::text_processing::Truncate;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::Mutex;

const MAX_WORKER: usize = 4;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console, js_name = log)]
    fn logv(x: &JsValue);
}

/// Representation of a worker thread for use in `WorkController`. These values are to be displayed
/// to the user.
#[derive(Debug)]
pub struct Worker {
    pub name: String,
    pub status: String,
    pub heartbeat: UnixTimestamp,
}

impl From<String> for Worker {
    fn from(val: String) -> Self {
        Worker {
            name: val,
            status: String::new(),
            heartbeat: datetime::now(),
        }
    }
}

#[wasm_bindgen]
pub struct WorkController {
    pub queue: WorkQueue,
    thread_end_tx: Sender<bool>,
    /// Worker threads that take up on jobs from self.queue
    pub threads: Arc<Mutex<HashMap<u32, Worker>>>,
    /// Special function threads that live indefinitely (eg watching a mailbox)
    pub static_threads: Arc<Mutex<HashMap<u32, Worker>>>,
    work_context: WorkContext,
    pool: Rc<PoolState>,
}

impl Drop for WorkController {
    fn drop(&mut self) {
        if let Ok(lock) = self.threads.lock() {
            for _ in 0..lock.len() {
                let _ = self.thread_end_tx.send(true);
            }
        }
    }
}

#[wasm_bindgen]
#[derive(Clone)]
pub struct WorkQueue {
    inner: Arc<Mutex<Vec<Work>>>,
    new_jobs_tx: Sender<bool>,
    work_context: WorkContext,
}

#[wasm_bindgen]
impl WorkQueue {
    #[wasm_bindgen(constructor)]
    pub fn new(new_jobs_tx: Sender<bool>, work_context: WorkContext) -> Self {
        Self {
            inner: Arc::new(Mutex::new(Vec::new())),
            new_jobs_tx,
            work_context,
        }
    }

    /// Blocks the current thread until work is available, then
    /// gets the data required to perform that work.
    ///
    /// # Errors
    /// Returns None if there is no more work in the queue.
    ///
    /// # Panics
    /// Panics if the underlying mutex became poisoned. This is exceedingly
    /// unlikely.
    fn get_work(&self) -> Option<Work> {
        // try to get a lock on the mutex.
        let maybe_queue = self.inner.lock();
        if let Ok(mut queue) = maybe_queue {
            if queue.is_empty() {
                return None;
            } else {
                return Some(queue.swap_remove(0));
            }
        } else {
            // poisoned mutex, some other thread holding the mutex has panicked!
            panic!("WorkQueue::get_work() tried to lock a poisoned mutex");
        }
    }

    // Both the controller (main thread) and workers can use this
    // function to add work to the queue.

    /// Blocks the current thread until work can be added, then
    /// adds that work to the end of the queue.
    /// Returns the amount of work now in the queue.
    ///
    /// # Panics
    /// Panics if the underlying mutex became poisoned. This is exceedingly
    /// unlikely.
    pub fn add_work(&self, work: Work) {
        if work.is_static {
            self.work_context.new_work.send(work).unwrap();
            return;
        }

        // As above, try to get a lock on the mutex.
        if let Ok(mut queue) = self.inner.lock() {
            /* Insert in position that maintains the queue sorted */
            let pos = match queue.binary_search_by(|probe| probe.cmp(&work)) {
                Ok(p) => p,
                Err(p) => p,
            };
            queue.insert(pos, work);

            /* inform threads that new job is available */
            self.new_jobs_tx.send(true).unwrap();
        } else {
            panic!("WorkQueue::add_work() tried to lock a poisoned mutex");
        }
    }
}

impl WorkController {
    pub fn new(pulse: Sender<ThreadEvent>) -> WorkController {
        todo!()
        /*
        let (new_jobs_tx, new_jobs_rx) = unbounded();

        /* create a channel for jobs to send new work to Controller thread */
        let (new_work_tx, new_work_rx) = unbounded();

        /* create a channel for jobs to set their names */
        let (set_name_tx, set_name_rx) = unbounded();

        /* create a channel for jobs to set their statuses */
        let (set_status_tx, set_status_rx) = unbounded();

        /* create a channel for jobs to announce their demise */
        let (finished_tx, finished_rx) = unbounded();

        /* each associated thread will hold a copy of this context item in order to communicate
         * with the controller thread */
        let work_context = WorkContext {
            new_work: new_work_tx,
            set_name: set_name_tx,
            set_status: set_status_tx,
            finished: finished_tx,
        };

        let queue: WorkQueue = WorkQueue::new(new_jobs_tx, work_context.clone());

        let active_threads = Arc::new(AtomicUsize::new(MAX_WORKER));
        // Create a SyncFlag to share whether or not there are more jobs to be done.
        let (thread_end_tx, thread_end_rx) = bounded(1);

        let threads_lock: Arc<Mutex<HashMap<u32, Worker>>> =
            Arc::new(Mutex::new(HashMap::default()));

        let static_threads_lock: Arc<Mutex<HashMap<u32, Worker>>> =
            Arc::new(Mutex::new(HashMap::default()));

        let pool = PoolState {
            workers: RefCell::new(Vec::with_capacity(6)),
            callback: Closure::wrap(Box::new(|event: Event| {
                logv(&format!("unhandled event: {}", event.type_()).into());
                logv(&event);
            }) as Box<dyn FnMut(Event)>),
        };
        let mut threads = threads_lock.lock().unwrap();
        /* spawn worker threads */
        for i in 0..MAX_WORKER {
            /* Each worker thread will wait on two channels: thread_end and new_jobs. thread_end
             * informs the worker that it should quit and new_jobs informs that there is a new job
             * available inside the queue. Only one worker will get each job, and others will
             * go back to waiting on the channels */
            let thread_queue = queue.clone();

            let active_threads = active_threads.clone();
            let thread_end_rx = thread_end_rx.clone();
            let new_jobs_rx = new_jobs_rx.clone();
            let new_jobs_rx = new_jobs_rx.clone();

            let work_context = work_context.clone();
            let pulse = pulse.clone();

            let handle = spawn_worker(
                thread_queue,
                active_threads,
                thread_end_rx,
                new_jobs_rx,
                work_context,
                pulse,
                i,
            )
            .unwrap();

            handle.set_onmessage(Some(pool.callback.as_ref().unchecked_ref()));
            handle.set_onerror(Some(pool.callback.as_ref().unchecked_ref()));
            /* add the handle for the newly spawned thread to the list of handles */
            threads.insert(i, String::from("idle-worker").into());
            pool.push(handle);
        }
        /* drop lock */
        drop(threads);

        {
            /* start controller thread */
            let threads_lock = threads_lock.clone();
            let _static_threads_lock = static_threads_lock.clone();
            let thread_queue = queue.clone();
            let thread_end_rx = thread_end_rx.clone();
            let work_context = work_context.clone();

            let handle = thread::spawn(move || 'control_loop: loop {
                select! {
                                    recv(thread_end_rx) -> _ => {
                                        debug!("received thread_end_rx, quitting");
                                        break 'control_loop;
                                    },
                                    recv(new_work_rx) -> work => {
                                        if let Ok(work) = work {
                                            if work.is_static {
                                                let work_context = work_context.clone();
                                                let closure = move || {work.compute(work_context)};
                                                let mut static_threads_lock = _static_threads_lock.lock().unwrap();
                                                 static_threads_lock.insert(static_threads_lock.len(), String::new().into());
                let work = Box::new(JsWork { func: Box::new(closure) });
                let ptr = Box::into_raw(work);
                let worker = match worker.post_message(&JsValue::from(ptr as u32)) {
                    Ok(()) => Ok(worker),
                    Err(e) => {
                        unsafe {
                            drop(Box::from_raw(ptr));
                        }
                        Err(e)
                    }
                };
                pool.push(worker.unwrap());
                                            } else {
                                                if active_threads.load(Ordering::SeqCst) == 0 {
                                                    let mut threads_lock = threads_lock.lock().unwrap();
                                                    let handle = spawn_worker(
                                                        thread_queue.clone(),
                                                        active_threads.clone(),
                                                        thread_end_rx.clone(),
                                                        new_jobs_rx.clone(),
                                                        work_context.clone(),
                                                        pulse.clone(),
                                                        threads_lock.len(),
                                                    );

                                                    /* add the handle for the newly spawned thread to the list of handles */
                                                    threads_lock.insert(threads_lock.len(), String::from("idle-worker").into());
                                                }
                                                thread_queue.add_work(work);
                                            }
                                        }
                                    }
                                    recv(set_name_rx) -> new_name => {
                                        if let Ok((thread_id, mut new_name)) = new_name {
                                            new_name.truncate_at_boundary(256);
                                            let mut threads = threads_lock.lock().unwrap();
                                            let mut static_threads = _static_threads_lock.lock().unwrap();
                                            let now = datetime::now();
                                            if threads.contains_key(&thread_id) {
                                                threads.entry(thread_id).and_modify(|e| {
                                                    e.name = new_name;
                                                    e.heartbeat = now;
                                                });
                                            } else if static_threads.contains_key(&thread_id) {
                                                static_threads.entry(thread_id).and_modify(|e| {

                                                    e.name = new_name;
                                                    e.heartbeat = now;
                                                });
                                            } else {
                                                 static_threads.insert(thread_id, Worker { heartbeat: now, .. new_name.into() });
                                                static_threads.entry(thread_id).and_modify(|e| {
                                                    e.heartbeat = now;
                                                });
                                            }
                                            pulse.send(ThreadEvent::Pulse).unwrap();
                                        }
                                    }
                                    recv(set_status_rx) -> new_status => {
                                        if let Ok((thread_id, mut new_status)) = new_status {
                                            new_status.truncate_at_boundary(256);
                                            let mut threads = threads_lock.lock().unwrap();
                                            let mut static_threads = _static_threads_lock.lock().unwrap();
                                            let now = datetime::now();
                                            if threads.contains_key(&thread_id) {
                                                threads.entry(thread_id).and_modify(|e| {
                                                    e.status = new_status;
                                                    e.heartbeat = now;
                                                });
                                            } else if static_threads.contains_key(&thread_id) {
                                                static_threads.entry(thread_id).and_modify(|e| {
                                                    e.status = new_status;
                                                    e.heartbeat = now;
                                                });
                                                debug!(&static_threads[&thread_id]);
                                            } else {
                                                 static_threads.insert(thread_id, Worker { status: new_status, heartbeat: now, .. String::new().into() });
                                            }
                                            pulse.send(ThreadEvent::Pulse).unwrap();
                                        }
                                    }
                                    recv(finished_rx) -> dead_thread_id => {
                                        if let Ok(thread_id) = dead_thread_id {
                                            let mut threads = threads_lock.lock().unwrap();
                                            let mut static_threads = _static_threads_lock.lock().unwrap();
                                            if threads.contains_key(&thread_id) {
                                                threads.remove(&thread_id);
                                            } else if static_threads.contains_key(&thread_id) {
                                                static_threads.remove(&thread_id);
                                            } else {
                                                /* Nothing to do */
                                            }
                                            pulse.send(ThreadEvent::Pulse).unwrap();
                                        }
                                    }
                                }
            });

            let mut static_threads = static_threads_lock.lock().unwrap();
            static_threads.insert(
                static_threads.len(),
                "WorkController-thread".to_string().into(),
            );
        }

        WorkController {
            queue,
            thread_end_tx,
            threads: threads_lock,
            static_threads: static_threads_lock,
            work_context,
            pool,
        }
        */
    }

    pub fn add_static_thread(&mut self, id: u32) {
        self.static_threads
            .lock()
            .unwrap()
            .insert(id, String::new().into());
    }

    pub fn get_context(&self) -> WorkContext {
        self.work_context.clone()
    }
}

fn spawn_worker(
    thread_queue: WorkQueue,
    active_threads: Arc<AtomicUsize>,
    thread_end_rx: crossbeam::Receiver<bool>,
    new_jobs_rx: crossbeam::Receiver<bool>,
    work_context: WorkContext,
    pulse: crossbeam::Sender<ThreadEvent>,
    id: u32,
) -> Result<WebWorker, JsValue> {
    logv("spawning new worker".into());
    // TODO: what do do about `./worker.js`:
    //
    // * the path is only known by the bundler. How can we, as a
    //   library, know what's going on?
    // * How do we not fetch a script N times? It internally then
    //   causes another script to get fetched N times...
    let worker = WebWorker::new("./worker.js")?;

    // With a worker spun up send it the module/memory so it can start
    // instantiating the wasm module. Later it might receive further
    // messages about code to run on the wasm module.
    let array = js_sys::Array::new();
    array.push(&wasm_bindgen::module());
    array.push(&wasm_bindgen::memory());
    worker.post_message(&array)?;
    let closure = move || 'work_loop: loop {
        debug!("Waiting for work");
        select! {
            recv(thread_end_rx) -> _ => {
                debug!("received thread_end_rx, quitting");
                active_threads.fetch_sub(1, Ordering::SeqCst);
                break 'work_loop;
            },
            recv(new_jobs_rx) -> _ => {
                active_threads.fetch_sub(1, Ordering::SeqCst);
                while let Some(work) = thread_queue.get_work() {
                    debug!("Got some work");
                    work.compute(work_context.clone());
                    debug!("finished work");
                    work_context.set_name.send((id, "idle-worker".to_string())).unwrap();
                    work_context.set_status.send((id, "inactive".to_string())).unwrap();
                    pulse.send(ThreadEvent::Pulse).unwrap();

                }
                active_threads.fetch_add(1, Ordering::SeqCst);
            },
        }
    };
    let work = Box::new(JsWork {
        func: Box::new(closure),
    });
    let ptr = Box::into_raw(work);
    match worker.post_message(&JsValue::from(ptr as u32)) {
        Ok(()) => Ok(worker),
        Err(e) => {
            unsafe {
                drop(Box::from_raw(ptr));
            }
            Err(e)
        }
    }
}

struct JsWork {
    func: Box<dyn FnOnce() + Send>,
}

use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{DedicatedWorkerGlobalScope, MessageEvent};
use web_sys::{ErrorEvent, Event, Worker as WebWorker};

//#[wasm_bindgen]
//pub struct WorkerPool {
//    state: Rc<PoolState>,
//}

struct PoolState {
    workers: RefCell<Vec<WebWorker>>,
    callback: Closure<dyn FnMut(Event)>,
}

impl PoolState {
    fn push(&self, worker: WebWorker) {
        let mut workers = self.workers.borrow_mut();
        for prev in workers.iter() {
            let prev: &JsValue = prev;
            let worker: &JsValue = &worker;
            assert!(prev != worker);
        }
        workers.push(worker);
    }
}

/// Entry point invoked by `worker.js`, a bit of a hack but see the "TODO" above
/// about `worker.js` in general.
#[wasm_bindgen]
pub fn child_entry_point(ptr: u32) -> Result<(), JsValue> {
    let ptr = unsafe { Box::from_raw(ptr as *mut Work) };
    let global = js_sys::global().unchecked_into::<DedicatedWorkerGlobalScope>();
    (ptr.func)();
    global.post_message(&JsValue::undefined())?;
    Ok(())
}
