content: {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE RecordWildCards #-}
  
  -- |
  -- Module      : $header$
  -- Copyright   : (c) Laurent P René de Cotret, 2019 - present
  -- License     : GNU GPL, version 2 or above
  -- Maintainer  : laurent.decotret@outlook.com
  -- Stability   : internal
  -- Portability : portable
  --
  -- This module defines types and functions that help
  -- with keeping track of figure specifications
  module Text.Pandoc.Filter.Plot.Parse
    ( plotToolkit,
      parseFigureSpec,
      ParseFigureResult (..),
      captionReader,
    )
  where
  
  import Control.Monad (join, unless, when)
  import Data.Char (isSpace)
  import Data.Default (def)
  import Data.List (find, intersperse)
  import qualified Data.Map.Strict as Map
  import Data.Maybe (fromJust, fromMaybe, isJust)
  import Data.String (fromString)
  import Data.Text (Text, pack, unpack)
  import qualified Data.Text as T
  import qualified Data.Text.IO as TIO
  import Data.Version (showVersion)
  import Paths_pandoc_plot (version)
  import System.FilePath (makeValid, normalise)
  import Text.Pandoc.Class (runPure)
  import Text.Pandoc.Definition
    ( Block (..),
      Format (..),
      Inline,
      Pandoc (..),
    )
  import Text.Pandoc.Filter.Plot.Monad
  import Text.Pandoc.Filter.Plot.Renderers
  import Text.Pandoc.Format (parseFlavoredFormat)
  import Text.Pandoc.Options (ReaderOptions (..))
  import Text.Pandoc.Readers (Reader (..), getReader)
  import Text.Pandoc (PandocError)
  
  tshow :: (Show a) => a -> Text
  tshow = pack . show
  
  data ParseFigureResult
    = -- | The block is not meant to become a figure
      NotAFigure
    | -- | The block is meant to become a figure
      PFigure FigureSpec
    | -- | The block is meant to become a figure, but the plotting toolkit is missing
      MissingToolkit Toolkit
    | -- | The block is meant to become a figure, but the figure format is incompatible
      --   with the plotting toolkit
      UnsupportedSaveFormat Toolkit SaveFormat
  
  -- | Determine inclusion specifications from @Block@ attributes.
  -- If an environment is detected, but the save format is incompatible,
  -- an error will be thrown.
  parseFigureSpec :: Block -> PlotM ParseFigureResult
  parseFigureSpec block@(CodeBlock (id', classes, attrs) _) = do
    -- The following would be a good case for the use of MaybeT
    -- but I don't want to bring in another dependency
    let mtk = plotToolkit block
    case mtk of
      Nothing -> return NotAFigure
      Just tk -> do
        r <- renderer tk
        figureSpec r
    where
      attrs' = Map.fromList attrs
      preamblePath = unpack <$> Map.lookup (tshow PreambleK) attrs'
  
      figureSpec :: Renderer -> PlotM ParseFigureResult
      figureSpec renderer_@Renderer {..} = do
        conf <- asks envConfig
        let toolkit = rendererToolkit
            saveFormat = maybe (defaultSaveFormat conf) (fromString . unpack) (Map.lookup (tshow SaveFormatK) attrs')
  
        -- Check if the saveformat is compatible with the toolkit
        if saveFormat `notElem` rendererSupportedSaveFormats
          then do
            err $ pack $ mconcat ["Save format ", show saveFormat, " not supported by ", show toolkit]
            return $ UnsupportedSaveFormat toolkit saveFormat
          else do
            let extraAttrs' = parseExtraAttrs toolkit attrs'
                header = rendererComment $ "Generated by pandoc-plot " <> (pack . showVersion) version
                defaultPreamble = preambleSelector toolkit conf
  
            includeScript <-
              maybe
                (return defaultPreamble)
                (liftIO . TIO.readFile)
                preamblePath
            let -- Filter attributes that are not relevant to pandoc-plot
                -- This presumes that inclusionKeys includes ALL possible keys, for all toolkits
                filteredAttrs = filter (\(k, _) -> k `notElem` (tshow <$> inclusionKeys)) attrs
                defWithSource = defaultWithSource conf
                defDPI = defaultDPI conf
  
            -- Decide between reading from file or using document content
            content <- parseContent block
  
            defaultExe <- executable rendererToolkit
  
            let caption = Map.findWithDefault mempty (tshow CaptionK) attrs'
                fsExecutable = maybe defaultExe (exeFromPath . unpack) $ Map.lookup (tshow ExecutableK) attrs'
                withSource = maybe defWithSource readBool (Map.lookup (tshow WithSourceK) attrs')
                script = mconcat $ intersperse "\n" [header, includeScript, content]
                directory = makeValid $ unpack $ Map.findWithDefault (pack $ defaultDirectory conf) (tshow DirectoryK) attrs'
                dpi = maybe defDPI (read . unpack) (Map.lookup (tshow DpiK) attrs')
                extraAttrs = Map.toList extraAttrs'
                blockAttrs = (id', filter (/= cls toolkit) classes, filteredAttrs)
  
            debug $ "Propagating attributes unrelated to pandoc-plot: " <> tshow blockAttrs
  
            let blockDependencies = parseFileDependencies $ fromMaybe mempty $ Map.lookup (tshow DependenciesK) attrs'
                dependencies = defaultDependencies conf <> blockDependencies
  
            -- This is the first opportunity to check save format compatibility
            _' <-
              unless (saveFormat `elem` rendererSupportedSaveFormats) $
                let msg = pack $ mconcat ["Save format ", show saveFormat, " not supported by ", show toolkit]
                 in err msg
  
            -- Ensure that the save format makes sense given the final conversion format, if known
            return $ PFigure (FigureSpec {..})
  -- Base case: block is not a CodeBlock
  parseFigureSpec _ = return NotAFigure
  
  -- | Parse script content from a block, if possible.
  -- The script content can either come from a file
  -- or from the code block itself. If both are present,
  -- the file is preferred.
  parseContent :: Block -> PlotM Script
  parseContent (CodeBlock (_, _, attrs) content) = do
    let attrs' = Map.fromList attrs
        mfile = normalise . unpack <$> Map.lookup (tshow FileK) attrs'
    when (content /= mempty && isJust mfile) $ do
      warning $
        mconcat
          [ "Figure refers to a file (",
            pack $ fromJust mfile,
            ") but also has content in the document.\nThe file content will be preferred."
          ]
    let loadFromFile fp = do
          info $ "Loading figure content from " <> pack fp
          liftIO $ TIO.readFile fp
    maybe (return content) loadFromFile mfile
  parseContent _ = return mempty
  
  -- | Determine which toolkit should be used to render the plot
  -- from a code block, if any.
  plotToolkit :: Block -> Maybe Toolkit
  plotToolkit (CodeBlock (_, classes, _) _) =
    find (\tk -> cls tk `elem` classes) toolkits
  plotToolkit _ = Nothing
  
  -- | Reader a caption, based on input document format
  captionReader :: Format -> Text -> Either PandocError [Inline]
  captionReader (Format f) t = fmap extractFromBlocks $
    runPure $ do
      fmt <- parseFlavoredFormat f
      (reader, exts) <- getReader fmt
      let readerOpts = def {readerExtensions = exts}
      -- Assuming no ByteString readers...
      case reader of
        TextReader fct -> fct readerOpts t
        _ -> return mempty
    where
      extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks
  
      extractInlines (Plain inlines) = inlines
      extractInlines (Para inlines) = inlines
      extractInlines (LineBlock multiinlines) = join multiinlines
      extractInlines _ = []
  
  -- | Flexible boolean parsing
  readBool :: Text -> Bool
  readBool s
    | s `elem` ["True", "true", "'True'", "'true'", "1"] = True
    | s `elem` ["False", "false", "'False'", "'false'", "0"] = False
    | otherwise = errorWithoutStackTrace $ unpack $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
  
  -- | Parse a list of file dependencies such as /[foo.bar, hello.txt]/.
  parseFileDependencies :: Text -> [FilePath]
  parseFileDependencies t
    | t == mempty = mempty
    | otherwise =
        fmap (normalise . unpack . T.dropAround isSpace)
          . T.splitOn ","
          . T.dropAround (\c -> c `elem` ['[', ']'])
          $ t
  