extern crate rand;
extern crate pancurses;

use rand::Rng;

//

const MOUSE_PRESS_LEFT   :u32 = 4;    // n.b. these constants are at least correct for pancurses' pdcurses-sys/win32a backend
const MOUSE_DBLCLICK_LEFT:u32 = 8;
const MOUSE_PRESS_RIGHT  :u32 = 4096;
// const MOUSE_PRESS_MIDDLE :u32 = 128; 

// 1..8: neighbour colours
const PALETTE_DEFAULT :i16 = 9;
const PALETTE_MINE    :i16 = 10;
const PALETTE_FLAG    :i16 = 11;

#[derive(Copy, Clone, Debug)]
struct MineCell {
    mine: bool,
    revealed: bool,
    flagged: bool,
}

struct MinesGame {
    wnd: pancurses::Window,
    w: i32,
    h: i32,
    board: Vec<MineCell>,
    cx: i32,
    cy: i32,
    n_mines: usize,
    game_running: bool,
}

fn i32clamp(val:i32, low_bound:i32, hi_bound:i32) -> i32 {
    std::cmp::min(hi_bound, std::cmp::max(low_bound, val))
}

impl MinesGame {

    fn new(w:i32, h:i32, n_mines:usize) -> MinesGame {
        if w <= 0 || h <= 0 || n_mines <= 0 {
            panic!("Invalid board parameters");
        }

        let mut boarddata = vec![ MineCell{ mine: false, revealed: false, flagged: false }; (w*h) as usize ];

        // fill in some of the elements
        'nextMine: for _ in 0..n_mines {

            loop {
                let pos = rand::thread_rng().gen_range(0, w*h) as usize;
                if ! boarddata[pos].mine {
                    boarddata[pos].mine = true;
                    continue 'nextMine;
                }
            }
        }

        let wnd = pancurses::initscr();
        wnd.nodelay(true);
        wnd.keypad(true);

        pancurses::resize_term(h as i32, w as i32);
        
        pancurses::start_color();
        pancurses::use_default_colors();
        pancurses::mousemask(pancurses::ALL_MOUSE_EVENTS, std::ptr::null_mut());

        pancurses::init_pair(1,               pancurses::COLOR_BLUE,    pancurses::COLOR_WHITE);
        pancurses::init_pair(2,               pancurses::COLOR_GREEN,   pancurses::COLOR_WHITE);
        pancurses::init_pair(3,               pancurses::COLOR_RED,     pancurses::COLOR_WHITE);
        pancurses::init_pair(4,               pancurses::COLOR_BLUE,    pancurses::COLOR_WHITE);
        pancurses::init_pair(5,               pancurses::COLOR_MAGENTA, pancurses::COLOR_WHITE);
        pancurses::init_pair(6,               pancurses::COLOR_CYAN,    pancurses::COLOR_WHITE);
        pancurses::init_pair(7,               pancurses::COLOR_BLACK,   pancurses::COLOR_WHITE);
        pancurses::init_pair(8,               pancurses::COLOR_BLACK,   pancurses::COLOR_WHITE);
        pancurses::init_pair(PALETTE_DEFAULT, pancurses::COLOR_BLACK,   pancurses::COLOR_WHITE);
        pancurses::init_pair(PALETTE_MINE,    pancurses::COLOR_WHITE,   pancurses::COLOR_RED);
        pancurses::init_pair(PALETTE_FLAG,    pancurses::COLOR_WHITE,   pancurses::COLOR_RED);
        
        MinesGame{
            w: w,
            h: h,
            wnd: wnd,
            board: boarddata,
            cx: 0,
            cy: 0,
            n_mines: n_mines,
            game_running: true,
        }
    }

    fn coord_to_idx(&self, x:i32, y:i32) -> usize {
        return (y * self.w + x) as usize
    }

    fn neighbour_cb<F>(&self, x:i32, y:i32, cb:&mut F) 
        where F: FnMut(i32,i32) -> ()
    {
        // Range up to 2, since the loop never reaches the topmost value
        for dy in (-1 as i32)..2 { 
            for dx in (-1 as i32)..2 {
                if dy == 0 && dx == 0 { continue; };

                let ty = y + dy;
                let tx = x + dx;
                if ty >= self.h || ty < 0 || tx >= self.w || tx < 0 { continue; };

                cb(tx, ty);
            }
        }
    }

    fn neighbours(&self, x:i32, y:i32) -> i32 {
        let mut neighbourct = 0;
        {
            let mut cb = |x:i32, y:i32| {
                // borrow the binding to `neighbourct`
                neighbourct += if self.board[self.coord_to_idx(x, y)].mine { 1 } else { 0 };
            };

            self.neighbour_cb(x, y, &mut cb);
        } // cut scope short, so that there are no outstanding borrowed references to `neighbourct`
        neighbourct
    }

    fn render_board(&mut self) {
        self.wnd.clear();
        
        // self.wnd.attrset(pancurses::COLOR_PAIR(1) | pancurses::A_NORMAL); // default

        for y in 0..self.h {
            for x in 0..self.w {

                let cell = self.board[self.coord_to_idx(x, y)];
                let celltext : char; // doesn't need to be mutable if we only assign it once, even if it's not immediately
                let palette :i16; // pancurses::chtype;

                if cell.revealed && cell.mine {
                    // bang
                    palette = PALETTE_MINE;
                    celltext = 'M';

                } else if cell.revealed {

                    // Determine neighbour count
                    let neighbourct = self.neighbours(x as i32, y as i32);
                    
                    if neighbourct == 0 {
                        palette = PALETTE_DEFAULT;
                        celltext = '.'
                    } else {
                        palette = neighbourct as i16; // Determine cell colour based on neighbour count
                        celltext = format!("{}", neighbourct).chars().nth(0).expect("???");
                    }


                } else if cell.flagged {
                    palette = PALETTE_FLAG;
                    celltext = '*';                    

                } else {
                    palette = PALETTE_DEFAULT;
                    celltext = '#';

                }

                self.wnd.attrset(
                    pancurses::COLOR_PAIR(palette as u64)
                    | if x == self.cx && y == self.cy { pancurses::A_BOLD } else { pancurses::A_DIM }
                );
                self.wnd.mvaddch(y as i32, x as i32, celltext);
                
            }
        }

        self.wnd.refresh();
    }

    fn action_move_to(&mut self, cx: i32, cy: i32) {
        self.cx = i32clamp(cx, 0, self.w - 1);
        self.cy = i32clamp(cy, 0, self.h - 1);
        self.gamestep();
    }

    fn action_move(&mut self, dx: i32, dy: i32) {
        let cx = self.cx;
        let cy = self.cy;
        self.action_move_to(cx + dx, cy + dy);
    }

    fn gamestep(&mut self) {
        self.render_board();
        // also check win condition

        if let Some(winstate) = self.gameover_result() {
            // show everything
            for i in 0..self.board.len() {
                self.board[i].revealed = true;
            }

            // re-render
            self.render_board();
            self.wnd.refresh();

            pancurses::napms(2000);
            println!("Game over ({})", winstate);
            self.game_running = false;
        }
    }

    fn gameover_result(&self) -> Option<bool> {
        // 
        let mut incorrect_flag_ct = 0;
        let mut correct_flag_ct = 0;
        let mut unrevealed_mine_ct = 0;
        let mut unrevealed_non_mine_ct = 0;
        for i in 0..self.board.len() {

            if self.board[i].mine {
                if self.board[i].revealed {
                    return Some(false); // game over, negative result
                } else {
                    unrevealed_mine_ct += 1;
                }
            }

            if self.board[i].flagged {
                if self.board[i].mine {
                    correct_flag_ct += 1;
                } else {
                    incorrect_flag_ct += 1;
                }
            }

            if !self.board[i].revealed && !self.board[i].mine {
                unrevealed_non_mine_ct += 1;
            }

        }

        // Win condition:
        // - no incorrect flags, AND
        //    - either all mines are flagged, or
        //    - the only squares left are unrevealed mines
        let any_incorrect_flags = incorrect_flag_ct != 0;
        let all_mines_are_flagged = correct_flag_ct == self.n_mines;
        let remaining_squares_are_all_unrevealed_mines = unrevealed_mine_ct == self.n_mines && unrevealed_non_mine_ct == 0;

        if !any_incorrect_flags && (all_mines_are_flagged || remaining_squares_are_all_unrevealed_mines) {
            return Some(true);
        }

        return None; // haven't yet met win/lose conditions
    }

    fn action_reveal_surroundings_at(&mut self, x:i32, y:i32) {
        let mut postx: Vec<i32> = vec![];
        let mut posty: Vec<i32> = vec![];

        {
            let mut cb = |x:i32, y:i32| {
                // Can't refer to 'self' in here - since that would require borrowing or copying self.
                // Copying is obviously no good for mutating the board; and the problem with borrowing is
                // we can't release the borrowed copy (you normally do this by closing the scope, but `self`
                // is going to be in scope the whole time).
                // Just push into a vec and we'll iterate afterward.
                // self.action_reveal_at(x, y);
                postx.push(x);
                posty.push(y);
            };
            self.neighbour_cb(x, y, &mut cb);
        } // stop borrowing postx/y

        for i in 0..postx.len() {
            if ! self.board[self.coord_to_idx(postx[i], posty[i])].flagged {
                self.action_reveal_at(postx[i], posty[i]);
            }
        }
    }

    fn action_reveal_surroundings(&mut self) {
        let cx = self.cx;
        let cy = self.cy; // shadow the variables to silence the borrow checker
        self.action_reveal_surroundings_at(cx, cy);
    }

    fn action_reveal_at(&mut self, x:i32, y:i32) {
        let idx = self.coord_to_idx(x, y);

        if self.board[idx].revealed {
            return; // skip the recurse
        }

        self.board[idx].revealed = true;
        if self.neighbours(x, y) == 0 {
            // also reveal the surroundings
            self.action_reveal_surroundings_at(x, y);
        }
    }

    fn action_flag(&mut self) {
        let idx = self.coord_to_idx(self.cx, self.cy);

        self.board[idx].flagged = !self.board[idx].flagged;
        self.gamestep();
    }

    fn action_reveal(&mut self) {
        let cx = self.cx;
        let cy = self.cy; // shadow the variables to silence the borrow checker
        self.action_reveal_at(cx, cy);
        self.gamestep();
    }

    fn game_loop(&mut self) {

        self.render_board();

        while self.game_running {

            match self.wnd.getch() { // blocking

                Some(pancurses::Input::KeyUp)    | Some(pancurses::Input::Character('w')) => self.action_move( 0, -1),
                Some(pancurses::Input::KeyRight) | Some(pancurses::Input::Character('d')) => self.action_move( 1,  0),
                Some(pancurses::Input::KeyDown)  | Some(pancurses::Input::Character('s')) => self.action_move( 0,  1),
                Some(pancurses::Input::KeyLeft)  | Some(pancurses::Input::Character('a')) => self.action_move(-1,  0),

                Some(pancurses::Input::KeyMouse) => {
                    if let Ok(mouse_ev) = pancurses::getmouse() {
                        // if mouse_ev.bstate
                        self.action_move_to(mouse_ev.x, mouse_ev.y);
                        
                        if mouse_ev.bstate == MOUSE_PRESS_LEFT {
                            // left-click
                            self.action_reveal();

                        } else if mouse_ev.bstate == MOUSE_DBLCLICK_LEFT {
                            // blast-reveal
                            // The "usual" mouse action for this is chorded left+right, but, we can't seem to pick
                            // up that button combination with this curses library
                            self.action_reveal();
                            self.action_reveal_surroundings();
                            self.gamestep();

                        } else if mouse_ev.bstate == MOUSE_PRESS_RIGHT {
                            self.action_flag();

                        }
                    }
                }

                Some(pancurses::Input::Character('f')) => self.action_flag(),

                Some(pancurses::Input::Character(' ')) => self.action_reveal(),

                Some(pancurses::Input::Character('q')) => {
                    return
                }
                _ => (),

            }
        }

    }
}

fn main() {

    // parse cli flags
    let mut board_width = 9;
    let mut board_height = 9;
    let mut num_mines = 10;
    let mut flag_parse_state = 0;
    for (i, arg) in std::env::args().enumerate() {
        if i == 0 {
            continue;
        }
        if flag_parse_state == 0 {
            if arg == "--help" {
                println!("Usage:");
                println!("  rustmines [options]");
                println!("");
                println!("Options:");
                println!("  --help            Display this message");
                println!("  --width NUM       Board width (default {})", board_width);
                println!("  --height NUM      Board height (default {})", board_height);
                println!("  --mines NUM       Number of mines (default {})", num_mines);
                println!("  --easy            9x9 board with 10 mines");
                println!("  --intermediate    16x16 board with 40 mines");
                println!("  --advanced        30x16 board with 99 mines");
                println!("");
                println!("Controls:");
                println!("  [arrow keys]      Move cursor");
                println!("  w a s d           Move cursor");
                println!("  f                 Flag square");
                println!("  [space]           Reveal square");
                println!("  [left-click]      Move cursor and reveal square");
                println!("  [right-click]     Move cursor and flag square");
                println!("  [double-click]    Move cursor, reveal square + unflagged neighbours");
                std::process::exit(0);
            } else if arg == "--width" {
                flag_parse_state = 1;
            } else if arg == "--height" {
                flag_parse_state = 2;
            } else if arg == "--mines" {
                flag_parse_state = 3;
            } else if arg == "--easy" {
                board_width  = 9;
                board_height = 9;
                num_mines    = 10;
            } else if arg == "--intermediate" {
                board_width  = 16;
                board_height = 16;
                num_mines    = 40;
            } else if arg == "--advanced" {
                board_width  = 30;
                board_height = 16;
                num_mines    = 99;
            } else {
                println!("Unknown argument '{}' (try --help)", arg);
                std::process::exit(1);
            }
        } else if flag_parse_state == 1 {
            board_width = arg.trim().parse().expect("Argument to --width should be a number");
            flag_parse_state = 0;
        } else if flag_parse_state == 2 {
            board_height = arg.trim().parse().expect("Argument to --height should be a number");
            flag_parse_state = 0;
        } else if flag_parse_state == 3 {
            num_mines = arg.trim().parse().expect("Argument to --mines should be a number");
            flag_parse_state = 0;
        }
    }
    if flag_parse_state != 0 {
        println!("Missing argument (try --help)");
        std::process::exit(1);
    }

    // run game loop

    let mut app = MinesGame::new(board_width, board_height, num_mines);
    app.game_loop();

    println!("Bye!");
}
