# LANGUAGE CPP #
# LANGUAGE RecordWildCards #
# LANGUAGE ScopedTypeVariables #
{-# LANGUAGE OverloadedStrings #-}

module Main where
import System.Process
import Control.Monad.Identity
#if MIN_VERSION_optparse_applicative(0,13,0)
import Data.Monoid ((<>))
#endif
import Data.Yaml hiding (Parser)
import Data.Yaml.Config
import qualified Data.Map as M
import Data.Map (Map)
import Data.Aeson.Types (typeMismatch)
import Options.Applicative
import Data.List (intercalate)
import System.Directory (createDirectoryIfMissing, canonicalizePath)
import Prelude hiding (log)

parens :: [Char] -> [Char]
parens x = "(" ++ x ++ ")"

-----------------------------------------
-- OPTIONS

data Command = Configure | Cabal [String] | Clean | Command :<> Command

withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc

parseExec :: Parser Command
parseExec = (\rest -> Cabal (["v2-exec","--"] ++ rest)) <$> some (argument str (metavar "COMMAND"))

parseRepl :: Parser Command
parseRepl = (\target -> Cabal (["v2-repl"] ++ maybe [] (:[]) target)) <$> optional (argument str (metavar "TARGET"))

parseCabal :: Parser Command
parseCabal = Cabal <$> some (argument str (metavar "COMMAND"))

parseCommand :: Parser Command
parseCommand = subparser $
    command "configure" (pure Configure `withInfo` "Re-configure the project on the basis of the styx.yaml file") <>
    command "clean"     (pure Clean `withInfo` "Remove all styx working files") <>
    command "build"     (pure (Cabal ["v2-build","all"]) `withInfo` "build all the packages") <>
    command "repl"      (parseRepl `withInfo` "Start a repl in the nix-shell'ed 1st component of the cabal project") <>
    command "exec"      (parseExec `withInfo` "Exec a command in the nix-shell'ed cabal project") <>
    command "cabal"     (parseCabal `withInfo` "Execute an arbitrary cabal command in the nix-shell")

main :: IO ()
main = run =<< execParser (parseCommand `withInfo` "Wrapper around nix-shell, cabal2nix and cabal")

--------------------------------
-- Configuration
data Repo = Repo {repoLocation :: String,
                  repoRevision :: Maybe String,
                  repoCabal2NixFlags :: Maybe String}
data Config =
  Config {cfgNixpkgsVersion :: Maybe SourceVersion
         ,cfgLocalPackages :: Map String Repo -- list of local packages (must be on the local filesystem)
         ,cfgExternalSourceDeps :: Map String Repo -- mapping of package names to locations as understood by cabal2nix
         ,cfgNixHsDeps :: [String]
  ^   to fetch directly from nix ( usually empty for a cabal project , as the cabal file will   )
         ,cfgNixOtherDeps :: [String]
         -- ^ Other nix dependencies (non haskell packages)
         ,cfgDefCompil :: Maybe String
         }

data ShellConfig = ShellConfig {}

instance FromJSON Config where
  parseJSON (Object v) = Config <$>
                         v .:? "nixpkgs" <*>
                         v .:? "local-packages" .!= M.empty  <*>
                         v .:? "source-deps" .!= M.empty  <*>
                         v .:? "nix-deps" .!= []  <*>
                         v .:? "non-haskell-deps" .!= [] <*>
                         v .:? "default-compiler"
  parseJSON invalid = typeMismatch "Config" invalid

instance FromJSON Repo where
  parseJSON (Object v) = Repo  <$>
                         v .: "location"  <*> -- location of the repo (in cabal2nix format)
                         v .:? "revision" <*>
                         v .:? "cabal2nix"

  parseJSON invalid = typeMismatch "Location" invalid

data SourceVersion = GitVersion {gitOwner :: String, gitCommit :: String, gitSha :: String}
                   | TarballVersion {tarballURL :: String}
instance FromJSON SourceVersion where
  parseJSON (Object v) = (GitVersion <$>
                         v .:? "owner" .!= "NixOS" <*>
                         v .: "commit" <*>
                         v .: "sha256")
                       <|> (TarballVersion <$> v .: "url")
  parseJSON invalid = typeMismatch "Git version" invalid


-----------------------------------------
  Program

locToNix :: String -> Repo -> IO ()
locToNix p (Repo {..}) = do
  cmd $ intercalate " " ["cabal2nix",
                          maybe "" ("--revision=" ++) repoRevision,
                          maybe "" id repoCabal2NixFlags,
                          repoLocation, "> .styx/" ++ p ++ ".nix"]

canonicalizeLocalPath :: Repo -> IO Repo
canonicalizeLocalPath (Repo {repoLocation = d,..}) = do
  repoLocation <- canonicalizePath d
  return (Repo {..})

run :: Command -> IO ()
run c = case c of
  a :<> b -> run a >> run b
  Configure -> configure
  Cabal args -> do
    _ <- cmd ("nix-shell .styx/shell.nix --pure --run " ++ show (intercalate " " ("cabal":args)))
    return ()
  Clean -> cmd "rm -rf .styx"

cmd :: String -> IO ()
cmd x = do
  putStrLn x
  callCommand x

log :: String -> IO ()
log msg = putStrLn $ "Styx: " ++ msg

configure :: IO ()
configure = do
  Config{..} <- loadYamlSettings ["styx.yaml"] [] ignoreEnv
  createDirectoryIfMissing False ".styx"

  log "Initializing cabal.project"
  writeFile "cabal.project" $ unlines $ ("packages:" : ["  " ++ repoLocation ++ "/" ++ projectName ++ ".cabal"
                                                       | (projectName,Repo {..}) <- M.assocs cfgLocalPackages ] )

  log "Running cabal2nix for all local and external packages"
  forM_ (M.assocs cfgLocalPackages) $ \(p,r) -> locToNix p =<< (canonicalizeLocalPath r)
  forM_ (M.assocs cfgExternalSourceDeps) (uncurry locToNix)


  log "Creating shell.nix file"
  writeFile ".styx/shell.nix" $ unlines $
    ["{ nixpkgs ? import <nixpkgs> {}"
    , maybe "" ((", compiler ? " ++) . show) cfgDefCompil
    ," }:"]
    ++ case cfgNixpkgsVersion of
      Nothing -> ["let nixpkgs' = nixpkgs;"]
      Just source -> ["let nixpkgs_source ="] ++ case source of
        GitVersion {..} -> ["  nixpkgs.fetchFromGitHub {"
                           ,"      owner = " ++ show gitOwner ++ ";"
                           ,"      repo = \"nixpkgs\";"
                           ,"      rev = " ++ show gitCommit ++ ";"
                           ,"      sha256 = " ++ show gitSha ++ ";"
                           ,"    };"
                           ]
        TarballVersion {..} -> ["fetchTarball " ++ show tarballURL ++ ";"]
       ++ ["  nixpkgs' = (import nixpkgs_source){};"]
    ++ ["in with nixpkgs'.pkgs;"
       ,"let hp = " ++ maybe "haskellPackages" (const "haskell.packages.${compiler}") cfgDefCompil ++ ".override{"
       ,"    overrides = self: super: {"
       ]
    ++ ["      " ++ n ++ " = self.callPackage ./" ++ n ++ ".nix {};"
       | n <- (M.keys cfgExternalSourceDeps ++ M.keys cfgLocalPackages)]
    ++ ["      };};"
       ,"     getHaskellDeps = ps: path:"
       ,"        let f = import path;"
       ,"            gatherDeps = { " ++ concat [d ++ " ? [], " | d <- depKinds] ++ "...}:"
       ,"               " ++ intercalate " ++ " depKinds ++ ";"
       ,"            x = f (builtins.intersectAttrs (builtins.functionArgs f)"
       ,"                                               (ps // ",
        "                                                nixpkgs'.pkgs) # can also depend on non-haskell packages",
        "                   // {lib = lib; mkDerivation = gatherDeps;});"
       ,"        in x;"
       ,"ghc = hp.ghcWithPackages (ps: with ps; lib.lists.subtractLists"
       , "[" ++ intercalate " " (M.keys cfgLocalPackages) ++ "]" -- Here we remove the packages that we provide locally in the sandbox
       , "([ cabal-install "
       , intercalate " " (M.keys cfgExternalSourceDeps ++ cfgNixHsDeps)
       ,"  ] " ++ concat [" ++ getHaskellDeps ps ./" ++ n ++ ".nix"| n <- M.keys cfgLocalPackages] ++ "));"
       ,"in"
       ,"pkgs.stdenv.mkDerivation {"
       ,"  name = \"my-haskell-env-0\";"
       ,"  buildInputs = [ glibcLocales ghc " ++ intercalate " " (map parens cfgNixOtherDeps) ++ "];" -- todo system build inputs here
       ,"  shellHook = ''"
       ," export LANG=en_US.UTF-8"
       ," eval $(egrep ^export ${ghc}/bin/ghc)"
       ,"'';"
       ,"}"]
  this will fail unless the sandbox dependencies are built first .

depKinds :: [String]
depKinds = ["buildDepends", "libraryHaskellDepends", "executableHaskellDepends", "libraryToolDepends", "executableToolDepends"]

-- Local Variables:
-- dante-methods: (impure-nix)
-- End:

# LANGUAGE OverloadedStrings #
---------------------------------------
 OPTIONS
------------------------------
 Configuration
 list of local packages (must be on the local filesystem)
 mapping of package names to locations as understood by cabal2nix
 ^ Other nix dependencies (non haskell packages)
 location of the repo (in cabal2nix format)
---------------------------------------
 Here we remove the packages that we provide locally in the sandbox
 todo system build inputs here
 Local Variables:
 dante-methods: (impure-nix)
 End:
# LANGUAGE CPP #
# LANGUAGE RecordWildCards #
# LANGUAGE ScopedTypeVariables #

module Main where
import System.Process
import Control.Monad.Identity
#if MIN_VERSION_optparse_applicative(0,13,0)
import Data.Monoid ((<>))
#endif
import Data.Yaml hiding (Parser)
import Data.Yaml.Config
import qualified Data.Map as M
import Data.Map (Map)
import Data.Aeson.Types (typeMismatch)
import Options.Applicative
import Data.List (intercalate)
import System.Directory (createDirectoryIfMissing, canonicalizePath)
import Prelude hiding (log)

parens :: [Char] -> [Char]
parens x = "(" ++ x ++ ")"


data Command = Configure | Cabal [String] | Clean | Command :<> Command

withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc

parseExec :: Parser Command
parseExec = (\rest -> Cabal (["v2-exec","--"] ++ rest)) <$> some (argument str (metavar "COMMAND"))

parseRepl :: Parser Command
parseRepl = (\target -> Cabal (["v2-repl"] ++ maybe [] (:[]) target)) <$> optional (argument str (metavar "TARGET"))

parseCabal :: Parser Command
parseCabal = Cabal <$> some (argument str (metavar "COMMAND"))

parseCommand :: Parser Command
parseCommand = subparser $
    command "configure" (pure Configure `withInfo` "Re-configure the project on the basis of the styx.yaml file") <>
    command "clean"     (pure Clean `withInfo` "Remove all styx working files") <>
    command "build"     (pure (Cabal ["v2-build","all"]) `withInfo` "build all the packages") <>
    command "repl"      (parseRepl `withInfo` "Start a repl in the nix-shell'ed 1st component of the cabal project") <>
    command "exec"      (parseExec `withInfo` "Exec a command in the nix-shell'ed cabal project") <>
    command "cabal"     (parseCabal `withInfo` "Execute an arbitrary cabal command in the nix-shell")

main :: IO ()
main = run =<< execParser (parseCommand `withInfo` "Wrapper around nix-shell, cabal2nix and cabal")

data Repo = Repo {repoLocation :: String,
                  repoRevision :: Maybe String,
                  repoCabal2NixFlags :: Maybe String}
data Config =
  Config {cfgNixpkgsVersion :: Maybe SourceVersion
         ,cfgNixHsDeps :: [String]
  ^   to fetch directly from nix ( usually empty for a cabal project , as the cabal file will   )
         ,cfgNixOtherDeps :: [String]
         ,cfgDefCompil :: Maybe String
         }

data ShellConfig = ShellConfig {}

instance FromJSON Config where
  parseJSON (Object v) = Config <$>
                         v .:? "nixpkgs" <*>
                         v .:? "local-packages" .!= M.empty  <*>
                         v .:? "source-deps" .!= M.empty  <*>
                         v .:? "nix-deps" .!= []  <*>
                         v .:? "non-haskell-deps" .!= [] <*>
                         v .:? "default-compiler"
  parseJSON invalid = typeMismatch "Config" invalid

instance FromJSON Repo where
  parseJSON (Object v) = Repo  <$>
                         v .:? "revision" <*>
                         v .:? "cabal2nix"

  parseJSON invalid = typeMismatch "Location" invalid

data SourceVersion = GitVersion {gitOwner :: String, gitCommit :: String, gitSha :: String}
                   | TarballVersion {tarballURL :: String}
instance FromJSON SourceVersion where
  parseJSON (Object v) = (GitVersion <$>
                         v .:? "owner" .!= "NixOS" <*>
                         v .: "commit" <*>
                         v .: "sha256")
                       <|> (TarballVersion <$> v .: "url")
  parseJSON invalid = typeMismatch "Git version" invalid


  Program

locToNix :: String -> Repo -> IO ()
locToNix p (Repo {..}) = do
  cmd $ intercalate " " ["cabal2nix",
                          maybe "" ("--revision=" ++) repoRevision,
                          maybe "" id repoCabal2NixFlags,
                          repoLocation, "> .styx/" ++ p ++ ".nix"]

canonicalizeLocalPath :: Repo -> IO Repo
canonicalizeLocalPath (Repo {repoLocation = d,..}) = do
  repoLocation <- canonicalizePath d
  return (Repo {..})

run :: Command -> IO ()
run c = case c of
  a :<> b -> run a >> run b
  Configure -> configure
  Cabal args -> do
    _ <- cmd ("nix-shell .styx/shell.nix --pure --run " ++ show (intercalate " " ("cabal":args)))
    return ()
  Clean -> cmd "rm -rf .styx"

cmd :: String -> IO ()
cmd x = do
  putStrLn x
  callCommand x

log :: String -> IO ()
log msg = putStrLn $ "Styx: " ++ msg

configure :: IO ()
configure = do
  Config{..} <- loadYamlSettings ["styx.yaml"] [] ignoreEnv
  createDirectoryIfMissing False ".styx"

  log "Initializing cabal.project"
  writeFile "cabal.project" $ unlines $ ("packages:" : ["  " ++ repoLocation ++ "/" ++ projectName ++ ".cabal"
                                                       | (projectName,Repo {..}) <- M.assocs cfgLocalPackages ] )

  log "Running cabal2nix for all local and external packages"
  forM_ (M.assocs cfgLocalPackages) $ \(p,r) -> locToNix p =<< (canonicalizeLocalPath r)
  forM_ (M.assocs cfgExternalSourceDeps) (uncurry locToNix)


  log "Creating shell.nix file"
  writeFile ".styx/shell.nix" $ unlines $
    ["{ nixpkgs ? import <nixpkgs> {}"
    , maybe "" ((", compiler ? " ++) . show) cfgDefCompil
    ," }:"]
    ++ case cfgNixpkgsVersion of
      Nothing -> ["let nixpkgs' = nixpkgs;"]
      Just source -> ["let nixpkgs_source ="] ++ case source of
        GitVersion {..} -> ["  nixpkgs.fetchFromGitHub {"
                           ,"      owner = " ++ show gitOwner ++ ";"
                           ,"      repo = \"nixpkgs\";"
                           ,"      rev = " ++ show gitCommit ++ ";"
                           ,"      sha256 = " ++ show gitSha ++ ";"
                           ,"    };"
                           ]
        TarballVersion {..} -> ["fetchTarball " ++ show tarballURL ++ ";"]
       ++ ["  nixpkgs' = (import nixpkgs_source){};"]
    ++ ["in with nixpkgs'.pkgs;"
       ,"let hp = " ++ maybe "haskellPackages" (const "haskell.packages.${compiler}") cfgDefCompil ++ ".override{"
       ,"    overrides = self: super: {"
       ]
    ++ ["      " ++ n ++ " = self.callPackage ./" ++ n ++ ".nix {};"
       | n <- (M.keys cfgExternalSourceDeps ++ M.keys cfgLocalPackages)]
    ++ ["      };};"
       ,"     getHaskellDeps = ps: path:"
       ,"        let f = import path;"
       ,"            gatherDeps = { " ++ concat [d ++ " ? [], " | d <- depKinds] ++ "...}:"
       ,"               " ++ intercalate " ++ " depKinds ++ ";"
       ,"            x = f (builtins.intersectAttrs (builtins.functionArgs f)"
       ,"                                               (ps // ",
        "                                                nixpkgs'.pkgs) # can also depend on non-haskell packages",
        "                   // {lib = lib; mkDerivation = gatherDeps;});"
       ,"        in x;"
       ,"ghc = hp.ghcWithPackages (ps: with ps; lib.lists.subtractLists"
       , "([ cabal-install "
       , intercalate " " (M.keys cfgExternalSourceDeps ++ cfgNixHsDeps)
       ,"  ] " ++ concat [" ++ getHaskellDeps ps ./" ++ n ++ ".nix"| n <- M.keys cfgLocalPackages] ++ "));"
       ,"in"
       ,"pkgs.stdenv.mkDerivation {"
       ,"  name = \"my-haskell-env-0\";"
       ,"  shellHook = ''"
       ," export LANG=en_US.UTF-8"
       ," eval $(egrep ^export ${ghc}/bin/ghc)"
       ,"'';"
       ,"}"]
  this will fail unless the sandbox dependencies are built first .

depKinds :: [String]
depKinds = ["buildDepends", "libraryHaskellDepends", "executableHaskellDepends", "libraryToolDepends", "executableToolDepends"]

