-- | Types and functions for manipulating boards.
module Hs2048.Board
    ( Board
    , canMove
    , canShift
    , empty
    , emptyPoints
    , move
    , parse
    , render
    , rotate
    , rotateFrom
    , rotateTo
    , score
    , set
    , shift
    ) where

import           Data.List        (transpose)
import qualified Hs2048.Direction as D
import qualified Hs2048.Point     as P
import qualified Hs2048.Tile      as T
import qualified Hs2048.Vector    as V

{- |
    Represents the game board. By convention, it is row-major.
-}
type Board = [V.Vector]

{- |
    Determines if the board can be moved in the given direction. See 'move'.

    >>> canMove [[Nothing, Just 2]] D.West
    True
-}
canMove :: Board -> D.Direction -> Bool
canMove b d = move b d /= b

{- |
    Determines if the board can be shifted. See 'shift'.

    >>> canShift [[Nothing, Just 2]]
    True
-}
canShift :: Board -> Bool
canShift b = shift b /= b

{- |
    Returns an empty board of the given size.

    >>> empty 2 1
    [[Nothing,Nothing]]
-}
empty :: Int -- ^ Width
    -> Int -- ^ Height
    -> Board
empty = flip replicate . V.empty

{- |
    Returns the points that don't contain tiles.

    >>> emptyPoints [[Nothing, Just 2]]
    [(0,0)]
-}
emptyPoints :: Board -> [P.Point]
emptyPoints b = zip [0 ..] (fmap V.emptyIndexes b) >>= go
  where
    go (x, ys) = fmap ((,) x) ys

{- |
    Moves the board in the given direction.

    >>> move [[Nothing, Just 2]] D.West
    [[Just 2,Nothing]]
-}
move :: Board -> D.Direction -> Board
move b d = rotateFrom (shift (rotateTo b d)) d

{- |
    Parses a string as a board. This is the inverse of 'render'.

    >>> parse "- 2\n4 -\n"
    [[Nothing,Just 2],[Just 4,Nothing]]
-}
parse :: String -> Board
parse = fmap V.parse . lines

{- |
    Renders a board as a string. This is the inverse of 'parse'.

    >>> render [[Nothing, Just 2], [Just 4, Nothing]]
    "- 2\n4 -\n"
-}
render :: Board -> String
render = unlines . fmap V.render

{- |
    Rotate the board 90 degrees clockwise.

    >>> rotate [[Nothing, Just 2], [Just 4, Nothing]]
    [[Just 4,Nothing],[Nothing,Just 2]]
-}
rotate :: Board -> Board
rotate = fmap reverse . transpose

{- |
    Rotates the board so that 'Hs2048.Direction.West' is at the left, assuming
    the given direction is currently at the left. This is the inverse of
    'rotateTo'.

    >>> rotateFrom [[Just 4, Nothing], [Nothing, Just 2]] D.South
    [[Nothing,Just 2],[Just 4,Nothing]]
-}
rotateFrom :: Board -> D.Direction -> Board
rotateFrom b d = head (drop n gs)
  where
    n = 1 + fromEnum (maxBound :: D.Direction) - fromEnum d
    gs = iterate rotate b

{- |
    Rotates the board so that the given direction is at the left. This is the
    inverse of 'rotateFrom'

    >>> rotateTo [[Nothing, Just 2], [Just 4, Nothing]] D.South
    [[Just 4,Nothing],[Nothing,Just 2]]
-}
rotateTo :: Board -> D.Direction -> Board
rotateTo b d = head (drop n gs)
  where
    n = fromEnum d
    gs = iterate rotate b

{- |
    Calculates the score of a board.

    >>> score [[Nothing, Just 2], [Just 4, Just 8]]
    20
-}
score :: Board -> Int
score = sum . fmap V.score

{- |
    Sets a tile at the given point in the board.

    >>> set [[Nothing, Just 2], [Just 4, Nothing]] (Just 8) (1, 1)
    [[Nothing,Just 2],[Just 4,Just 8]]
-}
set :: Board -> T.Tile -> P.Point -> Board
set b t p = zipWith go [0 ..] b
  where
    go i v = if i == P.x p then V.set v t (P.y p) else v

{- |
    Shifts a board toward the head. See 'Vector.shift'.

    >>> shift [[Nothing, Just 2], [Just 4, Nothing]]
    [[Just 2,Nothing],[Just 4,Nothing]]
-}
shift :: Board -> Board
shift = fmap V.shift