module Data.TableTennis
  ( Playable
  , Player (..)
  , Team (..)
  , GameLength (..)
  , Game (..)
  , Side (..)
  , newSinglesGame
  , newDoublesGame
  , winPoint
  , serviceLength
  , valueOf
  , serviceToChange
  , winner
  , changeService
  , swapSides
  ) where

class Playable a where
  rotateSide :: a -> a

data Player =
  Player
    { name :: String
    , handicap :: Int
    } deriving (Show)

data Team = Team
  { topPlayer    :: Player
  , bottomPlayer :: Player
  } deriving (Show)

instance Playable Player where
  rotateSide a = a

instance Playable Team where
  rotateSide (Team pTop pBottom) = Team pBottom pTop

data GameLength = Eleven | TwentyOne deriving (Show)
data Game a =
  Game
    { leftSide   :: a
    , rightSide  :: a
    , leftScore  :: Int
    , rightScore :: Int
    , gameLength :: GameLength
    , serving    :: Side
    } deriving (Show)

data Side = LeftSide | RightSide deriving (Show)

-- |Add a point to the specified Side's score
winPoint :: Playable a => Game a -> Side -> Game a
winPoint (Game l r ls rs gl s) LeftSide = Game l r (ls + 1) rs gl s
winPoint (Game l r ls rs gl s) RightSide = Game l r ls (rs + 1) gl s

-- |New SinglesGame with specified Players/GameLength/service Side, scores = 0
-- with the scores set to 0
newSinglesGame :: Player -> Player -> GameLength -> Side -> Game Player
newSinglesGame leftPlayer rightPlayer gameLen side =
  Game leftPlayer rightPlayer 0 0 gameLen side

-- |A new DoublesGame with specified Teams/GameLength/serving Side
-- with the scores set to 0
newDoublesGame :: Team -> Team -> GameLength -> Side -> Game Team
newDoublesGame leftTeam rightTeam gameLen side =
  Game leftTeam rightTeam 0 0 gameLen side

-- |Convert GameLength to numerical representation
valueOf :: GameLength -> Int
valueOf Eleven = 11
valueOf TwentyOne = 21

-- |How many serves each player gets according to GameLength
serviceLength :: GameLength -> Int
serviceLength Eleven = 3
serviceLength TwentyOne = 5

-- |True if it is another Player/Team's turn to serve
serviceToChange :: Playable a => Game a -> Bool
serviceToChange g@(Game _ _ lScore rScore gameLen _)
  | ((lScore + rScore) > (valueOf gameLen)) == True = True
  | (lScore + rScore) `rem` (serviceLength gameLen) == 0 = True
  | otherwise = False

-- |The winner of a Game or Nothing if nobody has won
winner :: Playable a => Game a -> Maybe a
winner (Game leftSide rightSide lScore rScore gameLen _)
  | (lScore - rScore) > 1 && lScore >= (valueOf gameLen) = Just leftSide
  | (rScore - lScore) > 1 && rScore >= (valueOf gameLen) = Just rightSide
  | otherwise = Nothing
    where totalScore = lScore + rScore

-- |Makes the receiver become the server, rotates the serving side
-- Precondition: we have already determined it is time to change service
changeService :: Playable a => Game a -> Game a
changeService g@(Game lSide rSide lScore rScore gameLen LeftSide) =
  Game (rotateSide lSide) rSide lScore rScore gameLen RightSide
changeService g@(Game lSide rSide lScore rScore gameLen RightSide) =
  Game lSide (rotateSide rSide) lScore rScore gameLen LeftSide

-- |Exchange sides of the table (typically after a Game is won)
swapSides :: Playable a => Game a -> Game a
swapSides (Game left right lS rS gameLen side) =
  Game right left lS rS gameLen side