{-# LANGUAGE RankNTypes, FlexibleContexts, ConstraintKinds #-}

{-
  This module is part of Antisplice.
  Copyleft (c) 2014 Marvin Cohrs

  All wrongs reversed. Sharing is an act of love, not crime.
  Please share Antisplice with everyone you like.

  Antisplice is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  Antisplice is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License
  along with Antisplice. If not, see <http://www.gnu.org/licenses/>.
-}

-- | A huge pile of utility functions for building our dungeon.
module Game.Antisplice.Rooms (
    -- * Room modification
    modifyRoomState,
    getRoomDesc,
    getRoomTitle,
    setRoomTitle,
    markRoom,
    -- * Moving around
    enterRoom,
    reenterCurrentRoom,
    enterAndAnnounce,
    changeRoom,
    -- * Room construction
    constructRoom,
    establishWay,
    addRoomObject,
    removeRoomObject,
    insertRoomObject,
    -- * Object construction
    constructObject,
    modifyObjectState,
    setObjectDesc,
    setObjectTitle,
    addObjectName,
    addObjectAttr,
    setObjectIsMob,
    setObjectIsAcquirable,
    addFeature,
    addDescSeg,
    addEquipSlot,
    -- * Object forms
    registerForm,
    instanciateForm,
    registerKind,
    setObjectKind,
    -- * Object investigation
    getObjectTitle,
    getObjectDesc,
    getObjectNames,
    matchObjectName,
    getObjectIsMob,
    getObjectIsAcquirable,
    roomOfObject,
    -- * Object actions
    setMobRoute,
    continueMobRoute,
    -- * Scheduling
    schedule,
    -- * Players
    subscribePlayer,
    setPlayerRoom,
    acquireObject,
    dropObject,
    equipObject,
    equipObjectAt,
    getEquipment,
    getCooldown,
    setCooldown,
    consumeAlcohol,
    -- * Currencies
    registerCurrency,
    getCurrency,
    modifyCurrency,
    -- * Fight
    damage,
    focusOpponent,
    dealDamage,
    -- * Masquerades
    withRoom,
    withPlayer,
    withObject,
    -- * Guardians
    guardRoom,
    guardObject,
    guardObjectInRoom,
    guardObjectNotInRoom,
    guardKindInRoom,
    guardKindNotInRoom,
    -- * In-/Output
    drunken
  ) where

import Text.Chatty.Printer
import Text.Chatty.Scanner
import Text.Chatty.Expansion
import System.Chatty.Misc
import Game.Antisplice.Monad.Dungeon
import Game.Antisplice.Monad
import Data.Chatty.Graph
import Data.Chatty.Counter
import Data.Chatty.BST
import Data.Chatty.AVL
import Data.Chatty.Atoms
import Data.Chatty.None
import Game.Antisplice.Errors
import Control.Arrow
import Control.Monad
import Control.Monad.Error
import Control.Monad.Trans.Class
import Data.Text (pack,unpack)
import Data.Maybe
import Data.List
import Data.Char
import Data.Time.Clock
import Text.Printf

-- | Modify the room state.
modifyRoomState :: MonadRoom m => (RoomState -> RoomState) -> m ()
modifyRoomState f = do
  s <- getRoomState
  putRoomState (f s)

-- | Get the current room's description
getRoomDesc :: (MonadRoom m,ChAtoms m) => m String
getRoomDesc = do
  s <- getRoomState
  --return (fromText $ roomDescOf s)
  let descseg (Described a) = getAtom a
      descseg _ = return none
  is <- liftM (filter $ not.null) $ mapM descseg $ concatMap (avlInorder.objectFeaturesOf) $ avlInorder $ roomObjectsOf s
  return $ concat $ intersperse " " is

-- | Get the current room's title
getRoomTitle :: (MonadRoom m,IsText t) => m t
getRoomTitle = do
  s <- getRoomState
  return (fromText $ roomTitleOf s)

{- | Set the current room's description
setRoomDesc :: (MonadRoom m,IsText t) => t -> m ()
setRoomDesc t = modifyRoomState $ \s -> s{roomDescOf=toText t}-}

-- | Set the current room's title
setRoomTitle :: (MonadRoom m,IsText t) => t -> m ()
setRoomTitle t = modifyRoomState $ \s -> s{roomTitleOf=toText t}

-- | Mark the current room as visited
markRoom :: MonadDungeon m => m ()
markRoom = do
  s <- getDungeonState
  case currentRoomOf s of
    Just r -> putDungeonState s{roomsOf=markNode r $ roomsOf s}

-- | Enter the given room and trigger most events, but don't announce it. Result tells whether this room is visited the first time.
enterRoom :: NodeId -> ChattyDungeonM Bool
enterRoom n = do
  rs0 <- getRoomState
  roomTriggerOnLeaveOf rs0
  s <- getDungeonState
  modifyPlayerState $ \p -> p{playerRoomOf=n,playerOpponentOf=FalseObject}
  let marked = nodeMarked $ getNode' n $ roomsOf s
  rs <- getRoomState
  unless marked $ do
    markRoom
    roomTriggerOnFirstEnterOf rs
  roomTriggerOnEachEnterOf rs
  sequence_ $ avlInorder $ flip fmap (roomObjectsOf rs) $ \os -> do
    unless (objectOnceSeenOf os) $ objectTriggerOnFirstSightOf os
    objectTriggerOnEachSightOf os
  putRoomState rs{roomObjectsOf=fmap (\os -> os{objectOnceSeenOf=True}) $ roomObjectsOf rs}
  return $ not marked

-- | Reenter the current room and trigger all events (but don't announce it).
reenterCurrentRoom :: ChattyDungeonM ()
reenterCurrentRoom = do
  s <- getDungeonState
  case currentRoomOf s of
    Just r -> void $ enterRoom r

-- | Enter the given room, trigger all events and announce it. On the first visit, look around.
enterAndAnnounce :: NodeId -> ChattyDungeonM ()
enterAndAnnounce n = do
  r <- enterRoom n
  rs <- getRoomState
  roomTriggerOnAnnounceOf rs
  --when r $
  id $
    roomTriggerOnLookOf rs

-- | Construct a new room using the room monad.
constructRoom :: (MonadDungeon m) => RoomT m a -> m NodeId
constructRoom m = do
  (_,rs) <- runRoomT m $ RoomState none none noneM noneM noneM noneM noneM
  s <- getDungeonState
  let (nid,g) = addNode' rs $ roomsOf s
  putDungeonState s{roomsOf=g}
  when (isNothing $ currentRoomOf s) $ modifyPlayerState $ \p -> p{playerRoomOf=nid}
  return nid

-- | Run a function in the context of the given room.
withRoom :: MonadDungeon m => NodeId -> RoomT m a -> m a
withRoom n m = do
  s <- getDungeonState
  let rs = getNode n $ roomsOf s
  (a,rs') <- runRoomT m rs
  putDungeonState s{roomsOf=setNode n rs' $ roomsOf s}
  return a

-- | Establish a path from one room to another one (one-way only).
establishWay :: MonadDungeon m => NodeId -> NodeId -> Direction -> PathState -> m ()
establishWay f t d c = do
  s <- getDungeonState
  let g = addEdge f t 0 d c $ roomsOf s
  putDungeonState s{roomsOf=g}

-- | Enter a neighbouring room by its direction.
changeRoom :: Direction -> ChattyDungeonM ()
changeRoom d = do
  s <- getDungeonState
  case currentRoomOf s of
    Just r -> case queryEdge r d (roomsOf s) of
                Just c -> do
                  b <- pathPrerequisiteOf c
                  unless b $ throwError CantWalkThereError
                  case followEdge r d (roomsOf s) of
                    Just n -> do
                      pathTriggerBeforeWalkOf c
                      enterAndAnnounce n
                      pathTriggerAfterWalkOf c
                Nothing -> throwError CantWalkThereError

-- | Add a new object to the current room. It is contructed using the object monad.
addRoomObject :: (ChCounter m,MonadRoom m) => ObjectT m a -> m ObjectId
addRoomObject m = do
  i <- liftM ObjectId countOn
  k <- liftM KindId countOn
  o <- constructObject m (Just i) k
  insertRoomObject o
  return i

-- | Construct a room object (but don't add it)
constructObject :: Monad m => ObjectT m a -> Maybe ObjectId -> KindId -> m ObjectState
constructObject m j k = do
  (_,o) <- runObjectT m $ ObjectState
       (if isJust j then (\(Just j) -> j) j else none) -- id
       k -- kid
       (pack "Something") -- title
       (pack "I don't know what this is.") -- desc
       none none False False False False -- names, attr, 1seen?, 1acq?, 1insp?, 1eq?
       100 100 -- maxhp, curhp
       none none -- route, feat
       none -- faction
       noneM -- on 1 sight
       noneM -- on sight
       noneM -- on 1 acq
       noneM -- on acq
       noneM -- on 1 insp
       noneM -- on insp
       (mprintLn "There is nothing special about this.") -- on look at
       (mprintLn "There is nothing inside this.") -- on look in
       (mprintLn "There is nothing written on this.") -- on read
       (mprintLn "You cannot enter this.") -- on enter
       noneM -- on enter room
       noneM -- on leave room
       noneM -- on announce
       noneM -- on drop
       noneM -- on 1 eq
       noneM -- on eq
       noneM -- on uneq
       noneM -- on die
       noneM -- on take damage
       noneM -- on use
       noneM -- on eat
       noneM -- on drink
  return o

-- | Remove an object from the current room and return its state.
removeRoomObject :: MonadRoom m => ObjectId -> m ObjectState
removeRoomObject i = do
  s <- getRoomState
  let Just o = avlLookup i $ roomObjectsOf s
  putRoomState s{roomObjectsOf=avlRemove i $ roomObjectsOf s}
  return o

-- | Insert an already constructed object to the current room.
insertRoomObject :: MonadRoom m => ObjectState -> m ()
insertRoomObject o = modifyRoomState $ \s -> s{roomObjectsOf=avlInsert o $ roomObjectsOf s}

-- | Modify the object state
modifyObjectState :: MonadObject m => (ObjectState -> ObjectState) -> m ()
modifyObjectState f = do
  s <- getObjectState
  putObjectState $ f s

-- | Get the object's title
getObjectTitle :: (MonadObject m,IsText t) => m t
getObjectTitle = do
  s <- getObjectState
  return (fromText $ objectTitleOf s)

-- | Set the object's title
setObjectTitle :: (MonadObject m,IsText t) => t -> m ()
setObjectTitle t = modifyObjectState $ \s -> s{objectTitleOf=toText t}

-- | Get the object's description
getObjectDesc :: (MonadObject m,IsText t) => m t
getObjectDesc = do
  s <- getObjectState
  return (fromText $ objectDescOf s)

-- | Set the object's description
setObjectDesc :: (MonadObject m,IsText t) => t -> m ()
setObjectDesc t = modifyObjectState $ \s -> s{objectDescOf=toText t}

-- | Get the object's names
getObjectNames :: MonadObject m => m [String]
getObjectNames = do
  s <- getObjectState
  return $ objectNamesOf s

-- | Add a name for the current object
addObjectName :: MonadObject m => String -> m ()
addObjectName t = modifyObjectState $ \s -> s{objectNamesOf=t:objectNamesOf s}

-- | Check if the given name matches our current object
matchObjectName :: MonadObject m => String -> m Bool
matchObjectName t = do
  s <- getObjectState
  return $ elem t $ objectNamesOf s

-- | Add an attribute for the current object
addObjectAttr :: MonadObject m => String -> m ()
addObjectAttr t = modifyObjectState $ \s -> s{objectAttributesOf=t:objectAttributesOf s}

-- | Create a new player using the player monad
subscribePlayer :: (MonadDungeon m,ChCounter m) => PlayerT m a -> m ()
subscribePlayer m = do
  s <- getDungeonState
  i <- liftM PlayerId countOn
  let r = rootNode $ roomsOf s
  (_,a) <- runPlayerT m $ PlayerState i r 100 none none none none none (avlInsert (Health,100) none) none none none 0 False
  putDungeonState s{playersOf=anyBstInsert a $ playersOf s}

-- | Move the current player to the given room, but don't trigger anything.
setPlayerRoom :: MonadPlayer m => NodeId -> m ()
setPlayerRoom r = modifyPlayerState $ \p -> p{playerRoomOf=r}

-- | Check if the current object is a mob.
getObjectIsMob :: (MonadObject m,Functor m) => m Bool
getObjectIsMob = fmap (isJust . avlLookup Mobile . objectFeaturesOf) getObjectState

-- | Set whether the current object is a mob.
setObjectIsMob :: MonadObject m => Bool -> m ()
setObjectIsMob b = modifyObjectState $ \o -> o{objectFeaturesOf=a b Mobile $ objectFeaturesOf o}
  where a True = avlInsert
        a False = avlRemove

-- | Set object kind.
setObjectKind :: MonadObject m => KindId -> m ()
setObjectKind k = modifyObjectState $ \o -> o{objectKindOf=k}

-- | Register an object kind.
registerKind :: ChCounter m => m KindId
registerKind = liftM KindId countOn

-- | Check if the current object is acquirable.
getObjectIsAcquirable :: (MonadObject m,Functor m) => m Bool
getObjectIsAcquirable = fmap (isJust . avlLookup Acquirable . objectFeaturesOf) getObjectState

-- | Set whether the current object is acquirable.
setObjectIsAcquirable :: MonadObject m => Bool -> m ()
setObjectIsAcquirable b = modifyObjectState $ \o -> o{objectFeaturesOf=a b $ objectFeaturesOf o}
  where a True = avlInsert Acquirable
        a False = avlRemove Acquirable

-- | Schedule an event for a given time offset (in milliseconds).
schedule :: (MonadDungeon m,ChClock m) => Integer -> Handler -> m ()
schedule ms t = do
  now <- mgetstamp
  let t' = now + (realToFrac ms / 1000)
  s <- getDungeonState
  putDungeonState s{timeTriggersOf=avlInsert (t',Handler t) $ timeTriggersOf s}

-- | Set the current mob's route
setMobRoute :: MonadObject m => [NodeId] -> m ()
setMobRoute rs = modifyObjectState $ \s -> s{objectRouteOf=rs}

-- | The given object may continue its route
continueMobRoute :: ObjectId -> Handler
continueMobRoute i = do
  rs <- roomOfObject i
  case rs of
    [r] -> do
      o <- withRoom r $ removeRoomObject i
      guardRoom r $ objectTriggerOnRoomLeaveOf o
      let (n:rs) = objectRouteOf o
      withRoom n $ insertRoomObject o{objectRouteOf=rs++[n]}
      guardRoom n $ objectTriggerOnRoomEnterOf o
      modifyPlayerState $ \s -> if playerOpponentOf s == i then s{playerOpponentOf=FalseObject} else s
      return ()

-- | Only run the given function if the player is inside the also given room.
guardRoom :: MonadDungeon m => NodeId -> m () -> m ()
guardRoom n m = do
  s <- getDungeonState
  when (currentRoomOf s == Just n) m

-- | Only run the given function if the given object exists
guardObject :: MonadDungeon m => ObjectId -> m () -> m ()
guardObject o m = do
  rs <- roomOfObject o
  case rs of
    [] -> return ()
    _ -> m

-- | Only run the given function if the given object is in the also given room
guardObjectInRoom :: MonadDungeon m => ObjectId -> NodeId -> m () -> m ()
guardObjectInRoom o r m = do
  rs <- roomOfObject o
  case rs of
    [r1] | r1 == r -> m
    _ -> return ()

-- | Only run the given function if the given object is not in the also given room
guardObjectNotInRoom :: MonadDungeon m => ObjectId -> NodeId -> m () -> m ()
guardObjectNotInRoom o r m = do
  rs <- roomOfObject o
  case rs of
    [r1] | r1 == r -> return ()
    _ -> m

-- | Only run the given function if an object of the given kind is in the also given room
guardKindInRoom :: MonadDungeon m => KindId -> NodeId -> m () -> m ()
guardKindInRoom k r m = do
  rs <- withRoom r getRoomState
  let b = null $ filter ((==k).objectKindOf) $ avlPreorder $ roomObjectsOf rs
  unless b m

-- | Only run the given function if no object of the given kind is in the also given room
guardKindNotInRoom :: MonadDungeon m => KindId -> NodeId -> m () -> m ()
guardKindNotInRoom k r m = do
  rs <- withRoom r getRoomState
  let b = null $ filter ((==k).objectKindOf) $ avlPreorder $ roomObjectsOf rs
  when b m

-- | Determine which rooms contain the given object (won't be more than one, but we're careful)
roomOfObject :: MonadDungeon m => ObjectId -> m [NodeId]
roomOfObject o = (return . map nodeId . filter (isJust . avlLookup o . roomObjectsOf . nodeContent) . allNodes . roomsOf) =<< getDungeonState

-- | Acquire the given object and put it in the player's inventory
acquireObject :: ObjectId -> ChattyDungeonM ()
acquireObject i = do
  rs <- roomOfObject i
  case rs of
    [r] -> do
      s <- withObject i getObjectState
      if isJust $ avlLookup Acquirable $ objectFeaturesOf s
        then do
          o <- withRoom r $ removeRoomObject i
          modifyPlayerState $ \s -> s{playerInventoryOf=avlInsert o $ playerInventoryOf s}
          objectTriggerOnEachAcquireOf o
        else throwError CantAcquireThatError
    [] -> throwError CantSeeOneError
    _ -> throwError WhichOneError

-- | Drop the given object and remove it from the player's inventory
dropObject :: ObjectId -> ChattyDungeonM ()
dropObject i = do
  Just r <- (return . currentRoomOf) =<< getDungeonState
  ps <- getPlayerState
  let Just o = avlLookup i $ playerInventoryOf ps
  putPlayerState ps{playerInventoryOf=avlRemove i $ playerInventoryOf ps}
  insertRoomObject o

-- | Add an object feature to the current object
addFeature :: MonadObject m => Feature -> m ()
addFeature f = modifyObjectState $ \s -> s{objectFeaturesOf=avlInsert f $ objectFeaturesOf s}

-- | Add a room description segment to the current object
addDescSeg :: (MonadObject m,ChAtoms m) => String -> m ()
addDescSeg s = do
  a <- newAtom
  putAtom a s
  addFeature $ Described a

-- | Register an object form and return its atom
registerForm :: (ChAtoms m) => ObjectT m () -> m (Atom ObjectState)
registerForm m = do
  a <- newAtom
  k <- liftM KindId countOn
  o <- constructObject m Nothing k
  putAtom a o
  return a

-- | Instanciate a registered form
instanciateForm :: (ChAtoms m,MonadRoom m) => Atom ObjectState -> m ObjectId
instanciateForm a = do
  i <- countOn
  o <- getAtom a
  insertRoomObject o{objectIdOf=ObjectId i}
  return $ ObjectId i

-- | Equip the given object at a given key
equipObjectAt :: (MonadPlayer m,MonadError SplErr m) => EquipKey -> ObjectState -> m (Maybe ObjectState)
equipObjectAt k o
  | isJust (avlLookup (Equipable k) $ objectFeaturesOf o) = do
      p <- getPlayerState
      let o1 = avlLookup k $ playerEquipOf p
      putPlayerState p{playerEquipOf=avlInsert (k,o) $ playerEquipOf p, playerInventoryOf=avlRemove (indexOf o) $ playerInventoryOf p}
      return o1
  | otherwise = throwError CantEquipThatThereError

-- | Equip the given object somewhere
equipObject :: (MonadPlayer m,MonadError SplErr m) => ObjectState -> m (Maybe ObjectState)
equipObject o =
  let equ (Equipable k) = [k]
      equ _ = none
  in case concatMap equ $ avlInorder $ objectFeaturesOf o of
    [] -> throwError CantEquipThatError
    [k] -> equipObjectAt k o
    ks -> do
      p <- getPlayerState
      case filter (isJust.flip avlLookup (playerEquipOf p)) ks of
        [] -> throwError WhereToEquipError
        [k] -> equipObjectAt k o
        _ -> throwError WhereToEquipError
    
-- | Get equipped object
getEquipment :: MonadPlayer m => EquipKey -> m (Maybe ObjectState)
getEquipment k = liftM (avlLookup k.playerEquipOf) getPlayerState

-- | Add equippable slot
addEquipSlot :: MonadObject m => EquipKey -> m ()
addEquipSlot = addFeature . Equipable

-- | Set/unset cooldown
setCooldown :: MonadPlayer m => CooldownId -> Bool -> m ()
setCooldown c b = modifyPlayerState $ \p -> p{playerCooldownsOf=(if b then avlInsert else avlRemove) c $ playerCooldownsOf p}

-- | Get cooldown state
getCooldown :: MonadPlayer m => CooldownId -> m Bool
getCooldown c = liftM (isJust . avlLookup c . playerCooldownsOf) getPlayerState

-- | Get currency state
getCurrency :: MonadPlayer m => CurrencyId -> m Int
getCurrency c = liftM (joinMaybe . avlLookup c . playerCurrenciesOf) getPlayerState

-- | Modify currency state
modifyCurrency :: MonadPlayer m => CurrencyId -> (Int -> Int) -> m ()
modifyCurrency c f = modifyPlayerState $ \p ->
  let c1 = joinMaybe $ avlLookup c $ playerCurrenciesOf p
  in p{playerCurrenciesOf=avlInsert (c,f c1) $ playerCurrenciesOf p}

-- | Register a currency
registerCurrency :: (ChCounter m,MonadDungeon m) => String -> String -> m CurrencyId
registerCurrency n d = do
  s <- getDungeonState
  i <- liftM CurrencyId countOn
  putDungeonState s{currenciesOf=avlInsert (Currency i d n) $ currenciesOf s}
  return i

-- | Run a function in the context of a given player
withPlayer :: MonadDungeon m => PlayerId -> PlayerT (RoomT m) a -> m a
withPlayer i m = do
  d <- getDungeonState
  case anyBstLookup i $ playersOf d of
    Just p -> do
      (a,p') <- withRoom (playerRoomOf p) $ runPlayerT m p
      d <- getDungeonState
      putDungeonState d{playersOf=anyBstInsert p' $ playersOf d}
      return a

-- | Run a function in the context of a given object
withObject :: MonadDungeon m => ObjectId -> ObjectT (RoomT m) a -> m a
withObject i m = do
  rs <- roomOfObject i
  ps <- getPlayerState
  case rs of
      [] -> case avlLookup i $ playerInventoryOf ps of
              Nothing -> error "Wtf, that object does not exist. Neither in a room nor in your inventory."
              Just o -> withRoom (playerRoomOf ps) $ do
                (a,o') <- runObjectT m o
                lift $ modifyPlayerState $ \p -> p{playerInventoryOf=avlInsert o' $ playerInventoryOf p}
                return a
      [r] -> withRoom r $ do
        rs <- getRoomState
        case anyBstLookup i $ roomObjectsOf rs of
          Just o -> do
            (a,o') <- runObjectT m o
            modifyRoomState $ \r -> r{roomObjectsOf=anyBstInsert o' $ roomObjectsOf r}
            return a

-- | Damage a target (no matter whether player or mob) without setting focus
damage :: DamageTarget -> Int -> ChattyDungeonM ()
damage (TargetPlayer p) v = withPlayer p $ modifyCurrency Health $ subtract v
damage (TargetObject o) v = do
  d <- withObject o $ do
    modifyObjectState $ \o -> o{objectCurHealthOf=objectCurHealthOf o - v}
    liftM ((<=0).objectCurHealthOf) getObjectState
  when d $ do
    objectTriggerOnDieOf =<< withObject o getObjectState
    s <- getPlayerState
    when (playerOpponentOf s == o) $ putPlayerState s{playerOpponentOf=FalseObject}
    void $ removeRoomObject o

-- | Focus an opponent
focusOpponent :: ObjectId -> ChattyDungeonM ()
focusOpponent o = do
  os <- withObject o getObjectState
  if (isJust $ avlLookup Damagable $ objectFeaturesOf os)
     then modifyPlayerState $ \p -> p{playerOpponentOf=o}
     else throwError WontHitThatError

-- | Deal damage to an opponent. Real damage is influenced by random.
dealDamage :: Int -> ChattyDungeonM ()
dealDamage d = do
  let dmin = truncate (fromIntegral d * 0.8)
      dmax = truncate (fromIntegral d * 1.2)
  r <- mrandomR (dmin,dmax)
  o <- liftM playerOpponentOf getPlayerState
  damage (TargetObject o) r
  
instance (Functor m,ChExpand m) => ChExpand (DungeonT m) where
  expand = lift . expand <=< liftM (replay.snd) . runRecorderT . expandDun

expandDun :: (ChPrinter m,MonadDungeon m) => String -> m ()
expandDun [] = return ()
expandDun ('\\':'#':ss) = do
  mprint "#"
  expandDun ss
expandDun ('#':'?':'{':ss) =
  let nm = takeBrace 0 ss
      rm = drop (length nm + 1) ss
      takeBrace 0 ('}':ss) = ""
      takeBrace n ('}':ss) = '}' : takeBrace (n-1) ss
      takeBrace n ('{':ss) = '{' : takeBrace (n+1) ss
      takeBrace n (s:ss) = s : takeBrace n ss
  in do
    o <- liftM playerOpponentOf getPlayerState
    case o of
      FalseObject -> expandDun rm
      _ -> do
        expandDun nm
        expandDun rm
expandDun ('#':'{':ss) =
  let (nm,rm) = (takeWhile (/='}') &&& tail.dropWhile (/='}')) ss
      prnum Nothing Nothing i = show i
      prnum (Just d) Nothing i = show (i `div` d)
      prnum Nothing (Just m) i = let exp s l = replicate (l-length s) '0' ++ s in exp (show (i `mod` m)) (length $ show (m-1))
      prnum (Just d) (Just m) i = prnum (Just d) Nothing i ++ "." ++ prnum Nothing (Just m) i
      prstr Nothing Nothing s = s
      prstr (Just d) _ s = take d s
      prstr Nothing (Just m) s = drop m s
      numfun (Just "inc") i = i + 1
      numfun (Just "dec") i = i - 1
      numfun Nothing i = i
      numfun _ _ = 0
      strfun (Just "up") s = map toUpper s
      strfun (Just "down") s = map toLower s
      strfun (Just "capital") "" = ""
      strfun (Just "capital") (s:ss) = toUpper s : ss
      strfun Nothing s = s
      strfun _ _ = ""
      replace :: (ChPrinter m,MonadDungeon m) => String -> Maybe Int -> Maybe Int -> Maybe String -> m ()
      replace "health" d m f = liftM (prnum d m . numfun f . joinMaybe . avlLookup Health . playerCurrenciesOf) getPlayerState >>= mprint
      replace "ohealth" d m f = do
        i <- liftM playerOpponentOf getPlayerState
        o <- withObject i getObjectState
        mprint $ prnum d m $ numfun f $ objectCurHealthOf o
      replace "otitle" d m f = do
        i <- liftM playerOpponentOf getPlayerState
        o <- withObject i getObjectState
        mprint $ prstr d m $ strfun f $ unpack $ objectTitleOf o
      replace s d m f = do
        cs <- liftM (avlInorder . currenciesOf) getDungeonState
        case filter ((==s).currencyNameOf) cs of
          [] -> return ()
          [c] -> liftM (prnum d m . numfun f . joinMaybe . avlLookup (currencyIdOf c) . playerCurrenciesOf) getPlayerState >>= mprint
          cs -> mprintLn $ concat $ map currencyNameOf cs
      (cm,nmc)
        | '!' `elem` nm = (Just $ takeWhile (/='!') nm, tail $ dropWhile (/='!') nm)
        | otherwise = (Nothing, nm)
      (nm1,dm1,mm1)
        | '/' `elem` nmc = (takeWhile (/='/') nmc, Just $ read $ tail $ dropWhile (/='/') nmc, Nothing)
        | '%' `elem` nmc = (takeWhile (/='%') nmc, Nothing, Just $ read $ tail $ dropWhile (/='%') nmc)
        | '.' `elem` nmc = let xz = Just $ read $ tail $ dropWhile (/='.') nmc in (takeWhile (/='.') nmc, xz, xz)
        | otherwise = (nmc,Nothing,Nothing)
  in do
    replace nm1 dm1 mm1 cm
    expandDun rm
expandDun ('#':ss) =
  let (nm,rm) = (takeWhile isAnum &&& dropWhile isAnum) ss
      isAnum = flip elem (['A'..'Z']++['a'..'z']++['0'..'9'])
  in expandDun ("#{"++nm++"}"++rm)
expandDun (s:ss) = do
  mprint [s]
  expandDun ss

-- | Consider the player's alcohol rate and mask the string with random underscores.
drunken :: (ChRandom m,MonadPlayer m) => String -> m String
drunken [] = return []
drunken ('}':cs) = do
  cx <- drunken cs
  return ('}':cx)
drunken ('\n':cs) = do
  cx <- drunken cs
  return ('\n':cx)
drunken ('%':'{':a:b:';':cs)
  | (a `elem` "DV") && (b `elem` ['0'..'7']) = do
    cx <- drunken cs
    return ('%':'{':a:b:';':cx)
drunken (c:cs) = do
  p <- liftM playerAlcoholOf getPlayerState
  r <- mrandomR (0,100)
  cx <- drunken cs
  return $ if r < p then '_' : cx else c : cx

-- | Consume alcohol.
consumeAlcohol :: (ChPrinter m,ChRandom m,MonadPlayer m,MonadDungeon m,ChClock m) => Int -> m ()
consumeAlcohol r = do
  modifyPlayerState $ \p -> p{playerAlcoholOf=r+playerAlcoholOf p}
  p <- getPlayerState
  case playerAlcoholOf p of
    a | a > 50 -> mprintLn =<< drunken "You really feel very, very drunk."
      | a > 30 -> mprintLn =<< drunken "You definitely feel drunk."
      | a > 15 -> mprintLn =<< drunken "You feel kind of drunk."
    _ -> return ()
  unless (playerSoberingActiveOf p) $ do
    modifyPlayerState $ \p -> p{playerSoberingActiveOf=True}
    let sobering :: Handler
        sobering = do
          modifyPlayerState $ \p -> p{playerAlcoholOf=max 0 (playerAlcoholOf p - 5)}
          schedule 120000 sobering
    schedule 120000 sobering