#![allow(dead_code)]

use std::ops::{Index, IndexMut};
use tiled;

pub mod coord;

use board::coord::{Coord, Pair};

pub const BOARD_WIDTH: u16 = 16;
pub const BOARD_HEIGHT: u16 = 12;
pub const TILE_SIZE: u16 = 24;
pub const HALF_TILE_SIZE: f32 = 24.0;

#[derive(PartialEq, Eq, Debug)]
pub struct Tile {
    pub sprite: Coord,
}

impl Tile {
    fn passable(&self) -> bool {
        true
    }
}

#[derive(PartialEq, Eq, Debug)]
pub struct Esse {
    pub sprite: Coord,
}

#[derive(Debug)]
pub struct Entity {
    loc: Pair<f32>,
    dir: Pair<f32>,
}

impl Entity {
    fn blank() -> Entity {
        Entity {
            loc: Pair::zero(),
            dir: Pair::zero(),
        }
    }

    fn check_collision(&self, b: &Board, dir: Pair<f32>) -> bool {
        // so, the deal here is that we are looking at the position of
        // the top-left corner of a sprite, not at the center. In the
        // optimal case (if the target is in the top-left corner of a
        // tile) we only need to check passability for a single tile,
        // but usually we'll need to check two to four:
        //
        // +---+---+
        // |   |   |
        // |  @---+|
        // +--|   |+
        // |  |   ||
        // |  +---+|
        // +---+---+
        //
        // The point @ in the above diagram is what's called `tgt`
        // here: the tile it's in is called `tl`, the tile to its
        // right is `tr`, the tile below is `bl`, and the tile below
        // and to the right is `br`.
        let tgt = (self.loc + dir).to_discrete(TILE_SIZE as f32);

        let tl = || b.passable(tgt);
        let tr = || b.passable(tgt + Pair::new(1, 0));
        let bl = || b.passable(tgt + Pair::new(0, 1));
        let br = || b.passable(tgt + Pair::new(1, 1));

        // If we're exactly boundary-aligned, we can check fewer of
        // these, but otherwise, we can only pass into the place in
        // question iff all of them are free. (They're all closures so
        // we can avoid doing unnecessary lookups if we don't have
        // to.)
        match tgt {
            Pair { x: 0, y: 0 } => tl(),
            Pair { x: 0, y: _ } => tl() && bl(),
            Pair { x: _, y: 0 } => tl() && tr(),
            Pair { x: _, y: _ } => tl() && tr() && bl() && br(),
        }
    }

    /// Get the set of entities present at the board location that
    /// this entity is currently looking at
    fn get_focus<'a>(&self, b: &'a Board) -> &'a Vec<Esse> {
        b.get_entity(self.game_coords())
    }

    /// Attempt to move this entity, figuring out whether it can move
    /// and how far to move if so. This does collision detection based
    /// on the grid and also does some nudging to push the player
    /// towards being grid-aligned.
    fn mv(&self, b: &Board) -> Delta {
        if self.dir == Pair::zero() {
            return Delta::zero();
        }
        let mut delta = Delta { dx: 0.0, dy: 0.0 };

        // figure out which directions we _can_ travel based on
        // collision data: if we're trying to move NW, but W is
        // blocked, then we should still be able to move N.
        if self.check_collision(b, self.dir) {
            delta.dx = self.dir.x;
            delta.dy = self.dir.y;
        } else if self.check_collision(b, Pair { x: 0.0, ..self.dir }) {
            delta.dy = self.dir.y;
        } else if self.check_collision(b, Pair { y: 0.0, ..self.dir }) {
            delta.dx = self.dir.x;
        }

        // if we're not aligned along a half-square, we should try to
        // push towards aligning there!
        if self.dir.x.eq(&0.0) && !self.dir.y.eq(&0.0) {
            let xm = self.loc.x % HALF_TILE_SIZE;
            if 0.0 < xm &&
                xm < HALF_TILE_SIZE &&
                self.check_collision(b, Pair { x: -1.0, y: 0.0 }) {
                delta.dx = delta.dx - 1.0;
            } else if xm >= HALF_TILE_SIZE &&
                       self.check_collision(b, Pair { x: 1.0, y: 0.0 }) {
                delta.dx = delta.dx + 1.0;
            }
        } else if !self.dir.x.eq(&0.0) && self.dir.y.eq(&0.0) {
            let ym = self.loc.y % HALF_TILE_SIZE;
            if 0.0 < ym && ym < HALF_TILE_SIZE && self.check_collision(b, Pair { x: 0.0, y: -1.0 }) {
                delta.dy = delta.dy - 1.0;
            } else if ym >= HALF_TILE_SIZE && self.check_collision(b, Pair { x: 0.0, y: -1.0 }) {
                delta.dy = delta.dy + 1.0;
            }
        }

        delta
    }

    /// Get this entity's current location in terms of the game
    /// coordinates
    fn game_coords(&self) -> Coord {
        self.loc.to_discrete(TILE_SIZE as f32)
    }
}

/// A `Delta` is the amount of calculated movement found for a given
/// entity when it attempts to move.
#[derive(Debug)]
struct Delta {
    dx: f32,
    dy: f32,
}

impl Delta {
    fn zero() -> Delta {
        Delta { dx: 0.0, dy: 0.0 }
    }
}

#[derive(Debug)]
pub struct Spritesheet {
    image: ::imagefmt::Image<u8>,
}

#[derive(Debug)]
pub struct Grid<T> {
    width: u16,
    height: u16,
    elems: Vec<T>,
}

impl<T> Grid<T> {
    pub fn new_with<F>(
        width: u16,
        height: u16,
        elem: F,
    ) -> Grid<T>
        where F: Fn(u16, u16) -> T
    {
        let mut elems = Vec::with_capacity((width * height) as usize);
        for y in 0..height {
            for x in 0..width {
                elems.push(elem(x, y))
            }
        }
        Grid { elems, width, height }
    }
}

impl<T: Clone> Grid<T> {
    pub fn new(width: u16, height: u16, elem: &T) -> Grid<T> {
        let mut elems = Vec::with_capacity((width * height) as usize);
        for _ in 0..height {
            for _ in 0..width {
                elems.push(elem.clone())
            }
        }
        Grid { elems, width, height }
    }
}

impl<T> Index<Coord> for Grid<T> {
    type Output = T;

    fn index(&self, Pair { x, y }: Coord) -> &T {
        &self.elems[(y * self.width + x) as usize]
    }
}

impl<T> IndexMut<Coord> for Grid<T> {
    fn index_mut(&mut self, Pair { x, y }: Coord) -> &mut T {
        &mut self.elems[(y * self.width + x) as usize]
    }
}

#[derive(Debug)]
pub struct Board {
    base: Grid<Tile>,
    entity: Grid<Vec<Esse>>,
}


impl Board {
    pub fn passable(&self, c: Coord) -> bool {
        self.base[c].passable()
    }

    pub fn tiles(&mut self) -> &mut Grid<Tile> {
        &mut self.base
    }

    pub fn esses(&mut self) -> &mut Grid<Vec<Esse>> {
        &mut self.entity
    }

    pub fn get_entity(&self, c: Coord) -> &Vec<Esse> {
        &self.entity[c]
    }

    /// Take a Tiled-format map and ingest it into our internal map
    /// format. (Might be a good idea eventually to use a custom
    /// format, but Tiled is great for now.)
    pub fn from_tiled(map: &tiled::Map) -> Board {
        fn offset_from_gid(n: u32) -> Option<Coord> {
            if n < 1 || n > 1024 {
                return None;
            }
            let x = (n - 1) as u16 % 32;
            let y = (n - 1) as u16 / 32;
            Some(Pair { x, y })
        }

        let w = map.width as u16;
        let h = map.height as u16;
        let base = Grid::new_with(w, h, |x, y| {
            let o = map.layers[0].tiles[y as usize][x as usize];
            Tile { sprite: offset_from_gid(o).unwrap() }
        });

        let entity = Grid::new_with(w, h, |x, y| {
            let o1 = map.layers[1].tiles[y as usize][x as usize];
            let o2 = map.layers[2].tiles[y as usize][x as usize];
            let mut v = Vec::new();
            if let Some(coord) =  offset_from_gid(o1) {
                v.push(Esse { sprite: coord });
            }
            if let Some(coord) =  offset_from_gid(o2) {
                v.push(Esse { sprite: coord });
            }
            v
        });
        Board { base, entity }
    }

    pub fn each_tile<F>(&self, mut f: F)
        where F: FnMut(usize, usize, &Tile)
    {
        for x in 0..self.base.width {
            for y in 0..self.base.height {
                f(x as usize, y as usize, &self.base[Pair::new(x, y)]);
            }
        }
    }

    pub fn each_esse<F>(&self, mut f: F)
        where F: FnMut(usize, usize, &Esse)
    {
        for x in 0..self.base.width {
            for y in 0..self.base.height {
                for e in self.entity[Pair::new(x, y)].iter() {
                    f(x as usize, y as usize, &e);
                }
            }
        }
    }
}
