    {-# LANGUAGE CPP #-}
    {-# LANGUAGE RecordWildCards #-}
    module Hpack (
    -- | /__NOTE:__/ This module is exposed to allow integration of Hpack into
    -- other tools.  It is not meant for general use by end users.  The following
    -- caveats apply:
    --
    -- * The API is undocumented, consult the source instead.
    --
    -- * The exposed types and functions primarily serve Hpack's own needs, not
    -- that of a public API.  Breaking changes can happen as Hpack evolves.
    --
    -- As an Hpack user you either want to use the @hpack@ executable or a build
    -- tool that supports Hpack (e.g. @stack@ or @cabal2nix@).

    -- * Version
      version

    -- * Running Hpack
    , hpack
    , hpackResult
    , printResult
    , Result(..)
    , Status(..)

    -- * Options
    , defaultOptions
    , setProgramName
    , setTarget
    , setDecode
    , getOptions
    , Verbose(..)
    , Options(..)
    , Force(..)

    #ifdef TEST
    , hpackResultWithVersion
    , header
    #endif
    ) where

    import           Control.Monad
    import           Data.Version (Version)
    import qualified Data.Version as Version
    import           System.FilePath
    import           System.Environment
    import           System.Exit
    import           System.IO (stderr)
    import           Data.Aeson (Value)

    import           Paths_hpack (version)
    import           Hpack.Options
    import           Hpack.Config
    import           Hpack.Render
    import           Hpack.Util
    import           Hpack.Utf8 as Utf8
    import           Hpack.CabalFile

    programVersion :: Version -> String
    programVersion v = "hpack version " ++ Version.showVersion v

    header :: FilePath -> Version -> Hash -> String
    header p v hash = unlines [
        "-- This file has been generated from " ++ takeFileName p ++ " by " ++ programVersion v ++ "."
      , "--"
      , "-- see: https://github.com/sol/hpack"
      , "--"
      , "-- hash: " ++ hash
      , ""
      ]

    data Options = Options {
      optionsDecodeOptions :: DecodeOptions
    , optionsForce :: Force
    , optionsToStdout :: Bool
    }

    getOptions :: FilePath -> [String] -> IO (Maybe (Verbose, Options))
    getOptions defaultPackageConfig args = do
      result <- parseOptions defaultPackageConfig args
      case result of
        PrintVersion -> do
          putStrLn (programVersion version)
          return Nothing
        PrintNumericVersion -> do
          putStrLn (Version.showVersion version)
          return Nothing
        Help -> do
          printHelp
          return Nothing
        Run options -> case options of
          ParseOptions verbose force toStdout file -> do
            return $ Just (verbose, Options defaultDecodeOptions {decodeOptionsTarget = file} force toStdout)
        ParseError -> do
          printHelp
          exitFailure

    printHelp :: IO ()
    printHelp = do
      name <- getProgName
      Utf8.hPutStrLn stderr $ unlines [
          "Usage: " ++ name ++ " [ --silent ] [ --force | -f ] [ PATH ] [ - ]"
        , "       " ++ name ++ " --version"
        , "       " ++ name ++ " --numeric-version"
        , "       " ++ name ++ " --help"
        ]

    hpack :: Verbose -> Options -> IO ()
    hpack verbose options = hpackResult options >>= printResult verbose

    defaultOptions :: Options
    defaultOptions = Options defaultDecodeOptions NoForce False

    setTarget :: FilePath -> Options -> Options
    setTarget target options@Options{..} =
      options {optionsDecodeOptions = optionsDecodeOptions {decodeOptionsTarget = target}}

    setProgramName :: ProgramName -> Options -> Options
    setProgramName name options@Options{..} =
      options {optionsDecodeOptions = optionsDecodeOptions {decodeOptionsProgramName = name}}

    setDecode :: (FilePath -> IO (Either String ([String], Value))) -> Options -> Options
    setDecode decode options@Options{..} =
      options {optionsDecodeOptions = optionsDecodeOptions {decodeOptionsDecode = decode}}

    data Result = Result {
      resultWarnings :: [String]
    , resultCabalFile :: String
    , resultStatus :: Status
    } deriving (Eq, Show)

    data Status =
        Generated
      | ExistingCabalFileWasModifiedManually
      | AlreadyGeneratedByNewerHpack
      | OutputUnchanged
      deriving (Eq, Show)

    printResult :: Verbose -> Result -> IO ()
    printResult verbose r = do
      printWarnings (resultWarnings r)
      when (verbose == Verbose) $ putStrLn $
        case resultStatus r of
          Generated -> "generated " ++ resultCabalFile r
          OutputUnchanged -> resultCabalFile r ++ " is up-to-date"
          AlreadyGeneratedByNewerHpack -> resultCabalFile r ++ " was generated with a newer version of hpack, please upgrade and try again."
          ExistingCabalFileWasModifiedManually -> resultCabalFile r ++ " was modified manually, please use --force to overwrite."
      case resultStatus r of
          Generated -> return ()
          OutputUnchanged -> return ()
          AlreadyGeneratedByNewerHpack -> exitFailure
          ExistingCabalFileWasModifiedManually -> exitFailure

    printWarnings :: [String] -> IO ()
    printWarnings = mapM_ $ Utf8.hPutStrLn stderr . ("WARNING: " ++)

    mkStatus :: [String] -> Version -> CabalFile -> Status
    mkStatus new v (CabalFile mOldVersion mHash old) = case (mOldVersion, mHash) of
      (Nothing, _) -> ExistingCabalFileWasModifiedManually
      (Just oldVersion, _) | oldVersion < makeVersion [0, 20, 0] -> Generated
      (_, Nothing) -> ExistingCabalFileWasModifiedManually
      (Just oldVersion, Just hash)
        | old == new -> OutputUnchanged
        | v < oldVersion -> AlreadyGeneratedByNewerHpack
        | sha256 (unlines old) /= hash -> ExistingCabalFileWasModifiedManually
        | otherwise -> Generated

    hpackResult :: Options -> IO Result
    hpackResult = hpackResultWithVersion version

    hpackResultWithVersion :: Version -> Options -> IO Result
    hpackResultWithVersion v (Options options force toStdout) = do
      DecodeResult pkg cabalVersion cabalFile warnings <- readPackageConfig options >>= either die return
      oldCabalFile <- readCabalFile cabalFile
      let
        body = renderPackage (maybe [] cabalFileContents oldCabalFile) pkg
        withoutHeader = cabalVersion ++ body
      let
        status = case force of
          Force -> Generated
          NoForce -> maybe Generated (mkStatus (lines withoutHeader) v) oldCabalFile
      case status of
        Generated -> do
          let hash = sha256 withoutHeader
              out  = cabalVersion ++ header (decodeOptionsTarget options) v hash ++ body
          if toStdout
            then Utf8.putStr out
            else Utf8.writeFile cabalFile out
        _ -> return ()
      return Result {
          resultWarnings = warnings
        , resultCabalFile = cabalFile
        , resultStatus = status
        }