{-# LANGUAGE TypeFamilies, TupleSections, OverloadedStrings #-}

module File.Binary.Classes (Field(..), Binary(..), pop, push) where

import Data.ByteString.Lazy (ByteString, unpack, singleton, cons')
import qualified Data.ByteString.Lazy.Char8 as BSLC ()
import Data.Bits ((.&.), (.|.), shiftL, shiftR)
import Data.Monoid (Monoid, mappend, mempty)
import Data.Word
import Control.Arrow (first, second)
import Control.Applicative
import Control.Monad

type AddBits b = ([Bool], b)

class Field f where
	type FieldArgument f
	fromBinary :: (Binary b, Applicative m, Monad m) => FieldArgument f -> b -> m (f, b)
	toBinary :: (Binary b, Applicative m, Monad m) => FieldArgument f -> f -> m b
	fromBits :: (Binary b, Applicative m, Monad m) =>
		FieldArgument f -> AddBits b -> m (f, AddBits b)
	consToBits :: (Binary b, Applicative m, Monad m) =>
		FieldArgument f -> f -> AddBits b -> m (AddBits b)

	fromBits a ([], b) = second ([] ,) `liftM` fromBinary a b
	fromBits _ _ = error "fromBits: not bytes (1 byte = 8 bits)"
	consToBits a f ([], b) = do
		ret <- (`mappend` b) <$> toBinary a f
		return ([], ret)
	consToBits _ _ (bits, _) =
		fail $ "consToBits: not bytes (1 byte = 8 bits) " ++ show bits

	fromBinary a b = do
		ret <- fromBits a ([], b)
		case ret of
			(f, ([], rest)) -> return (f, rest)
			_ -> fail "fromBinary: not bytes (1 byte = 8 bits)"
	toBinary a f = do
		ret <- consToBits a f ([], mempty)
		case ret of
			([], bin) -> return bin
			(bits, _) -> fail $
				"toBinary: not bytes (1 byte = 8 bits) " ++ show bits

pop :: Binary b => b -> AddBits b
pop = first (wtbs (8 :: Int) . head . unpack) . getBytes 1
	where
	wtbs 0 _ = []
	wtbs n w = toEnum (fromIntegral $ 1 .&. w) : wtbs (n - 1) (w `shiftR` 1)

push :: Binary b => AddBits b -> b
push = uncurry $ mappend . makeBinary . singleton . bstw
	where
	bstw = foldr (\b w -> w `shiftL` 1 .|. fromIntegral (fromEnum b)) 0

class (Eq b, Monoid b) => Binary b where
	getBytes :: Int -> b -> (ByteString, b)
	spanBytes :: (Word8 -> Bool) -> b -> (ByteString, b)
	unconsByte :: b -> (Word8, b)
	makeBinary :: ByteString -> b

	getBytes 0 b = ("", b)
	getBytes n b = let
		(h, t) = unconsByte b
		(r, b') = getBytes (n - 1) t in
		(h `cons'` r, b')

	spanBytes p b
		| b == makeBinary "" = ("", b)
		| p h = let (ret, rest) = spanBytes p t in (h `cons'` ret, rest)
		| otherwise = ("", b)
		where
		(h, t) = unconsByte b
