module Grid
( Grid
, makeGrid
, getSize
, getRows
, getCols
, getCell
, setCell
, update
-- Only functions are exported but not Data.Matrix module.
-- These functions might not be used always. So no need to
-- export module for that. It would be better to export it
-- manually when it is needed.
, toMatrix
, fromMatrix
, aliveCoords
) where

{-
    This module implements a grid of cells. Grid's
    indexes are C'like. The isOutOfRange function
    from Pos module is used to check whether
    indexes are valid or not.
-}

import Data.List
import qualified Data.Matrix as M

import qualified Cell as C
import Pos


-- Type of grid.
type Grid = M.Matrix C.Cell

-- Converts grid to matrix from Data.Matrix.
-- It allows to use all nice functions from
-- Data.Matrix over Grid.
toMatrix :: Grid -> M.Matrix C.Cell
toMatrix = id

-- Symmetric to toMatrix: once all nice functions
-- from Data.Matrix were applied, it is time to turn
-- matrix back into a grid.
fromMatrix :: M.Matrix C.Cell -> Grid
fromMatrix = id

-- Returns new grid of supplied size.
makeGrid :: Int -> Int -> Grid
makeGrid r c = M.matrix r c $ const C.dead 

-- Returns a number of rows in a grid.
getRows :: Grid -> Int
getRows = M.nrows

-- Returns a number of cols in a grid.
getCols :: Grid -> Int
getCols = M.ncols

-- Returns size of grid.
getSize :: Grid -> (Int, Int)
getSize g = (getRows g, getCols g)

-- Returns a cell by it's coordinates.
getCell :: Pos -> Grid -> C.Cell
getCell p@(y, x) g =
    let (r, c) = getSize g in
        if isOutOfRange r c p
            then error $ "getCell: " ++ show p ++ " is out of range!"
            else M.getElem (y + 1) (x + 1) g

-- Returns a neighbours of cell at supplied position.
getNeighborsOfCellByPos :: Pos -> Grid -> [C.Cell]
getNeighborsOfCellByPos p g =
    let (r, c) = getSize g
        pNs    = neighbors p
        vNs    = filter (not . isOutOfRange r c) pNs 
            in map (flip getCell g) vNs
        
-- Updates grid at supplied position.
update :: Grid -> Grid
update g =
    let (r, c) = getSize g
        in M.matrix r c
            (\ (y', x') ->
                let p    = (y' - 1, x' - 1)
                    cell = getCell p g
                    ns   = getNeighborsOfCellByPos p g
                        in C.update cell ns) 

-- Sets cell on grid.
setCell :: C.Cell -> Pos -> Grid -> Grid
setCell c (y, x) g = M.setElem c (y + 1, x + 1) g

-- Returns a list of coordinates of alive cells on a grid.
aliveCoords :: Grid -> [(Int, Int)]
aliveCoords g =
        -- Transfrom matrix to a list of lists.
    let lists  = M.toLists g
        -- Index each cell and each list.
        iLists = zip [0..] $ map (zip [0..]) lists
        -- Push list index to cell index.
        iCells = concatMap
            (\ (n, xs)
                -> map (\ (m, c) -> ((n, m), c)) xs
            ) iLists
        -- Filter alive cells.
        fCells = filter (C.isAlive . snd) iCells
        -- Get rid of cell.
        in map fst fCells
                    
    
