{-# LANGUAGE FlexibleContexts #-}

module Data.Hourglass.FuzzyParsing.Internal where

import Data.Char      (digitToInt, isDigit, toUpper)
import Data.Hourglass (Month)
import Data.List      (isPrefixOf)

import Text.Parsec

-- | Parser version of Prelude.read
tryRead :: (Read a, Stream s m Char) => String -> ParsecT s st m a
tryRead str =
  case reads str of
    [(res, "")] -> return res
    _ -> fail $ "Cannot read: " ++ str

tryReadInt :: (Stream s m Char, Num a) => String -> ParsecT s st m a
tryReadInt str =
  if all isDigit str
    then return $ fromIntegral $ foldl (\a b -> 10*a+b) 0 $ map digitToInt str
    else fail $ "Cannot read: " ++ str

-- | Apply parser N times
times :: (Stream s m Char)
      => Int
      -> ParsecT s st m t
      -> ParsecT s st m [t]
times 0 _ = return []
times n p = do
  ts <- times (n - 1) p
  t <- optionMaybe p
  case t of
    Just t' -> return (ts ++ [t'])
    Nothing -> return ts

-- | Parse natural number of N digits which is not greater than M
number :: (Stream s m Char, Num a, Ord a)
       => Int -- ^ Number of digits
       -> a   -- ^ Maximum value
       -> ParsecT s st m a
number n m = do
  t <- tryReadInt =<< (n `times` digit)
  if t > m
    then fail "number too large"
    else return t

pYear :: Stream s m Char => ParsecT s st m Int
pYear = do
  y <- number 4 10000
  if y < 2000
    then return (y + 2000)
    else return y

pMonth :: Stream s m Char => ParsecT s st m Month
pMonth = toEnum . pred <$> number 2 12

pDay :: Stream s m Char => ParsecT s st m Int
pDay = number 2 31

uppercase :: String -> String
uppercase = map toUpper

-- | Case-insensitive version of 'isPrefixOf'
isPrefixOfI :: String -> String -> Bool
p `isPrefixOfI` s = uppercase p `isPrefixOf` uppercase s

-- | Use a data type's Bounded, Enum and Show instances to determine if the
-- given string uniquely matches a constructor. The comparison is
-- case-insensitive and starts from the beginning of the strings (so a partial
-- constructor name can still match if there are enough characters for a
-- unique match)
--
-- For example:
--
-- @
--  data Things = Foo | Bar | Baz deriving (Bounded, Enum, Show)
--
--  -- Right Foo
--  uniqFuzzyMatch "f" :: Either [Things] Things
--
--  -- Left [Bar, Baz]
--  uniqFuzzyMatch "ba" :: Either [Things] Things
-- @
uniqFuzzyMatch :: (Bounded a, Enum a, Show a)
               => String
               -> Either [a] a -- ^ Either collection of matches or the unique match
uniqFuzzyMatch n = if length matches == 1
                      then Right (head matches)
                      else Left matches
  where
    possibilities = [minBound..maxBound]
    matches = filter (isPrefixOfI n . show) possibilities