// +--------------------------------------------------------------------------+
// | Copyright 2016 Matthew D. Steele <mdsteele@alum.mit.edu>                 |
// |                                                                          |
// | This file is part of System Syzygy.                                      |
// |                                                                          |
// | System Syzygy is free software: you can redistribute it and/or modify it |
// | under the terms of the GNU General Public License as published by the    |
// | Free Software Foundation, either version 3 of the License, or (at your   |
// | option) any later version.                                               |
// |                                                                          |
// | System Syzygy is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of               |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        |
// | General Public License for details.                                      |
// |                                                                          |
// | You should have received a copy of the GNU General Public License along  |
// | with System Syzygy.  If not, see <http://www.gnu.org/licenses/>.         |
// +--------------------------------------------------------------------------+

use num_integer::div_floor;
use std::collections::HashMap;
use std::mem;
use std::rc::Rc;

use crate::gui::{
    Action, Align, Canvas, Element, Event, Font, Point, Rect, Resources,
    Sprite,
};
use crate::save::plane::{PlaneGrid, PlaneObj};
use crate::save::Direction;

// ========================================================================= //

pub enum PlaneCmd {
    Changed,
    PushUndo(Vec<(Point, Point)>),
}

// ========================================================================= //

const TILE_USIZE: u32 = 24;
const TILE_ISIZE: i32 = TILE_USIZE as i32;

pub struct PlaneGridView {
    left: i32,
    top: i32,
    obj_sprites: Vec<Sprite>,
    pipe_sprites: Vec<Sprite>,
    drag_from: Option<Point>,
    changes: Vec<(Point, Point)>,
    font: Rc<Font>,
    letters: HashMap<Point, char>,
}

impl PlaneGridView {
    pub fn new(
        resources: &mut Resources,
        left: i32,
        top: i32,
    ) -> PlaneGridView {
        PlaneGridView {
            left,
            top,
            obj_sprites: resources.get_sprites("plane/objects"),
            pipe_sprites: resources.get_sprites("plane/pipes"),
            drag_from: None,
            changes: Vec::new(),
            font: resources.get_font("roman"),
            letters: HashMap::new(),
        }
    }

    pub fn cancel_drag_and_clear_changes(&mut self) {
        self.drag_from = None;
        self.changes.clear();
    }

    pub fn cancel_drag_and_undo_changes(&mut self, grid: &mut PlaneGrid) {
        self.drag_from = None;
        for &(coords1, coords2) in self.changes.iter().rev() {
            grid.toggle_pipe(coords1, coords2);
        }
        self.changes.clear();
    }

    pub fn add_letter(&mut self, coords: Point, letter: char) {
        self.letters.insert(coords, letter);
    }

    fn rect(&self, grid: &PlaneGrid) -> Rect {
        Rect::new(
            self.left,
            self.top,
            grid.num_cols() * TILE_USIZE,
            grid.num_rows() * TILE_USIZE,
        )
    }

    fn pt_to_coords(&self, grid: &PlaneGrid, pt: Point) -> Option<Point> {
        let col = div_floor(pt.x() - self.left, TILE_ISIZE);
        let row = div_floor(pt.y() - self.top, TILE_ISIZE);
        let coords = Point::new(col, row);
        if grid.contains_coords(coords) {
            Some(coords)
        } else {
            None
        }
    }

    fn draw_pipe_tip(
        &self,
        grid: &PlaneGrid,
        pos: Point,
        dir: Direction,
        canvas: &mut Canvas,
    ) {
        let obj = grid.objects().get(&pos).cloned();
        let sprite_index = match (dir, obj) {
            (Direction::West, Some(PlaneObj::Cross)) => 10,
            (Direction::West, Some(obj)) if obj.is_node() => 13,
            (Direction::West, _) => 0,
            (Direction::East, Some(PlaneObj::Cross)) => 11,
            (Direction::East, Some(obj)) if obj.is_node() => 15,
            (Direction::East, _) => 2,
            (Direction::South, Some(obj)) if obj.is_node() => 14,
            (Direction::South, _) => 1,
            (Direction::North, Some(obj)) if obj.is_node() => 16,
            (Direction::North, _) => 3,
        };
        let sprite = &self.pipe_sprites[sprite_index];
        canvas.draw_sprite(sprite, pos * TILE_ISIZE);
    }
}

impl Element<PlaneGrid, PlaneCmd> for PlaneGridView {
    fn draw(&self, grid: &PlaneGrid, canvas: &mut Canvas) {
        let mut canvas = canvas.subcanvas(self.rect(grid));
        canvas.clear((64, 64, 64));
        for row in 0..(grid.num_rows() as i32) {
            for col in 0..(grid.num_cols() as i32) {
                let coords = Point::new(col, row);
                if let Some(&obj) = grid.objects().get(&coords) {
                    let sprite_index = match obj {
                        PlaneObj::Wall => 0,
                        PlaneObj::Cross => 1,
                        PlaneObj::PurpleNode => 2,
                        PlaneObj::RedNode => 3,
                        PlaneObj::GreenNode => 4,
                        PlaneObj::BlueNode => 5,
                        PlaneObj::GrayNode => 6,
                    };
                    let sprite = &self.obj_sprites[sprite_index];
                    canvas.draw_sprite(sprite, coords * TILE_ISIZE);
                } else {
                    let pt = coords * TILE_ISIZE;
                    let rect = Rect::new(
                        pt.x() + 1,
                        pt.y() + 1,
                        TILE_USIZE - 2,
                        TILE_USIZE - 2,
                    );
                    canvas.draw_rect((72, 72, 72), rect);
                }
            }
        }
        for pipe in grid.pipes() {
            debug_assert!(pipe.len() >= 2);
            let mut start = pipe[0];
            let mut next = pipe[1];
            let mut dir = Direction::from_delta(next - start);
            self.draw_pipe_tip(grid, start, dir, &mut canvas);
            for index in 2..pipe.len() {
                start = next;
                next = pipe[index];
                let prev_dir = dir;
                dir = Direction::from_delta(next - start);
                let sprite_index = match (prev_dir, dir) {
                    (Direction::East, Direction::North) => 6,
                    (Direction::East, Direction::South) => 7,
                    (Direction::West, Direction::North) => 5,
                    (Direction::West, Direction::South) => 4,
                    (Direction::East, _) | (Direction::West, _) => {
                        let obj = grid.objects().get(&start).cloned();
                        if obj == Some(PlaneObj::Cross) {
                            12
                        } else {
                            8
                        }
                    }
                    (Direction::North, Direction::East) => 4,
                    (Direction::North, Direction::West) => 7,
                    (Direction::South, Direction::East) => 5,
                    (Direction::South, Direction::West) => 6,
                    (Direction::North, _) | (Direction::South, _) => 9,
                };
                let sprite = &self.pipe_sprites[sprite_index];
                canvas.draw_sprite(sprite, start * TILE_ISIZE);
            }
            dir = Direction::from_delta(start - next);
            self.draw_pipe_tip(grid, next, dir, &mut canvas);
        }
        for (&coords, &letter) in self.letters.iter() {
            let pt = Point::new(
                coords.x() * TILE_ISIZE + TILE_ISIZE / 2,
                coords.y() * TILE_ISIZE + TILE_ISIZE / 2 + 4,
            );
            canvas.draw_char(&self.font, Align::Center, pt, letter);
        }
    }

    fn handle_event(
        &mut self,
        event: &Event,
        grid: &mut PlaneGrid,
    ) -> Action<PlaneCmd> {
        match event {
            &Event::MouseDown(pt) if self.rect(grid).contains_point(pt) => {
                self.drag_from = self.pt_to_coords(grid, pt);
                Action::ignore().and_stop()
            }
            &Event::MouseDrag(pt) => {
                if let Some(coords1) = self.drag_from {
                    if let Some(coords2) = self.pt_to_coords(grid, pt) {
                        self.drag_from = Some(coords2);
                        if grid.toggle_pipe(coords1, coords2) {
                            self.changes.push((coords1, coords2));
                            return Action::redraw()
                                .and_return(PlaneCmd::Changed);
                        }
                    }
                }
                Action::ignore()
            }
            &Event::MouseUp => {
                self.drag_from = None;
                if self.changes.is_empty() {
                    Action::ignore()
                } else {
                    let vec = mem::replace(&mut self.changes, Vec::new());
                    Action::redraw().and_return(PlaneCmd::PushUndo(vec))
                }
            }
            _ => Action::ignore(),
        }
    }
}

// ========================================================================= //
