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

import qualified Data.Aeson           as J
import qualified Data.ByteString      as S
import qualified Data.ByteString.Lazy as L
import qualified Data.Yaml            as Y
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup ( Semigroup(..) )
#endif

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

import System.Exit    ( die )

import Common         ( dashToNothing, versionText, versionOption, numericVersionOption )

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

-- | Name of the executable.

self :: String
self = "json2yaml"

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

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

  where
  hdoc = Just $ vcat $ map pretty
    [ versionText self
    , "Convert JSON to YAML."
    ]
  fdoc = Just $ pretty $ concat
    [ "The old call pattern '"
    , self
    , " IN OUT' is also accepted, but deprecated."
    ]

  programOptions :: Parser Options
  programOptions = Options <$> oInput <*> oOutput

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

  oOutput :: Parser (Maybe FilePath)
  oOutput = dashToNothing <$> do
    oOutputExplicit <|> oOutputImplicit

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

  oOutputImplicit :: Parser FilePath
  oOutputImplicit =
    strArgument
      $  metavar "OUT"
      <> value "-"
      <> action "file"
      <> hidden
      <> help "The file to hold the produced YAML document; use '-' for stdout (default)."

-- | Exit with 'self'-stamped error message.

abort :: String -> IO a
abort msg = die $ concat [ self, ": ", msg ]

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

  -- Read JSON.
  mval <- J.decode <$> maybe L.getContents L.readFile optInput

  -- Write YAML.
  case mval of
    Nothing  -> abort "Invalid input JSON"
    Just val -> case optOutput of
      Nothing -> S.putStr $ Y.encode (val :: Y.Value)
      Just f  -> Y.encodeFile f val
