{-# LANGUAGE BlockArguments #-}
module TicTacToe where

import Test.HUnit
import Data.Maybe (isNothing, fromMaybe, isJust)
import Data.Char (digitToInt, intToDigit)
import GHC.Unicode (isDigit)

allTests :: Test
allTests = TestList [
  testGameFull,
  testCheckWinner,
  testPlacePlayer,
  testGameToString
  ]

data Player = PlayerX | PlayerO
  deriving (Eq, Show)

data Mark = X | O | E
  deriving (Eq, Show)

type Game = [[Mark]]

-------------------------------------------------
-- Aufgabe 2 (a)
-------------------------------------------------
gameFull :: Game -> Bool
gameFull game = 
  case game of
    [[]] -> False
    [firstColumn, secondColumn, thirdColumn] ->
      columnFull firstColumn
      && columnFull secondColumn
      && columnFull thirdColumn
    _ -> False

columnFull :: [Mark] -> Bool
columnFull column =
  case column of
    [first, second, third] -> (first /= E) && (second /= E) && (third /= E)
    _ -> False

-------------------------------------------------
-- Aufgabe 2 (a) Test
-------------------------------------------------
testGameFull:: Test
testGameFull = TestCase $ do
  assertEqual "test1" False (gameFull [[O, X, O],[O, O, O],[X, X, E]])
  assertEqual "test2" True (gameFull [[O, X, O],[O, O, O],[X, X, X]])
  assertEqual "test3" False (gameFull [[O, X, O],[O, E, O],[X, X, O]])

-------------------------------------------------
-- Aufgabe 2 (b)
-------------------------------------------------
checkWinner :: Game -> Maybe Player
checkWinner game =
  case game of
    [firstColumn, secondColumn, thirdColumn] ->
      let 
        winner1 = checkWinnerVertical firstColumn
        winner2 = checkWinnerVertical secondColumn
        winner3 = checkWinnerVertical thirdColumn
        winner4 = checkWinnerHorizontal (mapRow game 0)
        winner5 = checkWinnerHorizontal (mapRow game 1)
        winner6 = checkWinnerHorizontal (mapRow game 2)
        winner7 = checkWinnerDiagonal game
        resultList = [winner1, winner2, winner3, winner4, winner5, winner6, winner7]
        in
          getMaybeWinnerFromPlayerList resultList
    _ -> Nothing

-- Checks list of maybe player if containing a player, returns first
-- element which is a player.
getMaybeWinnerFromPlayerList :: [Maybe Player] -> Maybe Player
getMaybeWinnerFromPlayerList players =
  case players of
    [] -> Nothing
    (first:rest) ->
      if isNothing first
        then getMaybeWinnerFromPlayerList rest
        else first

-- Maps given mark to player.
mapMarkToPlayer :: Mark -> Maybe Player
mapMarkToPlayer mark =
  case mark of
    X -> Just PlayerX
    O -> Just PlayerO
    E -> Nothing

-- Checks given row for winner.
checkWinnerHorizontal :: [Mark] -> Maybe Player
checkWinnerHorizontal row =
  case row of
    [first, second, third] ->
      if first == second && second == third
        then
          mapMarkToPlayer first
        else Nothing
    _ -> Nothing

-- Checks given column for winner.
checkWinnerVertical :: [Mark] -> Maybe Player
checkWinnerVertical column =
  checkWinnerHorizontal column

-- Takes game and returns list containing elements
-- from one row, indicated by rowID.
mapRow :: Game -> Int -> [Mark]
mapRow game rowID =
  case rowID of
    0 -> (\[[first, _, _], [second, _, _], [third, _, _]] -> [first,second,third]) game
    1 -> (\[[_, first, _], [_, second, _], [_, third, _]] -> [first,second,third]) game
    2 -> (\[[_, _, first], [_, _, second], [_, _, third]] -> [first,second,third]) game
    _ -> []

-- Checks both diagonals for winner.
checkWinnerDiagonal :: Game -> Maybe Player
checkWinnerDiagonal game =
  case game of
    [[topLeftCorner, _, bottomLeftCorner],[_, middle, _],[topRightCorner, _, bottomRightCorner]] ->
      if topLeftCorner == middle && middle == bottomRightCorner
        then mapMarkToPlayer topLeftCorner
        else if topRightCorner == middle && middle == bottomLeftCorner
              then mapMarkToPlayer topRightCorner
              else Nothing
    _ -> Nothing

-------------------------------------------------
-- Aufgabe 2 (b) Test
-------------------------------------------------
testCheckWinner:: Test
testCheckWinner = TestCase $ do
  assertEqual "test column" (Just PlayerO) (checkWinner [[O, O, O],[E, E, E],[X, X, E]])
  assertEqual "test row" (Just PlayerO) (checkWinner [[O, E, E],[O, E, E],[O, E, E]])
  assertEqual "test empty game" Nothing (checkWinner [[E, E, E],[E, E, E],[E, E, E]])
  assertEqual "test DiagonalTopLeft to BottomRight" (Just PlayerX) (checkWinner [[X, E, E],[E, X, E],[E, E, X]])
  assertEqual "test DiagonalBottomLeft to TopRight" (Just PlayerX) (checkWinner [[E, E, X],[E, X, E],[X, E, E]])

-------------------------------------------------
-- Aufgabe 2 (c)
-------------------------------------------------
placePlayer :: Game -> (Int, Int) -> Player -> Maybe Game
placePlayer game (x, y) player =
  let [firstColumn, secondColumn, thirdColumn] = game
    in
      case x of
        0 -> let newFirstColumn = set y (mapPlayerToMark player) firstColumn
              in if isNothing newFirstColumn
                  then Nothing
                  else Just [fromMaybe [] newFirstColumn, secondColumn, thirdColumn]
        1 -> let newSecondColumn = set y (mapPlayerToMark player) secondColumn
              in if isNothing newSecondColumn
                  then Nothing
                  else Just [firstColumn, fromMaybe [] newSecondColumn, thirdColumn]
        2 -> let newThirdColumn = set y (mapPlayerToMark player) thirdColumn
              in if isNothing newThirdColumn
                  then Nothing
                  else Just [firstColumn, secondColumn, fromMaybe [] newThirdColumn]
        _ -> Nothing

-- Maps player to mark.
mapPlayerToMark :: Player -> Mark
mapPlayerToMark player =
  case player of
    PlayerO -> O
    PlayerX -> X

-- Sets given y position at column to mark,
-- if given position is empty and the index
-- of y is valid.
-- Returns given column, if invalid, and
-- mutated column if valid.
set :: Int -> Mark -> [Mark] -> Maybe [Mark]
set yPosition mark column =
  case column of
    [first, second, third] ->
      case yPosition of
        0 -> if first /= E
          then Nothing
          else Just [mark, second, third]
        1 -> if second /= E
          then Nothing
          else Just [first, mark, third]
        2 -> if third /= E
          then Nothing
          else Just [first, second, mark]
        _ -> Nothing
    _ -> Nothing

-------------------------------------------------
-- Aufgabe 2 (c) Test
-------------------------------------------------
testPlacePlayer:: Test
testPlacePlayer = TestCase $ do
  assertEqual "test place player O on empty field" (Just [[O, E, E],[E, E, E],[E, E, E]]) (placePlayer [[E, E, E],[E, E, E],[E, E, E]] (0,0) PlayerO)
  assertEqual "test place player X on empty field" (Just [[X, E, E],[E, E, E],[E, E, E]]) (placePlayer [[E, E, E],[E, E, E],[E, E, E]] (0,0) PlayerX)
  assertEqual "test place player O on X occupied field" Nothing (placePlayer [[X, E, E],[E, E, E],[E, E, E]] (0,0) PlayerO)
  assertEqual "test place player X on O occupied field" Nothing (placePlayer [[O, E, E],[E, E, E],[E, E, E]] (0,0) PlayerX)
  assertEqual "test place player invalid x index" Nothing (placePlayer [[X, E, E],[E, E, E],[E, E, E]] (3,0) PlayerO)
  assertEqual "test place player invalid y index" Nothing (placePlayer [[O, E, E],[E, E, E],[E, E, E]] (0,3) PlayerX)

-------------------------------------------------
-- Aufgabe 2 (d)
-------------------------------------------------
gameToString :: Game -> String
gameToString game =
  let [firstRow, secondRow, thirdRow] = game
    in let
      [e1, e2, e3] = firstRow
      [e4, e5, e6] = secondRow
      [e7, e8, e9] = thirdRow
      in  "| " ++ [markToChar e1] ++ " | " ++ [markToChar e2] ++ " | " ++ [markToChar e3] ++ " |\n"
       ++ "| " ++ [markToChar e4] ++ " | " ++ [markToChar e5] ++ " | " ++ [markToChar e6] ++ " |\n"
       ++ "| " ++ [markToChar e7] ++ " | " ++ [markToChar e8] ++ " | " ++ [markToChar e9] ++ " |\n"

markToChar :: Mark -> Char
markToChar mark =
  case mark of
    O -> 'o'
    X -> 'x'
    E -> '-'

-------------------------------------------------
-- Aufgabe 2 (d) Test
-------------------------------------------------
testGameToString:: Test
testGameToString = TestCase $ do
  assertEqual "test print empty game" "| - | - | - |\n| - | - | - |\n| - | - | - |\n" (gameToString [[E, E, E],[E, E, E],[E, E, E]])
  assertEqual "test print filled game" "| o | x | o |\n| o | o | o |\n| o | x | o |\n" (gameToString [[O, X, O],[O, O, O],[O, X, O]])

-------------------------------------------------
-- Aufgabe 2 (e)
-------------------------------------------------
getMove :: Player -> IO (Int, Int)
getMove player = do
  putStrLn ("Eingabe " ++ show player ++ " in der Form Zeile,Spalte:")
  input <- getLine
  if input /= ""
    then
      let (x,y) = getXYCoordinate input
        in if x > -1 && x < 3 && y > -1 && y < 3
          then pure (x,y)
          else do
            putStrLn ("Fehler in der Eingabe von " ++ show player ++ " bitte gebe die Koordinaten erneut ein.")
            getMove player
    else do
      putStrLn ("Fehler in der Eingabe von " ++ show player ++ " bitte gebe die Koordinaten erneut ein.")
      getMove player

getXYCoordinate :: String -> (Int,Int)
getXYCoordinate input  =
  case input of
    [x,comma,y] ->
      if isDigit x && comma == ',' && isDigit y
        then (digitToInt x, digitToInt y)
        else (-1,-1)
    _ -> (-1,-1)

-------------------------------------------------
-- Aufgabe 2 (f)
-------------------------------------------------
halfMove :: Game -> Player -> IO (Maybe Game)
halfMove game player = do
  putStr(gameToString game)
  move <- getMove player
  let
    nextGame = placePlayer game move player
    in
      if isJust nextGame
        then 
          if isJust (checkWinner (fromMaybe game nextGame))
             || gameFull (fromMaybe game nextGame)
            then pure Nothing
            else pure nextGame
        else halfMove game player

-------------------------------------------------
-- Aufgabe 2 (g)
-------------------------------------------------
fullGame :: IO ()
fullGame =
  fullGameWithTurn 1 emptyGame

fullGameWithTurn :: Int -> Game -> IO()
fullGameWithTurn turn game = do
  putStrLn("Zug " ++  [intToDigit turn])
  gameO <- halfMove game PlayerO
  gameX <- halfMove (fromMaybe emptyGame gameO) PlayerX
  fullGameWithTurn (turn+1) (fromMaybe emptyGame gameX)

emptyGame :: Game
emptyGame = [[E,E,E],[E,E,E],[E,E,E]]