{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}

module Main where

import Data.Maybe (listToMaybe, mapMaybe)
import Stackage.CLI
import Filesystem
import Control.Exception (Exception, catch)
import Control.Monad
import Control.Monad.Catch (MonadThrow, throwM)
import Control.Applicative
import Data.Monoid
import Data.Typeable (Typeable)
import Options.Applicative (Parser, flag, long, help)
import System.Process (readProcess)
import Data.Char (toLower)
import System.IO (stdout, stderr, hFlush, hPutStrLn)
import qualified Data.Text.Encoding as T
import qualified Data.Text as T
import System.Environment (getArgs)
import System.Exit (exitFailure)
import qualified Paths_stackage_cabal as CabalInfo

import Text.Parsec hiding ((<|>), many)
type ParsecParser = Parsec String ()

data Force = Prompt | Force
data PurgeOpts = PurgeOpts
  { purgeOptsForce :: Force }

data PackageGroup = PackageGroup
  { packageGroupDb :: String
  , packageGroupPackages :: [String]
  }

data PurgeException
  = ParsePackagesError ParseError
  deriving (Show, Typeable)
instance Exception PurgeException

prompt :: String -> IO String
prompt str = putStr str >> hFlush stdout >> getLine

whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
whenJust (Just a) f = f a
whenJust Nothing _ = return ()

pluralize :: Int -> a -> a -> a
pluralize 1 a _ = a
pluralize _ _ a = a

unregisterPackages :: String -> [String] -> IO ()
unregisterPackages packageDb = mapM_ unregister where
  unregister package = do
    putStrLn $ "Unregistering: " <> package
    _ <- readProcess "ghc-pkg" (args package) ""
    return ()
  args package =
    [ "unregister"
    , package
    , "--force"
    ] <> dbToArgs (Just packageDb)

parsePackageDb :: IO (Maybe String)
parsePackageDb = do
  cabalSandboxConfigExists <- isFile "cabal.sandbox.config"
  if cabalSandboxConfigExists
    then do
      t <- Filesystem.readTextFile "cabal.sandbox.config"
      let packageDbLine = T.stripPrefix "package-db: "
      return $ fmap T.unpack $ listToMaybe $ mapMaybe packageDbLine $ T.lines t
    else
      return Nothing

dbToArgs :: Maybe String -> [String]
dbToArgs Nothing = []
dbToArgs (Just packageDb) =
  [ "--package-db"
  , packageDb
  ]

getGlobalPackageDb :: IO (Maybe String)
getGlobalPackageDb = do
  let fakePackage = "asdklfjasdklfajsdlkghaiwojgadjfkq"
  output <- readProcess "ghc-pkg" ["list", fakePackage] ""
  return $ fmap init $ listToMaybe (lines output)
  -- fmap init is to get rid of the trailing colon

getPackages :: Maybe String -> IO [PackageGroup]
getPackages mPackageDb = parsePackages =<< readProcess "ghc-pkg" args "" where
  args = ["list"] <> dbToArgs mPackageDb

parsePackages :: MonadThrow m => String -> m [PackageGroup]
parsePackages
  = either (throwM . ParsePackagesError) return
  . parse packagesParser ""

-- #28
#if !MIN_VERSION_parsec(3,1,6)
crlf :: ParsecParser Char
crlf = char '\r' *> char '\n' <?> "crlf new-line"

endOfLine :: ParsecParser Char
endOfLine = newline <|> crlf <?> "new-line"
#endif

ending :: ParsecParser ()
ending = eof <|> void endOfLine

packagesParser :: ParsecParser [PackageGroup]
packagesParser = many1 parseGroup

parseGroup :: ParsecParser PackageGroup
parseGroup = PackageGroup <$> parseDb <*> parseDbPackages <* many endOfLine

parseDb :: ParsecParser String
parseDb = manyTill anyChar $ try (char ':' *> ending)

parseDbPackages :: ParsecParser [String]
parseDbPackages = try parseNoPackages <|> many1 parsePackage

parseNoPackages :: ParsecParser [String]
parseNoPackages = many1 (char ' ') *> string "(no packages)" *> ending *> pure []

parsePackage :: ParsecParser String
parsePackage = many1 (char ' ') *> manyTill anyChar ending


purge :: PurgeOpts -> IO ()
purge opts = do
  cabalConfigExists <- isFile "cabal.config"
  when cabalConfigExists $ do
    removeFile "cabal.config"

  globalPackageDbMay <- getGlobalPackageDb
  sandboxPackageDbMay <- parsePackageDb

  let displaySandbox s
        | Just s == globalPackageDbMay =
          "(Global) " <> s
        | Just s == sandboxPackageDbMay =
          "(Sandbox) " <> s
        | otherwise = s

  packages <- getPackages sandboxPackageDbMay
  forM_ packages $ \(PackageGroup db packages) -> do
    putStrLn $ displaySandbox db
    let nPackages = length packages
    let showNPackages
           = show nPackages
          <> " "
          <> pluralize nPackages "package" "packages"

    putStrLn
      $ "Detected "
     <> showNPackages
     <> " to purge from this database"

    when (nPackages > 0) $ do
      when (nPackages < 15) $ mapM_ putStrLn packages
      shouldUnregister <- case purgeOptsForce opts of
        Force -> do
          putStrLn $ "(--force) Unregistering " <> showNPackages
          return True
        Prompt -> do
          line <- prompt
            $ "Unregister " <> showNPackages <> " (y/n)? [default: n] "
          case map toLower line of
            "y"   -> return True
            "yes" -> return True
            _   -> return False
      when shouldUnregister $ unregisterPackages db packages
  return ()

purgeOptsParser :: Parser PurgeOpts
purgeOptsParser = PurgeOpts <$> forceOpt where
  forceOpt = flag Prompt Force mods
  mods = long "force"
      <> help "Purge all packages without prompt"

version :: String
version = $(simpleVersion CabalInfo.version)

header :: String
header = "Delete cabal.config and purge your package database"

progDesc :: String
progDesc = header

handlePurgeExceptions :: PurgeException -> IO ()
handlePurgeExceptions (ParsePackagesError _) = do
  hPutStrLn stderr $ "Failed to parse ghc-pkg output"
  exitFailure

main :: IO ()
main = do
  (opts, ()) <- simpleOptions
    version
    header
    progDesc
    purgeOptsParser -- global parser
    empty           -- subcommands
  purge opts `catch` handlePurgeExceptions
