/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic
//! interaction.
//!
//! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can
//! be used to access the element in this UI.
//!
//! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on
//! the same thread as the UI.
//!
//! See how this UI is used in [`crate::test`].

use super::model::{self, Application, Element};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{
    atomic::{AtomicBool, AtomicU8, Ordering::Relaxed},
    mpsc, Arc, Condvar, Mutex,
};

thread_local! {
    static INTERACT: RefCell<Option<Arc<State>>> = Default::default();
}

/// A test UI which allows access to the UI elements.
#[derive(Default)]
pub struct UI {
    interface: Mutex<Option<UIInterface>>,
}

impl UI {
    pub fn run_loop(&self, app: Application) {
        let (tx, rx) = mpsc::channel();
        let interface = UIInterface { work: tx };

        let elements = id_elements(&app);
        INTERACT.with(cc! { (interface) move |r| {
            if let Some(state) = &*r.borrow() {
                state.set_interface(interface);
            }
        }});
        *self.interface.lock().unwrap() = Some(interface.clone());

        // Close the UI when the root windows are closed.
        // Use a bitfield rather than a count in case the `close` event is fired multiple times.
        assert!(app.windows.len() <= 8);
        let mut windows = 0u8;
        for i in 0..app.windows.len() {
            windows |= 1 << i;
        }
        let windows = Arc::new(AtomicU8::new(windows));
        for (index, window) in app.windows.iter().enumerate() {
            if let Some(c) = &window.element_type.close {
                c.subscribe(cc! { (interface, windows) move |&()| {
                    let old = windows
                        .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
                        .unwrap();
                    if old == 1u8 << index {
                        interface.work.send(Command::Finish).unwrap();
                    }
                }});
            } else {
                // No close event, so we must assume a closed state (and assume that _some_ window
                // will have a close event registered so we don't drop the interface now).
                windows
                    .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index)))
                    .unwrap();
            }
        }

        while let Ok(f) = rx.recv() {
            match f {
                Command::Invoke(f) => f(),
                Command::Interact(f) => f(&elements),
                Command::Finish => break,
            }
        }

        *self.interface.lock().unwrap() = None;
        INTERACT.with(|r| {
            if let Some(state) = &*r.borrow() {
                state.clear_interface();
            }
        });
    }

    pub fn invoke(&self, f: model::InvokeFn) {
        let guard = self.interface.lock().unwrap();
        if let Some(interface) = &*guard {
            let _ = interface.work.send(Command::Invoke(f));
        }
    }
}

/// Test interaction interface.
#[derive(Clone)]
pub struct Interact {
    state: Arc<State>,
}

/// Spawned test interaction.
pub struct SpawnedInteract {
    interact: Interact,
    handle: ::std::mem::ManuallyDrop<::std::thread::JoinHandle<()>>,
    ignore_panic: bool,
}

impl SpawnedInteract {
    pub fn ignore_panic(&mut self) {
        self.ignore_panic = true;
    }
}

impl Drop for SpawnedInteract {
    fn drop(&mut self) {
        self.interact.cancel();
        if unsafe { ::std::mem::ManuallyDrop::take(&mut self.handle) }
            .join()
            .is_err()
        {
            if !std::thread::panicking() && !self.ignore_panic {
                // Make sure this thread panics so we see the panic from the interact thread.
                panic!("interact thread panicked");
            }
        }
    }
}

impl Interact {
    /// Spawn a thread which runs interactions according with the test UI.
    ///
    /// The returned `SpawnedInteract` should be dropped after the test UI is run.
    ///
    /// # Panic Safety
    /// If a panic occurs in the interact thread, it will not be re-thrown (we assume that the
    /// panic handler will still show the thread's panic message when the test completes).
    pub fn spawn<F: FnOnce(&Self) + Send + 'static>(f: F) -> SpawnedInteract {
        let interact = Self::hook();
        let handle = ::std::thread::spawn(cc! { (interact) move || {
            interact.wait_for_ready();
            f(&interact);

        }});
        SpawnedInteract {
            interact,
            handle: ::std::mem::ManuallyDrop::new(handle),
            ignore_panic: false,
        }
    }

    /// Create an interaction hook for the test UI.
    ///
    /// This should be done before running the UI, and must be done on the same thread that
    /// later runs it.
    fn hook() -> Interact {
        let v = Interact {
            state: Default::default(),
        };
        {
            let state = v.state.clone();
            INTERACT.with(move |r| *r.borrow_mut() = Some(state));
        }
        v
    }

    /// Wait for the render thread to be ready for interaction.
    fn wait_for_ready(&self) {
        let mut guard = self.state.interface.lock().unwrap();
        while guard.is_none() && !self.state.cancel.load(Relaxed) {
            guard = self.state.waiting_for_interface.wait(guard).unwrap();
        }
    }

    /// Cancel an Interact (which causes `wait_for_ready` to always return).
    fn cancel(&self) {
        let _guard = self.state.interface.lock().unwrap();
        self.state.cancel.store(true, Relaxed);
        self.state.waiting_for_interface.notify_all();
    }

    /// Run the given function on the element with the given type and identity.
    ///
    /// Panics if either the id is missing or the type is incorrect.
    pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R
    where
        &'b T: TryFrom<&'b model::ElementType>,
        F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a,
        R: Send + 'a,
    {
        self.interact(id, move |element: &IdElement| match element {
            IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)),
            IdElement::Window(_) => None,
        })
        .expect("incorrect element type")
    }

    /// Run the given function on the window with the given identity.
    ///
    /// Panics if the id is missing or the type is incorrect.
    pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R
    where
        F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a,
        R: Send + 'a,
    {
        self.interact(id, move |element| match element {
            IdElement::Window(e) => Some(f(&e.style, &e.element_type)),
            IdElement::Generic(_) => None,
        })
        .expect("incorrect element type")
    }

    fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R
    where
        F: FnOnce(&IdElement<'b>) -> R + Send + 'a,
        R: Send + 'a,
    {
        let (send, recv) = std::sync::mpsc::sync_channel(0);
        {
            let f: Box<dyn FnOnce(&IdElements<'b>) + Send + 'a> = Box::new(move |elements| {
                let _ = send.send(elements.get(id).map(f));
            });

            // # Safety
            // The function is run while `'a` is still valid (we wait here for it to complete).
            let f: Box<dyn FnOnce(&IdElements) + Send + 'static> =
                unsafe { std::mem::transmute(f) };

            let guard = self.state.interface.lock().unwrap();
            let interface = guard.as_ref().expect("renderer is not running");
            let _ = interface.work.send(Command::Interact(f));
        }
        recv.recv().unwrap().expect("failed to get element")
    }
}

#[derive(Clone)]
struct UIInterface {
    work: mpsc::Sender<Command>,
}

enum Command {
    Invoke(Box<dyn FnOnce() + Send + 'static>),
    Interact(Box<dyn FnOnce(&IdElements) + Send + 'static>),
    Finish,
}

enum IdElement<'a> {
    Generic(&'a Element),
    Window(&'a model::TypedElement<model::Window>),
}

type IdElements<'a> = HashMap<String, IdElement<'a>>;

#[derive(Default)]
struct State {
    interface: Mutex<Option<UIInterface>>,
    waiting_for_interface: Condvar,
    cancel: AtomicBool,
}

impl State {
    /// Set the interface for the interaction client to use.
    pub fn set_interface(&self, interface: UIInterface) {
        let mut guard = self.interface.lock().unwrap();
        *guard = Some(interface);
        self.waiting_for_interface.notify_all();
    }

    /// Clear the UI interface.
    pub fn clear_interface(&self) {
        *self.interface.lock().unwrap() = None;
    }
}

fn id_elements<'a>(app: &'a Application) -> IdElements<'a> {
    let mut elements: IdElements<'a> = Default::default();

    let mut windows_to_visit: Vec<_> = app.windows.iter().collect();

    let mut to_visit: Vec<&'a Element> = Vec::new();
    while let Some(window) = windows_to_visit.pop() {
        if let Some(id) = &window.style.id {
            elements.insert(id.to_owned(), IdElement::Window(window));
        }
        windows_to_visit.extend(&window.element_type.children);

        if let Some(content) = &window.element_type.content {
            to_visit.push(content);
        }
    }

    while let Some(el) = to_visit.pop() {
        if let Some(id) = &el.style.id {
            elements.insert(id.to_owned(), IdElement::Generic(el));
        }

        use model::ElementType::*;
        match &el.element_type {
            Button(model::Button {
                content: Some(content),
                ..
            })
            | Scroll(model::Scroll {
                content: Some(content),
            }) => {
                to_visit.push(content);
            }
            VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => {
                for item in items {
                    to_visit.push(item)
                }
            }
            _ => (),
        }
    }

    elements
}
