module RandomChess
( placeKings
, placePawns
, placeRemaining
, emptyBoard
, toFen
, ChessBoard
, Square (..)
, BoardState (..)
, getBoard
)
where

import Control.Monad.State (State, get, gets, put)
import Data.List (find, sortBy)
import System.Random (Random, RandomGen, StdGen, random, randomR)

type Pos = (Char, Int)

type ChessBoard = [(Square, Pos)]

data PieceRank  = King | Queen | Rook | Bishop | Knight | Pawn
  deriving (Enum, Bounded, Show, Eq, Ord)
data PieceColor = Black | White
  deriving (Enum, Bounded, Show, Eq, Ord)

data Square = ChessPiece PieceRank PieceColor | EmptySquare
  deriving (Eq, Ord)

type PieceCount = [(Square, Int)]

data BoardState = BoardState { board :: ChessBoard , generator :: StdGen }

instance Show Square where
  show (ChessPiece King   Black) = ""
  show (ChessPiece Queen  Black) = ""
  show (ChessPiece Rook   Black) = ""
  show (ChessPiece Bishop Black) = ""
  show (ChessPiece Knight Black) = ""
  show (ChessPiece Pawn   Black) = ""
  show (ChessPiece King   White) = ""
  show (ChessPiece Queen  White) = ""
  show (ChessPiece Rook   White) = ""
  show (ChessPiece Bishop White) = ""
  show (ChessPiece Knight White) = ""
  show (ChessPiece Pawn   White) = ""
  show EmptySquare               = " "

instance Random PieceRank where
  randomR (a, b) g = case randomR (fromEnum a, fromEnum b) g of
    (x, g'') -> (toEnum x, g'')
  random = randomR (minBound, maxBound)

instance Random PieceColor where
  randomR (a, b) g = case randomR (fromEnum a, fromEnum b) g of
    (x, g'') -> (toEnum x, g'')
  random = randomR (minBound, maxBound)

fullBoard :: PieceCount
fullBoard =
  [ (ChessPiece King Black  , 1)
  , (ChessPiece Queen Black , 1)
  , (ChessPiece Rook Black  , 2)
  , (ChessPiece Bishop Black, 2)
  , (ChessPiece Knight Black, 2)
  , (ChessPiece Pawn Black  , 8)
  , (ChessPiece King White  , 1)
  , (ChessPiece Queen White , 1)
  , (ChessPiece Rook White  , 2)
  , (ChessPiece Bishop White, 2)
  , (ChessPiece Knight White, 2)
  , (ChessPiece Pawn White  , 8)
  , (EmptySquare            , 32)
  ]

emptyBoard :: ChessBoard
emptyBoard = fmap (EmptySquare,) . (,) <$> ['a'..'h'] <*> [1..8]

replaceSquareByPos :: (Square, Pos) -> ChessBoard -> ChessBoard
replaceSquareByPos e@(_, p) = fmap (\x -> if p == snd x then e else x)

isPosOccupied :: Pos -> ChessBoard -> Bool
isPosOccupied p = occupied . find (\x -> p == snd x)
 where
  occupied (Just (EmptySquare, _)) = False
  occupied _                       = True

isAdjacent :: Pos -> Pos -> Bool
isAdjacent (x1, y1) (x2, y2) =
  let upOrDown    = (pred y1 == y2 || succ y1 == y2)
      leftOrRight = (pred x1 == x2 || succ x1 == x2)
  in  (x2 == x1 && upOrDown)
        || (pred x1 == x2 && upOrDown)
        || (succ x1 == x2 && upOrDown)
        || (leftOrRight && y1 == y2)

fen :: Square -> String
fen (ChessPiece King   Black) = "k"
fen (ChessPiece Queen  Black) = "q"
fen (ChessPiece Rook   Black) = "r"
fen (ChessPiece Bishop Black) = "b"
fen (ChessPiece Knight Black) = "n"
fen (ChessPiece Pawn   Black) = "p"
fen (ChessPiece King   White) = "K"
fen (ChessPiece Queen  White) = "Q"
fen (ChessPiece Rook   White) = "R"
fen (ChessPiece Bishop White) = "B"
fen (ChessPiece Knight White) = "N"
fen (ChessPiece Pawn   White) = "P"

boardSort :: (Square, Pos) -> (Square, Pos) -> Ordering
boardSort (_, (x1, y1)) (_, (x2, y2)) | y1 < y2  = GT
                                      | y1 > y2  = LT
                                      | y1 == y2 = compare x1 x2

toFen :: ChessBoard -> String
toFen [] = " w - - 0 1" <> []
toFen b = scanRow (fst <$> take 8 b) 0
  where
    scanRow [] 0               = nextRow
    scanRow [] n               = show n <> nextRow
    scanRow (EmptySquare:xs) n = scanRow xs (succ n)
    scanRow (x:xs) 0           = nextPiece x xs
    scanRow (x:xs) n           = show n <> nextPiece x xs
    nextRow = "/" <> toFen (drop 8 b)
    nextPiece x xs = fen x <> scanRow xs 0


withStateGen :: (StdGen -> (a, StdGen)) -> State BoardState a
withStateGen f = do
  currentState <- get
  let gen1 = generator currentState
  let (x, gen2) = f gen1
  put (currentState {generator = gen2})
  pure x

randomPos :: State BoardState Pos
randomPos = do
  boardState <- gets board
  chr <- withStateGen (randomR ('a', 'h'))
  num <- withStateGen (randomR (1, 8))
  let pos = (chr, num)
  if isPosOccupied pos boardState then
    randomPos
  else
    pure pos

randomPiece :: State BoardState Square
randomPiece = ChessPiece <$> withStateGen random <*> withStateGen random

placeKings :: State BoardState ()
placeKings = do
  currentState <- get
  p1 <- randomPos
  p2 <- randomPos
  if p1 `isAdjacent` p2 || p1 == p2
    then placeKings
    else do
      let updatedBoard = replaceSquareByPos (ChessPiece King White, p1) $
            replaceSquareByPos (ChessPiece King Black, p2) (board currentState)
      put currentState { board = updatedBoard }

placePawns :: State BoardState ()
placePawns = withStateGen (randomR (1, 16)) >>= go
  where
    go :: Int -> State BoardState ()
    go 0 = pure ()
    go n = do
      currentState <- get
      pos <- randomPos
      color <- withStateGen random
      let pawn = ChessPiece Pawn color
      let currentBoard = board currentState
      if promoted color == snd pos || isPosOccupied pos currentBoard ||
                           enpassant color == snd pos || firstPos color == snd pos
        then go n
        else do
          put currentState { board = replaceSquareByPos (pawn, pos) currentBoard }
          go $ pred n
    promoted White = 8
    promoted Black = 1
    enpassant White = 5
    enpassant Black = 4
    firstPos White = 1
    firstPos Black = 8

placeRemaining :: State BoardState ()
placeRemaining =
  withStateGen (randomR (5, sum $ fmap snd remaining)) >>= go remaining
  where
    remaining = filter (\case
                         (ChessPiece King _, _) -> False
                         (ChessPiece Pawn _, _) -> False
                         (EmptySquare, _) -> False
                         _ -> True)
                        fullBoard

    go :: PieceCount -> Int -> State BoardState ()
    go _ 0 = pure ()
    go remaining n = do
      currentState <- get
      let currentBoard = board currentState
      position <- randomPos
      piece <- randomPiece
      if not (isPermitted piece) || isPosOccupied position currentBoard
        then go remaining n
        else do
          let updatedBoard = replaceSquareByPos (piece, position) currentBoard
          put currentState { board = updatedBoard }
          go (consume piece remaining) (pred n)
      where
        isPermitted p =
          case find ((==p) . fst) remaining of
            Just (_, count) -> count > 0
            Nothing         -> False
        consume p'' = fmap (\(p, c) -> if p == p'' then (p, pred c) else (p, c))

getBoard :: State BoardState ChessBoard
getBoard = gets (sortBy boardSort . board)