use std::borrow::Cow;
use std::cmp::max;
use std::collections::hash_map::Entry::{ Occupied, Vacant };
use std::collections::HashMap;
use std::old_io::fs::File;
use std::num::Float;
use std::num::UnsignedInt;

use array::*;
use chunk::{BiomeId, BlockState, Chunk};
use cube;
use gfx::Device;
use minecraft::biome::Biomes;
use minecraft::data::BLOCK_STATES;
use minecraft::model;
use minecraft::model::OrthoRotation::*;
use minecraft::model::{ Model, OrthoRotation };
use serialize::json;
use shader::Vertex;
use texture::{ AtlasBuilder, Texture };
use vecmath::vec3_add;

use self::PolymorphDecision::*;

pub struct BlockStates {
    models: Vec<ModelAndBehavior>,
    texture: Texture
}

#[derive(PartialEq, Eq, Clone, Copy)]
pub enum RandomOffset {
    None,
    XZ,
    XYZ
}

#[derive(PartialEq, Eq, Clone, Copy)]
pub enum Dir {
    Down,
    Up,
    North,
    South,
    West,
    East,

    // Some diagonal directions (used by redstone).
    UpNorth,
    UpSouth,
    UpWest,
    UpEast
}

impl Dir {
    pub fn xyz(self) -> [i32; 3] {
        match self {
            Dir::Down => [0, -1, 0],
            Dir::Up => [0, 1, 0],
            Dir::North => [0, 0, -1],
            Dir::South => [0, 0, 1],
            Dir::West => [-1, 0, 0],
            Dir::East => [1, 0, 0],

            Dir::UpNorth => [0, 1, -1],
            Dir::UpSouth => [0, 1, 1],
            Dir::UpWest => [-1, 1, 0],
            Dir::UpEast => [1, 1, 0]
        }
    }
}

#[derive(Clone, Copy)]
pub enum PolymorphDecision {
    // Stop and use this block state ID for the model.
    PickBlockState(u16),

    // Each of these checks a condition and continues if true,
    // or jumps to the provided u8 'else' index otherwise.
    // Blocks are specified with a signed offset from the block itself.
    // The 'OrSolid' variants also check for any solid blocks.
    IfBlock(Dir, i8, u8),
    IfBlockOrSolid(Dir, i8, u8),
    //IfGroup(Dir, Group, u8),
    //IfGroupOrSolid(Dir, Group, u8)
}

struct Description {
    id: u16,
    name: &'static str,
    variant: Cow<'static, String, str>,
    random_offset: RandomOffset,
    polymorph_oracle: Vec<PolymorphDecision>
}

#[derive(Clone)]
pub struct ModelAndBehavior {
    pub model: Model,
    pub random_offset: RandomOffset,
    pub polymorph_oracle: Vec<PolymorphDecision>
}

impl ModelAndBehavior {
    pub fn empty() -> ModelAndBehavior {
        ModelAndBehavior {
            model: Model::empty(),
            random_offset: RandomOffset::None,
            polymorph_oracle: vec![]
        }
    }

    pub fn is_empty(&self) -> bool {
        self.model.is_empty()
    }
}

impl BlockStates {
    pub fn load<D: Device>(
        assets: &Path, d: &mut D
    ) -> BlockStates {
        let mut last_id = BLOCK_STATES.last().map_or(0, |state| state.0);
        let mut states = Vec::<Description>::with_capacity(BLOCK_STATES.len().next_power_of_two());
        let mut extras = vec![];
        let mut flower1 = None::<u16>;
        let mut flower2 = None::<u16>;
        for (i, &(id, name, variant)) in BLOCK_STATES.iter().enumerate() {
            let mut polymorph_oracle = vec![];
            let mut random_offset = RandomOffset::None;

            // Find double_plant.
            if variant == "half=upper" {
                if name != "paeonia" {
                    println!("Warning: unknown upper double_plant {}", name);
                }
                let (_, lower_name, lower_variant) = BLOCK_STATES[i - 1];
                assert!(lower_name == name && lower_variant == "half=lower");
                let lower = BLOCK_STATES.slice_to(i - 1).iter().enumerate().rev();
                let lower = lower.take_while(|&(i, &(id, _, variant))| {
                    id + 1 == BLOCK_STATES[i + 1].0 && variant == "half=lower"
                });
                // Note: excluding paeonia itself, which works as-is.
                let num_plants = lower.count();

                for j in range(i - 1 - num_plants, i - 1) {
                    last_id += 1;
                    let (_, lower_name, _) = BLOCK_STATES[j];
                    extras.push(Description {
                        id: last_id,
                        name: lower_name,
                        variant: Cow::Borrowed("half=upper"),
                        random_offset: RandomOffset::XZ,
                        polymorph_oracle: vec![]
                    });
                    states[j].random_offset = RandomOffset::XZ;

                    let next_index = polymorph_oracle.len() as u8;
                    polymorph_oracle.push_all(&[
                        IfBlock(Dir::Down, (BLOCK_STATES[j].0 - id) as i8, next_index + 2),
                        PickBlockState(last_id)
                    ]);
                }
                random_offset = RandomOffset::XZ;
                polymorph_oracle.push(PickBlockState(id));
            }

            if name == "dandelion" {
                flower1 = Some(id);
            } else if name == "poppy" {
                flower2 = Some(id);
            } else if ["dead_bush", "tall_grass", "fern"].contains(&name) {
                random_offset = RandomOffset::XYZ;
            }

            if flower1 == Some(id & !0xf) || flower2 == Some(id & !0xf) {
                random_offset = RandomOffset::XZ;
            }

            let variant = if variant.ends_with(",shape=outer_right") {
                Cow::Owned(format!("{}=straight", variant.slice_to(variant.len() - 12)))
            } else {
                Cow::Borrowed(variant)
            };

            states.push(Description {
                id: id,
                name: name,
                variant: variant,
                random_offset: random_offset,
                polymorph_oracle: polymorph_oracle
            });
        }
        states.extend(extras.into_iter());

        BlockStates::load_with_states(assets, d, states)
    }

    fn load_with_states<D: Device>(
        assets: &Path, d: &mut D,
        states: Vec<Description>
    ) -> BlockStates {
        struct Variant {
            model: String,
            rotate_x: OrthoRotation,
            rotate_y: OrthoRotation,
            uvlock: bool
        }

        let last_id = states.last().map_or(0, |state| state.id);
        let mut models = Vec::with_capacity(last_id as usize + 1);
        let mut atlas = AtlasBuilder::new(assets.join(Path::new("minecraft/textures")), 16, 16);
        let mut partial_model_cache = HashMap::new();
        let mut block_state_cache: HashMap<String, HashMap<String, Variant>> = HashMap::new();

        for state in states.into_iter() {
            let variants = match block_state_cache.entry(state.name.to_string()) {
                Occupied(entry) => entry.into_mut(),
                Vacant(entry) => entry.insert({
                    let name = state.name;
                    let path = assets.join(Path::new(format!("minecraft/blockstates/{}.json", name).as_slice()));
                    match json::Json::from_reader(&mut File::open(&path).unwrap()).unwrap() {
                        json::Json::Object(mut json) => match json.remove("variants").unwrap() {
                            json::Json::Object(variants) => variants.into_iter().map(|(k, v)| {
                                let mut variant = match v {
                                    json::Json::Object(o) => o,
                                    json::Json::Array(l) => {
                                        println!("ignoring {} extra variants for {}#{}",
                                                 l.len() - 1, name, k);
                                        match l.into_iter().next() {
                                            Some(json::Json::Object(o)) => Some(o),
                                            _ => None
                                        }.unwrap()
                                    }
                                    json => panic!("{}#{} has invalid value {}", name, k, json)
                                };
                                let model = match variant.remove("model").unwrap() {
                                    json::Json::String(s) => s,
                                    json => panic!("'model' has invalid value {}", json)
                                };
                                let rotate_x = variant.remove("x").map_or(Rotate0, |r| {
                                    match OrthoRotation::from_json(&r) {
                                        Some(r) => r,
                                        None => panic!("invalid rotation for x {}", r)
                                    }
                                });
                                let rotate_y = variant.remove("y").map_or(Rotate0, |r| {
                                    match OrthoRotation::from_json(&r) {
                                        Some(r) => r,
                                        None => panic!("invalid rotation for y {}", r)
                                    }
                                });
                                match variant.remove("z") {
                                    Some(r) => println!("ignoring z rotation {} in {}", r, name),
                                    None => {}
                                }
                                let uvlock = variant.remove("uvlock").map_or(false, |x| x.as_boolean().unwrap());
                                (k, Variant {
                                    model: model,
                                    rotate_x: rotate_x,
                                    rotate_y: rotate_y,
                                    uvlock: uvlock
                                })
                            }).collect(),
                            json => panic!("'variants' has invalid value {}", json)
                        },
                        json => panic!("root object has invalid value {}", json)
                    }
                })
            };

            let variant = match state.variant {
                Cow::Owned(ref variant) => variants.get(variant),
                Cow::Borrowed(variant) => variants.get(variant)
            }.unwrap();
            let mut model = Model::load(variant.model.as_slice(), assets,
                                        &mut atlas, &mut partial_model_cache);

            let rotate_faces = |&: m: &mut Model, ix: usize, iy: usize, rot_mat: [i32; 4]| {
                let [a, b, c, d] = rot_mat.map(|x: i32| x as f32);
                for face in m.faces.iter_mut() {
                    for vertex in face.vertices.iter_mut() {
                        let xyz = &mut vertex.xyz;
                        let [x, y] = [ix, iy].map(|i| xyz[i] - 0.5);
                        xyz[ix] = a * x + b * y + 0.5;
                        xyz[iy] = c * x + d * y + 0.5;
                    }
                    let fixup_cube_face = |&: f: cube::Face| {
                        let [a, b, c, d] = rot_mat;
                        let mut dir = f.direction();
                        let [x, y] = [dir[ix], dir[iy]];
                        dir[ix] = a * x + b * y;
                        dir[iy] = c * x + d * y;
                        cube::Face::from_direction(dir).unwrap()
                    };
                    face.cull_face = match face.cull_face {
                        None => None,
                        Some(f) => Some(fixup_cube_face(f))
                    };
                    face.ao_face = match face.ao_face {
                        None => None,
                        Some(f) => Some(fixup_cube_face(f))
                    };
                    if variant.uvlock {
                        // Skip over faces that are constant in the ix or iy axis.
                        let xs = face.vertices.map(|v| v.xyz[ix]);
                        if xs.map(|x| x == xs[0]) == [true, true, true, true] {
                            continue;
                        }
                        let ys = face.vertices.map(|v| v.xyz[iy]);
                        if ys.map(|y| y == ys[0]) == [true, true, true, true] {
                            continue;
                        }

                        let uvs = face.vertices.map(|x| x.uv);
                        let uv_min = [0, 1].map(|i| (uvs[0][i]).min(uvs[1][i])
                                                .min(uvs[2][i]).min(uvs[3][i]));
                        let [u_base, v_base] = uv_min.map(|x| (x / 16.0).floor() * 16.0);
                        for vertex in face.vertices.iter_mut() {
                            let uv = &mut vertex.uv;
                            let [u, v] = [uv[0] - u_base, uv[1] - v_base].map(|x| x - 8.0);
                            uv[0] = a * u - b * v + 8.0 + u_base;
                            uv[1] =-c * u + d * v + 8.0 + v_base;
                        }
                    }
                }
            };

            let rotate_faces = |&: m: &mut Model, ix: usize, iy: usize, r: OrthoRotation| {
                match r {
                    Rotate0 => {}
                    Rotate90 =>  rotate_faces(m, ix, iy, [ 0, -1,
                                                           1,  0]),
                    Rotate180 => rotate_faces(m, ix, iy, [-1,  0,
                                                           0, -1]),
                    Rotate270 => rotate_faces(m, ix, iy, [ 0,  1,
                                                          -1,  0]),
                }
            };

            rotate_faces(&mut model, 2, 1, variant.rotate_x);
            rotate_faces(&mut model, 0, 2, variant.rotate_y);

            let len = models.len();
            if state.id as usize >= len {
                models.resize(state.id as usize + 1, ModelAndBehavior::empty());
            }

            models[state.id as usize] = ModelAndBehavior {
                model: model,
                random_offset: state.random_offset,
                polymorph_oracle: state.polymorph_oracle
            };
        }

        drop(partial_model_cache);
        drop(block_state_cache);

        let texture = atlas.complete(d);
        let (width, height) = texture.get_size();
        let u_unit = 1.0 / (width as f32);
        let v_unit = 1.0 / (height as f32);

        for model in models.iter_mut() {
            for face in model.model.faces.iter_mut() {
                for vertex in face.vertices.iter_mut() {
                    vertex.uv[0] *= u_unit;
                    vertex.uv[1] *= v_unit;
                }
            }
        }

        BlockStates {
            models: models,
            texture: texture
        }
    }

    pub fn get_model<'a>(&'a self, i: BlockState) -> Option<&'a ModelAndBehavior> {
        let i = i.value as usize;
        if i >= self.models.len() || self.models[i].is_empty() {
            None
        } else {
            Some(&self.models[i])
        }
    }

    pub fn texture<'a>(&'a self) -> &'a Texture {
        &self.texture
    }

    pub fn get_opacity(&self, i: BlockState) -> model::Opacity {
        let i = i.value as usize;
        if i >= self.models.len() {
            model::Opacity::Transparent
        } else {
            self.models[i].model.opacity
        }
    }
}

pub fn fill_buffer(block_states: &BlockStates, biomes: &Biomes, buffer: &mut Vec<Vertex>,
                   coords: [i32; 3], chunks: [[[&Chunk; 3]; 3]; 3],
                   column_biomes: [[Option<&[[BiomeId; 16]; 16]>; 3]; 3]) {
    let chunk_xyz = coords.map(|x| x as f32 * 16.0);
    for y in range(0, 16) {
        for z in range(0, 16) {
            for x in range(0, 16) {
                let at = |&: dir: [i32; 3]| {
                    let [dx, dy, dz] = dir.map(|x| x as usize);
                    let [x, y, z] = [x + dx, y + dy, z + dz].map(|x| x + 16);
                    let chunk = chunks[y / 16][z / 16][x / 16];
                    let [x, y, z] = [x, y, z].map(|x| x % 16);
                    (chunk.blocks[y][z][x], chunk.light_levels[y][z][x])
                };
                let this_block = at([0, 0, 0]).0;
                let model = match block_states.get_model(this_block) {
                    Some(model) if !model.polymorph_oracle.is_empty() => {
                        let mut i = 0;
                        let result;
                        loop {
                            let (cond, idx) = match model.polymorph_oracle[i].clone() {
                                PickBlockState(id) => {
                                    result = &block_states.models[id as usize];
                                    break;
                                }
                                IfBlock(dir, offset, idx) => {
                                    let id = this_block.value + offset as u16;
                                    (at(dir.xyz()).0.value == id, idx)
                                }
                                IfBlockOrSolid(dir, offset, idx) => {
                                    let id = this_block.value + offset as u16;
                                    let other = at(dir.xyz()).0;
                                    (other.value == id ||
                                     block_states.get_opacity(other).is_opaque(), idx)
                                }
                                /*IfGroup(dir, group, idx) => {
                                    let other = at(dir.xyz()).0;
                                    (block_states.models[other.value].group == group, idx)
                                }
                                IfGroupOrSolid(dir, group, idx) => {
                                    let other = at(dir.xyz()).0;
                                    (block_states.models[other.value].group == group ||
                                     block_states.get_opacity(other).is_opaque(), idx)
                                }*/
                            };
                            if cond {
                                i += 1;
                            } else {
                                i = idx as usize;
                            }
                        }
                        result
                    }
                    Some(model) => model,
                    None => continue
                };
                let block_xyz = vec3_add([x, y, z].map(|x| x as f32), chunk_xyz);
                let block_xyz = match model.random_offset.clone() {
                    RandomOffset::None => block_xyz,
                    random_offset => {
                        let [x, _, z] = block_xyz;
                        let seed = (x as i32 * 3129871) as i64 ^ (z as i64) * 116129781;
                        let value = seed * seed * 42317861 + seed * 11;
                        let ox = (((value >> 16) & 15) as f32 / 15.0 - 0.5) * 0.5;
                        let oz = (((value >> 24) & 15) as f32 / 15.0 - 0.5) * 0.5;
                        let oy = if random_offset == RandomOffset::XYZ {
                            (((value >> 20) & 15) as f32 / 15.0 - 1.0) * 0.2
                        } else { 0.0 };
                        vec3_add(block_xyz, [ox, oy, oz])
                    }
                };
                let model = &model.model;
                for face in model.faces.iter() {
                    match face.cull_face {
                        Some(cull_face) => {
                            let (neighbor, _) = at(cull_face.direction());
                            if block_states.get_opacity(neighbor).is_opaque() {
                                continue;
                            }
                        }
                        None => {}
                    }

                    let tint_source = if face.tint {
                        model.tint_source
                    } else {
                        model::Tint::None
                    };

                    let v = face.vertices.map(|vertex| {
                        // Average tint and light around the vertex.
                        let (rgb, mut num_colors) = match tint_source {
                            model::Tint::None => ([0xff, 0xff, 0xff], 1.0),
                            model::Tint::Grass | model::Tint::Foliage => ([0x00, 0x00, 0x00], 0.0),
                            model::Tint::Redstone => ([0xff, 0x00, 0x00], 1.0)
                        };
                        let mut rgb = rgb.map(|x: u8| x as f32 / 255.0);
                        let (mut sum_light_level, mut num_light_level) = (0.0, 0.0);

                        let rounded_vertex_xyz = vertex.xyz.map(|x| x.round() as i32);
                        let [dx, dy, dz] = rounded_vertex_xyz;
                        for &dx in [dx - 1, dx].iter() {
                            for &dz in [dz - 1, dz].iter() {
                                for &dy in [dy - 1, dy].iter() {
                                    let (neighbor, light_level) = at([dx, dy, dz]);
                                    let light_level = max(light_level.block_light(),
                                                          light_level.sky_light());
                                    let mut light_level = light_level as f32;

                                    let use_block = match face.ao_face {
                                        Some(ao_face) => {
                                            let mut above = true;
                                            for (i, &a) in ao_face.direction().iter().enumerate() {
                                                let da = [dx, dy, dz][i];
                                                let va = rounded_vertex_xyz[i];
                                                let above_da = match a {
                                                    -1 => va - 1,
                                                    1 => va,
                                                    _ => da
                                                };
                                                if da != above_da {
                                                    above = false;
                                                    break;
                                                }
                                            }

                                            if above && block_states.get_opacity(neighbor).is_solid() {
                                                light_level = 0.0;
                                            }

                                            above
                                        }
                                        None => {
                                            !block_states.get_opacity(neighbor).is_opaque()
                                        }
                                    };

                                    if use_block {
                                        sum_light_level += light_level;
                                        num_light_level += 1.0;
                                    }
                                }
                                match tint_source {
                                    model::Tint::None | model::Tint::Redstone => continue,
                                    model::Tint::Grass | model::Tint::Foliage => {}
                                }
                                let [x, z] = [x + dx as usize, z + dz as usize].map(|x| x + 16);
                                let biome = match column_biomes[z / 16][x / 16] {
                                    Some(biome) => biomes[biome[z % 16][x % 16]],
                                    None => continue
                                };
                                rgb = vec3_add(rgb, match tint_source {
                                    model::Tint::None | model::Tint::Redstone => continue,
                                    model::Tint::Grass => biome.grass_color,
                                    model::Tint::Foliage => biome.foliage_color,
                                }.map(|x| x as f32 / 255.0));
                                num_colors += 1.0;
                            }
                        }

                        let light_factor = 0.2 + if num_light_level != 0.0 {
                            sum_light_level / num_light_level / 15.0 * 0.8
                        } else { 0.0 };

                        // Up, North and South, East and West, Down have different lighting.
                        let light_factor = light_factor * match face.ao_face {
                            Some(ao_face) => match ao_face {
                                cube::Up => 1.0,
                                cube::North | cube::South => 0.8,
                                cube::East | cube::West => 0.6,
                                cube::Down => 0.5
                            },
                            None => 1.0
                        };

                        Vertex {
                            xyz: vec3_add(block_xyz, vertex.xyz),
                            uv: vertex.uv,
                            // No clue why the difference of 2 exists.
                            rgb: rgb.map(|x| x * light_factor / num_colors - 2.0 / 255.0)
                        }
                    });

                    // Split the clockwise quad into two clockwise triangles.
                    buffer.push_all(&[v[0], v[1], v[2]]);
                    buffer.push_all(&[v[2], v[3], v[0]]);
                }
            }
        }
    }
}
