module Net.Mac.ByteString.Char8
  ( encode
  , encodeWith
  , decode
  , decodeWith
  , decodeLenient
  , builder
  , parser
  , parserWith
  , parserLenient
  ) where

import Net.Types (Mac(..),MacCodec(..),MacGrouping(..))
import Net.Mac (fromOctets)
import Data.ByteString (ByteString)
import Data.Attoparsec.ByteString.Char8 (Parser)
import Data.ByteString.Lazy.Builder (Builder)
import Net.Internal (rightToMaybe,c2w)
import Data.Text.Encoding (encodeUtf8, decodeUtf8')
import Data.Word (Word8)
import Data.Word.Synthetic (Word12)
import Data.Bits (unsafeShiftL,unsafeShiftR)
import Control.Monad
import Data.Monoid
import qualified Data.ByteString.Builder.Fixed as FB
import qualified Data.ByteString.Builder as Builder
import qualified Data.Attoparsec.ByteString as ABW
import qualified Data.Attoparsec.ByteString.Char8 as AB

encode :: Mac -> ByteString
encode = encodeWith defCodec

decode :: ByteString -> Maybe Mac
decode = decodeWith defCodec

decodeWith :: MacCodec -> ByteString -> Maybe Mac
decodeWith codec bs = rightToMaybe (AB.parseOnly (parserWith codec <* AB.endOfInput) bs)

decodeLenient :: ByteString -> Maybe Mac
decodeLenient bs = rightToMaybe (AB.parseOnly (parserLenient <* AB.endOfInput) bs)

-- | Make a bytestring builder from a 'Mac' address
--   using a colon as the separator.
builder :: Mac -> Builder
builder = Builder.byteString . encode

-- | Parser for a 'Mac' address using with a colon as the
--   separator (i.e. @FA:43:B2:C0:0F:99@).
parser :: Parser Mac
parser = parserWith defCodec

-- | Parser for a 'Mac' address using the provided settings.
parserWith :: MacCodec -> Parser Mac
parserWith (MacCodec g _) = case g of
  MacGroupingPairs s -> parserPairs (c2w s)
  MacGroupingTriples s -> parserTriples (c2w s)
  MacGroupingQuadruples s -> parserQuadruples (c2w s)
  MacGroupingNoSeparator -> parserNoSeparator

parserLenient :: Parser Mac
parserLenient = do
  a1 <- parseOneHex
  a2 <- parseOneHexLenient
  a3 <- parseOneHexLenient
  a4 <- parseOneHexLenient
  a5 <- parseOneHexLenient
  a6 <- parseOneHexLenient
  a7 <- parseOneHexLenient
  a8 <- parseOneHexLenient
  a9 <- parseOneHexLenient
  a10 <- parseOneHexLenient
  a11 <- parseOneHexLenient
  a12 <- parseOneHexLenient
  return $ fromOctets
    (unsafeShiftL a1 4 + a2)
    (unsafeShiftL a3 4 + a4)
    (unsafeShiftL a5 4 + a6)
    (unsafeShiftL a7 4 + a8)
    (unsafeShiftL a9 4 + a10)
    (unsafeShiftL a11 4 + a12)


parserNoSeparator :: Parser Mac
parserNoSeparator = fromOctets
  <$> parseTwoHex
  <*> parseTwoHex
  <*> parseTwoHex
  <*> parseTwoHex
  <*> parseTwoHex
  <*> parseTwoHex

parserPairs :: Word8 -> Parser Mac
parserPairs s = fromOctets
  <$> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex

parserTriples :: Word8 -> Parser Mac
parserTriples s = do
  a1 <- parseOneHex
  a2 <- parseOneHex
  a3 <- parseOneHex
  _ <- ABW.word8 s
  a4 <- parseOneHex
  a5 <- parseOneHex
  a6 <- parseOneHex
  _ <- ABW.word8 s
  a7 <- parseOneHex
  a8 <- parseOneHex
  a9 <- parseOneHex
  _ <- ABW.word8 s
  a10 <- parseOneHex
  a11 <- parseOneHex
  a12 <- parseOneHex
  return $ fromOctets
    (unsafeShiftL a1 4 + a2)
    (unsafeShiftL a3 4 + a4)
    (unsafeShiftL a5 4 + a6)
    (unsafeShiftL a7 4 + a8)
    (unsafeShiftL a9 4 + a10)
    (unsafeShiftL a11 4 + a12)

parserQuadruples :: Word8 -> Parser Mac
parserQuadruples s  = fromOctets
  <$> parseTwoHex <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <*> parseTwoHex <* ABW.word8 s
  <*> parseTwoHex <*> parseTwoHex

parseOneHex :: Parser Word8
parseOneHex = ABW.anyWord8 >>= parseWord8Hex

-- | Parse a single hexidecimal character. This will skip
--   at most one character to do this.
parseOneHexLenient :: Parser Word8
parseOneHexLenient = do
  a <- ABW.anyWord8
  flip tryParseWord8Hex a $ do
    b <- ABW.anyWord8
    tryParseWord8Hex (fail "invalid hexadecimal character") b

parseTwoHex :: Parser Word8
parseTwoHex = do
  a <- ABW.anyWord8 >>= parseWord8Hex
  b <- ABW.anyWord8 >>= parseWord8Hex
  return (unsafeShiftL a 4 + b)

-- | Kind of a confusing type signature. The Word8 that stands
--   alone represented an ascii-encoded value. The others actually
--   describes the numbers that would be decoded from this value.
tryParseWord8Hex :: Parser Word8 -> Word8 -> Parser Word8
tryParseWord8Hex a w
  | w >= 48 && w <= 57 = return (w - 48)
  | w >= 65 && w <= 70 = return (w - 55)
  | w >= 97 && w <= 102 = return (w - 87)
  | otherwise = a

parseWord8Hex :: Word8 -> Parser Word8
parseWord8Hex = tryParseWord8Hex (fail "invalid hexadecimal character")

defCodec :: MacCodec
defCodec = MacCodec (MacGroupingPairs ':') False

encodeWith :: MacCodec -> Mac -> ByteString
encodeWith (MacCodec g u) m = case g of
  MacGroupingNoSeparator -> case u of
    True -> FB.run (fixedBuilderNoSeparator FB.word8HexFixedUpper) m
    False -> FB.run (fixedBuilderNoSeparator FB.word8HexFixedLower) m
  MacGroupingPairs c -> case u of
    True -> FB.run (fixedBuilderPairs FB.word8HexFixedUpper) (Pair (c2w c) m)
    False -> FB.run (fixedBuilderPairs FB.word8HexFixedLower) (Pair (c2w c) m)
    -- withCasedBuilder u $ \bw8 -> FB.run (fixedBuilderPairs bw8) (Pair c m)
  MacGroupingTriples c -> case u of
    True -> FB.run (fixedBuilderTriples FB.word12HexFixedUpper) (Pair (c2w c) m)
    False -> FB.run (fixedBuilderTriples FB.word12HexFixedLower) (Pair (c2w c) m)
  MacGroupingQuadruples c -> case u of
    True -> FB.run (fixedBuilderQuadruples FB.word8HexFixedUpper) (Pair (c2w c) m)
    False -> FB.run (fixedBuilderQuadruples FB.word8HexFixedLower) (Pair (c2w c) m)

withCasedBuilder :: Bool -> (FB.Builder Word8 -> a) -> a
withCasedBuilder x f = case x of
  True -> f FB.word8HexFixedUpper
  False -> f FB.word8HexFixedLower
{-# INLINE withCasedBuilder #-}

withCasedBuilderTriple :: Bool -> (FB.Builder Word12 -> a) -> a
withCasedBuilderTriple x f = case x of
  True -> f FB.word12HexFixedUpper
  False -> f FB.word12HexFixedLower
{-# INLINE withCasedBuilderTriple #-}

data Pair = Pair
  { pairSep :: {-# UNPACK #-} !Word8
  , pairMac :: {-# UNPACK #-} !Mac
  }

fixedBuilderTriples :: FB.Builder Word12 -> FB.Builder Pair
fixedBuilderTriples tripBuilder =
     FB.contramapBuilder (word12At 36 . pairMac) tripBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word12At 24 . pairMac) tripBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word12At 12 . pairMac) tripBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word12At 0 . pairMac) tripBuilder
{-# INLINE fixedBuilderTriples #-}

fixedBuilderQuadruples :: FB.Builder Word8 -> FB.Builder Pair
fixedBuilderQuadruples pairBuilder =
     FB.contramapBuilder (word8At 40 . pairMac) pairBuilder
  <> FB.contramapBuilder (word8At 32 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 24 . pairMac) pairBuilder
  <> FB.contramapBuilder (word8At 16 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 8 . pairMac) pairBuilder
  <> FB.contramapBuilder (word8At 0 . pairMac) pairBuilder
{-# INLINE fixedBuilderQuadruples #-}

fixedBuilderPairs :: FB.Builder Word8 -> FB.Builder Pair
fixedBuilderPairs pairBuilder =
     FB.contramapBuilder (word8At 40 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 32 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 24 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 16 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 8 . pairMac) pairBuilder
  <> FB.contramapBuilder pairSep FB.word8
  <> FB.contramapBuilder (word8At 0 . pairMac) pairBuilder
{-# INLINE fixedBuilderPairs #-}

fixedBuilderNoSeparator :: FB.Builder Word8 -> FB.Builder Mac
fixedBuilderNoSeparator hexBuilder =
     FB.contramapBuilder (word8At 40) hexBuilder
  <> FB.contramapBuilder (word8At 32) hexBuilder
  <> FB.contramapBuilder (word8At 24) hexBuilder
  <> FB.contramapBuilder (word8At 16) hexBuilder
  <> FB.contramapBuilder (word8At 8) hexBuilder
  <> FB.contramapBuilder (word8At 0) hexBuilder
{-# INLINE fixedBuilderNoSeparator #-}

word8At :: Int -> Mac -> Word8
word8At i (Mac w) = fromIntegral (unsafeShiftR w i)
{-# INLINE word8At #-}

word12At :: Int -> Mac -> Word12
word12At i (Mac w) = fromIntegral (unsafeShiftR w i)
{-# INLINE word12At #-}