--------------------------------------------------------------------------------
-- Haskell client for Moss                                                    --
-- Copyright (c) 2018 Michael B. Gale (m.gale@warwick.ac.uk)                  --
--------------------------------------------------------------------------------

module Stanford.Moss (
    MossCfg(..),
    defaultMossCfg,

    Language(..),
    Moss,
    liftIO,

    withMoss,
    addBaseFile,
    addFile,
    addFilesForStudent,
    query
) where

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

import Control.Exception
import Control.Monad.State

import Data.Monoid

import Network.Simple.TCP

import System.IO
import System.PosixCompat

import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C

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

-- | Represents the configuration for a Moss connection.
data MossCfg = MossCfg {
    mossServer     :: HostName,
    mossPort       :: ServiceName,
    mossUser       :: BS.ByteString,
    mossDir        :: Maybe FilePath,
    mossX          :: Bool,
    mossMaxMatches :: Int,
    mossShow       :: Bool,
    mossLanguage   :: Language
}

-- | 'defaultMossCfg' is the default configuration for a Moss connection.
defaultMossCfg :: MossCfg
defaultMossCfg = MossCfg {
    mossServer     = "moss.stanford.edu",
    mossPort       = "7690",
    mossUser       = "",
    mossDir        = Nothing,
    mossX          = False,
    mossMaxMatches = 250,
    mossShow       = True,
    mossLanguage   = Haskell
}

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

-- | Enumerates programming languages supported by Moss.
data Language
    = C
    | CPP
    | Java
    | CSharp
    | Python
    | VisualBasic
    | Javascript
    | FORTRAN
    | ML
    | Haskell
    | Lisp
    | Scheme
    | Pascal
    | Modula2
    | Ada
    | Perl
    | TCL
    | Matlab
    | VHDL
    | Verilog
    | Spice
    | MIPS
    | A8086
    | HCL2
    deriving (Enum)

instance Show Language where
    show C       = "c"
    show CPP     = "cc"
    show Java    = "java"
    show ML      = "ml"
    show Pascal  = "pascal"
    show Ada     = "ada"
    show Lisp    = "lisp"
    show Scheme  = "scheme"
    show Haskell = "haskell"
    show FORTRAN = "fortran"

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

-- | Represents the state of a Moss connection.
data MossSt = MossSt {
    mossSocket  :: Socket,
    mossCounter :: Int,
    mossCfg     :: MossCfg
}

type Moss = StateT MossSt IO

-- | 'sendCmd' @socket bytestring@ sends @bytestring@ as a command over the
-- connection represented by @socket@.
sendCmd :: Socket -> BS.ByteString -> IO ()
sendCmd s xs = do
    putStr "Send: "
    putStrLn (show xs)
    send s (xs <> "\n")

-- | 'withMoss' @cfg m@ runs a computation @m@ using a Moss connection whose
-- configuration is reprsented by @cfg@.
withMoss :: MossCfg -> Moss a -> IO a
withMoss (cfg@MossCfg {..}) m =
    connect mossServer mossPort $ \(s, addr) -> do
        sendCmd s ("moss " <> mossUser)
        sendCmd s ("X " <> C.pack (show (fromEnum mossX)))
        sendCmd s ("maxmatches " <> C.pack (show mossMaxMatches))
        sendCmd s ("language " <> C.pack (show mossLanguage))

        ls <- recv s 1024

        case ls of
            Nothing -> error "No data received."
            Just "no" -> do
                sendCmd s "end"
                error "Language not supported"
            Just _ -> do
                putStrLn "Language supported."
                r <- evalStateT m (MossSt s 1 cfg)
                sendCmd s "end"
                return r

{-send = withSocketsDo $ do
    h <- connectTo mossServer mossPort
    hClose h-}

-- | 'uploadFile' @index name path@ uploads a file located at @path@ to Moss
-- and assigns it to the collection of files at @index@ (e.g. representing
-- a student) with the name given by @name@.
uploadFile :: Int -> String -> FilePath -> Moss ()
uploadFile i dn fp = do
    s <- gets mossSocket
    MossCfg{..} <- gets mossCfg

    liftIO $ do
        size <- fileSize <$> getFileStatus fp
        sendCmd s ( "file "
             <> C.pack (show i) <> " "
             <> C.pack (show mossLanguage) <> " "
             <> C.pack (show size) <> " "
             <> C.pack dn)

        xs <- BS.readFile fp
        send s xs

-- | 'addBaseFile' @file@ adds @file@ as part of the skeleton code.
addBaseFile :: String -> FilePath -> Moss ()
addBaseFile = uploadFile 0

-- | 'addFile' @name file@ adds @file@ as a submission to Moss with @name@.
addFile :: String -> FilePath -> Moss ()
addFile desc fp = do
    st <- get
    uploadFile (mossCounter st) desc fp
    put $ st { mossCounter = mossCounter st + 1 }

-- | 'addFilesForStudent' @filesWithNames@ uploads multiple files for
-- the same student. I.e. in the Moss submission they will share the same ID.
addFilesForStudent :: [(String, FilePath)] -> Moss ()
addFilesForStudent fs = do
    st <- get
    forM_ fs $ \(dn,fn) ->
        uploadFile (mossCounter st) dn fn
    put $ st { mossCounter = mossCounter st + 1 }

-- | 'query' @comment@ runs the plagiarism check on all submitted files
query :: BS.ByteString -> Moss (Maybe BS.ByteString)
query cmt = do
    s <- gets mossSocket
    liftIO $ do
        putStrLn "Querying, this may take several minutes..."
        sendCmd s ("query 0 " <> cmt)
        recv s 1024

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