-- ----------------------------------------------------------------------------

{- |
  Module     : Holumbus.Console.ServerConsole
  Copyright  : Copyright (C) 2009 Sebastian Reese
  License    : MIT

  Maintainer : Sebastian Reese (str@holumbus.org)
  Stability  : experimental
  Portability: portable
  Version    : 0.1

  This module provides a tiny and nice implementation of a little command 
  shell with communcation over a socket.
  
  It is basically a copy of Holumbus.Console.Console with some changes to fit network communication.

-}

-- ----------------------------------------------------------------------------
module Holumbus.Console.ServerConsole
    (
     -- * Console datatype
     ConsoleData
    
     -- * Operations
    , nextOption
    , parseOption
    , initializeConsole
    , addConsoleCommand
    , startServerConsole
    , defaultaction
    , defaultconverter
    )
where

import           Holumbus.Network.DoWithServer
import           System.IO
import           Control.Monad (forM)
import qualified Data.Map as Map
import           Holumbus.Common.Utils  ( handleAll )
import           Data.List

-- | Starts the server listening
startServerConsole ::
     ConsoleData a -- ^ the console data
  -> a             -- ^ the console config
  -> Int           -- ^ console port
  -> String        -- ^ a consoles prompt
  -> IO ()
startServerConsole cdata conf port prompt = doWithServer port (defaultaction cdata conf prompt) defaultconverter prompt

-- | This defaultimplementaion can be used if a simple INput -> Process command -> output patern is used
defaultaction :: ConsoleData a -> a -> String -> ServerAction String
defaultaction cdata conf prompt line sender clients = do
  clients' <-  handleAll (\_ -> return  $ [delete sender $ clients]) $ do
    forM (filter (==sender) clients) $
      \(Client _ handle _ _) -> do 
        result <- handleInput line cdata conf
        if result == exitString then do 
            hClose handle
            return . delete sender $ clients
          else do   
            hPutStrLn handle result
            hPutStr handle prompt
            hFlush handle
            return clients
  return . concat $ clients'

-- | default string to a converter. Converts the input lines into desired format. Here String
defaultconverter :: LineConverter String
defaultconverter = id


-- ----------------------------------------------------------------------------
-- datatypes
-- ----------------------------------------------------------------------------


-- | Map which contains all commands that the user can execute
type ConsoleData a = Map.Map String (ConsoleCommand a)


-- | Console command, only a pair of a function which will be executed 
--   and a description 
type ConsoleCommand a = ( Maybe (ConsoleFunction a), String )


-- | A console function. The string list represents the arguments
type ConsoleFunction a = (a -> [String] -> IO String )

-- ----------------------------------------------------------------------------
-- operations 
-- ----------------------------------------------------------------------------


-- | Creates a new console datatype
initializeConsole :: ConsoleData a
initializeConsole = Map.fromList [(exitString, exitCommand), (helpString, helpCommand)]


-- | Adds a new console command to the function, an existing command with the
--   same name will be overwritten
addConsoleCommand 
  :: String             -- ^ command string (the word the user has to enter when he wants to execute the command)
  -> ConsoleFunction a  -- ^ the function which should be executed
  -> String             -- ^ the function description
  -> ConsoleData a      -- ^ the old console data
  -> ConsoleData a
addConsoleCommand c f d m = Map.insert c (Just f, d) m


-- | The exit function string.
exitString  :: String
exitString = "exit"


-- | A dummy exit function (Just to print the help description, the command 
--   is handled in the main loop.
exitCommand :: ConsoleCommand a
exitCommand = ( Nothing, "exit the console")


-- | The help function string.
helpString :: String
helpString = "help"


-- | A dummy help function (Just to print the help description, the command 
--   is handled in the main loop.
helpCommand :: ConsoleCommand a
helpCommand = (Nothing, "print this help")


-- | gets the next option from the command line as string
nextOption :: [String] -> IO (Maybe String, [String])
nextOption o
  = handleAll (\_ -> return (Nothing, o)) $
      do
      if ( null o ) then
          return (Nothing, o)
        else 
          return (Just $ head o, tail o) 

-- | Simple "parser" for the commandline...
parseOption :: Read a => [String] -> IO (Maybe a, [String])
parseOption o
  = handleAll (\_ -> return (Nothing, o)) $
      do
      if ( null o ) then
          return (Nothing, o)
        else 
          return (Just $ read $ head o, tail o) 

-- | The main loop. You know... read stdin, parse the input, execute command.
--   You can quit it by the exit-command.
handleInput :: String -> ConsoleData a -> a -> IO String
handleInput line cdata conf
  = do
    input <- return (words line)
    cmd   <- return (command input)
    args  <- return (arguments input)
    if (cmd == exitString) 
      then do
        return exitString
      else do
        if (not $ null cmd) then handleCommand cdata conf cmd args else return "undefined"
    where
      command s = if (not $ null s) then head s else ""
      arguments s = tail s


-- | Picks the command an execute the command function.
handleCommand :: ConsoleData a -> a -> String -> [String] -> IO String
handleCommand cdata conf cmd args
  = do
    if (cmd == helpString)
      then do
        printHelp cdata
      else do
        handleCommand' (Map.lookup cmd cdata)
    where
        handleCommand' Nothing              = do printError
        handleCommand' (Just (Nothing, _ )) = do printNoHandler
        handleCommand' (Just (Just f, _ ))  = do f conf args


-- | Is executed when the function has no handler
printNoHandler :: IO String
printNoHandler = return "no function handler found"


-- | Prints the "command-not-found" message.
printError :: IO String
printError = return "unknown command, try help for a list of available commands"


-- | Prints the help text.
printHelp :: ConsoleData a -> IO String
printHelp cdata = return $ "available Commands:\n"++printCommands "" (Map.toAscList cdata)
    where
      printCommands acc [] = acc 
      printCommands acc (x:xs) = printCommands (acc ++ "\n" ++ printCommand x) xs
      printCommand (c, (_, t)) = (prettyCommandName 15 c) ++ " -  " ++ t


-- | Does some pretty printing for the function descriptions
prettyCommandName :: Int -> String -> String 
prettyCommandName n s
  | n <= 0 = s
  | (n > 0) && (null s) = ' ' : prettyCommandName (n-1) s
  | otherwise           = x : prettyCommandName (n-1) xs
    where
      (x:xs) = s