use crate::{
    column::ColumnSample,
    sim::{RiverKind, WorldSim},
    CONFIG,
};
use common::{
    terrain::{
        map::{Connection, ConnectionKind, MapConfig, MapSample},
        vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA,
    },
    vol::RectVolSize,
};
use std::{f32, f64};
use vek::*;

/// A sample function that grabs the connections at a chunk.
///
/// Currently this just supports rivers, but ideally it can be extended past
/// that.
///
/// A sample function that grabs surface altitude at a column.
/// (correctly reflecting settings like is_basement and is_water).
///
/// The altitude produced by this function at a column corresponding to a
/// particular chunk should be identical to the altitude produced by
/// sample_pos at that chunk.
///
/// You should generally pass a closure over this function into generate
/// when constructing a map for the first time.
/// However, if repeated construction is needed, or alternate base colors
/// are to be used for some reason, one should pass a custom function to
/// generate instead (e.g. one that just looks up the height in a cached
/// array).
pub fn sample_wpos(config: &MapConfig, sampler: &WorldSim, wpos: Vec2<i32>) -> f32 {
    let MapConfig {
        focus,
        gain,

        is_basement,
        is_water,
        ..
    } = *config;

    (sampler
        .get_wpos(wpos)
        .map(|s| {
            if is_basement { s.basement } else { s.alt }.max(if is_water {
                s.water_alt
            } else {
                -f32::INFINITY
            })
        })
        .unwrap_or(CONFIG.sea_level)
        - focus.z as f32)
        / gain as f32
}

/// Samples a MapSample at a chunk.
///
/// You should generally pass a closure over this function into generate
/// when constructing a map for the first time.
/// However, if repeated construction is needed, or alternate base colors
/// are to be used for some reason, one should pass a custom function to
/// generate instead (e.g. one that just looks up the color in a cached
/// array).
// NOTE: Deliberately not putting Rgb colors here in the config file; they
// aren't hot reloaded anyway, and for various reasons they're probably not a
// good idea to update in that way (for example, we currently want water colors
// to match voxygen's).  Eventually we'll fix these sorts of issues in some
// other way.
pub fn sample_pos(
    config: &MapConfig,
    sampler: &WorldSim,
    samples: Option<&[Option<ColumnSample>]>,
    pos: Vec2<i32>,
) -> MapSample {
    let map_size_lg = config.map_size_lg();
    let MapConfig {
        focus,
        gain,

        is_basement,
        is_water,
        is_shaded,
        is_temperature,
        is_humidity,
        // is_debug,
        ..
    } = *config;

    let true_sea_level = (CONFIG.sea_level as f64 - focus.z) / gain as f64;

    let (
        chunk_idx,
        alt,
        basement,
        water_alt,
        humidity,
        temperature,
        downhill,
        river_kind,
        spline_derivative,
        is_path,
    ) = sampler
        .get(pos)
        .map(|sample| {
            (
                Some(vec2_as_uniform_idx(map_size_lg, pos)),
                sample.alt,
                sample.basement,
                sample.water_alt,
                sample.humidity,
                sample.temp,
                sample.downhill,
                sample.river.river_kind,
                sample.river.spline_derivative,
                sample.path.0.is_way(),
            )
        })
        .unwrap_or((
            None,
            CONFIG.sea_level,
            CONFIG.sea_level,
            CONFIG.sea_level,
            0.0,
            0.0,
            None,
            None,
            Vec2::zero(),
            false,
        ));

    let humidity = humidity.min(1.0).max(0.0);
    let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
    let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
    let column_rgb_alt = samples
        .and_then(|samples| {
            chunk_idx
                .and_then(|chunk_idx| samples.get(chunk_idx))
                .map(Option::as_ref)
                .flatten()
        })
        .map(|sample| {
            // TODO: Eliminate the redundancy between this and the block renderer.
            let alt = sample.alt;
            let basement = sample.basement;
            let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement);
            let wposz = if is_basement { basement } else { alt };
            let rgb = if is_basement && wposz < alt - grass_depth {
                Lerp::lerp(
                    sample.sub_surface_color,
                    sample.stone_col.map(|e| e as f32 / 255.0),
                    (alt - grass_depth - wposz as f32) * 0.15,
                )
                .map(|e| e as f64)
            } else {
                Lerp::lerp(
                    sample.sub_surface_color,
                    sample.surface_color,
                    ((wposz as f32 - (alt - grass_depth)) / grass_depth).sqrt(),
                )
                .map(|e| e as f64)
            };

            (rgb, alt)
        });

    let downhill_wpos = downhill.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
    let alt = if is_basement {
        basement
    } else {
        column_rgb_alt.map_or(alt, |(_, alt)| alt)
    };

    let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64;
    let true_alt = (alt as f64 - focus.z) / gain as f64;
    let water_depth = (true_water_alt - true_alt).min(1.0).max(0.0);
    let alt = true_alt.min(1.0).max(0.0);

    let water_color_factor = 2.0;
    let g_water = 32.0 * water_color_factor;
    let b_water = 64.0 * water_color_factor;
    let default_rgb = Rgb::new(
        if is_shaded || is_temperature {
            1.0
        } else {
            0.0
        },
        if is_shaded { 1.0 } else { alt },
        if is_shaded || is_humidity { 1.0 } else { 0.0 },
    );
    let column_rgb = column_rgb_alt.map(|(rgb, _)| rgb).unwrap_or(default_rgb);
    let mut connections = [None; 8];
    let mut has_connections = false;
    // TODO: Support non-river connections.
    // TODO: Support multiple connections.
    let river_width = river_kind.map(|river| match river {
        RiverKind::River { cross_section } => cross_section.x,
        RiverKind::Lake { .. } | RiverKind::Ocean => TerrainChunkSize::RECT_SIZE.x as f32,
    });
    if let (Some(river_width), true) = (river_width, is_water) {
        let downhill_pos = downhill_wpos.map2(TerrainChunkSize::RECT_SIZE, |e, f| e / f as i32);
        NEIGHBOR_DELTA
            .iter()
            .zip((&mut connections).iter_mut())
            .filter(|&(&offset, _)| downhill_pos - pos == Vec2::from(offset))
            .for_each(|(_, connection)| {
                has_connections = true;
                *connection = Some(Connection {
                    kind: ConnectionKind::River,
                    spline_derivative,
                    width: river_width,
                });
            });
    };
    let rgb = match (river_kind, (is_water, true_alt >= true_sea_level)) {
        (_, (false, _)) | (None, (_, true)) | (Some(RiverKind::River { .. }), _) => {
            let (r, g, b) = (
                (column_rgb.r
                    * if is_temperature {
                        temperature as f64
                    } else {
                        column_rgb.r
                    })
                .sqrt(),
                column_rgb.g,
                (column_rgb.b
                    * if is_humidity {
                        humidity as f64
                    } else {
                        column_rgb.b
                    })
                .sqrt(),
            );
            Rgb::new((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
        },
        (None | Some(RiverKind::Lake { .. } | RiverKind::Ocean), _) => Rgb::new(
            0,
            ((g_water - water_depth * g_water) * 1.0) as u8,
            ((b_water - water_depth * b_water) * 1.0) as u8,
        ),
    };
    // TODO: Make principled.
    let rgb = if is_path {
        Rgb::new(0x37, 0x29, 0x23)
    } else {
        rgb
    };

    MapSample {
        rgba: Rgba::new(rgb.r, rgb.g, rgb.b, 255),
        alt: if is_water {
            true_alt.max(true_water_alt)
        } else {
            true_alt
        },
        downhill_wpos,
        connections: if has_connections {
            Some(connections)
        } else {
            None
        },
        is_path,
    }
}
