use common::{
    terrain::{
        map::{MapConfig, MapDebug, MapSample},
        uniform_idx_as_vec2, vec2_as_uniform_idx, TerrainChunkSize,
    },
    vol::RectVolSize,
};
use rayon::prelude::*;
use std::{f64, io::Write, path::PathBuf, time::SystemTime};
use tracing::{warn, Level};
use tracing_subscriber::{
    filter::{EnvFilter, LevelFilter},
    FmtSubscriber,
};
use vek::*;
use veloren_world::{
    sim::{self, get_horizon_map, sample_pos, sample_wpos, WorldOpts},
    util::Sampler,
    ColumnSample, World, CONFIG,
};

const W: usize = 1024;
const H: usize = 1024;

#[allow(clippy::needless_update)] // TODO: Pending review in #587
#[allow(clippy::unused_io_amount)] // TODO: Pending review in #587
fn main() {
    FmtSubscriber::builder()
        .with_max_level(Level::ERROR)
        .with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()))
        .init();

    // To load a map file of your choice, replace map_file with the name of your map
    // (stored locally in the map directory of your Veloren root), and swap the
    // sim::FileOpts::Save line below for the sim::FileOpts::Load one.
    let map_file =
        // "map_1575990726223.bin";
        // "map_1575987666972.bin";
        // "map_1576046079066.bin";
        // "maps/071090_2x.bin";
        "map_1579539133272.bin";
    let mut _map_file = PathBuf::from("./maps");
    _map_file.push(map_file);

    let (world, index) = World::generate(5284, WorldOpts {
        seed_elements: false,
        world_file: sim::FileOpts::LoadAsset(veloren_world::sim::DEFAULT_WORLD_MAP.into()),
        // world_file: sim::FileOpts::Load(_map_file),
        // world_file: sim::FileOpts::Save,
        ..WorldOpts::default()
    });
    let index = index.as_index_ref();
    tracing::info!("Sampling data...");
    let sampler = world.sim();
    let map_size_lg = sampler.map_size_lg();

    let samples_data = {
        let column_sample = world.sample_columns();
        (0..map_size_lg.chunks_len())
            .into_par_iter()
            .map(|posi| {
                column_sample.get((
                    uniform_idx_as_vec2(map_size_lg, posi)
                        * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
                    index,
                ))
            })
            .collect::<Vec<_>>()
            .into_boxed_slice()
    };
    let refresh_map_samples = |config: &MapConfig, samples: Option<&[Option<ColumnSample>]>| {
        (0..map_size_lg.chunks_len())
            .into_par_iter()
            .map(|posi| {
                sample_pos(
                    config,
                    sampler,
                    samples,
                    uniform_idx_as_vec2(map_size_lg, posi),
                )
            })
            .collect::<Vec<_>>()
            .into_boxed_slice()
    };
    let get_map_sample = |map_samples: &[MapSample], pos: Vec2<i32>| {
        if pos.reduce_partial_min() >= 0
            && pos.x < map_size_lg.chunks().x as i32
            && pos.y < map_size_lg.chunks().y as i32
        {
            map_samples[vec2_as_uniform_idx(map_size_lg, pos)].clone()
        } else {
            MapSample {
                alt: 0.0,
                rgba: Rgba::new(0, 0, 0, 255),
                connections: None,
                downhill_wpos: (pos + 1) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
                is_path: false,
            }
        }
    };

    let refresh_horizons = |is_basement, is_water| {
        get_horizon_map(
            map_size_lg,
            Aabr {
                min: Vec2::zero(),
                max: map_size_lg.chunks().map(|e| e as i32),
            },
            CONFIG.sea_level,
            CONFIG.sea_level + sampler.max_height,
            |posi| {
                let sample = sampler.get(uniform_idx_as_vec2(map_size_lg, posi)).unwrap();
                if is_basement {
                    sample.alt
                } else {
                    sample.basement
                }
                .max(if is_water {
                    sample.water_alt
                } else {
                    -f32::INFINITY
                })
            },
            |a| a,
            |h| h,
            /* |[al, ar]| [al, ar],
             * |[hl, hr]| [hl, hr], */
        )
        .ok()
    };

    let mut win =
        minifb::Window::new("World Viewer", W, H, minifb::WindowOptions::default()).unwrap();

    let mut focus = Vec3::new(0.0, 0.0, CONFIG.sea_level as f64);
    // Altitude is divided by gain and clamped to [0, 1]; thus, decreasing gain
    // makes smaller differences in altitude appear larger.
    let mut gain = /*CONFIG.mountain_scale*/sampler.max_height;
    // The Z component during normal calculations is multiplied by gain; thus,
    let mut fov = 1.0;
    let mut scale =
        (map_size_lg.chunks().x as f64 / W as f64).max(map_size_lg.chunks().y as f64 / H as f64);

    // Right-handed coordinate system: light is going left, down, and "backwards"
    // (i.e. on the map, where we translate the y coordinate on the world map to
    // z in the coordinate system, the light comes from -y on the map and points
    // towards +y on the map).  In a right handed coordinate system, the
    // "camera" points towards -z, so positive z is backwards "into" the camera.
    //
    // "In world space the x-axis will be pointing east, the y-axis up and the
    // z-axis will be pointing south"
    let mut light_direction = Vec3::new(-/*0.8*/1.3, -1.0, 0.3);

    let mut is_basement = false;
    let mut is_water = true;
    let mut is_shaded = true;
    let mut is_temperature = true;
    let mut is_humidity = true;

    let mut horizons = refresh_horizons(is_basement, is_water);
    let mut samples = None;

    let mut samples_changed = true;
    let mut map_samples: Box<[_]> = Box::new([]);
    while win.is_open() {
        let config = MapConfig {
            map_size_lg,
            dimensions: Vec2::new(W, H),
            focus,
            gain,
            fov,
            scale,
            light_direction,
            horizons: horizons.as_ref(), /* .map(|(a, b)| (&**a, &**b)) */

            is_basement,
            is_water,
            is_shaded,
            is_temperature,
            is_humidity,
            is_debug: true,
            is_contours: false,
            is_height_map: false,
            is_political: false,
            is_roads: false,
            rgba_alpha: 1.0,
        };

        if samples_changed {
            map_samples = refresh_map_samples(&config, samples);
        };

        let mut buf = vec![0; W * H];
        let MapDebug {
            rivers,
            lakes,
            oceans,
            quads,
        } = config.generate(
            |pos| get_map_sample(&map_samples, pos),
            |pos| sample_wpos(&config, sampler, pos),
            |pos, (r, g, b, a)| {
                let i = pos.x;
                let j = pos.y;
                buf[j * W + i] = u32::from_le_bytes([b, g, r, a]);
            },
        );

        if win.is_key_down(minifb::Key::F4) {
            // Feedback is important since on large maps it can be hard to tell if the
            // keypress registered or not.
            println!("Taking screenshot...");
            if let Some(len) = (W * H)
                .checked_mul(scale as usize)
                .and_then(|acc| acc.checked_mul(scale as usize))
            {
                let x = (W as f64 * scale) as usize;
                let y = (H as f64 * scale) as usize;
                let config = MapConfig {
                    dimensions: Vec2::new(x, y),
                    scale: 1.0,
                    ..config
                };
                let mut buf = vec![0u8; 4 * len];
                config.generate(
                    |pos| get_map_sample(&map_samples, pos),
                    |pos| sample_wpos(&config, sampler, pos),
                    |pos, (r, g, b, a)| {
                        let i = pos.x;
                        let j = pos.y;
                        (&mut buf[(j * x + i) * 4..]).write(&[r, g, b, a]).unwrap();
                    },
                );
                // TODO: Justify fits in u32.
                let world_map = image::RgbaImage::from_raw(x as u32, y as u32, buf)
                    .expect("Image dimensions must be valid");
                let mut path = PathBuf::from("./screenshots");
                if !path.exists() {
                    if let Err(e) = std::fs::create_dir(&path) {
                        warn!(?e, ?path, "Couldn't create folder for screenshot");
                    }
                }
                path.push(format!(
                    "worldmap_{}.png",
                    SystemTime::now()
                        .duration_since(SystemTime::UNIX_EPOCH)
                        .map(|d| d.as_millis())
                        .unwrap_or(0)
                ));
                if let Err(e) = world_map.save(&path) {
                    warn!(?e, ?path, "Couldn't save screenshot");
                }
            }
        }

        let spd = 32.0;
        let lspd = 0.1;
        if win.is_key_down(minifb::Key::P) {
            println!(
                "\
                 Gain / Shade gain: {:?} / {:?}\nScale / Focus: {:?} / {:?}\nLight: {:?}
                 Land(adjacent): (X = temp, Y = humidity): {:?}\nRivers: {:?}\nLakes: \
                 {:?}\nOceans: {:?}\nTotal water: {:?}\nTotal land(adjacent): {:?}",
                gain,
                fov,
                scale,
                focus,
                light_direction,
                quads,
                rivers,
                lakes,
                oceans,
                rivers + lakes + oceans,
                quads.iter().map(|x| x.iter().sum::<u32>()).sum::<u32>()
            );
        }
        if win.get_mouse_down(minifb::MouseButton::Left) {
            if let Some((mx, my)) = win.get_mouse_pos(minifb::MouseMode::Clamp) {
                let chunk_pos = (Vec2::<f64>::from(focus)
                    + (Vec2::new(mx as f64, my as f64) * scale))
                    .map(|e| e as i32);
                let block_pos = chunk_pos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e * f as i32);
                println!(
                    "Block: ({}, {}), Chunk: ({}, {})",
                    block_pos.x, block_pos.y, chunk_pos.x, chunk_pos.y
                );
                if let Some(chunk) = sampler.get(chunk_pos) {
                    //println!("Chunk info: {:#?}", chunk);
                    if let Some(id) = &chunk.place {
                        let place = world.civs().place(*id);
                        println!("Place {} info: {:#?}", id.id(), place);

                        if let Some(site) = world.civs().sites().find(|site| site.place == *id) {
                            println!("Site: {}", site);
                        }
                    }
                }
            }
        }
        let is_camera = win.is_key_down(minifb::Key::C);
        if win.is_key_down(minifb::Key::B) {
            is_basement ^= true;
            samples_changed = true;
            horizons = horizons.and_then(|_| refresh_horizons(is_basement, is_water));
        }
        if win.is_key_down(minifb::Key::H) {
            is_humidity ^= true;
            samples_changed = true;
        }
        if win.is_key_down(minifb::Key::T) {
            is_temperature ^= true;
            samples_changed = true;
        }
        if win.is_key_down(minifb::Key::O) {
            is_water ^= true;
            samples_changed = true;
            horizons = horizons.and_then(|_| refresh_horizons(is_basement, is_water));
        }
        if win.is_key_down(minifb::Key::L) {
            if is_camera {
                // TODO: implement removing horizon mapping.
                horizons = if horizons.is_some() {
                    None
                } else {
                    refresh_horizons(is_basement, is_water)
                };
                samples_changed = true;
            } else {
                is_shaded ^= true;
                samples_changed = true;
            }
        }
        if win.is_key_down(minifb::Key::M) {
            samples = samples.xor(Some(&*samples_data));
            samples_changed = true;
        }
        if win.is_key_down(minifb::Key::W) {
            if is_camera {
                light_direction.z -= lspd;
            } else {
                focus.y -= spd * scale;
            }
        }
        if win.is_key_down(minifb::Key::A) {
            if is_camera {
                light_direction.x -= lspd;
            } else {
                focus.x -= spd * scale;
            }
        }
        if win.is_key_down(minifb::Key::S) {
            if is_camera {
                light_direction.z += lspd;
            } else {
                focus.y += spd * scale;
            }
        }
        if win.is_key_down(minifb::Key::D) {
            if is_camera {
                light_direction.x += lspd;
            } else {
                focus.x += spd * scale;
            }
        }
        if win.is_key_down(minifb::Key::Q) {
            if is_camera {
                if (fov * 2.0).is_normal() {
                    fov *= 2.0;
                }
            } else {
                gain += 64.0;
            }
        }
        if win.is_key_down(minifb::Key::E) {
            if is_camera {
                if (fov / 2.0).is_normal() {
                    fov /= 2.0;
                }
            } else {
                gain = (gain - 64.0).max(64.0);
            }
        }
        if win.is_key_down(minifb::Key::R) {
            if is_camera {
                focus.z += spd * scale;
                samples_changed = true;
            } else if (scale * 2.0).is_normal() {
                scale *= 2.0;
            }
        }
        if win.is_key_down(minifb::Key::F) {
            if is_camera {
                focus.z -= spd * scale;
                samples_changed = true;
            } else if (scale / 2.0).is_normal() {
                scale /= 2.0;
            }
        }

        win.update_with_buffer(&buf, W, H).unwrap();
    }
}
