module Data.String.Interpolate.Util (unindent) where

import           Control.Arrow ((>>>))
import           Data.Char

-- | Remove indentation as much as possible while preserving relative
-- indentation levels.
--
-- `unindent` is useful in combination with `Data.String.Interpolate.i` to remove leading spaces that
-- resulted from code indentation.  That way you can freely indent your string
-- literals without the indentation ending up in the resulting strings.
--
-- Here is an example:
--
-- >>> :set -XQuasiQuotes
-- >>> import Data.String.Interpolate
-- >>> import Data.String.Interpolate.Util
-- >>> :{
--  putStr $ unindent [i|
--      def foo
--        23
--      end
--    |]
-- :}
-- def foo
--   23
-- end
--
-- To allow this, two additional things are being done, apart from removing
-- indentation:
--
-- - One empty line at the beginning will be removed and
-- - if the last newline character (@"\\n"@) is followed by spaces, the spaces are removed.
unindent :: String -> String
unindent =
      lines_
  >>> removeLeadingEmptyLine
  >>> trimLastLine
  >>> removeIndentation
  >>> concat
  where
    isEmptyLine :: String -> Bool
    isEmptyLine = all isSpace

    lines_ :: String -> [String]
    lines_ [] = []
    lines_ s = case span (/= '\n') s of
      (first, '\n' : rest) -> (first ++ "\n") : lines_ rest
      (first, rest) -> first : lines_ rest

    removeLeadingEmptyLine :: [String] -> [String]
    removeLeadingEmptyLine xs = case xs of
      y:ys | isEmptyLine y -> ys
      _ -> xs

    trimLastLine :: [String] -> [String]
    trimLastLine (a : b : r) = a : trimLastLine (b : r)
    trimLastLine [a] = if all (== ' ') a
      then []
      else [a]
    trimLastLine [] = []

    removeIndentation :: [String] -> [String]
    removeIndentation ys = map (dropSpaces indentation) ys
      where
        dropSpaces 0 s = s
        dropSpaces n (' ' : r) = dropSpaces (n - 1) r
        dropSpaces _ s = s
        indentation = minimalIndentation ys
        minimalIndentation =
            safeMinimum 0
          . map (length . takeWhile (== ' '))
          . removeEmptyLines
        removeEmptyLines = filter (not . isEmptyLine)

        safeMinimum :: Ord a => a -> [a] -> a
        safeMinimum x xs = case xs of
          [] -> x
          _ -> minimum xs