{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE OverloadedStrings #-}

module Slick.Pandoc
  ( markdownToHTML
  , markdownToHTML'
  , makePandocReader
  , makePandocReader'
  , loadUsing
  , loadUsing'
  , convert
  ) where

import Control.Lens
import Control.Monad
import Data.Aeson
import Data.Aeson.Lens
import qualified Data.Text as T
import Development.Shake hiding (Resource)
import Text.Pandoc
import Text.Pandoc.Highlighting
import Text.Pandoc.Shared

-- | Reasonable options for reading a markdown file
markdownOptions :: ReaderOptions
markdownOptions = def {readerExtensions = exts}
  where
    exts =
      mconcat
        [extensionsFromList [Ext_yaml_metadata_block], githubMarkdownExtensions]

-- | Reasonable options for rendering to HTML
html5Options :: WriterOptions
html5Options = def {writerHighlightStyle = Just tango}

-- | Handle possible pandoc failure within the Action Monad
unPandocM :: PandocPure a -> Action a
unPandocM = either (fail . show) return . runPure

-- | Convert markdown text into a 'Value';
-- The 'Value'  has a "content" key containing rendered HTML
-- Metadata is assigned on the respective keys in the 'Value'
markdownToHTML :: T.Text -> Action Value
markdownToHTML =
  loadUsing (readMarkdown markdownOptions) (writeHtml5String html5Options)

-- | Like 'markdownToHTML' but allows returning any JSON serializable object
markdownToHTML' :: (FromJSON a) => T.Text -> Action a
markdownToHTML' = markdownToHTML >=> convert

type PandocReader textType = textType -> PandocPure Pandoc

type PandocWriter = Pandoc -> PandocPure T.Text

-- | Given a reader from 'Text.Pandoc.Readers' this creates a loader which
-- given the source document will read its metadata into a 'Value'
-- returning both the 'Pandoc' object and the metadata within an 'Action'
makePandocReader :: PandocReader textType -> textType -> Action (Pandoc, Value)
makePandocReader readerFunc text = do
  pdoc@(Pandoc meta _) <- unPandocM $ readerFunc text
  return (pdoc, flattenMeta meta)

-- | Like 'makePandocReader' but will deserialize the metadata into any object
-- which implements 'FromJSON'. Failure to deserialize will fail the Shake
-- build.
makePandocReader' ::
     (FromJSON a)
  => PandocReader textType
  -> textType
  -> Action (Pandoc, a)
makePandocReader' readerFunc text = do
  (pdoc, meta) <- makePandocReader readerFunc text
  convertedMeta <- convert meta
  return (pdoc, convertedMeta)

-- | Load in a source document using the given 'PandocReader', then render the 'Pandoc'
-- into text using the given 'PandocWriter'.
-- Returns a 'Value' wherein the rendered text is set to the "content" key and 
-- any metadata is set to its respective key in the 'Value'
loadUsing :: PandocReader textType -> PandocWriter -> textType -> Action Value
loadUsing reader writer text = do
  (pdoc, meta) <- makePandocReader reader text
  outText <- unPandocM $ writer pdoc
  let withContent = meta & _Object . at "content" ?~ String outText
  return withContent

-- | Like 'loadUsing' but allows also deserializes the 'Value' into any object
-- which implements 'FromJSON'.  Failure to deserialize will fail the Shake
-- build.
loadUsing' :: (FromJSON a) => PandocReader textType -> PandocWriter -> textType -> Action a
loadUsing' reader writer text = loadUsing reader writer text >>= convert

-- | Attempt to convert between two JSON serializable objects (or 'Value's).
-- Failure to deserialize fails the Shake build.
convert :: (FromJSON a, ToJSON a, FromJSON b) => a -> Action b
convert a =
  case fromJSON (toJSON a) of
    Success r -> pure r
    Error err -> fail $ "json conversion error:" ++ err

-- | Flatten a Pandoc 'Meta' into a well-structured JSON object, rendering Pandoc
-- text objects into plain strings along the way.
flattenMeta :: Meta -> Value
flattenMeta (Meta meta) = toJSON $ fmap go meta
  where
    go :: MetaValue -> Value
    go (MetaMap m) = toJSON $ fmap go m
    go (MetaList m) = toJSONList $ fmap go m
    go (MetaBool m) = toJSON m
    go (MetaString m) = toJSON m
    go (MetaInlines m) = toJSON $ stringify m
    go (MetaBlocks m) = toJSON $ stringify m