use bevy::utils::HashSet;

use crate::audio::AudioEvent;
use crate::prelude::*;

pub(crate) struct GamePlugin;

impl Plugin for GamePlugin {
    fn build(&self, app: &mut App) {
        app.add_event::<Move>()
            .add_event::<Selection>()
            .add_state::<TurnState>()
            .insert_resource(Score { ..default() })
            .add_systems(Startup, setup_board)
            .add_systems(OnEnter(GameState::Play), hide_valid_moves)
            .add_systems(
                Update,
                (
                    menu::exit_to_menu
                        .run_if(in_state(GameState::Play).or_else(in_state(GameState::Endgame))),
                    update_board
                        .run_if(on_event::<Move>())
                        .after(handle_selection),
                    set_side.run_if(any_component_changed::<BoardIndex>),
                    cancel_place.run_if(|buttons: Res<Input<MouseButton>>| -> bool {
                        buttons.just_pressed(MouseButton::Right)
                    }),
                    handle_selection.run_if(on_event::<Selection>()),
                    show_valid_moves.run_if(any_component_added::<Selected>),
                    hide_valid_moves.run_if(any_component_removed::<Selected>()),
                    manage_score.run_if(any_component_added::<Captured>),
                    check_endgame.run_if(resource_changed::<Board>()),
                    reset_game.run_if(|keys: Res<Input<KeyCode>>| -> bool {
                        keys.just_pressed(KeyCode::R)
                    }),
                ),
            )
            .add_systems(OnEnter(GameState::Endgame), set_endgame)
            .add_systems(OnExit(GameState::Endgame), clear_endgame)
            .add_systems(
                PreUpdate,
                asserts::<display3d::Display3d>
                    .run_if(in_state(DisplayState::Display3d))
                    .run_if(in_state(GameState::Play)),
            )
            .add_systems(
                PostUpdate,
                (debug_board.run_if(resource_exists::<debug::DebugEnabled>()),),
            );
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
pub(crate) struct TurnState(pub Side);

impl std::ops::Not for TurnState {
    type Output = Self;

    fn not(self) -> Self::Output {
        let TurnState(side) = self;
        TurnState(!side)
    }
}

#[derive(Debug, Component, Clone, PartialEq, Copy, Hash)]
pub(crate) enum Piece {
    Pawn,
    Drone,
    Queen,
}

#[derive(Debug, Component, Clone, PartialEq)]
pub(crate) enum Tile {
    Dark,
    Light,
}

pub(crate) fn tiles() -> impl Iterator<Item = (BoardIndex, Tile)> {
    (0..32).map(|i| {
        let x = i % 8;
        let y = i / 8;
        let s = (x % 2) ^ (y % 2);

        let index = BoardIndex { x, y };
        let tile = if s == 0 { Tile::Dark } else { Tile::Light };

        (index, tile)
    })
}

#[derive(Debug, Component)]
pub(crate) struct BoardComponent;

#[derive(Debug, Component)]
pub(crate) struct ValidMove;

#[derive(Debug, Component)]
pub(crate) struct Captured;

// manually for the type.
impl std::fmt::Display for Piece {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Piece::Queen => write!(f, "@"),
            Piece::Drone => write!(f, "^"),
            Piece::Pawn => write!(f, "*"),
        }
    }
}

#[derive(Debug, PartialEq)]
pub(crate) enum GameError {
    NullMove,
    InvalidIndex,
    InvalidMove,
}

/// Tracks the score of each side of the game
#[derive(Debug, Resource, Default)]
pub(crate) struct Score {
    a: usize,
    b: usize,
    captures_a: usize,
    captures_b: usize,
}

impl Score {
    fn increment(&mut self, side: Side, piece: Piece) {
        let i = match piece {
            Piece::Queen => 3,
            Piece::Drone => 2,
            Piece::Pawn => 1,
        };
        match side {
            Side::A => {
                self.captures_a += 1;
                self.a += i;
            }
            Side::B => {
                self.captures_b += 1;
                self.b += i;
            }
        }
    }

    pub(crate) fn _get(&self, side: Side) -> usize {
        match side {
            Side::A => self.a,
            Side::B => self.b,
        }
    }

    pub(crate) fn captures(&self, side: Side) -> usize {
        match side {
            Side::A => self.captures_a,
            Side::B => self.captures_b,
        }
    }
}

/// The board is setup like this:
/// ```text
///       0  1  2  3  4  5  6  7
///     +--+--+--+-----+--+--+--+
/// a   |  |  |  |  l  | d| Q| Q|
///     +--+--+--+--l--+--+--+--+
/// b   |d |p |p |  l  | p| d| Q|
///     +--+--+--+--l--+--+--+--+
/// c   |Q |d |p |  l  | p| p| d|
///     +--+--+--+--l--+--+--+--+
/// d   |Q |Q |d |  l  |  |  |  |
///     +--+--+--+-----+--+--+--+
/// ````
#[derive(Debug, Resource)]
pub(crate) struct Board {
    inner: Vec<Vec<Option<Piece>>>,
    moves: Vec<Move>,
}

#[derive(Debug, Default, Event, Clone)]
pub(crate) struct Move {
    pub epoch: usize,
    pub from: BoardIndex,
    pub to: Option<BoardIndex>,
}

/// Enum for the Capture event flow
#[derive(Debug, Clone, Copy, Resource)]
pub(crate) enum CaptureFlow {
    // Run the "fade out" animation
    FadeOut(Entity),
    // Put the captured piece next to the board
    Store(Entity),
    // Run the "fade in" animation
    FadeIn(Entity),
}

impl CaptureFlow {
    /// The capture flow so we can move from one "capture flow state" to the next
    /// Fade out, then store, then fade in
    pub(crate) fn next(&self) -> Option<Self> {
        match self {
            Self::FadeOut(e) => Some(Self::Store(e.clone())),
            Self::Store(e) => Some(Self::FadeIn(e.clone())),
            Self::FadeIn(_) => None,
        }
    }
}

#[derive(Debug, Component, PartialEq, Clone, Default, Copy, Eq, Hash)]
pub(crate) struct BoardIndex {
    pub x: usize,
    pub y: usize,
}

#[derive(Debug, Component, PartialEq, Clone, Copy, Eq, Hash, Default)]
pub(crate) enum Side {
    A,
    // HACK: Opening animation starts on B side
    #[default]
    B,
}

impl std::ops::Not for Side {
    type Output = Self;

    fn not(self) -> Self::Output {
        match self {
            Side::A => Side::B,
            Side::B => Side::A,
        }
    }
}

impl Board {
    /// Returns the piece at the given location
    pub(crate) fn at(&self, BoardIndex { x, y }: BoardIndex) -> Option<&Piece> {
        self.inner[y][x].as_ref()
    }

    fn new() -> Board {
        use Piece::*;
        Board {
            moves: vec![],
            inner: vec![
                vec![
                    Some(Queen),
                    Some(Queen),
                    Some(Drone),
                    None,
                    None,
                    None,
                    None,
                    None,
                ],
                vec![
                    Some(Queen),
                    Some(Drone),
                    Some(Pawn),
                    None,
                    None,
                    Some(Pawn),
                    Some(Pawn),
                    Some(Drone),
                ],
                vec![
                    Some(Drone),
                    Some(Pawn),
                    Some(Pawn),
                    None,
                    None,
                    Some(Pawn),
                    Some(Drone),
                    Some(Queen),
                ],
                vec![
                    None,
                    None,
                    None,
                    None,
                    None,
                    Some(Drone),
                    Some(Queen),
                    Some(Queen),
                ],
            ],
        }
    }

    /// Show all pieces on one side of the board
    /// OPTIMIZE: This is only used to tell if a side is empty, so it is more work than we need to do.
    pub(crate) fn on(&self, side: Side) -> Vec<(&Piece, BoardIndex)> {
        match side {
            Side::A => {
                // X: 0..3, Y: 0..3
                (0..=3)
                    .flat_map(|x| {
                        (0..=3).map(move |y| {
                            self.at(BoardIndex { x, y })
                                .map(|p| (p, BoardIndex { x, y }))
                        })
                    })
                    .filter_map(|r| r)
                    .collect()
            }
            Side::B => {
                // X: 4..7, Y: 0..3
                (4..=7)
                    .flat_map(|x| {
                        (0..=3).map(move |y| {
                            self.at(BoardIndex { x, y })
                                .map(|p| (p, BoardIndex { x, y }))
                        })
                    })
                    .filter_map(|r| r)
                    .collect()
            }
        }
    }

    /// Returns a list of all pieces on the board with their location
    pub(crate) fn pieces(&self) -> Vec<(BoardIndex, Piece)> {
        self.inner
            .iter()
            .enumerate()
            .flat_map(|(y, nested)| {
                nested.iter().enumerate().filter_map(move |(x, p)| {
                    p.as_ref().map(|val| (BoardIndex { x, y }, val.clone()))
                })
            })
            .collect()
    }

    /// Moves a piece from -> to
    pub(crate) fn move_piece(
        &mut self,
        from: BoardIndex,
        to: BoardIndex,
    ) -> Result<Vec<Move>, GameError> {
        if from == to {
            Err(GameError::NullMove)
        } else {
            match self.at(from.clone()) {
                Some(from_piece) => {
                    // Check if this is a valid move for this piece
                    if self.valid_moves(from).contains(&to) {
                        // The current epoch is the last epoch + 1
                        let epoch = self.current_epoch();

                        // Local moves vec we can return
                        let mut moves = vec![];

                        // If the position we are moving to is occupied, capture the removal in the ledger
                        if self.inner[to.y][to.x].is_some() {
                            moves.push(Move {
                                epoch,
                                from: to.clone(),
                                to: None,
                            });
                        }

                        // Capture the intened move in the moves ledger
                        moves.push(Move {
                            epoch,
                            from: from.clone(),
                            to: Some(to.clone()),
                        });

                        self.inner[to.y][to.x] = Some(*from_piece);
                        self.inner[from.y][from.x] = None;

                        self.moves.extend(moves.clone());

                        Ok(moves)
                    } else {
                        Err(GameError::InvalidMove)
                    }
                }
                None => Err(GameError::NullMove),
            }
        }
    }

    /// Returns the Side of a piece
    pub(crate) fn side(BoardIndex { x, .. }: BoardIndex) -> Result<Side, GameError> {
        match x {
            0..=3 => Ok(Side::A),
            4..=7 => Ok(Side::B),
            _ => Err(GameError::InvalidIndex),
        }
    }

    /// Returns the possible moves the piece at this tile can make.
    /// TODO: Implement "no jumping" over pieces
    pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet<BoardIndex> {
        let BoardIndex { x, y } = current_board_index;

        let f = |(a, b): (Option<usize>, Option<usize>)| {
            if let (Some(this_x), Some(this_y)) = (a, b) {
                // This has a valid x position
                let valid_x = (0..=7).contains(&this_x);
                if valid_x {
                    // It has a valid y position
                    let valid_y = (0..=3).contains(&this_y);
                    if valid_y {
                        // The checked board index
                        let this_board_index = BoardIndex {
                            x: this_x,
                            y: this_y,
                        };
                        // Only propose tiles that are empty or capture a piece on the other side
                        let valid_capture = {
                            match self.at(this_board_index) {
                                Some(_) => {
                                    let same_side = Board::side(current_board_index);
                                    Board::side(this_board_index) != same_side
                                }
                                None => true,
                            }
                        };
                        if valid_capture {
                            // You cannot move a piece from A to B and then back to A when it was just moved from SideA->SideB
                            // Move rejection is not allowed
                            let rejection = {
                                if let Some(Move {
                                    from: last_from,
                                    to: Some(last_to),
                                    ..
                                }) = self.moves.last()
                                {
                                    // TODO: I think this is more logic than we need to express
                                    // the sentiment...
                                    *last_from == this_board_index
                                        && *last_to == current_board_index
                                        && Board::side(this_board_index)
                                            != Board::side(current_board_index)
                                } else {
                                    false
                                }
                            };
                            // If all tests pass, this is a valid move
                            (!rejection).then_some(this_board_index)
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                } else {
                    None
                }
            } else {
                None
            }
        };

        match self.at(BoardIndex { x, y }) {
            // One space in any diagonal
            Some(Piece::Pawn) => std::iter::empty()
                .chain(
                    (-1..=1)
                        .zip(-1..=1)
                        .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
                )
                .chain(
                    (-1..=1)
                        .zip((-1..=1).rev())
                        .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
                )
                .filter_map(f)
                .collect(),
            // One or two spaces in either horizontal
            Some(Piece::Drone) => std::iter::empty()
                .chain((-2..=2).map(|i| (x.checked_add_signed(i), Some(y))))
                .chain((-2..=2).map(|i| (Some(x), y.checked_add_signed(i))))
                .filter_map(f)
                .collect(),
            // Any distance in any straight line
            Some(Piece::Queen) => std::iter::empty()
                .chain((-7..=7).map(|i| (x.checked_add_signed(i), Some(y))))
                .chain((-3..=3).map(|i| (Some(x), y.checked_add_signed(i))))
                .chain(
                    (-3..=3)
                        .zip(-3..=3)
                        .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
                )
                .chain(
                    (-3..=3)
                        .zip((-3..=3).rev())
                        .map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
                )
                .filter_map(f)
                .collect(),
            None => std::iter::empty().collect(),
        }
    }

    pub(crate) fn current_epoch(&self) -> usize {
        self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1
    }
}

impl std::fmt::Display for Board {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.inner.iter().rev().for_each(|row| {
            let _ = write!(f, "+--+--+--+--+--+--+--+--+\n");
            let _ = write!(f, "|");
            row.iter().for_each(|piece| {
                let _ = match piece {
                    Some(p) => write!(f, "{} |", p),
                    None => write!(f, "  |"),
                };
            });
            let _ = write!(f, "\n");
        });
        let _ = write!(f, "+--+--+--+--+--+--+--+--+");
        Ok(())
    }
}

/// Marker component for currently selected entities
#[derive(Debug, Default, Component)]
pub(crate) struct Selected;

/// Marker component for selectable entities
#[derive(Debug, Default, Component)]
pub(crate) struct Selectable;

/// Event for selecting board indexes for Moves
#[derive(Debug, Default, Event, Clone)]
pub(crate) struct Selection(pub BoardIndex);

fn setup_board(mut commands: Commands) {
    commands.insert_resource(Board::new());
}

fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
    debug_info.set("board".into(), format!("\n{}", *board));
}

/// Update this method to use a diff between the board and the state of the 2d/3d worlds
pub(crate) fn update_board(
    mut audio_events: EventWriter<AudioEvent>,
    mut events: EventReader<Move>,
    mut pieces: Query<(Entity, &mut BoardIndex), With<Piece>>,
    selected: Query<Entity, With<Selected>>,
    mut commands: Commands,
    mut played: Local<bool>,
    curr_state: Res<State<TurnState>>,
    mut next_state: ResMut<NextState<TurnState>>,
) {
    events.read().for_each(|Move { from, to, .. }| {
        pieces.iter_mut().for_each(|(entity, mut index)| {
            if *index == *from {
                match to {
                    Some(to_idx) => {
                        info!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx);
                        *index = to_idx.clone();
                        if !(*played) {
                            audio_events.send(audio::AudioEvent::PutDown);
                            audio_events.send(audio::AudioEvent::StopIdle);
                            *played = true;
                            if *from != *to_idx {
                                let ns = !*curr_state.get();
                                info!("Piece moved, switching sides: {:?}", ns);
                                next_state.set(ns);
                            }
                        }
                    }
                    None => {
                        info!("Capturing piece {:?}", entity);
                        commands
                            .entity(entity)
                            .remove::<BoardIndex>()
                            .insert(Captured);
                    }
                }
            }
        });
        selected.iter().for_each(|entity| {
            debug!("De-selecting selected piece {:?}", entity);
            commands.entity(entity).remove::<Selected>();
        });
    });
    *played = false;
}

#[derive(Debug, Component)]
struct Endgame;

fn check_endgame(board: Res<Board>, mut next_state: ResMut<NextState<GameState>>) {
    if board.on(Side::A).is_empty() || board.on(Side::B).is_empty() {
        warn!("The game is over!");
        next_state.set(GameState::Endgame);
    }
}

fn set_endgame(score: Res<Score>, mut commands: Commands) {
    commands
        .spawn((
            Endgame,
            NodeBundle {
                style: Style {
                    width: Val::Percent(100.0),
                    height: Val::Percent(100.0),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    flex_direction: FlexDirection::Column,
                    position_type: PositionType::Absolute,
                    ..default()
                },
                background_color: Color::NONE.into(),
                visibility: Visibility::Inherited,
                ..default()
            },
        ))
        .with_children(|parent| {
            parent.spawn(TextBundle::from_section(
                "S C O R E",
                TextStyle {
                    font_size: 48.0,
                    color: Color::ORANGE_RED,
                    ..default()
                },
            ));
            parent.spawn(TextBundle::from_section(
                format!("BLUE {}", score.b),
                TextStyle {
                    font_size: 32.0,
                    color: Color::BLUE,
                    ..default()
                },
            ));
            parent.spawn(TextBundle::from_section(
                format!("RED {}", score.a),
                TextStyle {
                    font_size: 32.0,
                    color: Color::RED,
                    ..default()
                },
            ));
        });
}

fn clear_endgame(query: Query<Entity, With<Endgame>>, mut commands: Commands) {
    query.iter().for_each(|e| {
        commands.entity(e).despawn_recursive();
    })
}

/// We only track 3D (hack, for now) to prevent duplicates
/// TODO: We can calculate this declaratively:
/// * All pieces without a BoardIndex are "captured"
/// * All captured pieces have their captured side preserved
/// We can iterate over these pieces and calculate the score on the fly
fn manage_score(
    events: Query<(&Side, &Piece), (Added<Captured>, With<display3d::Display3d>)>,
    mut debug_info: ResMut<debug::DebugInfo>,
    mut score: ResMut<Score>,
) {
    events.iter().for_each(|(side, piece)| {
        score.increment(!*side, *piece);
        debug_info.set("score".into(), format!("A:{}|B:{}", score.a, score.b));
    });
}

pub(crate) fn set_side(
    mut events: Query<(&mut Side, &BoardIndex), Or<(Changed<BoardIndex>, Added<BoardIndex>)>>,
) {
    events
        .iter_mut()
        .for_each(|(mut side, idx)| match Board::side(*idx) {
            Ok(s) => {
                debug!("Set side event {:?} {:?} -> {:?}", idx, side, s);
                if *side != s {
                    *side = s
                }
            }
            Err(e) => warn!("{:?}", e),
        });
}

// TODO: Handle 3d Pickup (Not showing hover animation, but still selected?)
// TODO: Handle cancel move (currently 2d just drops it in place)
fn handle_selection(
    mut selections: EventReader<Selection>,
    mut move_events: EventWriter<Move>,
    selected: Query<(Entity, &BoardIndex), (With<Selected>, With<Piece>)>,
    pieces: Query<(Entity, &BoardIndex), (With<Selectable>, Without<Selected>, With<Piece>)>,
    mut board: ResMut<Board>,
    mut commands: Commands,
    mut audio_event: EventWriter<AudioEvent>,
    mut done: Local<bool>, // Tracks if moves/audio submitted already even if multiple pieces (2d/3d) are moved.
    mut latest: Local<Option<BoardIndex>>, // Tracks the last one worked on
) {
    selections.read().for_each(|Selection(index)| {
        // Skip indexes already processed
        if Some(*index) != *latest {
            // Set the latest index to the current index
            *latest = Some(*index);
            // Reset the "done" marker
            *done = false;

            // There are no currently selected entities
            // Mark the piece at this index as selected
            if selected.is_empty() {
                pieces
                    .iter()
                    .filter(|(_, this_index)| *this_index == index)
                    .for_each(|(piece, piece_index)| {
                        info!("Selecting {:?} at {:?}", piece, piece_index);
                        commands.entity(piece).insert(Selected);
                        if !(*done) {
                            audio_event.send(audio::AudioEvent::PickUp);
                            audio_event.send(AudioEvent::Idle);
                            *done = true;
                        }
                    });
            }
            // There is a currently selected entity, so submit moves
            else {
                assert!(
                    selected.iter().len() <= 2,
                    "There are too many pieces selected!"
                );
                selected.iter().for_each(|(_, current_index)| {
                    match board.move_piece(*current_index, *index) {
                        Ok(moves) => {
                            // De-select the piece
                            info!("Applying moves {:?}", moves);
                            if !(*done) {
                                moves.iter().for_each(|m| move_events.send(m.clone()));
                                *done = true;
                            }
                        }
                        Err(GameError::NullMove) => warn!("Null move!"),
                        Err(GameError::InvalidIndex) | Err(GameError::InvalidMove) => {
                            warn!("Invalid index/move!");
                            if !(*done) {
                                audio_event.send(AudioEvent::Invalid);
                                *done = true;
                            }
                        }
                    }
                });
            }
        }
    });
    *done = false;
    *latest = None;
}

/// Triggered when right-mouse-button clicked
fn cancel_place(current: Query<&BoardIndex, With<Selected>>, mut events: EventWriter<Move>) {
    current.iter().for_each(|board_index| {
        info!("De-selecting piece at {:?}", board_index);
        events.send(Move {
            from: board_index.clone(),
            to: Some(board_index.clone()),
            ..default()
        });
    });
}

/// Panics if more than two pieces are selected at a time
fn asserts<T: Component>(
    selected_pieces: Query<Entity, (With<Piece>, With<T>, With<Selected>)>,
    selected_tiles: Query<Entity, (With<Tile>, With<T>, With<Selected>)>,
    cameras: Query<Entity, (With<Camera>, With<T>)>,
) {
    if selected_pieces.iter().len() > 2 {
        selected_pieces.iter().for_each(|e| {
            info!("Too many pieces selected, one of which is: {:?}", e);
        });
        panic!(
            "More than two pieces selected {:?}",
            selected_pieces.iter().len()
        );
    }
    if selected_tiles.iter().len() > 2 {
        panic!(
            "More than two tiles selected {:?}",
            selected_tiles.iter().len()
        );
    }
    if cameras.iter().len() != 1 {
        panic!(
            "There should be 1 cameras in this state, but there is only {}",
            cameras.iter().len()
        );
    }
}

/// Spawn "Valid move" indicators when a piece is selected
/// Another system registers these new entities and associates the correct models and plays animations.
fn show_valid_moves(
    events: Query<&BoardIndex, (With<Piece>, Added<Selected>)>,
    board: Res<Board>,
    mut indicators: Query<(&BoardIndex, &mut Visibility), With<ValidMove>>,
) {
    // When a piece is selected
    events.iter().for_each(|idx| {
        // Iterate over all ValidMove entities
        // For each one with a ValidMove index, make it visible
        board.valid_moves(*idx).iter().for_each(|idx| {
            indicators.iter_mut().for_each(|(i, mut vis)| {
                if i == idx {
                    *vis = Visibility::Inherited;
                }
            });
        })
    });
}

/// Hide "Valid Move" indicators when a piece is de-selected
fn hide_valid_moves(mut indicators: Query<&mut Visibility, With<ValidMove>>) {
    indicators.iter_mut().for_each(|mut visibility| {
        *visibility = Visibility::Hidden;
    });
}

// Resets the board to the "stock" setup
// Somewhat conflicts with board initialization!
fn reset_game(
    pieces: Query<Entity, With<Piece>>,
    mut board: ResMut<Board>,
    mut score: ResMut<Score>,
    mut commands: Commands,
) {
    // Setup the board
    *board = Board::new();

    // Reset the score
    *score = Score { ..default() };

    // Move all pieces to their correct spots
    pieces
        .iter()
        .zip(board.pieces().iter())
        .for_each(|(e, (i, p))| {
            commands
                .entity(e)
                .insert((i.clone(), p.clone()))
                .remove::<Captured>();
        });
}
