import Control.Exception (throw)
import Control.Monad (forM_, when)
import Data.Maybe (fromMaybe)
import Distribution.PackageDescription
import Distribution.Simple
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo(..), localPkgDescr)
import Distribution.Simple.PreProcess
import Distribution.Simple.Program.Find
  ( defaultProgramSearchPath
  , findProgramOnSearchPath
  )
import Distribution.Simple.Setup
import Distribution.Simple.Utils
  ( IODataMode(IODataModeBinary)
  , maybeExit
  , rawSystemStdInOut
  )
import Distribution.Verbosity (Verbosity)
import qualified Distribution.Verbosity as Verbosity
import GHC.Generics
import System.Directory
  ( XdgDirectory(..)
  , copyFile
  , createDirectory
  , createDirectoryIfMissing
  , doesDirectoryExist
  , doesFileExist
  , getCurrentDirectory
  , getDirectoryContents
  , getHomeDirectory
  , getXdgDirectory
  )
import System.Environment
import System.FilePath ((</>))
import Text.Regex
import Text.Regex.Base

main :: IO ()
main = defaultMainWithHooks hooks
  where
    hooks =
      simpleUserHooks
        { preConf =
            \_ flags -> do
              rsMake (fromFlag $ configVerbosity flags)
              pure emptyHookedBuildInfo
        , hookedPreProcessors = knownSuffixHandlers
        , confHook = \a flags -> confHook simpleUserHooks a flags >>= rsAddDirs
        , postClean = \_ flags _ _ -> rsClean (fromFlag $ cleanVerbosity flags)
        }

rsFolder :: FilePath
rsFolder = "librustzcash-wrapper"

execCargo :: Verbosity -> String -> [String] -> IO ()
execCargo verbosity command args = do
  cargoPath <-
    findProgramOnSearchPath Verbosity.normal defaultProgramSearchPath "cargo"
  dir <- getCurrentDirectory
  let cargoExec =
        case cargoPath of
          Just (p, _) -> p
          Nothing -> "cargo"
      cargoArgs = command : args
      workingDir = Just (dir </> rsFolder)
      thirdComponent (_, _, c) = c
  maybeExit . fmap thirdComponent $
    rawSystemStdInOut
      verbosity
      cargoExec
      cargoArgs
      workingDir
      Nothing
      Nothing
      IODataModeBinary

rsMake :: Verbosity -> IO ()
rsMake verbosity = do
  execCargo verbosity "cbuild" []

rsAddDirs :: LocalBuildInfo -> IO LocalBuildInfo
rsAddDirs lbi' = do
  localData <- getXdgDirectory XdgData "zcash-haskell"
  createDirectoryIfMissing True localData
  dir <- getCurrentDirectory
  let rustIncludeDir =
        dir </> rsFolder </> "target/x86_64-unknown-linux-gnu/debug"
      rustLibDir = dir </> rsFolder </> "target/x86_64-unknown-linux-gnu/debug"
      updateLbi lbi = lbi {localPkgDescr = updatePkgDescr (localPkgDescr lbi)}
      updatePkgDescr pkgDescr =
        pkgDescr {library = updateLib <$> library pkgDescr}
      updateLib lib = lib {libBuildInfo = updateLibBi (libBuildInfo lib)}
      updateLibBi libBuild =
        libBuild
          { includeDirs = rustIncludeDir : includeDirs libBuild
          , extraLibDirs = rustLibDir : extraLibDirs libBuild
          }
  copyDir rustLibDir localData
  pure $ updateLbi lbi'

rsClean :: Verbosity -> IO ()
rsClean verbosity = execCargo verbosity "clean" []

cabalFlag :: FlagName -> ConfigFlags -> Bool
cabalFlag name =
  fromMaybe False . lookupFlagAssignment name . configConfigurationsFlags

unlessFlagM :: FlagName -> ConfigFlags -> IO () -> IO ()
unlessFlagM name flags action
  | cabalFlag name flags = pure ()
  | otherwise = action

applyUnlessM :: FlagName -> ConfigFlags -> (a -> IO a) -> a -> IO a
applyUnlessM name flags apply a
  | cabalFlag name flags = pure a
  | otherwise = apply a

copyDir :: FilePath -> FilePath -> IO ()
copyDir src dst = do
  whenM (not <$> doesDirectoryExist src) $
    throw (userError "source does not exist")
  --whenM (doesFileOrDirectoryExist dst) $
    --throw (userError "destination already exists")
  createDirectoryIfMissing True dst
  content <- getDirectoryContents src
  let xs = filter (`notElem` [".", ".."]) content
  forM_ xs $ \name -> do
    let srcPath = src </> name
    let dstPath = dst </> name
    isDirectory <- doesDirectoryExist srcPath
    if isDirectory
      then copyDir srcPath dstPath
      else copyFile srcPath dstPath
  where
    doesFileOrDirectoryExist x = orM [doesDirectoryExist x, doesFileExist x]
    orM xs = or <$> sequence xs
    whenM s r = s >>= flip when r