{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, DataKinds, TypeOperators, MultiParamTypeClasses, TypeFamilies, FlexibleContexts, FlexibleInstances, UndecidableInstances #-}

-- | Various useful functions.
module Magicbane.Util where

import           ClassyPrelude
import           Control.Monad.Except (MonadError)
import           Control.Error (hush)
import qualified Data.HashMap.Strict as HMS
import qualified Data.Text as T
import           Data.Char (isSpace)
import           Data.String.Conversions
import           Data.String.Conversions.Monomorphic
import           Data.Maybe (fromJust)
import           Data.Attoparsec.Text as AP
import           Data.Aeson
import           Network.URI
import           Network.HTTP.Types (hContentType)
import           Web.FormUrlEncoded hiding (parseMaybe)
import           Servant


-- | Merges two JSON objects recursively. When the values are not objects, just returns the left one.
mergeVal  Value  Value  Value
mergeVal (Object x) (Object y) = Object $ HMS.unionWith mergeVal x y
mergeVal x _ = x

-- | Encodes key-value data as application/x-www-form-urlencoded.
writeForm  (ConvertibleStrings α Text, ConvertibleStrings β Text, ConvertibleStrings LByteString γ)  [(α, β)]  γ
writeForm = fromLBS . mimeRender (Proxy  Proxy FormUrlEncoded) . map (toST *** toST)

-- | Decodes key-value data from application/x-www-form-urlencoded.
readForm  (ConvertibleStrings Text α, ConvertibleStrings Text β, ConvertibleStrings γ LByteString)  γ  Maybe [(α, β)]
readForm x = map (fromST *** fromST) <$> hush (mimeUnrender (Proxy  Proxy FormUrlEncoded) $ toLBS x)

-- | Reads a Servant incoming form as a list of key-value pairs (for use in FromForm instances).
formList  Form  [(Text, Text)]
formList = fromMaybe [] . hush . fromForm

-- | Converts a flat key-value form with keys in typical nesting syntax (e.g. "one[two][three]") to an Aeson Value with nesting (for use in FromForm instances).
formToObject  [(Text, Text)]  Value
formToObject f = foldl' assignProp (object []) $ (map . first) parseKey f
  where parseKey x = fromMaybe [ x ] $ hush $ parseOnly formKey x
        assignProp (Object o) ([k], v) = Object $ insertWith concatJSON k (toJSON [ v ]) o
        assignProp (Object o) (k : k' : ks, v) = Object $ insertWith (\_ o'  assignProp o' (k' : ks, v)) k (assignProp (object []) (k' : ks, v)) o
        assignProp x _ = x
        concatJSON (Array v1) (Array v2) = Array $ v1 ++ v2
        concatJSON (Array v1) _ = Array v1
        concatJSON _ (Array v2) = Array v2
        concatJSON _ _ = Null

formKey  Parser [Text]
formKey = do
  firstKey  AP.takeWhile (/= '[')
  restKeys  many' $ do
    void $ char '['
    s  AP.takeWhile (/= ']')
    void $ char ']'
    return s
  void $ option '_' $ char '[' >> char ']'
  return $ firstKey : filter (not . null) restKeys

-- | Parses any string into a URI.
parseUri  ConvertibleStrings α String  α  URI
parseUri = fromJust . parseURI . cs

-- | Prepares text for inclusion in a URL.
--
-- >>> :set -XOverloadedStrings
-- >>> slugify "Hello & World!"
-- "hello-and-world"
slugify  Text  Text
slugify = filter (not . isSpace) . intercalate "-" . words .
          T.replace "&" "and"  . T.replace "+" "plus" . T.replace "%" "percent" .
          T.replace "<" "lt"   . T.replace ">" "gt"   . T.replace "=" "eq" .
          T.replace "#" "hash" . T.replace "@" "at"   . T.replace "$" "dollar" .
          filter (`onotElem` asString "!^*?()[]{}`./\\'\"~|") .
          T.toLower . T.strip

-- | Creates a simple text/plain ServantErr.
errText  ServantErr  LByteString  ServantErr
errText e t = e { errHeaders = [ (hContentType, "text/plain; charset=utf-8") ]
                , errBody    = t }

-- | Creates and throws a simple text/plain ServantErr.
throwErrText  MonadError ServantErr μ  ServantErr  LByteString  μ α
throwErrText e t = throwError $ errText e t