{-# LANGUAGE OverloadedStrings, CPP, TypeSynonymInstances, FlexibleInstances, GeneralizedNewtypeDeriving, FlexibleContexts, UndecidableInstances, MultiParamTypeClasses, TypeFamilies, ConstraintKinds #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module HsDev.Server.Types (
	ServerMonadBase,
	SessionLog(..), Session(..), SessionMonad(..), askSession, ServerM(..),
	CommandOptions(..), CommandMonad(..), askOptions, ClientM(..),
	withSession, serverListen, serverSetLogLevel, serverWait, serverWaitClients,
	serverSqlDatabase, openSqlConnection, closeSqlConnection, withSqlConnection, withSqlTransaction, serverSetFileContents, inSessionGhc, inSessionUpdater, serverExit, commandRoot, commandNotify, commandLink, commandHold,
	ServerCommand(..), ConnectionPort(..), ServerOpts(..), silentOpts, ClientOpts(..), serverOptsArgs, Request(..),

	Command(..),
	FileSource(..), TargetFilter(..), SearchQuery(..), SearchType(..),
	FromCmd(..),
	) where

import Control.Applicative
import qualified Control.Concurrent.FiniteChan as F
import Control.Lens (view, set)
import Control.Monad.Base
import Control.Monad.Catch
import Control.Monad.Except
import Control.Monad.Morph
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Control.Monad.Trans.Control
import Data.Aeson hiding (Result(..), Error)
import qualified Data.Aeson.Types as A
import qualified Data.ByteString.Lazy.Char8 as L
import Data.Default
import Data.Maybe (fromMaybe)
import Data.Foldable (asum)
import Data.Text (Text)
import Data.String (fromString)
import qualified Database.SQLite.Simple as SQL
import Options.Applicative
import System.Log.Simple as Log

import Control.Concurrent.Worker
import System.Directory.Paths
import Text.Format (Formattable(..))

import HsDev.Error (hsdevError)
import HsDev.Server.Message
import HsDev.Watcher.Types (Watcher)
import HsDev.Tools.Ghc.Worker (GhcWorker, GhcM)
import HsDev.Tools.Types (Note, OutputMessage)
import HsDev.Tools.AutoFix (Refact)
import HsDev.Types (HsDevError(..))
import HsDev.Util

#if mingw32_HOST_OS
import System.Win32.FileMapping.NamePool (Pool)
#endif

type ServerMonadBase m = (MonadIO m, MonadMask m, MonadBaseControl IO m, Alternative m, MonadPlus m)

data SessionLog = SessionLog {
	sessionLogger :: Log,
	sessionListenLog :: IO [Log.Message],
	sessionLogWait :: IO () }

data Session = Session {
	sessionSqlDatabase :: SQL.Connection,
	sessionSqlPath :: String,
	sessionLog :: SessionLog,
	sessionWatcher :: Watcher,
	sessionFileContents :: Path -> Maybe Text -> IO (),
#if mingw32_HOST_OS
	sessionMmapPool :: Maybe Pool,
#endif
	sessionGhc :: GhcWorker,
	sessionUpdater :: Worker (ServerM IO),
	sessionExit :: IO (),
	sessionWait :: IO (),
	sessionClients :: F.Chan (IO ()),
	sessionDefines :: [(String, String)] }

class (ServerMonadBase m, MonadLog m) => SessionMonad m where
	getSession :: m Session
	localSession :: (Session -> Session) -> m a -> m a

askSession :: SessionMonad m => (Session -> a) -> m a
askSession f = liftM f getSession

newtype ServerM m a = ServerM { runServerM :: ReaderT Session m a }
	deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadIO, MonadReader Session, MonadTrans, MonadThrow, MonadCatch, MonadMask)

instance (MonadIO m, MonadMask m) => MonadLog (ServerM m) where
	askLog = ServerM $ asks (sessionLogger . sessionLog)
	localLog fn = ServerM . local setLog' . runServerM where
		setLog' sess = sess { sessionLog = (sessionLog sess) { sessionLogger = fn (sessionLogger (sessionLog sess)) } }

instance ServerMonadBase m => SessionMonad (ServerM m) where
	getSession = ask
	localSession = local

instance MonadBase b m => MonadBase b (ServerM m) where
	liftBase = ServerM . liftBase

instance MonadBaseControl b m => MonadBaseControl b (ServerM m) where
	type StM (ServerM m) a = StM (ReaderT Session m) a
	liftBaseWith f = ServerM $ liftBaseWith (\f' -> f (f' . runServerM))
	restoreM = ServerM . restoreM

instance MFunctor ServerM where
	hoist fn = ServerM . hoist fn . runServerM

instance SessionMonad m => SessionMonad (ReaderT r m) where
	getSession = lift getSession
	localSession = mapReaderT . localSession

instance (SessionMonad m, Monoid w) => SessionMonad (WriterT w m) where
	getSession = lift getSession
	localSession = mapWriterT . localSession

instance SessionMonad m => SessionMonad (StateT s m) where
	getSession = lift getSession
	localSession = mapStateT . localSession

data CommandOptions = CommandOptions {
	commandOptionsRoot :: FilePath,
	commandOptionsNotify :: Notification -> IO (),
	commandOptionsLink :: IO (),
	commandOptionsHold :: IO () }

instance Default CommandOptions where
	def = CommandOptions "." (const $ return ()) (return ()) (return ())

class (SessionMonad m, MonadPlus m) => CommandMonad m where
	getOptions :: m CommandOptions

askOptions :: CommandMonad m => (CommandOptions -> a) -> m a
askOptions f = liftM f getOptions

newtype ClientM m a = ClientM { runClientM :: ServerM (ReaderT CommandOptions m) a }
	deriving (Functor, Applicative, Alternative, Monad, MonadPlus, MonadIO, MonadThrow, MonadCatch, MonadMask)

instance MonadTrans ClientM where
	lift = ClientM . lift . lift

instance (MonadIO m, MonadMask m) => MonadLog (ClientM m) where
	askLog = ClientM askLog
	localLog fn = ClientM . localLog fn . runClientM

instance ServerMonadBase m => SessionMonad (ClientM m) where
	getSession = ClientM getSession
	localSession fn = ClientM . localSession fn . runClientM

instance ServerMonadBase m => CommandMonad (ClientM m) where
	getOptions = ClientM $ lift ask

instance MonadBase b m => MonadBase b (ClientM m) where
	liftBase = ClientM . liftBase

instance MonadBaseControl b m => MonadBaseControl b (ClientM m) where
	type StM (ClientM m) a = StM (ServerM (ReaderT CommandOptions m)) a
	liftBaseWith f = ClientM $ liftBaseWith (\f' -> f (f' . runClientM))
	restoreM = ClientM . restoreM

instance MFunctor ClientM where
	hoist fn = ClientM . hoist (hoist fn) . runClientM

instance CommandMonad m => CommandMonad (ReaderT r m) where
	getOptions = lift getOptions

instance (CommandMonad m, Monoid w) => CommandMonad (WriterT w m) where
	getOptions = lift getOptions

instance CommandMonad m => CommandMonad (StateT s m) where
	getOptions = lift getOptions

-- | Run action on session
withSession :: Session -> ServerM m a -> m a
withSession s act = runReaderT (runServerM act) s

-- | Listen server's log
serverListen :: SessionMonad m => m [Log.Message]
serverListen = join . liftM liftIO $ askSession (sessionListenLog . sessionLog)

-- | Set server's log config
serverSetLogLevel :: SessionMonad m => Level -> m Level
serverSetLogLevel lev = do
	l <- askSession (sessionLogger . sessionLog)
	cfg <- updateLogConfig l (set (componentCfg "") (Just lev))
	return $ fromMaybe def $ view (componentCfg "") cfg

-- | Wait for server
serverWait :: SessionMonad m => m ()
serverWait = join . liftM liftIO $ askSession sessionWait

-- | Wait while clients disconnects
serverWaitClients :: SessionMonad m => m ()
serverWaitClients = do
	clientChan <- askSession sessionClients
	liftIO (F.stopChan clientChan) >>= sequence_ . map liftIO

-- | Get sql connection
serverSqlDatabase :: SessionMonad m => m SQL.Connection
serverSqlDatabase = askSession sessionSqlDatabase

-- | Open new sql connection
openSqlConnection :: SessionMonad m => m SQL.Connection
openSqlConnection = do
	p <- askSession sessionSqlPath
	liftIO $ SQL.open p

-- | Close sql connection
closeSqlConnection :: SessionMonad m => SQL.Connection -> m ()
closeSqlConnection = liftIO . SQL.close

-- | Locally opens new connection, updating @Session@
withSqlConnection :: SessionMonad m => m a -> m a
withSqlConnection act = bracket openSqlConnection closeSqlConnection $ \conn ->
	localSession (\sess -> sess { sessionSqlDatabase = conn }) act

-- | With sql transaction
withSqlTransaction :: SessionMonad m => ServerM IO a -> m a
withSqlTransaction fn = do
	conn <- serverSqlDatabase
	sess <- getSession
	liftIO $ SQL.withTransaction conn $ withSession sess fn

-- | Set custom file contents
serverSetFileContents :: SessionMonad m => Path -> Maybe Text -> m ()
serverSetFileContents fpath mcts = do
	setCts <- askSession sessionFileContents
	liftIO $ setCts fpath mcts

-- | In ghc session
inSessionGhc :: SessionMonad m => GhcM a -> m a
inSessionGhc act = do
	ghcw <- askSession sessionGhc
	inWorkerWith (hsdevError . GhcError . displayException) ghcw act

-- | In updater
inSessionUpdater :: SessionMonad m => ServerM IO a -> m a
inSessionUpdater act = do
	uw <- askSession sessionUpdater
	inWorkerWith (hsdevError . OtherError . displayException) uw act

-- | Exit session
serverExit :: SessionMonad m => m ()
serverExit = join . liftM liftIO $ askSession sessionExit

commandRoot :: CommandMonad m => m FilePath
commandRoot = askOptions commandOptionsRoot

commandNotify :: CommandMonad m => Notification -> m ()
commandNotify n = join . liftM liftIO $ askOptions commandOptionsNotify <*> pure n

commandLink :: CommandMonad m => m ()
commandLink = join . liftM liftIO $ askOptions commandOptionsLink

commandHold :: CommandMonad m => m ()
commandHold = join . liftM liftIO $ askOptions commandOptionsHold

-- | Server control command
data ServerCommand =
	Version |
	Start ServerOpts |
	Run ServerOpts |
	Stop ClientOpts |
	Connect ClientOpts |
	Remote ClientOpts Bool Command
		deriving (Show)

data ConnectionPort = NetworkPort Int | UnixPort String deriving (Eq, Read)

instance Default ConnectionPort where
	def = NetworkPort 4567

instance Show ConnectionPort where
	show (NetworkPort p) = show p
	show (UnixPort s) = "unix " ++ s

instance Formattable ConnectionPort

-- | Server options
data ServerOpts = ServerOpts {
	serverPort :: ConnectionPort,
	serverTimeout :: Int,
	serverLog :: Maybe FilePath,
	serverLogLevel :: String,
	serverDbFile :: Maybe FilePath,
	serverSilent :: Bool }
		deriving (Show)

instance Default ServerOpts where
	def = ServerOpts def 0 Nothing "info" Nothing False

-- | Silent server with no connection, useful for ghci
silentOpts :: ServerOpts
silentOpts = def { serverSilent = True }

-- | Client options
data ClientOpts = ClientOpts {
	clientPort :: ConnectionPort,
	clientPretty :: Bool,
	clientStdin :: Bool,
	clientTimeout :: Int,
	clientSilent :: Bool }
		deriving (Show)

instance Default ClientOpts where
	def = ClientOpts def False False 0 False

instance FromCmd ServerCommand where
	cmdP = serv <|> remote where
		serv = subparser $ mconcat [
			cmd "version" "hsdev version" (pure Version),
			cmd "start" "start remote server" (Start <$> cmdP),
			cmd "run" "run server" (Run <$> cmdP),
			cmd "stop" "stop remote server" (Stop <$> cmdP),
			cmd "connect" "connect to send commands directly" (Connect <$> cmdP)]
		remote = Remote <$> cmdP <*> noFileFlag <*> cmdP

instance FromCmd ServerOpts where
	cmdP = ServerOpts <$>
		(connectionArg <|> pure (serverPort def)) <*>
		(timeoutArg <|> pure (serverTimeout def)) <*>
		optional logArg <*>
		(logLevelArg <|> pure (serverLogLevel def)) <*>
		optional dbFileArg <*>
		serverSilentFlag

instance FromCmd ClientOpts where
	cmdP = ClientOpts <$>
		(connectionArg <|> pure (clientPort def)) <*>
		prettyFlag <*>
		stdinFlag <*>
		(timeoutArg <|> pure (clientTimeout def)) <*>
		silentFlag

portArg :: Parser ConnectionPort
connectionArg :: Parser ConnectionPort
timeoutArg :: Parser Int
logArg :: Parser FilePath
logLevelArg :: Parser String
noFileFlag :: Parser Bool
prettyFlag :: Parser Bool
serverSilentFlag :: Parser Bool
stdinFlag :: Parser Bool
silentFlag :: Parser Bool
dbFileArg :: Parser FilePath

portArg = NetworkPort <$> option auto (long "port" <> metavar "number" <> help "connection port")
#if mingw32_HOST_OS
connectionArg = portArg
#else
unixArg :: Parser ConnectionPort
unixArg = UnixPort <$> strOption (long "unix" <> metavar "name" <> help "unix connection port")
connectionArg = portArg <|> unixArg
#endif
timeoutArg = option auto (long "timeout" <> metavar "msec" <> help "query timeout")
logArg = strOption (long "log" <> short 'l' <> metavar "file" <> help "log file")
logLevelArg = strOption (long "log-level" <> metavar "level" <> help "log level: trace/debug/info/warning/error/fatal")
noFileFlag = switch (long "no-file" <> help "don't use mmap files")
prettyFlag = switch (long "pretty" <> help "pretty json output")
serverSilentFlag = switch (long "silent" <> help "no stdout/stderr")
stdinFlag = switch (long "stdin" <> help "pass data to stdin")
silentFlag = switch (long "silent" <> help "supress notifications")
dbFileArg = strOption (long "db" <> metavar "path" <> help "path to sql database")

serverOptsArgs :: ServerOpts -> [String]
serverOptsArgs sopts = concat [
	portArgs (serverPort sopts),
	["--timeout", show $ serverTimeout sopts],
	marg "--log" (serverLog sopts),
	["--log-level", serverLogLevel sopts],
	marg "--db" (serverDbFile sopts),
	["--silent" | serverSilent sopts]]
	where
		marg :: String -> Maybe String -> [String]
		marg n (Just v) = [n, v]
		marg _ _ = []
		portArgs :: ConnectionPort -> [String]
		portArgs (NetworkPort n) = ["--port", show n]
		portArgs (UnixPort s) = ["--unix", s]

data Request = Request {
	requestCommand :: Command,
	requestDirectory :: FilePath,
	requestNoFile :: Bool,
	requestTimeout :: Int,
	requestSilent :: Bool }
		deriving (Show)

instance ToJSON Request where
	toJSON (Request c dir f tm s) = object ["current-directory" .= dir, "no-file" .= f, "timeout" .= tm, "silent" .= s] `objectUnion` toJSON c

instance FromJSON Request where
	parseJSON = withObject "request" $ \v -> Request <$>
		parseJSON (Object v) <*>
		((v .:: "current-directory") <|> pure ".") <*>
		((v .:: "no-file") <|> pure False) <*>
		((v .:: "timeout") <|> pure 0) <*>
		((v .:: "silent") <|> pure False)

-- | Command from client
data Command =
	Ping |
	Listen (Maybe String) |
	SetLogLevel String |
	Scan {
		scanProjects :: [Path],
		scanCabal :: Bool,
		scanSandboxes :: [Path],
		scanFiles :: [FileSource],
		scanPaths :: [Path],
		scanGhcOpts :: [String],
		scanDocs :: Bool,
		scanInferTypes :: Bool } |
	SetFileContents Path (Maybe Text) |
	RefineDocs {
		docsProjects :: [Path],
		docsFiles :: [Path] } |
	InferTypes {
		inferProjects :: [Path],
		inferFiles :: [Path] } |
	Remove {
		removeProjects :: [Path],
		removeCabal :: Bool,
		removeSandboxes :: [Path],
		removeFiles :: [Path] } |
	RemoveAll |
	InfoPackages |
	InfoProjects |
	InfoSandboxes |
	InfoSymbol SearchQuery [TargetFilter] Bool Bool |
	InfoModule SearchQuery [TargetFilter] Bool Bool |
	InfoProject (Either Text Path) |
	InfoSandbox Path |
	Lookup Text Path |
	Whois Text Path |
	Whoat Int Int Path |
	ResolveScopeModules SearchQuery Path |
	ResolveScope SearchQuery Path |
	FindUsages Text |
	Complete Text Bool Path |
	Hayoo {
		hayooQuery :: String,
		hayooPage :: Int,
		hayooPages :: Int } |
	CabalList { cabalListPackages :: [Text] } |
	UnresolvedSymbols {
		unresolvedFiles :: [Path] } |
	Lint {
		lintFiles :: [FileSource] } |
	Check {
		checkFiles :: [FileSource],
		checkGhcOpts :: [String],
		checkClear :: Bool } |
	CheckLint {
		checkLintFiles :: [FileSource],
		checkLintGhcOpts :: [String],
		checkLinkClear :: Bool } |
	Types {
		typesFiles :: [FileSource],
		typesGhcOpts :: [String],
		typesClear :: Bool } |
	AutoFix [Note OutputMessage] |
	Refactor [Note Refact] [Note Refact] Bool |
	Rename Text Text Path |
	GhcEval { ghcEvalExpressions :: [String], ghcEvalSource :: Maybe FileSource } |
	Langs |
	Flags |
	Link { linkHold :: Bool } |
	StopGhc |
	Exit
		deriving (Show)

data FileSource = FileSource { fileSource :: Path, fileContents :: Maybe Text } deriving (Show)
data TargetFilter =
	TargetProject Text |
	TargetFile Path |
	TargetModule Text |
	TargetPackage Text |
	TargetInstalled |
	TargetSourced |
	TargetStandalone
		deriving (Eq, Show)
data SearchQuery = SearchQuery Text SearchType deriving (Show)
data SearchType = SearchExact | SearchPrefix | SearchInfix | SearchSuffix deriving (Show)

instance Paths Command where
	paths f (Scan projs c cs fs ps ghcs docs infer) = Scan <$>
		traverse (paths f) projs <*>
		pure c <*>
		traverse (paths f) cs <*>
		traverse (paths f) fs <*>
		traverse (paths f) ps <*>
		pure ghcs <*>
		pure docs <*>
		pure infer
	paths f (SetFileContents p cts) = SetFileContents <$> paths f p <*> pure cts
	paths f (RefineDocs projs fs) = RefineDocs <$> traverse (paths f) projs <*> traverse (paths f) fs
	paths f (InferTypes projs fs) = InferTypes <$> traverse (paths f) projs <*> traverse (paths f) fs
	paths f (Remove projs c cs fs) = Remove <$> traverse (paths f) projs <*> pure c <*> traverse (paths f) cs <*> traverse (paths f) fs
	paths _ RemoveAll = pure RemoveAll
	paths f (InfoSymbol q t h l) = InfoSymbol <$> pure q <*> traverse (paths f) t <*> pure h <*> pure l
	paths f (InfoModule q t h i) = InfoModule <$> pure q <*> traverse (paths f) t <*> pure h <*> pure i
	paths f (InfoProject (Right proj)) = InfoProject <$> (Right <$> paths f proj)
	paths f (InfoSandbox fpath) = InfoSandbox <$> paths f fpath
	paths f (Lookup n fpath) = Lookup <$> pure n <*> paths f fpath
	paths f (Whois n fpath) = Whois <$> pure n <*> paths f fpath
	paths f (Whoat l c fpath) = Whoat <$> pure l <*> pure c <*> paths f fpath
	paths f (ResolveScopeModules q fpath) = ResolveScopeModules q <$> paths f fpath
	paths f (ResolveScope q fpath) = ResolveScope q <$> paths f fpath
	paths _ (FindUsages nm) = pure $ FindUsages nm
	paths f (Complete n g fpath) = Complete n g <$> paths f fpath
	paths f (UnresolvedSymbols fs) = UnresolvedSymbols <$> traverse (paths f) fs
	paths f (Lint fs) = Lint <$> traverse (paths f) fs
	paths f (Check fs ghcs c) = Check <$> traverse (paths f) fs <*> pure ghcs <*> pure c
	paths f (CheckLint fs ghcs c) = CheckLint <$> traverse (paths f) fs <*> pure ghcs <*> pure c
	paths f (Types fs ghcs c) = Types <$> traverse (paths f) fs <*> pure ghcs <*> pure c
	paths f (GhcEval e mf) = GhcEval e <$> traverse (paths f) mf
	paths _ c = pure c

instance Paths FileSource where
	paths f (FileSource fpath mcts) = FileSource <$> paths f fpath <*> pure mcts

instance Paths TargetFilter where
	paths f (TargetFile fpath) = TargetFile <$> paths f fpath
	paths _ t = pure t

instance FromCmd Command where
	cmdP = subparser $ mconcat [
		cmd "ping" "ping server" (pure Ping),
		cmd "listen" "listen server log" (Listen <$> optional logLevelArg),
		cmd "set-log" "set log level" (SetLogLevel <$> strArgument idm),
		cmd "scan" "scan sources" $ Scan <$>
			many projectArg <*>
			cabalFlag <*>
			many sandboxArg <*>
			many cmdP <*>
			many (pathArg $ help "path") <*>
			ghcOpts <*>
			docsFlag <*>
			inferFlag,
		cmd "set-file-contents" "set edited file contents, which will be used instead of contents in file until it updated" $
			SetFileContents <$> fileArg <*> optional contentsArg,
		cmd "docs" "scan docs" $ RefineDocs <$> many projectArg <*> many fileArg,
		cmd "infer" "infer types" $ InferTypes <$> many projectArg <*> many fileArg,
		cmd "remove" "remove modules info" $ Remove <$>
			many projectArg <*>
			cabalFlag <*>
			many sandboxArg <*>
			many fileArg,
		cmd "remove-all" "remove all data" (pure RemoveAll),
		cmd "packages" "list packages" (pure InfoPackages),
		cmd "projects" "list projects" (pure InfoProjects),
		cmd "sandboxes" "list sandboxes" (pure InfoSandboxes),
		cmd "symbol" "get symbol info" (InfoSymbol <$> cmdP <*> many cmdP <*> headerFlag <*> localsFlag),
		cmd "module" "get module info" (InfoModule <$> cmdP <*> many cmdP <*> headerFlag <*> inspectionFlag),
		cmd "project" "get project info" (InfoProject <$> ((Left <$> projectArg) <|> (Right <$> pathArg idm))),
		cmd "sandbox" "get sandbox info" (InfoSandbox <$> pathArg (help "locate sandbox in parent of this path")),
		cmd "lookup" "lookup for symbol" (Lookup <$> textArgument idm <*> ctx),
		cmd "whois" "get info for symbol" (Whois <$> textArgument idm <*> ctx),
		cmd "whoat" "get info for symbol under cursor" (Whoat <$> argument auto (metavar "line") <*> argument auto (metavar "column") <*> ctx),
		cmd "scope" "get declarations accessible from module or within a project" (
			subparser (cmd "modules" "get modules accessible from module or within a project" (ResolveScopeModules <$> cmdP <*> ctx)) <|>
			ResolveScope <$> cmdP <*> ctx),
		cmd "usages" "find usages of fully qualified symbol (qualified with module its defined in)" (FindUsages <$> textArgument idm),
		cmd "complete" "show completions for input" (Complete <$> textArgument idm <*> wideFlag <*> ctx),
		cmd "hayoo" "find declarations online via Hayoo" (Hayoo <$> strArgument idm <*> hayooPageArg <*> hayooPagesArg),
		cmd "cabal" "cabal commands" (subparser $ cmd "list" "list cabal packages" (CabalList <$> many (textArgument idm))),
		cmd "unresolveds" "list unresolved symbols in source file" (UnresolvedSymbols <$> many fileArg),
		cmd "lint" "lint source files or file contents" (Lint <$> many cmdP),
		cmd "check" "check source files or file contents" (Check <$> many cmdP <*> ghcOpts <*> clearFlag),
		cmd "check-lint" "check and lint source files or file contents" (CheckLint <$> many cmdP <*> ghcOpts <*> clearFlag),
		cmd "types" "get types for file expressions" (Types <$> many cmdP <*> ghcOpts <*> clearFlag),
		cmd "autofixes" "get autofixes by output messages" (AutoFix <$> option readJSON (long "data" <> metavar "message" <> help "messages to make fixes for")),
		cmd "refactor" "apply some refactors and get rest updated" (Refactor <$>
			option readJSON (long "data" <> metavar "message" <> help "messages to fix") <*>
			option readJSON (long "rest" <> metavar "correction" <> short 'r' <> help "update corrections") <*>
			pureFlag),
		cmd "rename" "get rename refactors" (Rename <$> textArgument idm <*> textArgument idm <*> ctx),
		cmd "ghc" "ghc commands" (subparser $ cmd "eval" "evaluate expression" (GhcEval <$> many (strArgument idm) <*> optional cmdP)),
		cmd "langs" "ghc language options" (pure Langs),
		cmd "flags" "ghc flags" (pure Flags),
		cmd "link" "link to server" (Link <$> holdFlag),
		cmd "stop-ghc" "stop ghc sessions" (pure StopGhc),
		cmd "exit" "exit" (pure Exit)]

instance FromCmd FileSource where
	cmdP = option readJSON (long "contents") <|> (FileSource <$> fileArg <*> pure Nothing)

instance FromCmd TargetFilter where
	cmdP = asum [
		TargetProject <$> projectArg,
		TargetFile <$> fileArg,
		TargetModule <$> moduleArg,
		TargetPackage <$> packageArg,
		flag' TargetInstalled (long "installed"),
		flag' TargetSourced (long "src"),
		flag' TargetStandalone (long "stand")]

instance FromCmd SearchQuery where
	cmdP = SearchQuery <$> (textArgument idm <|> pure "") <*> asum [
		flag' SearchExact (long "exact"),
		flag' SearchInfix (long "infix"),
		flag' SearchSuffix (long "suffix"),
		pure SearchPrefix <* switch (long "prefix")]

readJSON :: FromJSON a => ReadM a
readJSON = str >>= maybe (readerError "Can't parse JSON argument") return . decode . L.pack

textOption :: Mod OptionFields String -> Parser Text
textOption = fmap fromString . strOption

textArgument :: Mod ArgumentFields String -> Parser Text
textArgument = fmap fromString . strArgument

cabalFlag :: Parser Bool
clearFlag :: Parser Bool
contentsArg :: Parser Text
ctx :: Parser Path
docsFlag :: Parser Bool
fileArg :: Parser Path
ghcOpts :: Parser [String]
hayooPageArg :: Parser Int
hayooPagesArg :: Parser Int
headerFlag :: Parser Bool
holdFlag :: Parser Bool
inferFlag :: Parser Bool
inspectionFlag :: Parser Bool
localsFlag :: Parser Bool
moduleArg :: Parser Text
packageArg :: Parser Text
pathArg :: Mod OptionFields String -> Parser Path
projectArg :: Parser Path
pureFlag :: Parser Bool
sandboxArg :: Parser Path
wideFlag :: Parser Bool

cabalFlag = switch (long "cabal")
clearFlag = switch (long "clear" <> short 'c' <> help "clear run, drop previous state")
contentsArg = textOption (long "contents" <> help "text contents")
ctx = fileArg
docsFlag = switch (long "docs" <> help "scan source file docs")
fileArg = textOption (long "file" <> metavar "path" <> short 'f')
ghcOpts = many (strOption (long "ghc" <> metavar "option" <> short 'g' <> help "options to pass to GHC"))
hayooPageArg = option auto (long "page" <> metavar "n" <> short 'p' <> help "page number (0 by default)" <> value 0)
hayooPagesArg = option auto (long "pages" <> metavar "count" <> short 'n' <> help "pages count (1 by default)" <> value 1)
headerFlag = switch (long "header" <> short 'h' <> help "show only header of module")
holdFlag = switch (long "hold" <> short 'h' <> help "don't return any response")
inferFlag = switch (long "infer" <> help "infer types")
inspectionFlag = switch (long "inspection" <> short 'i' <> help "return inspection data")
localsFlag = switch (long "locals" <> short 'l' <> help "look in local declarations")
moduleArg = textOption (long "module" <> metavar "name" <> short 'm' <> help "module name")
packageArg = textOption (long "package" <> metavar "name" <> help "module package")
pathArg f = textOption (long "path" <> metavar "path" <> short 'p' <> f)
projectArg = textOption (long "project" <> long "proj" <> metavar "project")
pureFlag = switch (long "pure" <> help "don't modify actual file, just return result")
sandboxArg = textOption (long "sandbox" <> metavar "path" <> help "path to cabal sandbox")
wideFlag = switch (long "wide" <> short 'w' <> help "wide mode - complete as if there were no import lists")

instance ToJSON Command where
	toJSON Ping = cmdJson "ping" []
	toJSON (Listen lev) = cmdJson "listen" ["level" .= lev]
	toJSON (SetLogLevel lev) = cmdJson "set-log" ["level" .= lev]
	toJSON (Scan projs cabal sboxes fs ps ghcs docs' infer') = cmdJson "scan" [
		"projects" .= projs,
		"cabal" .= cabal,
		"sandboxes" .= sboxes,
		"files" .= fs,
		"paths" .= ps,
		"ghc-opts" .= ghcs,
		"docs" .= docs',
		"infer" .= infer']
	toJSON (SetFileContents f cts) = cmdJson "set-file-contents" ["file" .= f, "contents" .= cts]
	toJSON (RefineDocs projs fs) = cmdJson "docs" ["projects" .= projs, "files" .= fs]
	toJSON (InferTypes projs fs) = cmdJson "infer" ["projects" .= projs, "files" .= fs]
	toJSON (Remove projs cabal sboxes fs) = cmdJson "remove" ["projects" .= projs, "cabal" .= cabal, "sandboxes" .= sboxes, "files" .= fs]
	toJSON RemoveAll = cmdJson "remove-all" []
	toJSON InfoPackages = cmdJson "packages" []
	toJSON InfoProjects = cmdJson "projects" []
	toJSON InfoSandboxes = cmdJson "sandboxes" []
	toJSON (InfoSymbol q tf h l) = cmdJson "symbol" ["query" .= q, "filters" .= tf, "header" .= h, "locals" .= l]
	toJSON (InfoModule q tf h i) = cmdJson "module" ["query" .= q, "filters" .= tf, "header" .= h, "inspection" .= i]
	toJSON (InfoProject p) = cmdJson "project" $ either (\pname -> ["name" .= pname]) (\ppath -> ["path" .= ppath]) p
	toJSON (InfoSandbox p) = cmdJson "sandbox" ["path" .= p]
	toJSON (Lookup n f) = cmdJson "lookup" ["name" .= n, "file" .= f]
	toJSON (Whois n f) = cmdJson "whois" ["name" .= n, "file" .= f]
	toJSON (Whoat l c f) = cmdJson "whoat" ["line" .= l, "column" .= c, "file" .= f]
	toJSON (ResolveScopeModules q f) = cmdJson "scope modules" ["query" .= q, "file" .= f]
	toJSON (ResolveScope q f) = cmdJson "scope" ["query" .= q, "file" .= f]
	toJSON (FindUsages nm) = cmdJson "usages" ["name" .= nm]
	toJSON (Complete q w f) = cmdJson "complete" ["prefix" .= q, "wide" .= w, "file" .= f]
	toJSON (Hayoo q p ps) = cmdJson "hayoo" ["query" .= q, "page" .= p, "pages" .= ps]
	toJSON (CabalList ps) = cmdJson "cabal list" ["packages" .= ps]
	toJSON (UnresolvedSymbols fs) = cmdJson "unresolveds" ["files" .= fs]
	toJSON (Lint fs) = cmdJson "lint" ["files" .= fs]
	toJSON (Check fs ghcs c) = cmdJson "check" ["files" .= fs, "ghc-opts" .= ghcs, "clear" .= c]
	toJSON (CheckLint fs ghcs c) = cmdJson "check-lint" ["files" .= fs, "ghc-opts" .= ghcs, "clear" .= c]
	toJSON (Types fs ghcs c) = cmdJson "types" ["files" .= fs, "ghc-opts" .= ghcs, "clear" .= c]
	toJSON (AutoFix ns) = cmdJson "autofixes" ["messages" .= ns]
	toJSON (Refactor ns rests pure') = cmdJson "refactor" ["messages" .= ns, "rest" .= rests, "pure" .= pure']
	toJSON (Rename n n' f) = cmdJson "rename" ["name" .= n, "new-name" .= n', "file" .= f]
	toJSON (GhcEval exprs f) = cmdJson "ghc eval" ["exprs" .= exprs, "file" .= f]
	toJSON Langs = cmdJson "langs" []
	toJSON Flags = cmdJson "flags" []
	toJSON (Link h) = cmdJson "link" ["hold" .= h]
	toJSON StopGhc = cmdJson "stop-ghc" []
	toJSON Exit = cmdJson "exit" []

instance FromJSON Command where
	parseJSON = withObject "command" $ \v -> asum [
		guardCmd "ping" v *> pure Ping,
		guardCmd "listen" v *> (Listen <$> v .::? "level"),
		guardCmd "set-log" v *> (SetLogLevel <$> v .:: "level"),
		guardCmd "scan" v *> (Scan <$>
			v .::?! "projects" <*>
			(v .:: "cabal" <|> pure False) <*>
			v .::?! "sandboxes" <*>
			v .::?! "files" <*>
			v .::?! "paths" <*>
			v .::?! "ghc-opts" <*>
			(v .:: "docs" <|> pure False) <*>
			(v .:: "infer" <|> pure False)),
		guardCmd "set-file-contents" v *> (SetFileContents <$> v .:: "file" <*> v .:: "contents"),
		guardCmd "docs" v *> (RefineDocs <$> v .::?! "projects" <*> v .::?! "files"),
		guardCmd "infer" v *> (InferTypes <$> v .::?! "projects" <*> v .::?! "files"),
		guardCmd "remove" v *> (Remove <$>
			v .::?! "projects" <*>
			(v .:: "cabal" <|> pure False) <*>
			v .::?! "sandboxes" <*>
			v .::?! "files"),
		guardCmd "remove-all" v *> pure RemoveAll,
		guardCmd "packages" v *> pure InfoPackages,
		guardCmd "projects" v *> pure InfoProjects,
		guardCmd "sandboxes" v *> pure InfoSandboxes,
		guardCmd "symbol" v *> (InfoSymbol <$> v .:: "query" <*> v .::?! "filters" <*> v .:: "header" <*> (v .:: "locals" <|> pure False)),
		guardCmd "module" v *> (InfoModule <$> v .:: "query" <*> v .::?! "filters" <*> v .:: "header" <*> v .:: "inspection"),
		guardCmd "project" v *> (InfoProject <$> asum [Left <$> v .:: "name", Right <$> v .:: "path"]),
		guardCmd "sandbox" v *> (InfoSandbox <$> v .:: "path"),
		guardCmd "lookup" v *> (Lookup <$> v .:: "name" <*> v .:: "file"),
		guardCmd "whois" v *> (Whois <$> v .:: "name" <*> v .:: "file"),
		guardCmd "whoat" v *> (Whoat <$> v .:: "line" <*> v .:: "column" <*> v .:: "file"),
		guardCmd "scope modules" v *> (ResolveScopeModules <$> v .:: "query" <*> v .:: "file"),
		guardCmd "scope" v *> (ResolveScope <$> v .:: "query" <*> v .:: "file"),
		guardCmd "usages" v *> (FindUsages <$> v .:: "name"),
		guardCmd "complete" v *> (Complete <$> v .:: "prefix" <*> (v .:: "wide" <|> pure False) <*> v .:: "file"),
		guardCmd "hayoo" v *> (Hayoo <$> v .:: "query" <*> (v .:: "page" <|> pure 0) <*> (v .:: "pages" <|> pure 1)),
		guardCmd "cabal list" v *> (CabalList <$> v .::?! "packages"),
		guardCmd "unresolveds" v *> (UnresolvedSymbols <$> v .::?! "files"),
		guardCmd "lint" v *> (Lint <$> v .::?! "files"),
		guardCmd "check" v *> (Check <$> v .::?! "files" <*> v .::?! "ghc-opts" <*> (v .:: "clear" <|> pure False)),
		guardCmd "check-lint" v *> (CheckLint <$> v .::?! "files" <*> v .::?! "ghc-opts" <*> (v .:: "clear" <|> pure False)),
		guardCmd "types" v *> (Types <$> v .::?! "files" <*> v .::?! "ghc-opts" <*> (v .:: "clear" <|> pure False)),
		guardCmd "autofixes" v *> (AutoFix <$> v .:: "messages"),
		guardCmd "refactor" v *> (Refactor <$> v .:: "messages" <*> v .::?! "rest" <*> (v .:: "pure" <|> pure True)),
		guardCmd "rename" v *> (Rename <$> v .:: "name" <*> v .:: "new-name" <*> v .:: "file"),
		guardCmd "ghc eval" v *> (GhcEval <$> v .::?! "exprs" <*> v .::? "file"),
		guardCmd "langs" v *> pure Langs,
		guardCmd "flags" v *> pure Flags,
		guardCmd "link" v *> (Link <$> (v .:: "hold" <|> pure False)),
		guardCmd "stop-ghc" v *> pure StopGhc,
		guardCmd "exit" v *> pure Exit]

instance ToJSON FileSource where
	toJSON (FileSource fpath mcts) = object ["file" .= fpath, "contents" .= mcts]

instance FromJSON FileSource where
	parseJSON = withObject "file-contents" $ \v -> FileSource <$> v .:: "file" <*> v .::? "contents"

instance ToJSON TargetFilter where
	toJSON (TargetProject pname) = object ["project" .= pname]
	toJSON (TargetFile fpath) = object ["file" .= fpath]
	toJSON (TargetModule mname) = object ["module" .= mname]
	toJSON (TargetPackage pname) = object ["package" .= pname]
	toJSON TargetInstalled = toJSON ("installed" :: String)
	toJSON TargetSourced = toJSON ("sourced" :: String)
	toJSON TargetStandalone = toJSON ("standalone" :: String)

instance FromJSON TargetFilter where
	parseJSON j = obj j <|> str' where
		obj = withObject "target-filter" $ \v -> asum [
			TargetProject <$> v .:: "project",
			TargetFile <$> v .:: "file",
			TargetModule <$> v .:: "module",
			TargetPackage <$> v .:: "package"]
		str' = do
			s <- parseJSON j :: A.Parser String
			case s of
				"installed" -> return TargetInstalled
				"sourced" -> return TargetSourced
				"standalone" -> return TargetStandalone
				_ -> empty

instance ToJSON SearchQuery where
	toJSON (SearchQuery q st) = object ["input" .= q, "type" .= st]

instance FromJSON SearchQuery where
	parseJSON = withObject "search-query" $ \v -> SearchQuery <$> (v .:: "input" <|> pure "") <*> (v .:: "type" <|> pure SearchPrefix)

instance ToJSON SearchType where
	toJSON SearchExact = toJSON ("exact" :: String)
	toJSON SearchPrefix = toJSON ("prefix" :: String)
	toJSON SearchInfix = toJSON ("infix" :: String)
	toJSON SearchSuffix = toJSON ("suffix" :: String)

instance FromJSON SearchType where
	parseJSON v = do
		str' <- parseJSON v :: A.Parser String
		case str' of
			"exact" -> return SearchExact
			"prefix" -> return SearchPrefix
			"infix" -> return SearchInfix
			"suffix" -> return SearchInfix
			_ -> empty