{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.String
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable
--
-- String manipulation utilities

module Yi.String (isBlank,
                  chomp,
                  capitalize,
                  capitalizeFirst,
                  dropSpace,
                  fillText,
                  onLines,
                  mapLines,
                  lines',
                  unlines',
                  padLeft, padRight,
                  commonTPrefix,
                  commonTPrefix',
                  listify,
                  showT,
                  overInit, overTail
                 ) where

import           Data.Char   (isAlphaNum, isSpace, toLower, toUpper)
import           Data.List   (isSuffixOf)
import           Data.Maybe  (fromMaybe)
import           Data.Monoid ((<>))
import qualified Data.Text   as T (Text, break, commonPrefixes, empty,
                                   intercalate, pack, splitAt, splitOn, toUpper)
import qualified Yi.Rope     as R (YiString, all, cons, head, init, intercalate,
                                   last, length, lines', snoc, tail, unwords,
                                   withText, words)

-- | Helper that shows then packs the 'Text', for all those cases
-- where we use 'show'.
showT :: Show a => a -> T.Text
showT = T.pack . show

-- | This is kind of like the default Show instance for lists except
-- over 'T.Text'. It does not leave the elements in extra quotes and
-- should not be attempted to be 'show'n and 'read' back.
listify :: [R.YiString] -> R.YiString
listify t = '[' `R.cons` R.intercalate ", " t `R.snoc` ']'

-- | Works by resupplying the found prefix back into the list,
-- eventually either finding the prefix or not matching.
commonTPrefix :: [T.Text] -> Maybe T.Text
commonTPrefix (x:y:xs) = case T.commonPrefixes x y of
  Nothing -> Nothing
  Just (p, _, _) -> commonTPrefix (p : xs)
commonTPrefix [x] = Just x
commonTPrefix _ = Nothing

-- | Like 'commonTPrefix' but returns empty text on failure.
commonTPrefix' :: [T.Text] -> T.Text
commonTPrefix' = fromMaybe T.empty . commonTPrefix

capitalize :: String -> String
capitalize [] = []
capitalize (c:cs) = toUpper c : map toLower cs

capitalizeFirst :: R.YiString -> R.YiString
capitalizeFirst = R.withText go
  where
    go x = case T.break isAlphaNum x of
      (f, b) -> f <> case T.splitAt 1 b of
        (h, hs) -> T.toUpper h <> hs

-- | Remove any trailing strings matching /irs/ (input record separator)
-- from input string. Like perl's chomp(1).
chomp :: String -> String -> String
chomp irs st
    | irs `isSuffixOf` st
    = let st' = reverse $ drop (length irs) (reverse st) in chomp irs st'
    | otherwise = st
{-# INLINE chomp #-}


-- | Trim spaces at beginning /and/ end
dropSpace :: String -> String
dropSpace = let f = reverse . dropWhile isSpace in f . f

isBlank :: R.YiString -> Bool
isBlank = R.all isSpace

-- | Fills lines up to the given length, splitting the text up if
-- necessary.
fillText :: Int -> R.YiString -> [R.YiString]
fillText margin = map (R.unwords . reverse) . fill 0 [] . R.words
  where
    fill _ acc [] = [acc]
    fill n acc (w:ws)
      | n + R.length w >= margin = acc : fill (R.length w) [w] ws
      | otherwise = fill (n + 1 + R.length w) (w:acc) ws

-- | @overInit f@ runs f over the 'R.init' of the input if possible,
-- preserving the 'R.last' element as-is. If given a string with
-- length ≤ 1, it effectively does nothing.
--
-- Also see 'overTail'.
overInit :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
overInit f t = case (R.init t, R.last t) of
  (Just xs, Just x) -> f xs `R.snoc` x
  _ -> t

-- | @overInit f@ runs f over the 'R.tail' of the input if possible,
-- preserving the 'R.head' element as-is. If given a string with
-- length ≤ 1, it effectively does nothing.
--
-- Also see 'overInit'.
overTail :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
overTail f t = case (R.head t, R.tail t) of
  (Just x, Just xs) -> x `R.cons` f xs
  _ -> t

-- | Inverse of 'lines''. In contrast to 'Prelude.unlines', this does
-- not add an empty line at the end.
unlines' :: [T.Text] -> T.Text
unlines' = T.intercalate "\n"

-- | Split a Text in lines. Unlike 'Prelude.lines', this does not
-- remove any empty line at the end.
lines' :: T.Text -> [T.Text]
lines' = T.splitOn "\n"

-- | A helper function for creating functions suitable for
-- 'modifySelectionB' and 'modifyRegionB'.
-- To be used when the desired function should map across
-- the lines of a region.
mapLines :: (R.YiString -> R.YiString) -> R.YiString -> R.YiString
mapLines f = onLines $ fmap f

onLines :: ([R.YiString] -> [R.YiString]) -> R.YiString -> R.YiString
onLines f = mconcat . f . R.lines'

padLeft, padRight :: Int -> String -> String
padLeft n [] = replicate n ' '
padLeft n (x:xs) = x : padLeft (n-1) xs

padRight n = reverse . padLeft n . reverse