use super::time_helper::*;
use std::collections::HashMap;
use std::time::Instant;

/// Default mouse sensibility, as this is an arbitrary unit, default is 1.
/// Setting it to a negative value will invert mouse.
pub const DEFAULT_MOUSE_SENSIBILITY: f64 = 1.0;

/// Keyboard sensibility, one way to think of it is "how many pixels should be
/// a corresponding mouse move, if it lasted 1 second". So moving the
/// mouse of 1000 pixels in 1 second is equivalent to keeping a move
/// arrow pressed for 1 second.
/// Setting it to negative value will invert keyboard.
pub const DEFAULT_KEYBOARD_SENSIBILITY: f64 = 1000.0;

/// Minimum intensity.
pub const INTENSITY_MIN: f64 = 0.0;

/// Maximum intensity.
pub const INTENSITY_MAX: f64 = 1.0;

/// When first pressing a key, this value is put in the accumulator.
/// This is to avoid waiting for the first repeat to actually do something.
pub const BASE_KEYBOARD_ACC_VALUE: f64 = 0.01f64;

#[derive(Debug, Clone, Copy)]
struct KeyState {
    acc: f64,
    intensity: f64,
    last_press: Option<Instant>,
}

impl KeyState {
    fn new(intensity: f64, now: Option<Instant>) -> KeyState {
        KeyState {
            acc: BASE_KEYBOARD_ACC_VALUE,
            intensity,
            last_press: now,
        }
    }

    fn bump(&mut self, intensity: f64, now: Option<Instant>) {
        let now = unwrap_now(now);
        match self.last_press {
            Some(last_press) => {
                if now < last_press {
                    return;
                }
                let d = now - last_press;
                self.acc += self.intensity * d.as_secs_f64();
            }
            None => {
                self.acc = BASE_KEYBOARD_ACC_VALUE;
            }
        }
        self.intensity = intensity;
        self.last_press = Some(now);
    }

    fn release(&mut self, now: Option<Instant>) {
        if let Some(_) = self.last_press {
            self.bump(self.intensity, now);

            // So now we decrease by BASE_KEYBOARD_ACC_VALUE.
            // The idea follows: we've initialized the key
            // to that value so when it is released we should
            // "reimburse" the debt. If it is negative, it means
            // we moved too much already.
            self.acc -= BASE_KEYBOARD_ACC_VALUE;
            if self.acc < 0f64 {
                self.acc = 0f64;
            }
            self.intensity = 0f64;
            self.last_press = None;
        }
    }

    fn flush(&mut self, now: Option<Instant>) -> f64 {
        if let Some(_) = self.last_press {
            self.bump(self.intensity, now);
        }
        let acc = self.acc;
        self.acc = 0f64;
        acc
    }
}

/// An object to tract steering, that is, going left, or going right.
/// This does not actually poll/reacts to the physical controllers,
/// it just collects "go right" or "go left" informations then
/// aggregates and consolidates them.
#[derive(Debug)]
pub struct InputSteer {
    pub mouse_sensibility: f64,
    pub keyboard_sensibility: f64,
    mouse_acc: f64,
    right_keys: HashMap<i64, KeyState>,
    left_keys: HashMap<i64, KeyState>,
}

impl InputSteer {
    /// Create a new input steer manager.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let _input_steer = InputSteer::new();
    /// ```
    pub fn new() -> InputSteer {
        InputSteer {
            mouse_sensibility: DEFAULT_MOUSE_SENSIBILITY,
            keyboard_sensibility: DEFAULT_KEYBOARD_SENSIBILITY,
            mouse_acc: 0f64,
            right_keys: HashMap::new(),
            left_keys: HashMap::new(),
        }
    }

    fn flush_mouse(&mut self) -> f64 {
        let acc = self.mouse_acc;
        self.mouse_acc = 0f64;
        acc
    }

    /// Register a move of N pixels of the mouse.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_steer = InputSteer::new();
    /// // Player moved the mouse of 42 pixels to the left.
    /// input_steer.mouse_move(-42.0);
    /// ```
    pub fn mouse_move(&mut self, delta_px: f64) {
        self.mouse_acc += delta_px;
    }

    fn bump_key(keys: &mut HashMap<i64, KeyState>, id: i64, intensity: f64, now: Option<Instant>) {
        let intensity = if intensity > INTENSITY_MIN {
            if intensity < INTENSITY_MAX {
                intensity
            } else {
                INTENSITY_MAX
            }
        } else {
            INTENSITY_MIN
        };
        if intensity <= 0.0 {
            return;
        }
        match keys.get_mut(&id) {
            Some(key_state) => {
                key_state.bump(intensity, now);
            }
            None => {
                let key_state = KeyState::new(intensity, now);
                keys.insert(id.clone(), key_state);
            }
        }
    }

    fn release_key(keys: &mut HashMap<i64, KeyState>, id: i64, now: Option<Instant>) {
        if let Some(key_state) = keys.get_mut(&id) {
            key_state.release(now);
        }
    }

    fn flush_keys(keys: &mut HashMap<i64, KeyState>, now: Option<Instant>) -> f64 {
        let mut acc: f64 = 0f64;
        for key_state in keys.values_mut() {
            acc += key_state.flush(now);
        }
        acc
    }

    fn handle_key(
        keys: &mut HashMap<i64, KeyState>,
        id: i64,
        intensity: f64,
        state: bool,
        now: Option<Instant>,
    ) {
        if state {
            Self::bump_key(keys, id, intensity, now);
        } else {
            Self::release_key(keys, id, now);
        }
    }

    /// Register a change on a key handling the "move to the right".
    ///
    /// One needs to provide an ID (typically, a scancode, but it can
    /// be anything) as the manager is stateful and keeps track of what
    /// is pressed or released.
    ///
    /// The intensity can be used to make in account the fact that a
    /// key could be "half pressed". It could typically be used for
    /// an analog joystick, which does not act as a mouse, it is
    /// rather a virtual key which can be pressed from 0 to 100%.
    ///
    /// A true state means pressed, to be called on key down events.
    /// A false state means released, to be called on key up events.
    ///
    /// The timestamp is used to handle repeats and polling properly,
    /// by passing None, the current timestamp, AKA now(), is used.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_steer = InputSteer::new();
    /// // Player press arrow right key, pretending keycode is 42.
    /// input_steer.right_key(42, 0.5, true, None);
    /// ```
    pub fn right_key(&mut self, id: i64, intensity: f64, state: bool, now: Option<Instant>) {
        Self::handle_key(&mut self.right_keys, id, intensity, state, now);
    }

    /// Register a change on a key handling the "move to the left".
    ///
    /// One needs to provide an ID (typically, a scancode, but it can
    /// be anything) as the manager is stateful and keeps track of what
    /// is pressed or released.
    ///
    /// The intensity can be used to make in account the fact that a
    /// key could be "half pressed". It could typically be used for
    /// an analog joystick, which does not act as a mouse, it is
    /// rather a virtual key which can be pressed from 0 to 100%.
    ///
    /// A true state means pressed, to be called on key down events.
    /// A false state means released, to be called on key up events.
    ///
    /// The timestamp is used to handle repeats and polling properly,
    /// by passing None, the current timestamp, AKA now(), is used.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_steer = InputSteer::new();
    /// // Player moved joystick half-way to the left, pretending keycode is 17.
    /// input_steer.left_key(17, 0.5, true, None);
    /// ```
    pub fn left_key(&mut self, id: i64, intensity: f64, state: bool, now: Option<Instant>) {
        Self::handle_key(&mut self.left_keys, id, intensity, state, now);
    }

    /// Get the information about steering.
    ///
    /// The information returned accumulates all the informations from
    /// the mouse, the keys, and outputs a read-to-use, all-in-one
    /// number which can be used to figure out
    /// "should I go right or should I go left".
    ///
    /// The timestamp is used to handle repeats and polling properly,
    /// by passing None, the current timestamp, AKA now(), is used.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_steer = InputSteer::new();
    /// // Player pressed left arrow, pretending keycode is 41.
    /// input_steer.left_key(41, 1.0, true, None);
    /// let steer = input_steer.pop_steer(None);
    /// assert!(steer < 0.0);
    /// ```
    pub fn pop_steer(&mut self, now: Option<Instant>) -> f64 {
        let mouse_steer = self.flush_mouse();
        let key_steer = Self::flush_keys(&mut self.right_keys, now)
            - Self::flush_keys(&mut self.left_keys, now);
        mouse_steer * DEFAULT_MOUSE_SENSIBILITY + key_steer * DEFAULT_KEYBOARD_SENSIBILITY
    }
}

impl std::default::Default for InputSteer {
    fn default() -> Self {
        Self::new()
    }
}
