use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::SystemTime;

use noise::{NoiseFn, Seedable, SuperSimplex};
use rayon::iter::ParallelIterator;
pub use valence::prelude::*;
use vek::Lerp;

pub fn main() -> ShutdownResult {
    tracing_subscriber::fmt().init();

    let seconds_per_day = 86_400;

    let seed = (SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs()
        / seconds_per_day) as u32;

    valence::start_server(
        Game {
            player_count: AtomicUsize::new(0),
            density_noise: SuperSimplex::new().set_seed(seed),
            hilly_noise: SuperSimplex::new().set_seed(seed.wrapping_add(1)),
            stone_noise: SuperSimplex::new().set_seed(seed.wrapping_add(2)),
            gravel_noise: SuperSimplex::new().set_seed(seed.wrapping_add(3)),
            grass_noise: SuperSimplex::new().set_seed(seed.wrapping_add(4)),
        },
        None,
    )
}

struct Game {
    player_count: AtomicUsize,
    density_noise: SuperSimplex,
    hilly_noise: SuperSimplex,
    stone_noise: SuperSimplex,
    gravel_noise: SuperSimplex,
    grass_noise: SuperSimplex,
}

const MAX_PLAYERS: usize = 10;

#[async_trait]
impl Config for Game {
    type ServerState = Option<PlayerListId>;
    type ClientState = EntityId;
    type EntityState = ();
    type WorldState = ();
    /// If the chunk should stay loaded at the end of the tick.
    type ChunkState = bool;
    type PlayerListState = ();
    type InventoryState = ();

    async fn server_list_ping(
        &self,
        _server: &SharedServer<Self>,
        _remote_addr: SocketAddr,
        _protocol_version: i32,
    ) -> ServerListPing {
        ServerListPing::Respond {
            online_players: self.player_count.load(Ordering::SeqCst) as i32,
            max_players: MAX_PLAYERS as i32,
            player_sample: Default::default(),
            description: "Hello Valence!".color(Color::AQUA),
            favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()),
        }
    }

    fn init(&self, server: &mut Server<Self>) {
        server.worlds.insert(DimensionId::default(), ());
        server.state = Some(server.player_lists.insert(()).0);
    }

    fn update(&self, server: &mut Server<Self>) {
        let (world_id, world) = server.worlds.iter_mut().next().unwrap();

        server.clients.retain(|_, client| {
            if client.created_this_tick() {
                if self
                    .player_count
                    .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
                        (count < MAX_PLAYERS).then_some(count + 1)
                    })
                    .is_err()
                {
                    client.disconnect("The server is full!".color(Color::RED));
                    return false;
                }

                match server
                    .entities
                    .insert_with_uuid(EntityKind::Player, client.uuid(), ())
                {
                    Some((id, entity)) => {
                        entity.set_world(world_id);
                        client.state = id
                    }
                    None => {
                        client.disconnect("Conflicting UUID");
                        return false;
                    }
                }

                client.respawn(world_id);
                client.set_flat(true);
                client.set_game_mode(GameMode::Creative);
                client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
                client.set_player_list(server.state.clone());

                if let Some(id) = &server.state {
                    server.player_lists.get_mut(id).insert(
                        client.uuid(),
                        client.username(),
                        client.textures().cloned(),
                        client.game_mode(),
                        0,
                        None,
                    );
                }

                client.send_message("Welcome to the terrain example!".italic());
            }

            let player = server.entities.get_mut(client.state).unwrap();
            while let Some(event) = client.next_event() {
                event.handle_default(client, player);
            }

            let dist = client.view_distance();
            let p = client.position();

            for pos in chunks_in_view_distance(ChunkPos::at(p.x, p.z), dist) {
                if let Some(chunk) = world.chunks.get_mut(pos) {
                    chunk.state = true;
                } else {
                    world.chunks.insert(pos, UnloadedChunk::default(), true);
                }
            }

            if client.is_disconnected() {
                self.player_count.fetch_sub(1, Ordering::SeqCst);
                if let Some(id) = &server.state {
                    server.player_lists.get_mut(id).remove(client.uuid());
                }
                server.entities.remove(client.state);

                return false;
            }

            true
        });

        // Remove chunks outside the view distance of players.
        world.chunks.retain(|_, chunk| {
            if chunk.state {
                chunk.state = false;
                true
            } else {
                false
            }
        });

        // Generate chunk data for chunks created this tick.
        world.chunks.par_iter_mut().for_each(|(pos, chunk)| {
            if !chunk.created_this_tick() {
                return;
            }

            for z in 0..16 {
                for x in 0..16 {
                    let block_x = x as i64 + pos.x as i64 * 16;
                    let block_z = z as i64 + pos.z as i64 * 16;

                    let mut in_terrain = false;
                    let mut depth = 0;

                    for y in (0..chunk.height()).rev() {
                        let b = terrain_column(
                            self,
                            block_x,
                            y as i64,
                            block_z,
                            &mut in_terrain,
                            &mut depth,
                        );
                        chunk.set_block_state(x, y, z, b);
                    }

                    // Add grass
                    for y in (0..chunk.height()).rev() {
                        if chunk.block_state(x, y, z).is_air()
                            && chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK
                        {
                            let density = fbm(
                                &self.grass_noise,
                                [block_x, y as i64, block_z].map(|a| a as f64 / 5.0),
                                4,
                                2.0,
                                0.7,
                            );

                            if density > 0.55 {
                                if density > 0.7 && chunk.block_state(x, y + 1, z).is_air() {
                                    let upper = BlockState::TALL_GRASS
                                        .set(PropName::Half, PropValue::Upper);
                                    let lower = BlockState::TALL_GRASS
                                        .set(PropName::Half, PropValue::Lower);

                                    chunk.set_block_state(x, y + 1, z, upper);
                                    chunk.set_block_state(x, y, z, lower);
                                } else {
                                    chunk.set_block_state(x, y, z, BlockState::GRASS);
                                }
                            }
                        }
                    }
                }
            }
        });
    }
}

fn terrain_column(
    g: &Game,
    x: i64,
    y: i64,
    z: i64,
    in_terrain: &mut bool,
    depth: &mut u32,
) -> BlockState {
    const WATER_HEIGHT: i64 = 55;

    if has_terrain_at(g, x, y, z) {
        let gravel_height = WATER_HEIGHT
            - 1
            - (fbm(
                &g.gravel_noise,
                [x, y, z].map(|a| a as f64 / 10.0),
                3,
                2.0,
                0.5,
            ) * 6.0)
                .floor() as i64;

        if *in_terrain {
            if *depth > 0 {
                *depth -= 1;
                if y < gravel_height {
                    BlockState::GRAVEL
                } else {
                    BlockState::DIRT
                }
            } else {
                BlockState::STONE
            }
        } else {
            *in_terrain = true;
            let n = noise01(&g.stone_noise, [x, y, z].map(|a| a as f64 / 15.0));

            *depth = (n * 5.0).round() as u32;

            if y < gravel_height {
                BlockState::GRAVEL
            } else if y < WATER_HEIGHT - 1 {
                BlockState::DIRT
            } else {
                BlockState::GRASS_BLOCK
            }
        }
    } else {
        *in_terrain = false;
        *depth = 0;
        if y < WATER_HEIGHT {
            BlockState::WATER
        } else {
            BlockState::AIR
        }
    }
}

fn has_terrain_at(g: &Game, x: i64, y: i64, z: i64) -> bool {
    let hilly = Lerp::lerp_unclamped(
        0.1,
        1.0,
        noise01(&g.hilly_noise, [x, y, z].map(|a| a as f64 / 400.0)).powi(2),
    );

    let lower = 15.0 + 100.0 * hilly;
    let upper = lower + 100.0 * hilly;

    if y as f64 <= lower {
        return true;
    } else if y as f64 >= upper {
        return false;
    }

    let density = 1.0 - lerpstep(lower, upper, y as f64);

    let n = fbm(
        &g.density_noise,
        [x, y, z].map(|a| a as f64 / 100.0),
        4,
        2.0,
        0.5,
    );
    n < density
}

fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 {
    if x <= edge0 {
        0.0
    } else if x >= edge1 {
        1.0
    } else {
        (x - edge0) / (edge1 - edge0)
    }
}

fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persistence: f64) -> f64 {
    let mut freq = 1.0;
    let mut amp = 1.0;
    let mut amp_sum = 0.0;
    let mut sum = 0.0;

    for _ in 0..octaves {
        let n = noise01(noise, p.map(|a| a * freq));
        sum += n * amp;
        amp_sum += amp;

        freq *= lacunarity;
        amp *= persistence;
    }

    // Scale the output to [0, 1]
    sum / amp_sum
}

fn noise01(noise: &SuperSimplex, xyz: [f64; 3]) -> f64 {
    (noise.get(xyz) + 1.0) / 2.0
}