module Main where

import Cabin.Package
import Cabin.Internal
import Control.Monad (filterM, liftM)

import Data.Maybe (fromMaybe)
import Data.Monoid (mempty, (<>))

import Options.Applicative
import Options.Applicative.Types (Parser(NilP))

import System.Directory
import System.Environment (lookupEnv)
import System.Exit (exitWith, ExitCode(..))
import System.FilePath
import System.Posix.Files (createSymbolicLink)
import System.Process

--------------------------------------------------------------------------------
-- Parsers
--------------------------------------------------------------------------------

data InstallOpts = InstallOpts {
    ioName :: Maybe String
  , ioPackages :: [String]
}

installOpts :: Parser InstallOpts
installOpts = InstallOpts
  <$> optional (strOption (
         long "name"
      <> short 'n'
      <> metavar "NAME"
      <> help ("Name for the new cabin. If not provided, the first package "
          ++ "name is used.")
      ))
  <*> some (argument str (metavar "PACKAGES..."))

data LoadOpts = LoadOpts {
  loNames :: [String]
}

loadOpts :: Parser LoadOpts
loadOpts = LoadOpts
  <$> some (argument str (metavar "CABINS..."))

data ListOpts = ListOpts {
  loOnlyActive :: Bool
}

listOpts :: Parser ListOpts
listOpts = ListOpts
  <$> switch (   long "active"
              <> short 'a'
              <> help "Show only active cabins."
             )

emptyOpts :: Parser ()
emptyOpts = NilP $ Just ()

data Command = Install InstallOpts
             | List ListOpts
             | Load LoadOpts
             | Unload LoadOpts
             | Reindex ()

commandParser :: Parser Command
commandParser = subparser (
      command "install" (info (fmap Install installOpts)
        (progDesc "Install a program into a new cabin."))
   <> command "list" (info (fmap List listOpts) (
        progDesc "List available cabins."))
   <> command "load" (info (fmap Load loadOpts) (
        progDesc "Load an installed cabin."))
   <> command "unload" (info (fmap Unload loadOpts) (
        progDesc "Unload a loaded cabin."))
   <> command "reindex" (info (fmap Reindex emptyOpts) (
        progDesc "Rebuild the cabin DB."))
  )

--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------

main :: IO ()
main = execParser opts >>= run where
  opts = info (helper <*> commandParser) (
       fullDesc
    <> progDesc "Cabin - cabal binary tool."
    <> header "Cabin."
    )

run :: Command -> IO ()
run (Install opts) = install opts
run (List opts) = list opts
run (Reindex _) = reindex
run (Load opts) = load opts
run (Unload opts) = unload opts
--------------------------------------------------------------------------------
-- Commands
--------------------------------------------------------------------------------

install :: InstallOpts -> IO ()
install opts = do
  prepareEnvironment
  let pkgs = ioPackages opts
      name = fromMaybe (head pkgs) $ ioName opts
  cabal <- getCabalPath
  cabinPath <- fmap (</> name) getCabinPath
  cabinDB <- readCabinDB
  -- Create directory, sandbox init, cabal init
  createDirectoryIfMissing True cabinPath
  let sboxInitProc = (proc cabal ["sandbox","init"]) {
        cwd = Just cabinPath
      }
      cabalInstallProc = (proc cabal ("install" : pkgs)) {
        cwd = Just cabinPath
      }
  ec <- cmdSeq [sboxInitProc, cabalInstallProc]
  case ec of
    ExitSuccess -> do
      cabin <- infoCabin cabinPath
      let cabinDB' = cabinDB <> (CabinDB [cabin])
      writeCabinDB cabinDB'
    ExitFailure _ -> removeDirectoryRecursive cabinPath
  exitWith ec

list :: ListOpts -> IO ()
list opts = do
    dataDir <- getCabinPath
    createDirectoryIfMissing True dataDir
    cabs <- readCabinDB
    cabStats <- mapM (\a -> cabinStatus a >>= \b -> return (a,b)) $ listCabins cabs
    mapM_ (putStrLn . showCab) cabStats
  where showCab ((Cabin name _, status)) = name ++ " (" ++ show status ++ ")"

load :: LoadOpts -> IO ()
load opts = do
    prepareEnvironment
    cdb <- readCabinDB
    binPath <- getBinaryPath
    mapM_ (go cdb binPath) $ loNames opts
  where
    go cdb binPath name =
      let cabin = findCabinByName name cdb
          linkPath a = binPath </> (takeFileName a)
      in case cabin of
        Nothing -> putStrLn $ "No cabin "++name++" found!"
        Just c@(Cabin _ (CabinInfo _ bins)) -> do
          stat <- cabinStatus c
          case stat of
            Loaded -> return ()
            Unloaded -> do
              mapM_ (\a -> createSymbolicLink a (linkPath a)) bins
              writeCabinStatus c Loaded

unload :: LoadOpts -> IO ()
unload opts = do
    prepareEnvironment
    cdb <- readCabinDB
    binPath <- getBinaryPath
    mapM_ (go cdb binPath) $ loNames opts
  where
    go cdb binPath name =
      let cabin = findCabinByName name cdb
          linkPath a = binPath </> (takeFileName a)
      in case cabin of
        Nothing -> putStrLn $ "No cabin "++name++" found!"
        Just c@(Cabin _ (CabinInfo _ bins)) -> do
          stat <- cabinStatus c
          case stat of
            Unloaded -> return ()
            Loaded -> do
              mapM_ (\a -> removeFile $ linkPath a) bins
              writeCabinStatus c Unloaded

reindex :: IO ()
reindex = do
  prepareEnvironment
  cp <- getCabinPath
  putativeCabins <- liftM (filter (
                            \a -> notElem (takeFileName a) [".", ".."]
                          )) $
                    getDirectoryContents cp
                  |> liftM (cp </>)
                  >>= filterM doesDirectoryExist
  putStrLn . show $ putativeCabins
  cabins <- mapM infoCabin putativeCabins
  writeCabinDB $ CabinDB cabins

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

prepareEnvironment :: IO ()
prepareEnvironment = do
  getCabinPath >>= createDirectoryIfMissing True
  getBinaryPath >>= createDirectoryIfMissing True
  getLoadedPath >>= createDirectoryIfMissing True