{-# LANGUAGE OverloadedStrings #-}

-- | MOO command parsing and execution
module MOO.Command (

  -- * Data Structures
    Command(..)

  -- * Parsing Typed Commands
  , parseWords
  , parseCommand

  -- * Executing MOO Code
  , runCommand

  ) where

import Control.Applicative ((<$>))
import Control.Monad (void, foldM)
import Data.Char (isSpace, isDigit)
import Data.Monoid (Monoid(mempty, mappend, mconcat), First(First, getFirst))
import Data.Text (Text)
import Text.Parsec (parse, try, many, many1, char, anyChar, noneOf, spaces,
                    satisfy, between, eof, (<|>))
import Text.Parsec.Text (Parser)

import qualified Data.IntSet as IS
import qualified Data.Text as T

import {-# SOURCE #-} MOO.Connection
import MOO.Object
import {-# SOURCE #-} MOO.Task
import MOO.Types
import MOO.Verb

import qualified MOO.List as Lst
import qualified MOO.String as Str

commandWord :: Parser Text
commandWord = do
  word <- many1 wordChar
  spaces
  return (T.concat word)

  where wordChar = T.singleton <$> satisfy nonspecial <|>
                   T.pack      <$> quotedChars        <|>
                   T.singleton <$> backslashChar      <|>
                   trailingBackslash

        nonspecial '\"' = False
        nonspecial '\\' = False
        nonspecial  c   = not $ isSpace c

        quotedChars     = between (char '"') quoteEnd $
                          many (noneOf "\"\\" <|> backslashChar)
        quoteEnd        = void (char '"') <|> eof <|> void trailingBackslash

        backslashChar   = try (char '\\' >> anyChar)

        trailingBackslash = try (char '\\' >> eof) >> return ""

commandWords :: Parser [Text]
commandWords = between spaces eof $ many commandWord

builtinCommand :: Parser Text
builtinCommand = say <|> emote <|> eval
  where say   = char '\"' >> return "say"
        emote = char ':'  >> return "emote"
        eval  = char ';'  >> return "eval"

command :: Parser (Text, Text)
command = between spaces eof $ do
  verb <- builtinCommand <|> commandWord <|> return ""
  argstr <- T.pack <$> many anyChar
  return (verb, argstr)

matchPrep :: [StrT] -> (StrT, (PrepSpec, StrT), StrT)
matchPrep = matchPrep' id prepPhrases
  where matchPrep' dobj _ [] = (Str.unwords $ dobj [], (PrepNone, ""), "")
        matchPrep' dobj ((spec,phrase):phrases) args
          | phrase == argsPhrase =
            (Str.unwords $ dobj [], (spec, Str.unwords argsPhrase),
             Str.unwords iobj)
          | otherwise = matchPrep' dobj phrases args
          where (argsPhrase, iobj) = splitAt (length phrase) args
        matchPrep' dobj [] (arg:args) =
          matchPrep' (dobj . (arg :)) prepPhrases args

-- | A structure describing a player's parsed command
data Command = Command {
    commandVerb     :: StrT
  , commandArgs     :: [StrT]
  , commandArgStr   :: StrT
  , commandDObjStr  :: StrT
  , commandPrepSpec :: PrepSpec
  , commandPrepStr  :: StrT
  , commandIObjStr  :: StrT
  }

-- | Split a typed command into words according to the MOO rules for quoting
-- and escaping.
parseWords :: Text -> [StrT]
parseWords argstr = map Str.fromText args
  where Right args = parse commandWords "" argstr

-- | Return a 'Command' value describing the arguments of a typed command as
-- parsed into verb, preposition, direct and indirect object, etc.
parseCommand :: Text -> Command
parseCommand cmd = Command {
    commandVerb     = Str.fromText verb
  , commandArgs     = args
  , commandArgStr   = Str.fromText argstr
  , commandDObjStr  = dobjstr
  , commandPrepSpec = prepSpec
  , commandPrepStr  = prepstr
  , commandIObjStr  = iobjstr
  }
  where Right (verb, argstr) = parse command "" cmd
        args = parseWords argstr
        (dobjstr, (prepSpec, prepstr), iobjstr) = matchPrep args

objectNumber :: Parser ObjId
objectNumber = fmap read $ between (char '#') eof $ many1 (satisfy isDigit)

matchObject :: ObjId -> StrT -> MOO ObjId
matchObject player str
  | Str.null str = return nothing
  | otherwise    = case parse objectNumber "" (Str.toText str) of
    Right oid -> do
      obj <- getObject oid
      case obj of
        Just _  -> return oid
        Nothing -> matchObject' player str
    Left _ -> matchObject' player str

  where matchObject' :: ObjId -> StrT -> MOO ObjId
        matchObject' player str = case str of
          "me"   -> return player
          "here" -> maybe nothing (objectForMaybe . objectLocation) <$>
                    getObject player
          _      -> do
            maybePlayer <- getObject player
            case maybePlayer of
              Nothing      -> return failedMatch
              Just player' -> do
                let holding   = objectContents player'
                    maybeRoom = objectLocation player'
                roomContents <-
                  maybe (return IS.empty)
                  (fmap (maybe IS.empty objectContents) . getObject)
                  maybeRoom
                matchName str $ IS.toList (holding `IS.union` roomContents)

        matchName :: StrT -> [ObjId] -> MOO ObjId
        matchName str = fmap (uncurry matchResult) .
                        foldM (matchName' str) ([], [])

        matchName' :: StrT -> ([ObjId], [ObjId]) -> ObjId ->
                      MOO ([ObjId], [ObjId])
        matchName' str matches@(exact, prefix) oid = do
          maybeAliases <- readProperty oid "aliases"
          maybeObj <- getObject oid
          let aliases = case maybeAliases of
                Just (Lst v) -> Lst.toList v
                _            -> []
              names = maybe id ((:) . Str . objectName) maybeObj aliases
          return $ case searchNames str names of
            ExactMatch  -> (oid : exact, prefix)
            PrefixMatch -> (exact, oid : prefix)
            NoMatch     -> matches

        matchResult :: [ObjId] -> [ObjId] -> ObjId
        matchResult [oid] _     = oid
        matchResult []    [oid] = oid
        matchResult []    []    = failedMatch
        matchResult _     _     = ambiguousMatch

        searchNames :: StrT -> [Value] -> Match
        searchNames str = mconcat . map (nameMatch str)

        nameMatch :: StrT -> Value -> Match
        nameMatch str (Str name)
          | str ==               name = ExactMatch
          | str `Str.isPrefixOf` name = PrefixMatch
        nameMatch _ _                 = NoMatch

data Match = NoMatch | PrefixMatch | ExactMatch

instance Monoid Match where
  mempty = NoMatch

  NoMatch `mappend` match      = match
  _       `mappend` ExactMatch = ExactMatch
  match   `mappend` _          = match

-- | Execute a typed command by locating and calling an appropriate MOO verb
-- for the current player, matching @dobj@ and @iobj@ objects against the
-- strings in the typed command.
runCommand :: Command -> MOO Value
runCommand Command { commandVerb = "" } = return zero
runCommand command = do
  player <- getPlayer
  dobj <- matchObject player (commandDObjStr command)
  iobj <- matchObject player (commandIObjStr command)

  room <- objectForMaybe . (objectLocation =<<) <$> getObject player
  maybeVerb <- getFirst . mconcat . map First <$>
               mapM (locateVerb dobj iobj) [player, room, dobj, iobj]
  case maybeVerb of
    Just (this, spec) -> callCommandVerb player spec this command dobj iobj
    Nothing -> findVerb verbPermX "huh" room >>= \found -> case found of
      (Just verbLoc, Just verb) ->
        callCommandVerb player (verbLoc, verb) room command dobj iobj
      _ -> notify player "I couldn't understand that." >> return zero

  where locateVerb :: ObjId -> ObjId -> ObjId ->
                      MOO (Maybe (ObjId, (ObjId, Verb)))
        locateVerb dobj iobj this =
          let acceptable verb =
                objMatch this (verbDirectObject   verb) dobj &&
                objMatch this (verbIndirectObject verb) iobj &&
                prepMatch (verbPreposition verb) (commandPrepSpec command)
          in findVerb acceptable (commandVerb command) this >>= \found ->
          case found of
            (Just verbLoc, Just verb) -> return $ Just (this, (verbLoc, verb))
            _                         -> return Nothing