module Salak.Internal.Val where

import           Control.Applicative  ((<|>))
import           Data.ByteString      (ByteString)
import           Data.Heap            (Heap)
import qualified Data.Heap            as H
import           Data.Int
import           Data.List            (intercalate)
import           Data.Scientific
import           Data.Text            (Text)
import qualified Data.Text            as T
import           Data.Text.Encoding   (decodeUtf8)
import           Data.Time
import           Salak.Internal.Key
import           Text.Megaparsec
import           Text.Megaparsec.Char
#if __GLASGOW_HASKELL__ < 804
import           Data.Semigroup       ((<>))
#endif


data Val v = Val !Int !v deriving (Eq, Show)

data ModType
  = Add
  | Mod
  | Del
  | Noop deriving (Eq, Show)

type Priority = Int

{-# INLINE priority #-}
priority :: Val v -> Int
priority (Val i _) = i

data VRef
  = VRT !Text
  | VRR !Keys ![VRef]
  deriving Eq

instance Show VRef where
  show (VRT t)  = T.unpack t
  show (VRR k m) = "${" <> show k <> (if null m then go m else ":" <> go m) <> "}"
    where
      {-# INLINE go #-}
      go = foldr ((<>) . show) ""

data Value
  = VT  !Text
  | VI  !Scientific
  | VB  !Bool
  | VLT !LocalTime
  | VD  !Day
  | VH  !TimeOfDay
  | VU  !UTCTime
  | VR  ![VRef]
  deriving Eq

{-# INLINE nullValue #-}
nullValue :: Value -> Bool
nullValue (VT x)  = T.null x
nullValue (VR []) = True
nullValue _       = False

instance Show Value where
  show v = let (a,b) = typeOfV v in b ++ "::" ++ a

{-# INLINE typeOfV #-}
typeOfV :: Value -> (String, String)
typeOfV (VT  b) = ("Str",       show b)
typeOfV (VI  b) = ("Num",       show b)
typeOfV (VB  b) = ("Bool",      show b)
typeOfV (VLT b) = ("LocalTime", show b)
typeOfV (VD  b) = ("Day",       show b)
typeOfV (VH  b) = ("TimeOfDay", show b)
typeOfV (VU  b) = ("UTCTime",   show b)
typeOfV (VR  b) = ("Ref",       show b)

{-# INLINE getType #-}
getType :: Value -> String
getType = fst . typeOfV

{-# INLINE mkValue #-}
mkValue :: Value -> Either String Value
mkValue (VT v) = if T.null v
  then Right (VT v)
  else case parse vref "" v of
    Left  e   -> Left (errorBundlePretty e)
    Right y   -> Right $ case y of
      [VRT x] -> VT x
      vs      -> VR vs
mkValue v      = Right v

{-# INLINE exprChar #-}
exprChar :: Parser Char
exprChar = noneOf go <|> (char '\\' >> oneOf go)
  where
    go :: [Token Text]
    go = "${}\\"

vref :: Parser [VRef]
vref = some (go <|> (VRT . T.pack <$> some exprChar))
  where
    {-# INLINE go #-}
    go = do
      _ <- string "${"
      k <- keyExpr
      v <- option [] $ char ':' >> option [VRT ""] vref
      _ <- char '}'
      return (VRR (fromKeys k) v)

newtype Vals = Vals { unVals :: Heap (Val Value) } deriving Eq

instance Show Vals where
  show (Vals v) = intercalate "," $ go <$> H.toUnsortedList v
    where
      go (Val i x) = '#' : show i ++ ('.' : show x)

instance Eq v => Ord (Val v) where
  compare (Val a _) (Val b _) = compare a b

{-# INLINE nullVals #-}
nullVals :: Vals -> Bool
nullVals (Vals v) = H.null v

{-# INLINE minimumVals #-}
minimumVals :: Vals -> Val Value
minimumVals (Vals h) = H.minimum h

{-# INLINE emptyVals #-}
emptyVals :: Vals
emptyVals = Vals H.empty

{-# INLINE deleteVals #-}
deleteVals :: Int -> Vals -> (Bool, Vals)
deleteVals i (Vals v) =
  let (a,b) = H.partition ((==i) . priority) v
  in (H.null a, Vals b)

{-# INLINE getVal #-}
getVal :: Vals -> Maybe Value
getVal (Vals v)
  | H.null v = Nothing
  | otherwise = let Val _ x = H.minimum v in Just x

class ToValue a where
  toVal :: a -> Value

instance ToValue Value where
  {-# INLINE toVal #-}
  toVal = id

instance ToValue Text where
  {-# INLINE toVal #-}
  toVal = VT

instance ToValue ByteString where
  {-# INLINE toVal #-}
  toVal = VT . decodeUtf8

instance ToValue String where
  {-# INLINE toVal #-}
  toVal = VT . T.pack

instance ToValue Scientific where
  {-# INLINE toVal #-}
  toVal = VI

instance ToValue Integer where
  {-# INLINE toVal #-}
  toVal = VI . fromInteger

instance ToValue Int where
  {-# INLINE toVal #-}
  toVal = VI . fromInteger . toInteger

instance ToValue Int64 where
  {-# INLINE toVal #-}
  toVal = VI . fromInteger . toInteger

instance ToValue Double where
  {-# INLINE toVal #-}
  toVal = VI . realToFrac

instance ToValue Bool where
  {-# INLINE toVal #-}
  toVal = VB

instance ToValue UTCTime where
  {-# INLINE toVal #-}
  toVal = VU

{-# INLINE delVals #-}
delVals :: Int -> Vals -> Vals
delVals p (Vals v) = Vals $ H.filter ((/=p) . priority) v

modVals :: Val Value -> Vals -> Either String Vals
modVals (Val p x) (Vals v) = case mkValue x of
  Left  e -> Left e
  Right y -> Right $ Vals $ H.insert (Val p y) $ H.filter ((/=p) . priority) v

{-# INLINE singletonVals #-}
singletonVals :: Val Value -> Either String Vals
singletonVals (Val p x) = case mkValue x of
  Left  e -> Left e
  Right y -> Right $ Vals $ H.singleton $ Val p y

modVals' :: Vals -> Vals -> Either String Vals
modVals' (Vals v) vals = if H.null v then Right vals else modVals (H.minimum v) vals