{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import Control.Monad (join, msum, void, when)
import Data.List (intersperse, (\\))
import Data.Text (unpack)
import qualified Data.Text.IO as TIO
import Data.Version (parseVersion, showVersion)
import qualified Data.Version as V
import Development.GitRev (gitHash)
import ExampleConfig (embedExampleConfig)
import GHC.IO.Encoding (setLocaleEncoding, utf8)
import ManPage (embedManualHtml)
import OpenFile (openFile)
import Options.Applicative
  ( Alternative ((<|>)),
    Parser,
    command,
    execParser,
    flag,
    footerDoc,
    fullDesc,
    header,
    help,
    helper,
    info,
    long,
    metavar,
    optional,
    progDesc,
    short,
    strArgument,
    strOption,
    subparser,
    value,
    (<**>),
  )
import qualified Options.Applicative.Help.Pretty as P
import System.Directory (doesFileExist, getTemporaryDirectory)
import System.Environment (lookupEnv)
import System.FilePath ((</>))
import System.IO (hPutStrLn, stderr)
import Text.Pandoc (pandocVersion)
import Text.Pandoc.Definition (pandocTypesVersion)
import Text.Pandoc.Filter.Plot
  ( Configuration (..),
    availableToolkits,
    configuration,
    defaultConfiguration,
    pandocPlotVersion,
    plotFilter,
  )
import Text.Pandoc.Filter.Plot.Internal
  ( cleanOutputDirs,
    cls,
    configurationPathMeta,
    executable,
    pathToExe,
    readDoc,
    runPlotM,
    supportedSaveFormats,
    toolkits,
  )
import Text.Pandoc.JSON (toJSONFilter)
import Text.ParserCombinators.ReadP (readP_to_S)

-- The difference between commands and flags is that commands require knowledge of
-- the configuration, while flags only display static information.

-- Please note that for some reason, makeVersion [2, 11, 0, 0] > makeVersion [2, 11]
minimumPandocVersion :: V.Version
minimumPandocVersion = V.makeVersion [2, 11]

data Command
  = Clean (Maybe FilePath) FilePath
  | WriteConfig FilePath
  | Toolkits (Maybe FilePath)

data Flag
  = Version
  | FullVersion
  | Manual
  deriving (Eq)

main :: IO ()
main = do
  setLocaleEncoding utf8
  join $ execParser opts
  where
    opts =
      info
        (optparse <**> helper)
        ( fullDesc
            <> progDesc
              ( unlines
                  [ "This pandoc filter generates plots from code blocks using a multitude of ",
                    "possible renderers. This allows to keep documentation and figures in",
                    "perfect synchronicity."
                  ]
              )
            <> header (mconcat ["pandoc-plot ", V.showVersion pandocPlotVersion, " - generate figures directly in documents"])
            <> footerDoc
              ( Just $
                  P.vsep
                    [ "More information can be found via the manual (pandoc-plot --manual) or the",
                      "repository README, located at https://github.com/LaurentRDC/pandoc-plot"
                    ]
              )
        )

    optparse = do
      flag_ <- flagParser
      command_ <- commandParser
      -- The extra optional input below only serves to show
      -- to the user that the last argument is the AST from pandoc
      -- The parsed input is never used
      input <- optional $ strArgument (metavar "AST")
      return $ go flag_ command_ input

    go :: Maybe Flag -> Maybe Command -> Maybe String -> IO ()
    go (Just Version) _ _ = putStrLn (V.showVersion pandocPlotVersion)
    go (Just FullVersion) _ _ = showFullVersion
    go (Just Manual) _ _ = showManPage
    go _ (Just (Toolkits mfp)) _ = showAvailableToolkits mfp
    go _ (Just (Clean mfp fp)) _ = clean mfp fp
    go _ (Just (WriteConfig fp)) _ = writeFile fp $(embedExampleConfig)
    go Nothing Nothing _ = toJSONFilterWithConfig

flagParser :: Parser (Maybe Flag)
flagParser = versionP <|> fullVersionP <|> manualP
  where
    versionP =
      flag
        Nothing
        (Just Version)
        ( mconcat
            [ long "version",
              short 'v',
              help "Show version number and exit."
            ]
        )

    fullVersionP =
      flag
        Nothing
        (Just FullVersion)
        ( mconcat
            [ long "full-version",
              help "Show full version information and exit."
            ]
        )

    manualP =
      flag
        Nothing
        (Just Manual)
        ( mconcat
            [ long "manual",
              short 'm',
              help "Open the manual page in the default web browser and exit."
            ]
        )

commandParser :: Parser (Maybe Command)
commandParser =
  optional $
    subparser $
      mconcat
        [ command
            "toolkits"
            ( info (toolkitsP <**> helper) (progDesc "Show information on toolkits and exit.")
            ),
          command
            "clean"
            ( info
                (cleanP <**> helper)
                ( progDesc
                    ( unlines
                        [ "Clean output directories where figures from FILE and log files might be stored.",
                          "WARNING: All files in those directories will be deleted."
                        ]
                    )
                )
            ),
          command
            "write-example-config"
            ( info (writeConfigP <**> helper) (progDesc "Write example configuration to a file and exit.")
            )
        ]
  where
    configP = optional $ strOption (mconcat [long "config", metavar "PATH", help "Path to optional configuration file."])
    toolkitsP = Toolkits <$> configP
    cleanP = Clean <$> configP <*> strArgument (metavar "FILE")
    writeConfigP =
      WriteConfig
        <$> strOption
          ( mconcat
              [ long "path",
                metavar "FILE",
                value ".example-pandoc-plot.yml",
                help "Target location of the configuration file. Default is \".example-pandoc-plot.yml\""
              ]
          )

-- | Determine configuration and run filter.
--
-- Priority for configuration:
--
--     (1) Loaded from filepath stored in document metadata, under the key @plot-configuration@;
--
--     (2) Loaded from file @.pandoc-plot.yml@ in current work directory;
--
--     (3) Default configuration
toJSONFilterWithConfig :: IO ()
toJSONFilterWithConfig = do
  upToDatePandoc <- checkRuntimePandocVersion
  when upToDatePandoc $
    toJSONFilter $ \mfmt doc -> do
      c <- maybe localConfig configuration (configurationPathMeta doc)
      plotFilter c mfmt doc

-- | Check that the runtime version of Pandoc is at least 2.11. The return value
-- indicates whether the Pandoc version is new enough or not.
checkRuntimePandocVersion :: IO Bool
checkRuntimePandocVersion = do
  -- Pandoc runs filters in an environment with two variables:
  -- PANDOV_VERSION and PANDOC_READER_OPTS
  -- We can use the former to ensure that people are not
  -- using an old version of pandoc
  pandocV <- lookupEnv "PANDOC_VERSION"
  case pandocV >>= readVersion of
    Nothing -> return True
    Just v ->
      if v < minimumPandocVersion
        then do
          hPutStrLn stderr $
            mconcat
              [ "ERROR (pandoc-plot) The pandoc-plot filter only ",
                "supports Pandoc 2.11 and newer. ",
                "but you are using Pandoc ",
                showVersion v
              ]
          return False
        else return True
  where
    readVersion = fmap fst . lastMaybe . readP_to_S parseVersion
    lastMaybe xs = if length xs > 1 then Just (last xs) else Nothing

-- | Load configuration from local file @.pandoc-plot.yml@.
-- If the file does not exist, the default configuration will be used.
localConfig :: IO Configuration
localConfig = do
  configExists <- doesFileExist ".pandoc-plot.yml"
  if configExists
    then configuration ".pandoc-plot.yml"
    else return defaultConfiguration

showFullVersion :: IO ()
showFullVersion = do
  putStrLn $ "pandoc-plot " <> V.showVersion pandocPlotVersion
  putStrLn $ "Git revision " <> $gitHash
  putStrLn $
    mconcat
      [ "Compiled with pandoc ",
        V.showVersion pandocVersion,
        " and pandoc-types ",
        V.showVersion pandocTypesVersion,
        " using GHC ",
        TOOL_VERSION_ghc -- Constant defined by CPP
      ]

showAvailableToolkits :: Maybe FilePath -> IO ()
showAvailableToolkits mfp = do
  c <- maybe localConfig configuration mfp

  putStrLn "\nAVAILABLE TOOLKITS\n"
  available <- availableToolkits c
  mapM_ (availToolkitInfo c) available
  putStrLn "\nUNAVAILABLE TOOLKITS\n"
  -- We don't use unavailableToolkits because this would force
  -- more IO actions
  let unavailable = toolkits \\ available
  mapM_ (unavailToolkitInfo c) unavailable
  where
    toolkitInfo avail conf tk = do
      putStrLn $ "Toolkit: " <> show tk
      when avail $ do
        exe <- runPlotM Nothing conf $ executable tk
        putStrLn $ "    Executable: " <> pathToExe exe
      putStrLn $ "    Code block trigger: " <> (unpack . cls $ tk)
      putStrLn $ "    Supported save formats: " <> (mconcat . intersperse ", " . fmap show $ supportedSaveFormats tk)
      putStrLn mempty
    availToolkitInfo = toolkitInfo True
    unavailToolkitInfo = toolkitInfo False

-- | Clean output directories associated with a file
--
-- Priority for configuration are the same as @toJSONFilterWithConfig@.
clean ::
  Maybe FilePath -> -- Use configuration file?
  FilePath -> -- Document to clean
  IO ()
clean mfp fp = do
  doc <- readDoc fp
  -- Note the priority for configuration:
  --   (1) path of argument --config
  --   (2) document metadata
  --   (3) local .pandoc-plot.yml
  --   (4) default config
  conf <- maybe localConfig configuration $ firstJusts [configurationPathMeta doc, mfp]
  void (cleanOutputDirs conf doc)
  where
    firstJusts :: [Maybe a] -> Maybe a
    firstJusts = msum

showManPage :: IO ()
showManPage = do
  manualPath <- (</> "pandoc-plot-manual.html") <$> getTemporaryDirectory
  TIO.writeFile manualPath $(embedManualHtml)
  openFile ("file:///" <> manualPath)
