{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE RecordWildCards      #-}
{-# LANGUAGE ScopedTypeVariables  #-}

module Text.Enum.Utf8
  ( EnumUtf8(..)
  , Utf8Parsable(..)
  , EnumUtf8Config(..)
  , defaultEnumUtf8Config
  ) where

import           Data.Array
import qualified Data.ByteString.Char8          as B
import qualified Data.ByteString.Lazy           as BL
import qualified Data.ByteString.Builder        as BB
import           Data.Coerce
import           Data.Hashable
import           Data.Possibly
import qualified Data.HashMap.Strict            as HM
import           Data.String
import           Text.Utf8


-- | a class for 'T.Text' parsers.
class Utf8Parsable a where
  parseUtf8 :: Utf8 -> Possibly a

{- | our toolkit for enumerated types which should be defined as follows:

@
import Text.Enum.Utf8
import Text.Utf8

data Foo = FOO_bar | FOO_bar_baz
  deriving (Bounded,Enum,Eq,Ord,Show)

instance EnumUtf8     Foo
instance Renderable   Foo where render    = renderEnumUtf8
instance Utf8Parsable Foo where parseUtf8 = parseEnumUtf8

main :: IO ()
main = mapM_ (cvtLn . render) [minBound..maxBound::Foo]
@

-}
class ( Renderable    e
      , Bounded       e
      , Enum          e
      , Eq            e
      , Ord           e
      , Show          e
      , Utf8Parsable  e
      ) => EnumUtf8 e where

  -- | Configures the textual representation of @e@ generated by renderEnumUtf8.
  configEnumUtf8 :: e -> EnumUtf8Config
  configEnumUtf8 _ = defaultEnumUtf8Config

  -- | Generate the standard textual representation according to
  -- 'configEnumUtf8' by default.
  renderEnumUtf8 :: e -> Utf8
  renderEnumUtf8 e = enumUtf8Array ! I e

  -- | Parses an @e@ according to the 'renderEnumUtf8' render.
  parseEnumUtf8 :: Utf8 -> Possibly e
  parseEnumUtf8 u = maybe (Left m) Right $ HM.lookup b hashmap_b
    where
      m = "parseEnumUtf8: enumeration not recognised: "++show b
      b = utf8_to_bs u

  -- | A cassava field encoder, using 'the renderEnumUtf8' format.
  toFieldEnumUtf8 :: e -> B.ByteString
  toFieldEnumUtf8 e = enumByteStringArray ! I e

  -- | A cassava field parser using the 'renderEnumUtf8' format.
  fromFieldEnumUtf8_ :: Monad m => B.ByteString -> m e
  fromFieldEnumUtf8_ bs = maybe (fail msg) return $ HM.lookup bs hashmap_b
    where
      msg = "fromFieldEnumUtf8_: enumeration not recognised: "++show bs

  -- | For hashing @e@ with the 'renderEnumUtf8' representation.
  hashWithSaltEnumUtf8 :: Int -> e -> Int
  hashWithSaltEnumUtf8 n = hashWithSalt n . toFieldEnumUtf8


-------------------------------------------------------------------------------
-- EnumUtf8Config, defaultEnumUtf8Config
-------------------------------------------------------------------------------

-- | configures the default implementation of 'renderEnumUtf8'
data EnumUtf8Config =
  EnumUtf8Config
    { _etc_text_prep :: String -> String  -- ^ applied to the output of 'show';
                                          -- by default strips each data
                                          -- constructor up to and including
                                          -- the first '_'
    , _etc_char_prep :: Char -> Char      -- ^ applied to each character of
                                          -- the outpout of '_etc_text_prep'
    }

defaultEnumUtf8Config :: EnumUtf8Config
defaultEnumUtf8Config =
  EnumUtf8Config
    { _etc_text_prep = defaultTextPrep
    , _etc_char_prep = defaultCharPrep
    }

defaultTextPrep :: String -> String
defaultTextPrep s =
  case dropWhile (/='_') s of
    _:rst@(_:_) -> fromString rst
    _ -> error $ "defaultTextPrep: bad data constructor: "++s

defaultCharPrep :: Char -> Char
defaultCharPrep c = case c of
    '_' -> '-'
    _   -> c


-------------------------------------------------------------------------------
-- arrays
-------------------------------------------------------------------------------

newtype I a = I { _I :: a }
  deriving (Eq,Ord)

instance EnumUtf8 e => Ix (I e) where
  range   (l,h)   = coerce [_I l.._I h]
  index   (l,_) x = fromEnum (_I x) - fromEnum (_I l)
  inRange (l,h) x = _I l <= _I x && _I x <= _I h

-- | array of texts constructed with 'configEnumUtf8'
enumUtf8Array :: forall e . EnumUtf8 e => Array (I e) Utf8
enumUtf8Array =
    listArray (I minBound,I maxBound)
      [ fromString $ map _etc_char_prep $ _etc_text_prep $ show e
        | e <- [minBound..maxBound :: e]
        ]
  where
    EnumUtf8Config{..} = configEnumUtf8 (minBound :: e)

-- | array of 'B.ByteString' generated from 'renderEnumUtf8'
enumByteStringArray :: forall e . EnumUtf8 e => Array (I e) B.ByteString
enumByteStringArray =
  listArray (I minBound,I maxBound)
    [ utf8_to_bs $ renderEnumUtf8 e
      | e <- [minBound..maxBound :: e]
      ]


-------------------------------------------------------------------------------
-- hashmap
-------------------------------------------------------------------------------

-- | 'B.ByteString' 'HM.HashMap' based on 'renderEnumUtf8' representation
hashmap_b :: EnumUtf8 e => HM.HashMap B.ByteString e
hashmap_b =
  HM.fromList
    [ (utf8_to_bs $ renderEnumUtf8 c,c)
      | c <- [minBound..maxBound]
      ]


-------------------------------------------------------------------------------
-- utf8_to_bs
-------------------------------------------------------------------------------

utf8_to_bs :: Utf8 -> B.ByteString
utf8_to_bs = BL.toStrict . BB.toLazyByteString . coerce