{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}

import Data.Aeson           (encode, Value)
import qualified Data.ByteString      as S (getContents)
import qualified Data.ByteString.Lazy as L (putStr, writeFile)
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup       (Semigroup(..))
#endif
import Data.Yaml            (decodeFileEither, decodeEither')
import System.Exit          (die)

import Options.Applicative
  ( Parser
  , action, execParser, headerDoc, help, helper, info
  , long, metavar, short, strArgument, strOption, value
  )
import Options.Applicative.Help.Pretty
  ( vcat, pretty )

import Common
  ( versionText, versionOption, numericVersionOption, dashToNothing )

data Options = Options
  { optInput :: Maybe FilePath
      -- ^ 'Nothing' means @stdin@.
  , optOutput :: Maybe FilePath
      -- ^ 'Nothing' means @stdout@.
  }

-- | Name of the executable.

self :: String
self = "yaml2json"

-- | Parse options; handle parse errors, @--help@, @--version@.

options :: IO Options
options =
  execParser $
    info (helper <*> versionOption self <*> numericVersionOption <*> programOptions)
         (headerDoc hdoc)

  where
  hdoc = Just $ vcat $ map pretty
    [ versionText self
    , "Convert YAML to JSON."
    ]

  programOptions = Options <$> oInput <*> oOutput

  oInput :: Parser (Maybe FilePath)
  oInput = dashToNothing <$> do
    strArgument
      $  metavar "IN"
      <> value "-"
      <> action "file"
      <> help "The input file containing the YAML document; use '-' for stdin (default)."

  oOutput :: Parser (Maybe FilePath)
  oOutput = dashToNothing <$> do
    strOption
      $  long "output"
      <> short 'o'
      <> value "-"
      <> metavar "OUT"
      <> action "file"
      <> help "The file to hold the produced JSON document; use '-' for stdout (default)."


main :: IO ()
main = do
  Options{ optInput, optOutput } <- options

  -- Input YAML.
  result <- case optInput of
    -- strict getContents will read in all of stdin at once
    Nothing -> decodeEither' <$> S.getContents
    Just f  -> decodeFileEither f

  -- Output JSON.
  case result of
    Left  err -> die $ show err
    Right val -> do
      let json = encode (val :: Value)
      maybe L.putStr L.writeFile optOutput json
