mod audio;
mod metronome;
mod tracker;

use std::{cell::RefCell, time::Duration};

use tracker::BeatTracker;

use neon::prelude::*;

use self::tracker::TrackerMode;

type BoxedTracker = JsBox<RefCell<BeatTracker>>;
impl Finalize for BeatTracker {}

pub fn get_beat_tracker(mut cx: FunctionContext) -> JsResult<JsObject> {
    let obj = cx.empty_object();
    let value_obj = cx.empty_object();

    let boxed_tracker = cx.boxed(RefCell::new(BeatTracker::new(Duration::from_secs(2))));
    value_obj.set(&mut cx, "_rust_ptr", boxed_tracker)?;

    let set_config_function = JsFunction::new(&mut cx, set_config)?;
    value_obj.set(&mut cx, "setConfig", set_config_function)?;

    let tap_function = JsFunction::new(&mut cx, tap)?;
    value_obj.set(&mut cx, "tap", tap_function)?;

    let get_progress_function = JsFunction::new(&mut cx, get_progress)?;
    value_obj.set(&mut cx, "getProgress", get_progress_function)?;

    let get_current_bass_function = JsFunction::new(&mut cx, get_current_bass)?;
    value_obj.set(&mut cx, "getCurrentBass", get_current_bass_function)?;

    let close_function = JsFunction::new(&mut cx, close)?;
    value_obj.set(&mut cx, "close", close_function)?;

    let get_graph_points_function = JsFunction::new(&mut cx, get_graph_points)?;
    value_obj.set(&mut cx, "getGraphPoints", get_graph_points_function)?;

    obj.set(&mut cx, "value", value_obj)?;

    let success_string = cx.string("success".to_string());
    obj.set(&mut cx, "type", success_string)?;

    Ok(obj)
}

pub fn set_config(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.this();
    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    let ref mut config = boxed_tracker.borrow_mut().config;

    let arg = cx.argument::<JsObject>(0)?;

    let mode = arg
        .get(&mut cx, "mode")?
        .downcast_or_throw::<JsString, _>(&mut cx)?
        .value(&mut cx);

    config.mode = match mode.as_str() {
        "auto" => TrackerMode::AUTO,
        "manual" => TrackerMode::MANUAL,
        s => {
            return cx.throw_error(format!("Invalid config mode: '{}'", s.to_string()));
        }
    };

    let ac_threshold = arg
        .get(&mut cx, "acThreshold")?
        .downcast_or_throw::<JsNumber, _>(&mut cx)?
        .value(&mut cx);

    config.ac_threshold = ac_threshold as f32;

    let zero_crossing_beat_delay = arg
        .get(&mut cx, "zeroCrossingBeatDelay")?
        .downcast_or_throw::<JsNumber, _>(&mut cx)?
        .value(&mut cx);

    config.zero_crossing_beat_delay = zero_crossing_beat_delay as i64;

    Ok(cx.undefined())
}

pub fn tap(mut cx: FunctionContext) -> JsResult<JsValue> {
    let this = cx.this();

    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    boxed_tracker.borrow_mut().tap();

    Ok(JsUndefined::new(&mut cx).upcast())
}

pub fn get_progress(mut cx: FunctionContext) -> JsResult<JsObject> {
    let this = cx.this();

    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    let obj = cx.empty_object();

    let type_string;
    match boxed_tracker.borrow_mut().current_beat_progress() {
        Some(progress) => {
            type_string = cx.string("some".to_string());

            let progress = cx.number(progress);
            obj.set(&mut cx, "value", progress)?;
        }
        None => {
            type_string = cx.string("none".to_string());
        }
    }

    obj.set(&mut cx, "type", type_string)?;

    Ok(obj)
}

fn get_current_bass(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.this();

    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    let bass = boxed_tracker.borrow().get_current_bass();

    Ok(cx.number(bass))
}

fn get_graph_points(mut cx: FunctionContext) -> JsResult<JsObject> {
    let this = cx.this();

    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    let obj = cx.empty_object();

    let (bass_filtered, auto_correlated) = boxed_tracker.borrow().get_graph_points();

    let arr_bf = cx.empty_array();
    for (i, f) in bass_filtered.iter().enumerate() {
        let num = cx.number(*f);
        arr_bf.set(&mut cx, i as u32, num)?;
    }
    obj.set(&mut cx, "bassFiltered", arr_bf)?;

    let arr_ac = cx.empty_array();
    for (i, f) in auto_correlated.iter().enumerate() {
        let num = cx.number(*f);
        arr_ac.set(&mut cx, i as u32, num)?;
    }
    obj.set(&mut cx, "autoCorrelated", arr_ac)?;

    Ok(obj)
}

pub fn close(mut cx: FunctionContext) -> JsResult<JsObject> {
    let this = cx.this();

    let boxed_tracker = this
        .get(&mut cx, "_rust_ptr")?
        .downcast_or_throw::<BoxedTracker, _>(&mut cx)?;

    let obj = cx.empty_object();
    let success_string;

    match boxed_tracker.borrow_mut().stop() {
        Ok(()) => {
            success_string = cx.string("success");
        }
        Err(e) => {
            success_string = cx.string("error");

            let error_message = cx.string(e.to_string());
            obj.set(&mut cx, "message", error_message)?;
        }
    }
    obj.set(&mut cx, "type", success_string)?;

    Ok(obj)
}
