{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts  #-}
{-|
Module      : $header$
Description : Pandoc filter to create figures from code blocks using your plotting toolkit of choice
Copyright   : (c) Laurent P René de Cotret, 2020
License     : GNU GPL, version 2 or above
Maintainer  : laurent.decotret@outlook.com
Stability   : unstable
Portability : portable

This module defines a Pandoc filter @plotTransform@ and related functions
that can be used to walk over a Pandoc document and generate figures from
code blocks, using a multitude of plotting toolkits.

The syntax for code blocks is simple. Code blocks with the appropriate class
attribute will trigger the filter:

*   @matplotlib@ for matplotlib-based Python plots;
*   @plotly_python@ for Plotly-based Python plots;
*   @plotly_r@ for Plotly-based R plots;
*   @matlabplot@ for MATLAB plots;
*   @mathplot@ for Mathematica plots;
*   @octaveplot@ for GNU Octave plots;
*   @ggplot2@ for ggplot2-based R plots;
*   @gnuplot@ for gnuplot plots;
*   @graphviz@ for Graphviz graphs;
*   @bokeh@ for Bokeh-based Python plots;
*   @plotsjl@ for Plots.jl-based Julia plots;

For example, in Markdown:

@
    This is a paragraph.

    ```{.matlabplot}
    figure()
    plot([1,2,3,4,5], [1,2,3,4,5], '-k')
    ```
@

The code block will be reworked into a script and the output figure will be captured. Optionally, the source code
 used to generate the figure will be linked in the caption.

Here are the possible attributes what pandoc-plot understands for ALL toolkits:

    * @directory=...@ : Directory where to save the figure. This path should be specified with 
      respect to the current working directory, and not with respect to the document.
    * @source=true|false@ : Whether or not to link the source code of this figure in the caption. 
      Ideal for web pages, for example. Default is false.
    * @format=...@: Format of the generated figure. This can be an extension or an acronym, 
      e.g. @format=PNG@.
    * @caption="..."@: Specify a plot caption (or alternate text). Format 
      for captions is specified in the documentation for the @Configuration@ type.
    * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Certain toolkits ignore this.
    * @dependencies=[...]@: Specify files/directories on which a figure depends, e.g. data file. 
      Figures will be re-rendered if one of those file/directory changes. These paths should 
      be specified with respect to the current working directory, and not with respect to the document.
    * @preamble=...@: Path to a file to include before the code block. Ideal to avoid repetition over 
      many figures.
    * @file=...@: Path to a file from which to read the content of the figure. The content of the 
      code block will be ignored. This path should be specified with respect to the current working 
      directory, and not with respect to the document.

Default values for the above attributes are stored in the @Configuration@ datatype. These can be specified in a 
YAML file. 

Here is an example code block which will render a figure using gnuplot, in Markdown:

@
    ```{.gnuplot format=png caption="Sinusoidal function" source=true}
    sin(x)

    set xlabel "x"
    set ylabel "y"
    ```
@
-}
module Text.Pandoc.Filter.Plot (
    -- * Operating on single Pandoc blocks
      makePlot
    -- * Operating on whole Pandoc documents
    , plotTransform
    -- * Cleaning output directories
    , cleanOutputDirs
    -- * Runtime configuration
    , configuration
    , defaultConfiguration
    , Configuration(..)
    , Verbosity(..)
    , LogSink(..)
    , SaveFormat(..)
    , Script
    -- * Determining available plotting toolkits
    , Toolkit(..)
    , availableToolkits
    , unavailableToolkits
    , toolkits
    , supportedSaveFormats
    -- * Version information
    , pandocPlotVersion
    -- * For testing and internal purposes ONLY
    -- ** Running the @PlotM@ monad
    , make
    , makeEither
    , PlotM
    , runPlotM
    , PandocPlotError(..)
    -- ** Code block parameters
    , InclusionKey(..)
    , inclusionKeys
    -- ** Utilities
    , extension
    , readDoc
    , captionReader
    , cls
    , configurationPathMeta
    , executable
    -- ** Embedding HTML content
    , extractPlot
    ) where

import Control.Concurrent.Async.Lifted   (mapConcurrently)
import Data.Text                         (Text, unpack)
import Data.Version                      (Version)

import Paths_pandoc_plot                 (version)

import Text.Pandoc.Definition            (Pandoc(..), Block)
import Text.Pandoc.Walk                  (walkM, Walkable)

import Text.Pandoc.Filter.Plot.Internal

-- | Walk over an entire Pandoc document, transforming appropriate code blocks
-- into figures. This function will operate on blocks in parallel if possible.
--
-- Failing to render a figure does not stop the filter, so that you may run the filter
-- on documents without having all necessary toolkits installed. In this case, error
-- messages are printed to stderr, and blocks are left unchanged.
plotTransform :: Configuration -- ^ Configuration for default values
              -> Pandoc        -- ^ Input document
              -> IO Pandoc
plotTransform conf (Pandoc meta blocks) =
    runPlotM conf $ mapConcurrently make blocks >>= return . Pandoc meta


-- | Highest-level function that can be walked over a Pandoc tree.
-- All code blocks that have the appropriate class names will be considered
-- figures, e.g. @.matplotlib@.
--
-- This function can be made to operation on whole @Pandoc@ documents. However,
-- you should prefer the @plotTransform@ function for whole documents, as it
-- is optimized for parallel operations.
--
-- Failing to render a figure does not stop the filter, so that you may run the filter
-- on documents without having all necessary toolkits installed. In this case, error
-- messages are printed to stderr, and blocks are left unchanged.
makePlot :: Walkable Block a
         => Configuration -- ^ Configuration for default values
         -> a             -- ^ Input block or document
         -> IO a
makePlot conf = runPlotM conf . walkM make


-- | The version of the pandoc-plot package.
--
-- @since 0.8.0.0
pandocPlotVersion :: Version
pandocPlotVersion = version


-- | Try to process the block with `pandoc-plot`. If a failure happens (or the block)
-- was not meant to become a figure, return the block as-is.
make :: Block -> PlotM Block
make blk = either (const (return blk) ) return =<< makeEither blk


-- | Try to process the block with `pandoc-plot`, documenting the error.
makeEither :: Block -> PlotM (Either PandocPlotError Block)
makeEither block =
    parseFigureSpec block
        >>= maybe
                (return $ Right block)
                (\s -> runScriptIfNecessary s >>= handleResult s)
    where
        -- Logging of errors has been taken care of in @runScriptIfNecessary@ 
        handleResult :: FigureSpec -> ScriptResult -> PlotM (Either PandocPlotError Block)
        handleResult _ (ScriptFailure msg code)       = return $ Left (ScriptRuntimeError msg code)
        handleResult _ (ScriptChecksFailed msg)       = return $ Left (ScriptChecksFailedError msg)
        handleResult _ (ToolkitNotInstalled tk')      = return $ Left (ToolkitNotInstalledError tk')
        handleResult spec ScriptSuccess = asks envConfig >>= \c -> Right <$> toFigure (captionFormat c) spec


data PandocPlotError
    = ScriptRuntimeError Text Int
    | ScriptChecksFailedError Text
    | ToolkitNotInstalledError Toolkit

instance Show PandocPlotError where
    show (ScriptRuntimeError _ exitcode) = "ERROR (pandoc-plot) The script failed with exit code " <> show exitcode <> "."
    show (ScriptChecksFailedError msg)   = "ERROR (pandoc-plot) A script check failed with message: " <> unpack msg <> "."
    show (ToolkitNotInstalledError tk)   = "ERROR (pandoc-plot) The " <> show tk <> " toolkit is required but not installed."