{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.Extra
import Data.Ini.Config
import Data.List.Extra
import qualified Data.Text.IO as T
import SimpleCmd
import SimpleCmdArgs
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.Process

import Paths_stack_all (version)

-- FIXME allow specific snapshots?
-- FIXME lts latest
data Snapshot = LTS Int | Nightly
  deriving (Eq, Ord)

readSnap :: String -> Snapshot
readSnap "nightly" = Nightly
readSnap snap =
  if "lts" `isPrefixOf` snap then
    let major = read (dropPrefix "lts-" snap) in LTS major
  else error' $ "malformed snapshot " ++ snap

showSnap :: Snapshot -> String
showSnap Nightly = "nightly"
showSnap (LTS ver) = "lts-" ++ show ver

defaultOldest :: Snapshot
defaultOldest = LTS 11

allSnaps :: [Snapshot]
allSnaps = [Nightly, LTS 16, LTS 14, LTS 13, LTS 12, LTS 11,
            LTS 10, LTS 9, LTS 8, LTS 6, LTS 5, LTS 4, LTS 2, LTS 1]

data VersionSpec = DefaultVersions | Oldest Snapshot | AllVersions | VersionList [Snapshot]

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  unlessM (doesFileExist "stack.yaml") $
    error' "no stack.yaml found"
  simpleCmdArgs (Just version) "Build over Stackage versions"
    "stack-all builds projects easily across different Stackage versions" $
    main' <$>
    switchWith 'c' "create-config" "Create a project .stack-all file" <*>
    switchWith 'd' "debug" "Verbose stack build output on error" <*>
    optional (readSnap <$> strOptionWith 'n' "newest" "lts-MAJOR" "Newest LTS release to build from") <*>
    optional (strOptionWith 'C' "cmd" "COMMAND" "Specify a stack command [default: build]") <*>
    versionSpec
  where
    versionSpec =
      Oldest . readSnap <$> strOptionWith 'o' "oldest" "lts-MAJOR" "Oldest compatible LTS release" <|>
      VersionList . map readSnap <$> some (strArg "LTS") <|>
      flagWith DefaultVersions AllVersions 'a' "all-lts" "Try to build back to LTS 1 even"

main' :: Bool -> Bool -> Maybe Snapshot -> Maybe String -> VersionSpec -> IO ()
main' createConfig debug mnewest mcmd versionSpec = do
  if createConfig then
    case versionSpec of
      Oldest oldest -> createStackAll oldest
      _ -> error' "creating .stack-all requires --oldest LTS"
    else do
    versions <-
      case versionSpec of
        DefaultVersions -> do
          oldest <- fromMaybeM (return defaultOldest) getOldestLTS
          return $ filter (>= oldest) allSnaps
        AllVersions -> return allSnaps
        Oldest ver -> return $ filter (>= ver) allSnaps
        VersionList vers -> return vers
    configs <- filter isStackConf <$> listDirectory "."
    let newestFilter = maybe id (filter . (>=)) mnewest
    mapM_ (stackBuild configs debug mcmd) (newestFilter versions)
  where
    isStackConf :: FilePath -> Bool
    isStackConf f = "stack-" `isPrefixOf` f && "yaml" `isExtensionOf` f

stackAllFile :: FilePath
stackAllFile = ".stack-all"

createStackAll :: Snapshot -> IO ()
createStackAll snap = do
  exists <- doesFileExist stackAllFile
  if exists then error' $ stackAllFile ++ " already exists"
    else do
    writeFile stackAllFile $
      "[versions]\n# reason comment\noldest = " ++ showSnap snap ++ "\n"

getOldestLTS :: IO (Maybe Snapshot)
getOldestLTS = do
  haveConfig <- doesFileExist stackAllFile
  if haveConfig then
    Just . readSnap <$> readIniConfig stackAllFile rcParser id
    else return Nothing
  where
    rcParser :: IniParser String
    rcParser =
      section "versions" $
      fieldOf "oldest" string

    readIniConfig :: FilePath -> IniParser a -> (a -> b) -> IO b
    readIniConfig inifile iniparser fn = do
      ini <- T.readFile inifile
      return $ either error fn $ parseIniFile ini iniparser

stackBuild :: [FilePath] -> Bool -> Maybe String -> Snapshot -> IO ()
stackBuild configs debug mcmd snap = do
  let command = maybe ["build"] words mcmd
      config =
        case sort (filter (snapConfig <=) configs) of
          [] -> []
          (cfg:_) -> ["--stack-yaml", cfg]
      args = ["-v" | debug] ++ ["--resolver", showSnap snap] ++
             config ++ command
  if debug
    then debugBuild args
    else cmd_ "stack" args
  putStrLn ""
  where
    snapConfig :: FilePath
    snapConfig = "stack-" ++ compactSnap snap <.> "yaml"
      where
        compactSnap :: Snapshot -> String
        compactSnap Nightly = "nightly"
        compactSnap (LTS ver) = "lts" ++ show ver

    debugBuild :: [String] -> IO ()
    debugBuild args = do
      putStr $ "stack " ++ unwords args
      (ret,out,err) <- readProcessWithExitCode "stack" args ""
      putStrLn "\n"
      unless (null out) $ putStrLn out
      unless (ret == ExitSuccess) $ do
        -- stack verbose includes info line with all stackages (> 500kbytes)
        mapM_ putStrLn $ filter ((<10000) . length) . lines $ err
        error' $ showSnap snap ++ " build failed"
