module Oden.CLI where

import qualified Oden.Output                             as Output
import           Oden.Output.Backend                     ()
import           Oden.Output.Compiler                    ()
import           Oden.Output.Compiler.Resolution         ()
import           Oden.Output.Compiler.Validation.Typed   ()
import           Oden.Output.Compiler.Validation.Untyped ()
import           Oden.Output.Desugar                     ()
import           Oden.Output.Imports                     ()
import           Oden.Output.Infer                       ()
import           Oden.Output.Instantiate                 ()
import           Oden.Output.Parser                      ()
import           Oden.Path


import           Control.Monad.Except
import           Control.Monad.Reader
import           Data.Maybe

import           System.Console.GetOpt
import           System.Environment
import           System.Exit
import           System.IO

type CLI = ReaderT Options (ExceptT String IO)

data Options = Options { showHelp        :: Bool
                       , showVersion     :: Bool
                       , odenPath        :: OdenPath
                       , outPath         :: FilePath
                       , printMonochrome :: Bool
                       , warnings        :: Bool
                       } deriving (Show, Eq, Ord)

orMaybe :: Maybe a -> Maybe a -> Maybe a
(Just v) `orMaybe` _ = Just v
Nothing `orMaybe` m = m

setOdenPath :: Maybe String -> Options -> IO Options
setOdenPath path opts = do
  envOdenPath <- lookupEnv "ODEN_PATH"
  let specifiedPath = parseOdenPath <$> (path `orMaybe` envOdenPath)
      odenPath' = fromMaybe (odenPath opts) specifiedPath
  return (opts { odenPath = odenPath' })

setOutPath :: Maybe FilePath -> Options -> IO Options
setOutPath path opts = return (opts { outPath = fromMaybe (outPath opts) path })

toggleWarnings :: Maybe String -> Options -> IO Options
toggleWarnings (Just "none") opts = return (opts { warnings = False })
toggleWarnings _ opts = return (opts { warnings = True })

defaultOptions :: Options
defaultOptions = Options { showHelp = False
                         , showVersion = False
                         , warnings = False
                         , odenPath = OdenPath ["."]
                         , outPath = "target/go"
                         , printMonochrome = False }

defaultOptionsForArgs :: [String] -> Options
defaultOptionsForArgs ("lint":_) = defaultOptions { warnings = True }
defaultOptionsForArgs _ = defaultOptions

options :: [OptDescr (Options -> IO Options)]
options =
  [ Option ['h'] ["help"]
    (NoArg (\opts -> return $ opts { showHelp = True }))
    "Print this help message"
  , Option ['V'] ["version"]
    (NoArg (\opts -> return $ opts { showVersion = True }))
    "Print the CLI version"
  , Option ['W'] ["warn"]
    (OptArg toggleWarnings "all|none")
    "Enable or disable compiler warnings"
  , Option ['p'] ["oden-path"]
    (OptArg setOdenPath "DIR")
    "Search path for Oden sources"
  , Option ['o'] ["out-path"]
    (OptArg setOutPath "DIR")
    "GOPATH output directory"
  , Option ['M'] ["monochrome"]
    (NoArg (\opts -> return $ opts { printMonochrome = True }))
    "Print without colors"
  ]

usage :: String
usage = "Usage: oden command [options]"

commandsInfo :: String
commandsInfo = unlines [
    "Commands:",
    "",
    "  build            compile all source files in the Oden path",
    "  run              run a specific Oden file as main",
    "  lint             check an Oden file for errors and warnings",
    "  print-inferred   infer types and print the package in the given file",
    "  print-compiled   compile and print the package in the given file",
    "  print-codegen    compile and print generated code for the package in",
    "                   the given file",
    ""
  ]

help :: String
help = usageInfo (usage ++ "\n\n" ++ commandsInfo ++ "Options:\n") options

getOptions :: IO (Either String (Options, [String]))
getOptions = do
  argv <- getArgs
  case getOpt Permute options argv of
    (o, args, []) -> do
      o' <- foldM (flip ($)) (defaultOptionsForArgs args) o
      return (Right (o', args))
    (_, _, errs) -> return $ Left (concat errs ++ "\n" ++ help)

printOutput :: Output.OdenOutput o => o -> CLI String
printOutput o = do
  opts <- ask
  let settings = Output.OutputSettings{ Output.monochrome = printMonochrome opts,
                                        Output.markdown = True }
  return (Output.print settings o)

printOutputs :: Output.OdenOutput o => [o] -> CLI String
printOutputs = fmap unlines . mapM printOutput

liftEither :: Output.OdenOutput e => Either e b -> CLI b
liftEither = either (printOutput >=> throwError) return

liftEither' :: Output.OdenOutput e => Either [e] b -> CLI b
liftEither' = either (printOutputs >=> throwError) return

logWarning :: Output.OdenOutput o => o -> CLI ()
logWarning o = do
  opts <- ask
  when (warnings opts) $ do
    s <- printOutput o
    liftIO (hPutStrLn stderr s)

exitWithMessage :: String -> IO ()
exitWithMessage err = do
  hPutStrLn stderr err
  exitFailure
