{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
   Module      : Text.Pandoc.Lua.Module.Structure
   Copyright   : © 2023 Albert Krewinkel
   License     : GPL-2.0-or-later
   Maintainer  : Albert Krewinkel <albert+pandoc@tarleb.com>

Command line helpers
-}
module Text.Pandoc.Lua.Module.Structure
  ( documentedModule
  ) where

import Control.Applicative ((<|>), optional)
import Data.Default (Default (..))
import Data.Maybe (fromMaybe)
import Data.Version (makeVersion)
import HsLua ( DocumentedFunction, LuaError, Module (..), Peeker
             , (###), (<#>), (=#>), (#?)
             , defun, functionResult, getfield, isnil, lastly, liftLua
             , opt, liftPure, parameter , peekBool, peekIntegral
             , peekFieldRaw, peekText, pop, pushIntegral, since, top )
import Text.Pandoc.Chunks ( ChunkedDoc (..), PathTemplate (..)
                          , tocToList, splitIntoChunks )
import Text.Pandoc.Definition (Pandoc (..), Block)
import Text.Pandoc.Error (PandocError)
import Text.Pandoc.Lua.PandocLua ()
import Text.Pandoc.Lua.Marshal.AST ( peekBlocksFuzzy, peekPandoc
                                   , pushBlock, pushBlocks )
import Text.Pandoc.Lua.Marshal.Chunks
import Text.Pandoc.Lua.Marshal.WriterOptions ( peekWriterOptions )
import Text.Pandoc.Options (WriterOptions (writerTOCDepth,
                                           writerNumberSections))
import Text.Pandoc.Slides (getSlideLevel, prepSlides)
import Text.Pandoc.Writers.Shared (toTableOfContents)
import qualified Data.Text as T
import qualified Text.Pandoc.Shared as Shared

-- | Push the pandoc.structure module on the Lua stack.
documentedModule :: Module PandocError
documentedModule = Module
  { moduleName = "pandoc.structure"
  , moduleDescription =
    "Access to the higher-level document structure, including " <>
    "hierarchical sections and the table of contents."
  , moduleFields = []
  , moduleFunctions =
      [ make_sections     `since` makeVersion [3,0]
      , slide_level       `since` makeVersion [3,0]
      , split_into_chunks `since` makeVersion [3,0]
      , table_of_contents `since` makeVersion [3,0]
      ]
  , moduleOperations = []
  , moduleTypeInitializers = []
  }

make_sections :: LuaError e => DocumentedFunction e
make_sections = defun "make_sections"
  ### (\blks mopts ->
         let (numSects, baseLevel, mslideLevel) =
               fromMaybe (defNumSec, Nothing, Nothing) mopts
             blks' = case mslideLevel of
                       Just l | l <= 0 -> prepSlides (getSlideLevel blks) blks
                       Just sl -> prepSlides sl blks
                       Nothing -> blks
         in pure $ Shared.makeSections numSects baseLevel blks')
  <#> parameter peekBodyBlocks "Blocks|Pandoc" "blocks"
        "document blocks to process"
  <#> opt (parameter peekOpts "table" "opts" "options")
  =#> functionResult pushBlocks "Blocks"
        "processed blocks"
  #? T.unlines
     [ "Puts [[Blocks]] into a hierarchical structure: a list of sections"
     , "(each a Div with class \"section\" and first element a Header)."
     , ""
     , "The optional `opts` argument can be a table; two settings are"
     , "recognized: If `number_sections` is true, a `number` attribute"
     , "containing the section number will be added to each `Header`. If"
     , "`base_level` is an integer, then `Header` levels will be"
     , "reorganized so that there are no gaps, with numbering levels"
     , "shifted by the given value. Finally, an integer `slide_level`"
     , "value triggers the creation of slides at that heading level."
     , ""
     , "Note that a [[WriterOptions]] object can be passed as the opts"
     , "table; this will set the `number_section` and `slide_level` values"
     , "to those defined on the command line."
     , ""
     , "Usage:"
     , ""
     , "    local blocks = {"
     , "      pandoc.Header(2, pandoc.Str 'first'),"
     , "      pandoc.Header(2, pandoc.Str 'second'),"
     , "    }"
     , "    local opts = PANDOC_WRITER_OPTIONS"
     , "    local newblocks = pandoc.structure.make_sections(blocks, opts)"
     ]
  where
    defNumSec = False
    peekOpts idx = do
      numberSections <- fromMaybe defNumSec <$> do
        liftLua $ getfield idx "number_sections"
        optional (peekBool top `lastly` pop 1)
      baseLevel <- do
        liftLua $ getfield idx "base_level"
        optional (peekIntegral top `lastly` pop 1)
      slideLevel <- do
        liftLua $ getfield idx "slide_level"
        optional (peekIntegral top `lastly` pop 1)
      return (numberSections, baseLevel, slideLevel)

slide_level :: LuaError e => DocumentedFunction e
slide_level = defun "slide_level"
  ### liftPure getSlideLevel
  <#> parameter peekBodyBlocks "Blocks|Pandoc" "blocks" "document body"
  =#> functionResult pushIntegral "integer" "slide level"
  #? T.unlines
  [ "Find level of header that starts slides (defined as the least"
  , "header level that occurs before a non-header/non-hrule in the"
  , "blocks)."
  ]

-- | Split 'Pandoc' into 'Chunk's.
split_into_chunks :: LuaError e => DocumentedFunction e
split_into_chunks = defun "split_into_chunks"
  ### (\doc mopts -> pure $
          let defOpts = (defPathTmpl, defNumSects, Nothing, defLvl)
              (pathTempl, numberSect, mbBaseLevel, chunkLevel) =
                fromMaybe defOpts mopts
          in splitIntoChunks pathTempl numberSect mbBaseLevel chunkLevel doc)
  <#> parameter peekPandoc "Pandoc" "doc" "document to split"
  <#> opt (parameter peekSplitOpts "table" "opts" optionsDescr)
  =#> functionResult pushChunkedDoc "ChunkedDoc" ""
  #? T.unlines
     [ "Converts a [[Pandoc]] document into a [[ChunkedDoc]]." ]
 where
  defPathTmpl = PathTemplate "chunk-%n"
  defNumSects = False
  defLvl = 1
  peekSplitOpts idx = (,,,)
    <$> peekFieldRaw ((fmap PathTemplate . peekText) `orDefault` defPathTmpl)
                     "path_template" idx
    <*> peekFieldRaw (peekBool `orDefault` defNumSects) "number_sections" idx
    <*> peekFieldRaw (optional . peekIntegral) "base_heading_level" idx
    <*> peekFieldRaw (peekIntegral `orDefault` defLvl) "chunk_level" idx
  orDefault p defaultValue idx' = liftLua (isnil idx') >>= \case
    True  -> pure defaultValue
    False -> p idx'
  optionsDescr = T.unlines
    [ "Splitting options."
    , ""
    , "The following options are supported:"
    , ""
    , "    `path_template`"
    , "    :   template used to generate the chunks' filepaths"
    , "        `%n` will be replaced with the chunk number (padded with"
    , "        leading 0s to 3 digits), `%s` with the section number of"
    , "        the heading, `%h` with the (stringified) heading text,"
    , "        `%i` with the section identifier. For example,"
    , "        `\"section-%s-%i.html\"` might be resolved to"
    , "        `\"section-1.2-introduction.html\"`."
    , ""
    , "        Default is `\"chunk-%n\"` (string)"
    , ""
    , "    `number_sections`"
    , "    :   whether sections should be numbered; default is `false`"
    , "        (boolean)"
    , ""
    , "    `chunk_level`"
    , "    :   The heading level the document should be split into"
    , "        chunks. The default is to split at the top-level, i.e.,"
    , "        `1`. (integer)"
    , ""
    , "    `base_heading_level`"
    , "    :   The base level to be used for numbering. Default is `nil`"
    , "        (integer|nil)"
    ]

-- | Generate a table of contents.
table_of_contents :: DocumentedFunction PandocError
table_of_contents = defun "table_of_contents"
  ### (\tocSource mwriterOpts -> pure $
          let writerOpts = fromMaybe def mwriterOpts
          in case tocSource of
               Left blks  -> toTableOfContents writerOpts blks
               Right tree -> tocToList (writerNumberSections writerOpts)
                                       (writerTOCDepth writerOpts) tree
      )
  <#> parameter peekTocSource "Blocks|Pandoc|ChunkedDoc" "toc_source"
        "list of command line arguments"
  <#> opt (parameter peekWriterOptions "WriterOptions" "opts" "options")
  =#> functionResult pushBlock "Block"
        "Table of contents as a BulletList object"
  #? T.unlines
     [ "Generates a table of contents for the given object." ]
 where
  peekTocSource idx =
    (Left <$> peekBodyBlocks idx) <|>
    (Right . chunkedTOC <$> peekChunkedDoc idx)

-- | Retrieves the body blocks of a 'Pandoc' object or from a list of
-- blocks.
peekBodyBlocks :: LuaError e => Peeker e [Block]
peekBodyBlocks idx =
  ((\(Pandoc _ blks) -> blks) <$> peekPandoc idx) <|>
  peekBlocksFuzzy idx
