{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}

module Main (main) where

import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson as A hiding (Options, defaultOptions)
import qualified Data.Aeson.Key             as A
import qualified Data.Aeson.KeyMap          as HM
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.List as L
import Data.String.Interpolate
import Data.Text as T hiding (count)
import qualified Data.Vector as V
import qualified Data.Yaml as Yaml
import GHC.Generics
import Options.Applicative hiding (info)
import System.Exit
import System.FilePath
import Test.Sandwich
import UnliftIO.Exception
import UnliftIO.MVar
import UnliftIO.Process
import UnliftIO.QSem


data Args = Args {
  countFilePath :: FilePath
  , topN :: Int
  , parallelism :: Int
  , juliaAttr :: Text
  }

argsParser :: Parser Args
argsParser = Args
  <$> strOption (long "count-file" <> short 'c' <> help "YAML file containing package names and counts")
  <*> option auto (long "top-n" <> short 'n' <> help "How many of the top packages to build" <> showDefault <> value 100 <> metavar "INT")
  <*> option auto (long "parallelism" <> short 'p' <> help "How many builds to run at once" <> showDefault <> value 10 <> metavar "INT")
  <*> strOption (long "julia-attr" <> short 'a' <> help "Which Julia attr to build with" <> showDefault <> value "julia" <> metavar "STRING")

data NameAndCount = NameAndCount {
  name :: Text
  , count :: Int
  , uuid :: Text
  } deriving (Show, Eq, Generic, FromJSON)

newtype JuliaPath = JuliaPath FilePath
  deriving Show

julia :: Label "julia" (MVar (Maybe JuliaPath))
julia = Label

main :: IO ()
main = do
  clo <- parseCommandLineArgs argsParser (return ())
  let args@(Args {..}) = optUserOptions clo

  namesAndCounts :: [NameAndCount] <- Yaml.decodeFileEither countFilePath >>= \case
    Left err -> throwIO $ userError ("Couldn't decode names and counts YAML file: " <> show err)
    Right x -> pure x

  runSandwichWithCommandLineArgs' defaultOptions argsParser $ parallel $ do
    miscTests args

    describe ("Building environments for top " <> show topN <> " Julia packages") $
      introduce "Introduce parallel semaphore" parallelSemaphore (liftIO $ newQSem parallelism) (const $ return ()) $
      parallel $
      forM_ (L.take topN namesAndCounts) $ \(NameAndCount {..}) ->
      around "Claim semaphore" claimRunSlot $
      testExpr args name [i|#{juliaAttr}.withPackages ["#{name}"]|]

miscTests :: Args -> SpecFree ctx IO ()
miscTests args@(Args {..}) = describe "Misc tests" $ do
  describe "works for a package outside the General registry" $ do
    testExpr args "HelloWorld" [iii|(#{juliaAttr}.withPackages.override {
                                      packageOverrides = {
                                        "HelloWorld" = pkgs.fetchFromGitHub {
                                          owner = "codedownio";
                                          repo = "HelloWorld.jl";
                                          rev = "9b41c55df76eb87830dd3bd0b5601ee2582a37c6";
                                          sha256 = "sha256-G+xpMRb0RopW/xWA8KCFF/S8wuHTQbpj0qwm9CihfSc=";
                                        };
                                      };
                                    }) [ "HelloWorld" ]|]

  describe "misc cases" $ do
    testExpr args "Optimization" [iii|(#{juliaAttr}.withPackages) [ "Optimization" "OptimizationOptimJL" ]|]

-- * Low-level

testExpr :: Args -> Text -> String -> SpecFree ctx IO ()
testExpr _args name expr = do
  introduce' (defaultNodeOptions { nodeOptionsVisibilityThreshold = 0 }) (T.unpack name) julia (newMVar Nothing) (const $ return ()) $ do
    it "Builds" $ do
      let cp = proc "nix" ["build", "--impure", "--no-link", "--json", "--expr", [i|with import ../../../../. {}; #{expr}|]]
      output <- readCreateProcessWithLogging cp ""
      juliaPath <- case A.eitherDecode (BL8.pack output) of
        Right (A.Array ((V.!? 0) -> Just (A.Object (aesonLookup "outputs" -> Just (A.Object (aesonLookup "out" -> Just (A.String t))))))) -> do
          info [i|built: #{t}|]
          pure (JuliaPath ((T.unpack t) </> "bin" </> "julia"))
        x -> expectationFailure ("Couldn't parse output: " <> show x)

      getContext julia >>= flip modifyMVar_ (const $ return (Just juliaPath))

    it "Uses" $ do
      getContext julia >>= readMVar >>= \case
        Nothing -> expectationFailure "Build step failed."
        Just (JuliaPath juliaPath) -> do
          let cp = proc juliaPath ["-e", "using " <> T.unpack name]
          createProcessWithLogging cp >>= waitForProcess >>= (`shouldBe` ExitSuccess)

  where
    aesonLookup :: Text -> HM.KeyMap v -> Maybe v
    aesonLookup = HM.lookup . A.fromText

claimRunSlot :: (HasParallelSemaphore ctx) => ExampleT ctx IO a -> ExampleT ctx IO ()
claimRunSlot f = do
  s <- getContext parallelSemaphore
  bracket_ (liftIO $ waitQSem s) (liftIO $ signalQSem s) (void f)
