use bevy_app::{App, Plugin, PreUpdate};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::{In, IntoSystem};
use bevy_ecs::{
  event::EventReader,
  system::{Res, ResMut, Resource},
};
use bevy_input::{
  keyboard::KeyCode,
  mouse::{MouseButton, MouseMotion, MouseScrollUnit, MouseWheel},
  ButtonInput,
};
use bevy_utils::HashMap;

use crate::{
  contract::{Action, GameState, InputsContainer},
  resource::*,
};

pub struct ControlsPlugin<A: Action, Ic: InputsContainer<A>, Gs: GameState> {
  _action: std::marker::PhantomData<A>,
  _inputs_container: std::marker::PhantomData<Ic>,
  _game_state: std::marker::PhantomData<Gs>,
  controls: Controls<A, Gs>,
}

impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Default for ControlsPlugin<A, Ic, Gs> {
  fn default() -> Self {
    Self {
      _action: std::marker::PhantomData,
      _inputs_container: std::marker::PhantomData,
      _game_state: std::marker::PhantomData,
      controls: Controls::<A, Gs>::default(),
    }
  }
}

impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs> {
  pub fn new(controls: Controls<A, Gs>) -> Self {
    Self {
      _action: std::marker::PhantomData,
      _inputs_container: std::marker::PhantomData,
      _game_state: std::marker::PhantomData,
      controls,
    }
  }
}

#[derive(Resource, Default, Debug, Deref, DerefMut)]
struct TrigeredInputs(HashMap<InputType, InputValue>);

impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> Plugin for ControlsPlugin<A, Ic, Gs> {
  fn build(&self, app: &mut App) {
    app
      .insert_resource(self.controls.clone())
      .init_resource::<Ic>()
      .init_resource::<TrigeredInputs>()
      .insert_state::<Gs>(Gs::default())
      .add_systems(
        PreUpdate,
        Self::collect_inputs.pipe(Self::fill_players_inputs),
      );

    #[cfg(feature = "reflect")]
    app.register_type::<Controls<A, Gs>>();
  }
}

impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs> {
  // Separate collecting and filling is necessary becouse order of complicate imputs in future
  fn collect_inputs(
    keyboard_button: Res<ButtonInput<KeyCode>>,
    mouse_buttons: Res<ButtonInput<MouseButton>>,
    mut scroll_evr: EventReader<MouseWheel>,
    mut motion_evr: EventReader<MouseMotion>,
    // mut collected_inputs: ResMut<TrigeredInputs>,
  ) -> TrigeredInputs {
    let mut collected_inputs = TrigeredInputs(HashMap::new());
    for key in keyboard_button.get_pressed() {
      collected_inputs.insert(InputType::Keyboard(*key), InputValue::Boolean(true));
    }

    // buttons.get_just_released()

    for button in mouse_buttons.get_pressed() {
      collected_inputs.insert(
        InputType::Mouse(MouseInput::Button(*button)),
        InputValue::Boolean(true),
      );
    }

    for ev in motion_evr.read() {
      collected_inputs.insert(
        InputType::Mouse(MouseInput::Axis(AxisName::Horizontal)),
        InputValue::Float(ev.delta.x),
      );
      collected_inputs.insert(
        InputType::Mouse(MouseInput::Axis(AxisName::Vertical)),
        InputValue::Float(ev.delta.y),
      );
    }

    for ev in scroll_evr.read() {
      match ev.unit {
        MouseScrollUnit::Line => {
          collected_inputs.insert(
            InputType::Mouse(MouseInput::Wheel(AxisName::Horizontal)),
            InputValue::Float(ev.x),
          );
          collected_inputs.insert(
            InputType::Mouse(MouseInput::Wheel(AxisName::Vertical)),
            InputValue::Float(ev.y),
          );
        }
        MouseScrollUnit::Pixel => {
          panic!("I'm do not accept you to use touch pad in my game");
        }
      }
    }

    collected_inputs
  }

  // TODO: ! Zero out values that did not arrive
  fn fill_players_inputs(
    In(collected_inputs): In<TrigeredInputs>,
    mut inputs_container: ResMut<Ic>,
    controls: Res<Controls<A, Gs>>,
    game_state: Res<State<Gs>>,
  ) {
    if let Some(inputs) = inputs_container.me_mut() {
      // TODO: only on controls change
      let mut action_binding_pairs: Vec<(&A, &BindingConfig<Gs>, &Binding<Gs>)> = Vec::new();
      for (action, config) in controls.iter() {
        for binding in config.bindings.iter() {
          action_binding_pairs.push((action, config, binding));
        }
      }

      action_binding_pairs.sort_by(|a, b| b.2.input.len().cmp(&a.2.input.len()));

      'bindings_loop: for (action, _config, binding) in action_binding_pairs.iter() {
        // check binding conditions
        for condition in &binding.conditions {
          match condition {
            BindingCondition::InGameState(state) => {
              if *state != *game_state.get() {
                continue 'bindings_loop;
              }
            }
          }
        }

        //log::info!("{:#?}", collected_inputs);
        //log::info!("{:#?}", action_binding_pairs[0]);

        match &binding.input {
          ButtonCombination::Single(input_type) => {
            // TODO: exclude if used
            if let Some(value) = collected_inputs.get(&*input_type) {
              inputs.forced_set(**action, *value);
            } else {
              inputs.forced_set(**action, InputValue::Empty);
            }
          }
        }
      }
    } else {
      // TODO: print onece
      log::warn!("cannot find me in inputs container");
    }
  }
}
