/*
    Hurry Curry! - a game about cooking
    Copyright 2024 metamuffin
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
*/
use crate::{
    config::Config,
    helper::InterpolateExt,
    render::{
        misc::MiscTextures,
        sprite::{Sprite, SpriteDraw},
        AtlasLayout, Renderer,
    },
    tilemap::Tilemap,
    State,
};
use hurrycurry_client_lib::{network::sync::Network, spatial_index::SpatialIndex, Involvement};
use hurrycurry_protocol::{
    glam::{IVec2, Vec2},
    movement::MovementBase,
    Gamedata, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC, PacketS, PlayerClass,
    PlayerID, RecipeIndex, Score, TileIndex,
};
use log::{info, warn};
use sdl2::{
    keyboard::{KeyboardState, Scancode},
    rect::Rect,
};
use std::collections::{HashMap, HashSet};
pub struct Game {
    network: Network,
    data: Gamedata,
    tiles: HashMap,
    tilemap: Tilemap,
    walkable: HashSet,
    players: HashMap,
    players_spatial_index: SpatialIndex,
    items_removed: Vec- ,
    my_id: PlayerID,
    camera_center: Vec2,
    misc_textures: MiscTextures,
    item_sprites: Vec,
    movement_send_cooldown: f32,
    interacting: bool,
    score: Score,
}
pub struct Tile {
    _kind: TileIndex,
    item: Option- ,
}
pub struct Player {
    movement: MovementBase,
    item: Option- ,
    message_persist: Option<(Message, MessageTimeout)>,
    _name: String,
    _character: i32,
    _class: PlayerClass,
    interact_target_anim: Vec2,
    interact_target_anim_pressed: f32,
}
pub struct Item {
    position: Vec2,
    parent_position: Vec2,
    kind: ItemIndex,
    alive: f32,
    active: Option,
}
impl Game {
    pub fn new(mut network: Network, config: &Config, layout: &AtlasLayout) -> Self {
        network.queue_out.push_back(PacketS::Join {
            id: None,
            name: config.username.clone(),
            class: PlayerClass::Chef,
            character: 0,
        });
        Self {
            network,
            tiles: HashMap::new(),
            players: HashMap::new(),
            tilemap: Tilemap::default(),
            my_id: PlayerID(0),
            data: Gamedata::default(),
            walkable: HashSet::new(),
            movement_send_cooldown: 0.,
            misc_textures: MiscTextures::init(layout),
            item_sprites: Vec::new(),
            items_removed: Vec::new(),
            interacting: false,
            score: Score::default(),
            players_spatial_index: SpatialIndex::default(),
            camera_center: Vec2::ZERO,
        }
    }
    pub fn tick(
        &mut self,
        dt: f32,
        keyboard: &KeyboardState,
        layout: &AtlasLayout,
    ) -> Option> {
        if let Err(e) = self.network.poll() {
            eprintln!("network error: {e}");
            return Some(Box::new(State::Quit));
        }
        for packet in self.network.queue_in.drain(..).collect::>() {
            self.packet_in(packet, layout);
        }
        let mut direction = IVec2::new(
            keyboard.is_scancode_pressed(Scancode::D) as i32
                - keyboard.is_scancode_pressed(Scancode::A) as i32,
            keyboard.is_scancode_pressed(Scancode::S) as i32
                - keyboard.is_scancode_pressed(Scancode::W) as i32,
        )
        .as_vec2();
        let boost = keyboard.is_scancode_pressed(Scancode::K);
        let interact = keyboard.is_scancode_pressed(Scancode::Space)
            | keyboard.is_scancode_pressed(Scancode::J);
        if interact {
            direction *= 0.;
        }
        self.movement_send_cooldown -= dt;
        let send_movement = self.movement_send_cooldown < 0.;
        if send_movement {
            self.movement_send_cooldown += 0.04
        }
        self.score.time_remaining -= dt as f64;
        self.score.time_remaining -= self.score.time_remaining.max(0.);
        if interact != self.interacting {
            if interact {
                self.network.queue_out.push_back(PacketS::Interact {
                    player: self.my_id,
                    pos: Some(self.players[&self.my_id].movement.get_interact_target()),
                });
            } else {
                self.network.queue_out.push_back(PacketS::Interact {
                    player: self.my_id,
                    pos: None,
                });
            }
            self.interacting = interact;
        }
        if let Some(player) = self.players.get_mut(&self.my_id) {
            player.movement.input(direction, boost);
            if send_movement {
                self.network
                    .queue_out
                    .push_back(player.movement.movement_packet_s(self.my_id));
            }
            player.interact_target_anim.exp_to(
                player.movement.get_interact_target().as_vec2() + Vec2::new(0., -0.4),
                dt * 20.,
            );
            player
                .interact_target_anim_pressed
                .exp_to(if interact { 1. } else { 0. }, dt * 10.);
            self.camera_center.exp_to(player.movement.position, dt * 5.);
        }
        for (&pid, player) in &mut self.players {
            player.movement.update(&self.walkable, dt);
            if let Some((_, timeout)) = &mut player.message_persist {
                timeout.remaining -= dt;
                if timeout.remaining < 0. {
                    player.message_persist = None;
                }
            }
            self.players_spatial_index
                .update_entry(pid, player.movement.position);
        }
        self.players_spatial_index.all(|p1, pos1| {
            self.players_spatial_index.query(pos1, 2., |p2, _pos2| {
                if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) {
                    a.movement.collide(&mut b.movement, dt)
                }
            })
        });
        for player in self.players.values_mut() {
            if let Some(item) = &mut player.item {
                item.parent_position = player.movement.position;
                item.tick(1., dt);
            }
        }
        for tile in self.tiles.values_mut() {
            if let Some(item) = &mut tile.item {
                item.tick(1., dt)
            }
        }
        self.items_removed.retain_mut(|i| {
            i.tick(0., dt);
            i.alive > 0.01
        });
        None
    }
    pub fn packet_in(&mut self, packet: PacketC, layout: &AtlasLayout) {
        match packet {
            PacketC::Joined { id } => self.my_id = id,
            PacketC::Data { data } => {
                self.tilemap.init(&data.tile_names, layout);
                self.item_sprites = data
                    .item_names
                    .iter()
                    .map(|name| {
                        Sprite::new(
                            layout
                                .get(&format!("{name}+a"))
                                .copied()
                                .unwrap_or_else(|| {
                                    warn!("no sprite for item {name:?}");
                                    Rect::new(0, 0, 32, 24)
                                }),
                            Vec2::new(0., 0.0),
                            0.1,
                        )
                    })
                    .collect();
                self.data = data;
            }
            PacketC::UpdateMap {
                tile,
                kind,
                neighbors,
            } => {
                if let Some(kind) = kind {
                    self.tiles.insert(
                        tile,
                        Tile {
                            _kind: kind,
                            item: None,
                        },
                    );
                    if self.data.tile_collide[kind.0] {
                        self.walkable.remove(&tile);
                    } else {
                        self.walkable.insert(tile);
                    }
                } else {
                    self.tiles.remove(&tile);
                    self.walkable.remove(&tile);
                }
                self.tilemap.set(tile, kind, neighbors);
            }
            PacketC::AddPlayer {
                id,
                position,
                character,
                name,
                class,
            } => {
                info!("add player {} {name:?}", id.0);
                self.players.insert(
                    id,
                    Player {
                        interact_target_anim: position,
                        interact_target_anim_pressed: 0.,
                        _class: class,
                        _character: character,
                        _name: name,
                        message_persist: None,
                        item: None,
                        movement: MovementBase {
                            position,
                            input_direction: Vec2::ZERO,
                            input_boost: false,
                            facing: Vec2::X,
                            rotation: 0.,
                            velocity: Vec2::ZERO,
                            boosting: false,
                            stamina: 0.,
                        },
                    },
                );
            }
            PacketC::RemovePlayer { id } => {
                info!("remove player {}", id.0);
                self.players_spatial_index.remove_entry(id);
                self.players.remove(&id);
            }
            PacketC::Movement {
                player,
                pos,
                rot,
                boost,
                dir,
            } => {
                if player != self.my_id {
                    if let Some(p) = self.players.get_mut(&player) {
                        p.movement.position = pos;
                        p.movement.rotation = rot;
                        p.movement.input(dir, boost);
                    }
                }
            }
            PacketC::MoveItem { from, to } => {
                let mut item = self.get_item(from).take();
                if let Some(item) = &mut item {
                    item.parent_position = self.get_location_position(to);
                }
                *self.get_item(to) = item;
            }
            PacketC::SetItem { location, item } => {
                let position = self.get_location_position(location);
                let slot = match location {
                    ItemLocation::Tile(pos) => &mut self.tiles.get_mut(&pos).unwrap().item,
                    ItemLocation::Player(pid) => &mut self.players.get_mut(&pid).unwrap().item,
                };
                self.items_removed.extend(slot.take());
                *slot = item.map(|kind| Item {
                    kind,
                    parent_position: position,
                    alive: 0.,
                    position,
                    active: None,
                })
            }
            PacketC::ClearProgress { item } => self.get_item(item).as_mut().unwrap().active = None,
            PacketC::SetProgress {
                item,
                position,
                speed,
                player,
                warn,
            } => {
                self.get_item(item).as_mut().unwrap().active = Some(Involvement {
                    position,
                    speed,
                    player,
                    warn,
                    recipe: RecipeIndex(0),
                });
            }
            PacketC::ServerMessage { .. } => {
                // TODO
            }
            PacketC::Score(score) => {
                self.score = score;
            }
            PacketC::SetIngame { state: _, lobby: _ } => {
                // TODO
            }
            PacketC::Communicate {
                player,
                message,
                timeout: Some(timeout),
            } => {
                if let Some(player) = self.players.get_mut(&player) {
                    player.message_persist = message.map(|m| (m, timeout));
                }
            }
            _ => (),
        }
    }
    pub fn get_item(&mut self, location: ItemLocation) -> &mut Option-  {
        match location {
            ItemLocation::Tile(pos) => &mut self.tiles.get_mut(&pos).unwrap().item,
            ItemLocation::Player(pid) => &mut self.players.get_mut(&pid).unwrap().item,
        }
    }
    pub fn get_location_position(&self, location: ItemLocation) -> Vec2 {
        match location {
            ItemLocation::Tile(pos) => pos.as_vec2() + 0.5,
            ItemLocation::Player(p) => self.players[&p].movement.position,
        }
    }
    pub fn draw(&self, ctx: &mut Renderer) {
        ctx.set_world_view(
            -self.camera_center + (ctx.size / ctx.get_world_scale() / 2.),
            ctx.size.min_element() / 32. / 10.,
        );
        self.tilemap.draw(ctx);
        if let Some(me) = self.players.get(&self.my_id) {
            ctx.draw_world(
                self.misc_textures
                    .interact_target
                    .at(me.interact_target_anim)
                    .tint(
                        100,
                        100 + (me.interact_target_anim_pressed * 150.) as u8,
                        100 + ((1. - me.interact_target_anim_pressed) * 150.) as u8,
                    ),
            )
        }
        for p in self.players.values() {
            p.draw(ctx, &self.item_sprites)
        }
        for tile in self.tiles.values() {
            if let Some(item) = &tile.item {
                item.draw(ctx, &self.item_sprites)
            }
        }
        for item in &self.items_removed {
            item.draw(ctx, &self.item_sprites)
        }
    }
}
impl Item {
    pub fn tick(&mut self, alive: f32, dt: f32) {
        self.position.exp_to(self.parent_position, dt * 20.);
        self.alive.exp_to(alive, dt * 20.);
        if let Some(active) = &mut self.active {
            active.position += active.speed * dt;
        }
    }
    pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) {
        ctx.draw_world(
            item_sprites[self.kind.0]
                .at(self.position)
                .alpha(self.alive),
        );
        if let Some(Involvement { position, warn, .. }) = self.active {
            let (bg, fg) = if warn {
                ([100, 0, 0, 200], [255, 0, 0, 200])
            } else {
                ([0, 100, 0, 200], [0, 255, 0, 200])
            };
            ctx.draw_world(SpriteDraw::overlay(
                ctx.misc_textures.solid,
                self.position + Vec2::new(-0.5, -1.3),
                Vec2::new(1., 0.2),
                Some(bg),
            ));
            ctx.draw_world(SpriteDraw::overlay(
                ctx.misc_textures.solid,
                self.position + Vec2::new(-0.5, -1.3),
                Vec2::new(position, 0.2),
                Some(fg),
            ))
        }
    }
}
impl Player {
    pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) {
        ctx.draw_world(
            match self._class {
                PlayerClass::Chef | PlayerClass::Bot => &ctx.misc_textures.chef,
                PlayerClass::Customer => &ctx.misc_textures.customer,
            }
            .at(self.movement.position),
        );
        if let Some((message, _timeout)) = &self.message_persist {
            match message {
                Message::Text(_) => (), // TODO
                Message::Item(item) => {
                    ctx.draw_world(ctx.misc_textures.itembubble.at(self.movement.position));
                    ctx.draw_world(
                        item_sprites[item.0]
                            .at(self.movement.position)
                            .elevate(1.2)
                            .scale(0.8),
                    );
                }
                _ => (),
            }
        }
        if let Some(item) = &self.item {
            item.draw(ctx, item_sprites)
        }
    }
}