use std::convert::TryInto;
use js_sys::{Array, JsString, Object};
use serde::Serialize;
use serde_wasm_bindgen;
use wasm_bindgen::{prelude::*, JsCast};
use crate::{
    local::{Position, RoomName},
    objects::{CostMatrix, RoomPosition},
};
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen]
    pub type PathFinder;
    #[wasm_bindgen(static_method_of = PathFinder, js_name = search)]
    fn search_internal(origin: &RoomPosition, goal: &JsValue, options: &JsValue) -> SearchResults;
}
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen]
    pub type JsSearchOptions;
    #[wasm_bindgen(method, setter = roomCallback)]
    pub fn room_callback(
        this: &JsSearchOptions,
        callback: &Closure<dyn FnMut(JsString) -> JsValue>,
    );
    #[wasm_bindgen(method, setter = plainCost)]
    pub fn plain_cost(this: &JsSearchOptions, cost: u8);
    #[wasm_bindgen(method, setter = swampCost)]
    pub fn swamp_cost(this: &JsSearchOptions, cost: u8);
    #[wasm_bindgen(method, setter = flee)]
    pub fn flee(this: &JsSearchOptions, val: bool);
    #[wasm_bindgen(method, setter = maxOps)]
    pub fn max_ops(this: &JsSearchOptions, ops: u32);
    #[wasm_bindgen(method, setter = maxRooms)]
    pub fn max_rooms(this: &JsSearchOptions, rooms: u8);
    #[wasm_bindgen(method, setter = maxCost)]
    pub fn max_cost(this: &JsSearchOptions, cost: f64);
    #[wasm_bindgen(method, setter = heuristicWeight)]
    pub fn heuristic_weight(this: &JsSearchOptions, weight: f64);
}
impl JsSearchOptions {
    pub fn new() -> JsSearchOptions {
        Object::new().unchecked_into()
    }
}
impl Default for JsSearchOptions {
    fn default() -> Self {
        Self::new()
    }
}
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen]
    pub type SearchResults;
    #[wasm_bindgen(method, getter, js_name = path)]
    fn path_internal(this: &SearchResults) -> Array;
    #[wasm_bindgen(method, getter)]
    pub fn ops(this: &SearchResults) -> u32;
    #[wasm_bindgen(method, getter)]
    pub fn cost(this: &SearchResults) -> u32;
    #[wasm_bindgen(method, getter)]
    pub fn incomplete(this: &SearchResults) -> bool;
}
impl SearchResults {
    pub fn path(&self) -> Vec<Position> {
        self.path_internal()
            .iter()
            .map(|p| p.unchecked_into())
            .map(|p: RoomPosition| p.into())
            .collect()
    }
    pub fn opaque_path(&self) -> Array {
        self.path_internal()
    }
}
pub trait RoomCostResult: Into<JsValue> {}
#[derive(Default)]
pub enum MultiRoomCostResult {
    CostMatrix(CostMatrix),
    Impassable,
    #[default]
    Default,
}
impl RoomCostResult for MultiRoomCostResult {}
impl From<MultiRoomCostResult> for JsValue {
    fn from(v: MultiRoomCostResult) -> JsValue {
        match v {
            MultiRoomCostResult::CostMatrix(m) => m.into(),
            MultiRoomCostResult::Impassable => JsValue::from_bool(false),
            MultiRoomCostResult::Default => JsValue::undefined(),
        }
    }
}
#[derive(Default)]
pub enum SingleRoomCostResult {
    CostMatrix(CostMatrix),
    #[default]
    Default,
}
impl RoomCostResult for SingleRoomCostResult {}
impl From<SingleRoomCostResult> for JsValue {
    fn from(v: SingleRoomCostResult) -> JsValue {
        match v {
            SingleRoomCostResult::CostMatrix(m) => m.into(),
            SingleRoomCostResult::Default => JsValue::undefined(),
        }
    }
}
pub struct SearchOptions<F>
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    callback: F,
    inner: InnerSearchOptions,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct InnerSearchOptions {
    plain_cost: Option<u8>,
    swamp_cost: Option<u8>,
    flee: Option<bool>,
    max_ops: Option<u32>,
    max_rooms: Option<u8>,
    max_cost: Option<f64>,
    heuristic_weight: Option<f64>,
}
impl<F> SearchOptions<F>
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    pub(crate) fn as_js_options<R>(&mut self, callback: impl Fn(&JsSearchOptions) -> R) -> R {
        let js_options: JsSearchOptions = serde_wasm_bindgen::to_value(&self.inner)
            .expect("Unable to serialize search options.")
            .unchecked_into();
        let boxed_callback: Box<dyn FnMut(JsString) -> JsValue> = Box::new(move |room| {
            let room = room
                .try_into()
                .expect("expected room name in room callback");
            (self.callback)(room).into()
        });
        let boxed_callback_lifetime_erased: Box<dyn 'static + FnMut(JsString) -> JsValue> =
            unsafe { std::mem::transmute(boxed_callback) };
        let closure = Closure::wrap(boxed_callback_lifetime_erased);
        js_options.room_callback(&closure);
        callback(&js_options)
    }
}
impl Default for SearchOptions<fn(RoomName) -> MultiRoomCostResult> {
    fn default() -> Self {
        fn cost_matrix(_: RoomName) -> MultiRoomCostResult {
            MultiRoomCostResult::Default
        }
        SearchOptions {
            callback: cost_matrix,
            inner: Default::default(),
        }
    }
}
impl<F> SearchOptions<F>
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    #[inline]
    pub fn new(callback: F) -> Self {
        SearchOptions {
            callback,
            inner: Default::default(),
        }
    }
    pub fn room_callback<F2>(self, callback: F2) -> SearchOptions<F2>
    where
        F2: FnMut(RoomName) -> MultiRoomCostResult,
    {
        SearchOptions {
            callback,
            inner: self.inner,
        }
    }
    #[inline]
    pub fn plain_cost(mut self, cost: u8) -> Self {
        self.inner.plain_cost = Some(cost);
        self
    }
    #[inline]
    pub fn swamp_cost(mut self, cost: u8) -> Self {
        self.inner.swamp_cost = Some(cost);
        self
    }
    #[inline]
    pub fn flee(mut self, flee: bool) -> Self {
        self.inner.flee = Some(flee);
        self
    }
    #[inline]
    pub fn max_ops(mut self, ops: u32) -> Self {
        self.inner.max_ops = Some(ops);
        self
    }
    #[inline]
    pub fn max_rooms(mut self, rooms: u8) -> Self {
        self.inner.max_rooms = Some(rooms);
        self
    }
    #[inline]
    pub fn max_cost(mut self, cost: f64) -> Self {
        self.inner.max_cost = Some(cost);
        self
    }
    #[inline]
    pub fn heuristic_weight(mut self, weight: f64) -> Self {
        self.inner.heuristic_weight = Some(weight);
        self
    }
}
#[wasm_bindgen]
pub struct SearchGoal {
    pos: Position,
    range: u32,
}
impl SearchGoal {
    pub fn new(pos: Position, range: u32) -> Self {
        SearchGoal { pos, range }
    }
}
#[wasm_bindgen]
impl SearchGoal {
    #[wasm_bindgen(getter)]
    pub fn pos(&self) -> RoomPosition {
        self.pos.into()
    }
    #[wasm_bindgen(getter)]
    pub fn range(&self) -> u32 {
        self.range
    }
}
pub fn search<F>(
    from: Position,
    to: Position,
    range: u32,
    options: Option<SearchOptions<F>>,
) -> SearchResults
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    let goal = SearchGoal { pos: to, range };
    let goal = JsValue::from(goal);
    search_real(from, &goal, options)
}
pub fn search_many<F>(
    from: Position,
    to: impl Iterator<Item = SearchGoal>,
    options: Option<SearchOptions<F>>,
) -> SearchResults
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    let goals: Array = to.map(JsValue::from).collect();
    search_real(from, goals.as_ref(), options)
}
fn search_real<F>(
    from: Position,
    goal: &JsValue,
    options: Option<SearchOptions<F>>,
) -> SearchResults
where
    F: FnMut(RoomName) -> MultiRoomCostResult,
{
    let from = from.into();
    if let Some(mut options) = options {
        options.as_js_options(|js_options| PathFinder::search_internal(&from, goal, js_options))
    } else {
        PathFinder::search_internal(&from, goal, &JsValue::UNDEFINED)
    }
}