module Main where

import System.Random
import Data.List (intercalate, find, minimumBy)
import System.Environment (getArgs)
import Data.Char (digitToInt)
import Data.Maybe (listToMaybe, mapMaybe)
import Control.Monad (guard)
import Data.Ord (comparing)



tictactoe :: String -> Bool
tictactoe a = tictactoeFor 'X' a /= tictactoeFor 'O' a



tictactoeFor :: Char -> String -> Bool
tictactoeFor n [a,b,c,d,e,f,g,h,i] =
    [n,n,n] `elem` [[a,b,c],[d,e,f],[g,h,i],[a,d,g],
                    [b,e,h],[c,f,i],[a,e,i],[c,e,g]]


start :: String
start = "         "


isPossible :: Int -> String -> Bool
isPossible n game = (game !! n) `notElem` "XO"




place :: Int -> Char -> String -> Either String String
place i c game =
    if isPossible i game
    then Right $ take i game ++ [c] ++ drop (i + 1) game
    else Left game







developGame :: Bool -> Int -> Int -> Char -> String -> (Int, Char, String)
developGame iterateMore moves i player game
    | i > 8 = 
        
        
        if iterateMore
        then developGame False moves 0 player game
        
        else (moves, player, game)
        
    | moves == 9 || tictactoeFor player game = (moves, player, game)
        
    | otherwise = case place i otherPlayer game of
        
        Left _ -> developGame iterateMore moves (i + 1)
                    otherPlayer game
        
        
        Right newGame -> developGame iterateMore (moves + 1) 0
                    otherPlayer newGame
        where
            otherPlayer = changePlayer player




bestMoveFor :: Char -> String -> Int
bestMoveFor player game = bestMove
    where
        
        continuations = [ (x, developGame True 0 x player game) |
            x <- [0..8] ]
        
        
        move (_, (m, _, _)) = m
        (bestMove, _) = minimumBy (comparing move) continuations




canBlock :: Char -> String -> Maybe Int
canBlock p [a,b,c,d,e,f,g,h,i] =
    listToMaybe $ mapMaybe blockable [[a,b,c],[d,e,f],[g,h,i],[a,d,g],
                                      [b,e,h],[c,f,i],[a,e,i],[c,e,g]]
    where
        blockable xs = do          
          guard $ length (filter (== otherPlayer) xs) == 2
          x <- find (`elem` "123456789") xs
          return $ digitToInt x
        otherPlayer = changePlayer p


showGame :: String -> String
showGame [a,b,c,d,e,f,g,h,i] =
    topBottom ++
    "|    | 1 | 2 | 3 |\n" ++
    topBottom ++
    row "0" [[a],[b],[c]] ++
    row "3" [[d],[e],[f]] ++
    row "6" [[g],[h],[i]]
    where
        topBottom = "+
        row n x = "| " ++ n ++ "+ | " ++
            intercalate " | " x ++ " |\n" ++ topBottom


enterNumber :: IO Int
enterNumber = do
    c <- getChar
    if c `elem` "123456789"
    then do
        putStrLn ""
        return $ digitToInt c
    else do
        putStrLn "\nPlease enter a digit!"
        enterNumber





turn :: (Int, Char, String) -> IO (Int, Char, String)
turn (count, player, game) = do
    putStr $ "Please tell me where you want to put an " ++
        [player] ++ ": "
    pos <- enterNumber
    case place (pos - 1) player game of
        Left oldGame -> do
            putStrLn "That place is already taken!\n"
            turn (count, player, oldGame)
        Right newGame ->
            return (count + 1, changePlayer player, newGame)


changePlayer :: Char -> Char
changePlayer 'O' = 'X'
changePlayer 'X' = 'O'








autoTurn :: Bool -> (Int, Char, String) -> IO (Int, Char, String)
autoTurn forceRandom (count, player, game) = do
    
    
    i <- if count == 0 || forceRandom
            then randomRIO (0,8)
            else return $
                case canBlock player game of
                    
                    
                    Nothing -> bestMoveFor player game
                    
                    Just blockPos -> blockPos
    
    
    case place i player game of
        Left oldGame -> autoTurn True (count, player, oldGame)
        Right newGame -> do
            putStrLn $ "It's player " ++ [player] ++ "'s turn."
            return (count + 1, changePlayer player, newGame)




play :: Int -> (Int, Char, String) -> IO ()
play auto cpg@(_, player, game) = do
    newcpg@(newCount, newPlayer, newGame) <- case auto of
        
        0 -> turn cpg
        
        1 -> autoTurn False cpg
        
        2 -> if player == 'X' then autoTurn False cpg else turn cpg
        
        3 -> if player == 'O' then autoTurn False cpg else turn cpg
    putStrLn $ "\n" ++ showGame newGame
    if tictactoe newGame
    then putStrLn $ "Player " ++ [changePlayer newPlayer] ++ " wins!\n"
    else
        if newCount == 9
        then putStrLn "Draw!\n"
        else play auto newcpg




main :: IO ()
main = do
    a <- getArgs
    if null a
    then usage
    else do
        let option = head a
        if option `elem` ["0","1","2","3"]
        then do
            putStrLn $ "\n" ++ showGame start
            let m = read option :: Int
            play m (0, 'X', start)
        else usage

usage :: IO ()
usage = do
    putStrLn "TIC-TAC-TOE GAME\n================\n"
    putStrLn "How do you want to play?"
    putStrLn "Run the program with one of the following options."
    putStrLn "0: both players are human"
    putStrLn "1: both players are computer"
    putStrLn "2: player X is computer and player O is human"
    putStrLn "3: player X is human and player O is computer"
    putStrLn "Player X always begins."