module Penny.Copper.Util where

import Control.Applicative ((<*), pure, (<$))
import qualified Data.Char as C
import qualified Data.Foldable as F
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as X
import qualified Penny.Lincoln.HasText as HT
import qualified Penny.Lincoln.TextNonEmpty as TNE
import Text.Parsec (char, many, skipMany)
import Text.Parsec.Text (Parser)

rangeLettersToSymbols :: Char -> Bool
rangeLettersToSymbols c = case C.generalCategory c of
  C.UppercaseLetter -> True
  C.LowercaseLetter -> True
  C.TitlecaseLetter -> True
  C.ModifierLetter -> True
  C.OtherLetter -> True
  C.DecimalNumber -> True
  C.LetterNumber -> True
  C.OtherNumber -> True
  C.ConnectorPunctuation -> True
  C.DashPunctuation -> True
  C.OpenPunctuation -> True
  C.ClosePunctuation -> True
  C.InitialQuote -> True
  C.FinalQuote -> True
  C.OtherPunctuation -> True
  C.MathSymbol -> True
  C.CurrencySymbol -> True
  C.ModifierSymbol -> True
  C.OtherSymbol -> True
  _ -> False

rangeLetters :: Char -> Bool
rangeLetters c = case C.generalCategory c of
  C.UppercaseLetter -> True
  C.LowercaseLetter -> True
  C.TitlecaseLetter -> True
  C.ModifierLetter -> True
  C.OtherLetter -> True
  _ -> False

rangeMathCurrency :: Char -> Bool
rangeMathCurrency c = case C.generalCategory c of
  C.MathSymbol -> True
  C.CurrencySymbol -> True
  _ -> False

rangeSymbols :: Char -> Bool
rangeSymbols c = case C.generalCategory c of
  C.MathSymbol -> True
  C.CurrencySymbol -> True
  C.ModifierSymbol -> True
  C.OtherSymbol -> True
  _ -> False

rangeLettersNumbers :: Char -> Bool
rangeLettersNumbers c = case C.generalCategory c of
  C.UppercaseLetter -> True
  C.LowercaseLetter -> True
  C.TitlecaseLetter -> True
  C.ModifierLetter -> True
  C.OtherLetter -> True
  C.DecimalNumber -> True
  C.LetterNumber -> True
  C.OtherNumber -> True
  _ -> False

-- | Creates a new parser that behaves like the old one, but also
-- parses any whitespace remaining afterward.
lexeme :: Parser a -> Parser a
lexeme p = p <* skipMany (char ' ')

-- | Parses any trailing whitespace followed by a newline followed by
-- additional whitespace.
eol :: Parser ()
eol = pure ()
      <* skipMany (char ' ')
      <* char '\n'
      <* skipMany (char ' ')

-- | Parses a run of spaces.
spaces :: Parser ()
spaces = () <$ many (char ' ')

-- | Applied to a non-empty list of pairs, with the first element of
-- the pair being a predicate that returns True if a character is OK
-- and the second element being something of an arbitrary type, and to
-- something that has a Text. The pairs must be ordered from most
-- restrictive to least restrictive predicates. If at least one of the
-- predicates indicates that the Text is valid, returns the leftmost b
-- associated with that predicate. If none of the predicates indicates
-- that the Text is valid, returns the rightmost error.
--
-- Here, most restrictive means the predicate that indicates True for
-- the narrowest range of characters, while least restrictive means
-- the predicate that indicates True for the widest range of
-- characters.
checkText ::
  HT.HasText a
  => NE.NonEmpty ((Char -> Bool), b)
  -> a
  -> Maybe b
checkText ps a = let
  t = HT.text a
  results = fmap (g . f) ps where
    f (p, b) = (X.find (not . p) t, b)
    g (p, b) = case p of
      Nothing -> Right b
      Just c -> Left c
  folder x y = case x of
    Right b -> Right b
    Left _ -> y
  in case F.foldr1 folder results of
    Left _ -> Nothing
    Right b -> return b

listIsOK ::
  HT.HasTextNonEmptyList a
  => (Char -> Bool) -- ^ Returns True for characters that are allowed
  -> a
  -> Bool
listIsOK p = F.all (TNE.all p) . HT.textNonEmptyList

firstCharOfListIsOK ::
  HT.HasTextNonEmptyList a
  => (Char -> Bool) -- ^ Returns True if the first character is allowed
  -> a
  -> Bool
firstCharOfListIsOK p ls = let
  firstText = NE.head . HT.textNonEmptyList $ ls
  in p (TNE.first firstText)

-- | Takes a field that may or may not be present and a function that
-- renders it. If the field is not present at all, returns an empty
-- Text. Otherwise will succeed or fail depending upon whether the
-- rendering function succeeds or fails.
renMaybe :: Maybe a -> (a -> Maybe X.Text) -> Maybe X.Text
renMaybe mx f = case mx of
  Nothing -> Just X.empty
  Just a -> f a

-- | Merges a list of words into one Text; however, if any given Text
-- is empty, that Text is first dropped from the list.
txtWords :: [X.Text] -> X.Text
txtWords xs = case filter (not . X.null) xs of
  [] -> X.empty
  rs -> X.unwords rs