{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_HADDOCK hide #-}
module Text.Markdown.Block
    ( Block (..)
    , ListType (..)
    , toBlocks
    ) where

import Prelude
import Data.Conduit
import qualified Data.Conduit.Text as CT
import qualified Data.Conduit.List as CL
import Data.Text (Text)
import qualified Data.Text as T
import Data.Functor.Identity (runIdentity)
import Data.Char (isDigit)

data ListType = Ordered | Unordered
  deriving (Show, Eq)

data Block inline
    = BlockPara inline
    | BlockList ListType (Either inline [Block inline])
    | BlockCode (Maybe Text) Text
    | BlockQuote [Block inline]
    | BlockHtml Text
    | BlockRule
    | BlockHeading Int inline
  deriving (Show, Eq)

instance Functor Block where
    fmap f (BlockPara i) = BlockPara (f i)
    fmap f (BlockList lt (Left i)) = BlockList lt $ Left $ f i
    fmap f (BlockList lt (Right bs)) = BlockList lt $ Right $ map (fmap f) bs
    fmap _ (BlockCode a b) = BlockCode a b
    fmap f (BlockQuote bs) = BlockQuote $ map (fmap f) bs
    fmap _ (BlockHtml t) = BlockHtml t
    fmap _ BlockRule = BlockRule
    fmap f (BlockHeading level i) = BlockHeading level (f i)

toBlocks :: Monad m => Conduit Text m (Block Text)
toBlocks = mapOutput noCR CT.lines =$= toBlocksLines

toBlocksLines :: Monad m => GLInfConduit Text m (Block Text)
toBlocksLines = awaitForever start

noCR :: Text -> Text
noCR t
    | T.null t = t
    | T.last t == '\r' = T.init t
    | otherwise = t

start :: Monad m => Text -> GLConduit Text m (Block Text)
start t
    | T.null $ T.strip t = return ()
    | isRule t = yield BlockRule
    | Just lang <- T.stripPrefix "~~~" t = do
        (finished, ls) <- takeTill (== "~~~") >+> withUpstream CL.consume
        if finished
            then yield $ BlockCode (if T.null lang then Nothing else Just lang) $ T.intercalate "\n" ls
            else mapM_ leftover (reverse $ T.cons ' ' t : ls)
    | Just t' <- T.stripPrefix "> " t = do
        ls <- takeQuotes >+> CL.consume
        let blocks = runIdentity $ mapM_ yield (t' : ls) $$ toBlocksLines =$ CL.consume
        yield $ BlockQuote blocks
    | Just (level, t') <- stripHeading t = yield $ BlockHeading level t'
    | Just t' <- T.stripPrefix "    " t = do
        ls <- getIndented 4 >+> CL.consume
        yield $ BlockCode Nothing $ T.intercalate "\n" $ t' : ls
    | T.isPrefixOf "<" t = do
        ls <- takeTill (T.null . T.strip) >+> CL.consume
        yield $ BlockHtml $ T.intercalate "\n" $ t : ls
    | Just (ltype, t') <- listStart t = do
        let (spaces, t'') = T.span (== ' ') t'
        if T.length spaces >= 2
            then do
                let leader = T.length t - T.length t''
                ls <- getIndented leader >+> CL.consume
                let blocks = runIdentity $ mapM_ yield (t'' : ls) $$ toBlocksLines =$ CL.consume
                yield $ BlockList ltype $ Right blocks
            else yield $ BlockList ltype $ Left t''

    | otherwise = do
        -- Check for underline headings
        t2 <- CL.peek
        case t2 >>= getUnderline of
            Nothing -> do
                ls <- takeTill (T.null . T.strip) >+> CL.consume
                yield $ BlockPara $ T.intercalate "\n" $ t : ls
            Just level -> do
                CL.drop 1
                yield $ BlockHeading level t

takeTill :: Monad m => (i -> Bool) -> Pipe l i i u m Bool
takeTill f =
    loop
  where
    loop = await >>= maybe (return False) (\x -> if f x then return True else yield x >> loop)

listStart :: Text -> Maybe (ListType, Text)
listStart t
    | Just t' <- T.stripPrefix "* " t = Just (Unordered, t')
    | Just t' <- stripNumber t, Just t'' <- stripSeparator t' = Just (Ordered, t'')
    | otherwise = Nothing

stripNumber :: Text -> Maybe Text
stripNumber x
    | T.null y = Nothing
    | otherwise = Just z
  where
    (y, z) = T.span isDigit x

stripSeparator :: Text -> Maybe Text
stripSeparator x =
    case T.uncons x of
        Nothing -> Nothing
        Just ('.', y) -> Just y
        Just (')', y) -> Just y
        _ -> Nothing

getIndented :: Monad m => Int -> GLConduit Text m Text
getIndented leader =
    go []
  where
    go blanks = await >>= maybe (mapM_ leftover blanks) (go' blanks)

    go' blanks t
        | T.null $ T.strip t = go (T.drop leader t : blanks)
        | T.length x == leader && T.null (T.strip x) = do
            mapM_ yield $ reverse blanks
            yield y
            go []
        | otherwise = mapM_ leftover (t:blanks)
      where
        (x, y) = T.splitAt leader t

takeQuotes :: Monad m => GLConduit Text m Text
takeQuotes =
    await >>= maybe (return ()) go
  where
    go ">" = yield "" >> takeQuotes
    go t
        | Just t' <- T.stripPrefix "> " t = yield t' >> takeQuotes
        | otherwise = leftover t

isRule :: Text -> Bool
isRule "* * *" = True
isRule "***" = True
isRule "*****" = True
isRule "- - -" = True
isRule t = T.length (T.takeWhile (== '-') t) >= 5

stripHeading :: Text -> Maybe (Int, Text)
stripHeading t
    | T.null x = Nothing
    | otherwise = Just (T.length x, T.strip $ T.dropWhileEnd (== '#') y)
  where
    (x, y) = T.span (== '#') t

getUnderline :: Text -> Maybe Int
getUnderline t
    | T.length t < 2 = Nothing
    | T.all (== '=') t = Just 1
    | T.all (== '-') t = Just 2
    | otherwise = Nothing