{-# LANGUAGE TemplateHaskell #-}
-- | Convenience quasiquoter to ease the pain working with multiline strings
module System.Directory.Layout.QQ (dedent) where

import           Control.Applicative
import           Data.Char (isSpace)
import           Data.Foldable (Foldable(..), toList)
import           Data.List (intercalate)
import           Data.Sequence (Seq, ViewL(..), ViewR(..), (|>))
import qualified Data.Sequence as Seq
import           Language.Haskell.TH.Quote (QuasiQuoter(..))
import           Language.Haskell.TH.Syntax (liftString)
import           Language.Haskell.TH (Q, Exp)
import           Prelude hiding (foldr)


-- | A handy quasiquoter to work with the multiline file contents
--
-- Strips the longest common leading spaces segment. All spacey characters are treated
-- equally. The first line is ignored if it's spaces only.
--
-- >>> :set -XQuasiQuotes
-- >>> :{
-- putStr [dedent|
--   hello
--     world
--     !
--   |]
-- :}
-- hello
--   world
--   !
dedent :: QuasiQuoter
dedent = quoter $
  liftString . withLines (strip . trim (all isSpace))

withLines :: (Seq String -> Seq String) -> String -> String
withLines f = unsplit '\n' . toList . f . Seq.fromList . split '\n'

split :: Eq a => a -> [a] -> [[a]]
split sep xs = case break (== sep) xs of
  (ys, [])     -> ys : []
  (ys, _ : zs) -> ys : split sep zs

unsplit :: a -> [[a]] -> [a]
unsplit = intercalate . pure

-- | The first line can be safely dropped if it consists only of spaces,
-- but we want to preserve the last newline, thus last line can only be trimmed
trim :: (String -> Bool) -> Seq String -> Seq String
trim f = trimLast f . dropFirst f

dropFirst :: (String -> Bool) -> Seq String -> Seq String
dropFirst p xs = case Seq.viewl xs of
  y :< ys | p y -> ys
  _ -> xs

trimLast :: (String -> Bool) -> Seq String -> Seq String
trimLast p xs = case Seq.viewr xs of
  ys :> y | p y -> ys |> ""
  _ -> xs

strip :: Seq String -> Seq String
strip xs = case Seq.viewr xs of
  vs :> "" -> stripCommonLeadingWhitespace vs |> ""
  _        -> stripCommonLeadingWhitespace xs

stripCommonLeadingWhitespace :: (Functor f, Foldable f) => f String -> f String
stripCommonLeadingWhitespace xs = drop (commonLeadingWhitespace xs) <$> xs

commonLeadingWhitespace :: (Functor f, Foldable f) => f String -> Int
commonLeadingWhitespace = minimumOr 0 . fmap (length . takeWhile isSpace)

minimumOr :: (Foldable f, Ord a) => a -> f a -> a
minimumOr n = maybe n id . foldr (lmin . Just) Nothing
 where
  lmin (Just x) (Just y) = Just (min x y)
  lmin Nothing x = x
  lmin x Nothing = x

quoter :: (String -> Q Exp) -> QuasiQuoter
quoter quote = QuasiQuoter
  { quoteExp  = quote
  , quotePat  = failure "patterns"
  , quoteType = failure "types"
  , quoteDec  = failure "declarations"
  }
 where
  failure kind =
    fail $ "this quasiquoter does not support splicing " ++ kind