{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
{- |
Module      : Graphics.V4L2.Format
Maintainer  : claude@mathr.co.uk
Stability   : no
Portability : no
-}
module Graphics.V4L2.Format
  ( BufferType(..)
  , Direction(..)
  , Format(..)
  , FormatID()
  , FormatDescription(..)
  , FormatFlag(..)
  , queryFormats
  , FrameSize(..)
  , FrameSizes(..)
  , queryFrameSizes
  , FrameIntervals(..)
  , queryFrameIntervals
  , ImageFormat(..)
  ) where

import Control.Exception as E (catch, throwIO)
import Control.Monad (when)
import Data.Data (Data)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Set (Set)
import qualified Data.Set as S
import Data.Typeable (Typeable)
import Data.Word (Word32)
import GHC.IO.Exception (IOErrorType(InvalidArgument), ioe_type)

import Bindings.Linux.VideoDev2

import Foreign.Extra.BitSet (fromBitSet)
import Foreign.Extra.CEnum (toCEnum)
import Foreign.Extra.String (fromString)
import Graphics.V4L2.Device (Device)
import Graphics.V4L2.IOCtl (ioctl, zero)
import Graphics.V4L2.Field (Field)
import Graphics.V4L2.Field.Internal (fromField, toField)
import Graphics.V4L2.PixelFormat (PixelFormat)
import Graphics.V4L2.PixelFormat.Internal (fromPixelFormat, toPixelFormat)
import Graphics.V4L2.ColorSpace (ColorSpace)
import Graphics.V4L2.ColorSpace.Internal (fromColorSpace, toColorSpace)
import Graphics.V4L2.Types (Fraction(..))
import Graphics.V4L2.Types.Internal (fromFraction)

-- FIXME {
{-
c'V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9
c'V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10
V4L2_BUF_TYPE_VIDEO_OVERLAY
V4L2_BUF_TYPE_VBI_CAPTURE
V4L2_BUF_TYPE_VBI_OUTPUT
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY
V4L2_BUF_TYPE_PRIVATE
-}
-- FIXME }

{- |  Buffer types. -}
data BufferType = BufferVideoCapture | BufferVideoOutput | BufferUnknown Word32
  deriving (Eq, Ord, Read, Show, Data, Typeable)

toBufferType :: BufferType -> C'v4l2_buf_type
toBufferType = toCEnum
  [ ( BufferVideoCapture , c'V4L2_BUF_TYPE_VIDEO_CAPTURE )
  , ( BufferVideoOutput  , c'V4L2_BUF_TYPE_VIDEO_OUTPUT  )
  ] isU unU
  where
    isU (BufferUnknown _) = True
    isU _ = False
    unU (BufferUnknown b) = fromIntegral b
    unU _ = error "err"

{- |  Transfer types. -}
data Direction = Capture | Output
  deriving (Eq, Ord, Enum, Bounded, Read, Show, Data, Typeable)

{- |  Buffer formats. -}
class Format f where
  {- |  Corresponding buffer type. -}
  formatBufferType :: f -> Direction -> BufferType
  {- |  Query the format. -}
  getFormat :: Device -> Direction -> IO f
  {- |  Select the format. -}
  setFormat :: Device -> Direction -> f -> IO f
  {- |  Test the format. -}
  tryFormat :: Device -> Direction -> f -> IO f

{- |  Image format. -}
data ImageFormat = ImageFormat
  { imageWidth, imageHeight :: Int
  , imagePixelFormat :: PixelFormat
  , imageField :: Field
  , imageBytesPerLine :: Int
  , imageSize :: Int
  , imageColorSpace :: ColorSpace
  }
  deriving (Eq, Ord, Read, Show, Data, Typeable)

instance Format ImageFormat where
  formatBufferType _ = imageBufferType
  getFormat d dir   =
    return . decodeImageFormat . c'v4l2_format_u'pix . c'v4l2_format'fmt =<< ioctl d C'VIDIOC_G_FMT . (\s->s{ c'v4l2_format'type = toBufferType $ imageBufferType dir }) =<< zero
  setFormat d dir f = do
    u <- (`u'v4l2_format_u'pix` encodeImageFormat f) =<< zero
    return . decodeImageFormat . c'v4l2_format_u'pix . c'v4l2_format'fmt =<< ioctl d C'VIDIOC_S_FMT =<< (\s->s{ c'v4l2_format'type = toBufferType $ imageBufferType dir, c'v4l2_format'fmt = u }) `fmap` zero
  tryFormat d dir f = do
    u <- (`u'v4l2_format_u'pix` encodeImageFormat f) =<< zero
    return . decodeImageFormat . c'v4l2_format_u'pix . c'v4l2_format'fmt =<< ioctl d C'VIDIOC_TRY_FMT =<< (\s->s{ c'v4l2_format'type = toBufferType $ imageBufferType dir, c'v4l2_format'fmt = u }) `fmap` zero

encodeImageFormat :: ImageFormat -> C'v4l2_pix_format
encodeImageFormat f = C'v4l2_pix_format
  { c'v4l2_pix_format'width = fromIntegral $ imageWidth f
  , c'v4l2_pix_format'height = fromIntegral $ imageHeight f
  , c'v4l2_pix_format'pixelformat = toPixelFormat $ imagePixelFormat f
  , c'v4l2_pix_format'field = toField $ imageField f
  , c'v4l2_pix_format'bytesperline = fromIntegral $ imageBytesPerLine f
  , c'v4l2_pix_format'sizeimage = fromIntegral $ imageSize f
  , c'v4l2_pix_format'colorspace = toColorSpace $ imageColorSpace f
  , c'v4l2_pix_format'priv = 0
  }     

decodeImageFormat :: C'v4l2_pix_format -> ImageFormat
decodeImageFormat f = ImageFormat
  { imageWidth = fromIntegral $ c'v4l2_pix_format'width f
  , imageHeight = fromIntegral $ c'v4l2_pix_format'height f
  , imagePixelFormat = fromPixelFormat $ c'v4l2_pix_format'pixelformat f
  , imageField = fromField $ c'v4l2_pix_format'field f
  , imageBytesPerLine = fromIntegral $ c'v4l2_pix_format'bytesperline f
  , imageSize = fromIntegral $ c'v4l2_pix_format'sizeimage f
  , imageColorSpace = fromColorSpace $ c'v4l2_pix_format'colorspace f
  }

imageBufferType :: Direction -> BufferType
imageBufferType Capture = BufferVideoCapture
imageBufferType Output = BufferVideoOutput


{- |  Format flags. -}
data FormatFlag
  = FormatCompressed
  | FormatEmulated
  | FormatUnknown Word32
  deriving (Eq, Ord, Read, Show, Data, Typeable)
  
fromFormatFlag :: Word32 -> Set FormatFlag
fromFormatFlag = fromBitSet
  [ ( FormatCompressed , c'V4L2_FMT_FLAG_COMPRESSED )
  , ( FormatEmulated   , c'V4L2_FMT_FLAG_EMULATED   )
  ]   FormatUnknown

{- | Video format ID. -}
newtype FormatID = FormatID Int
  deriving (Eq, Ord, Enum, Bounded, Read, Show, Data, Typeable, Num, Integral, Real)

{- | Video format description -}
data FormatDescription = FormatDescription
  { formatDescription :: String
  , formatPixelFormat :: PixelFormat
  , formatFlags :: Set FormatFlag
  }
  deriving (Eq, Ord, Read, Show, Data, Typeable)

enumfmt :: Device -> BufferType -> FormatID -> IO FormatDescription
enumfmt h t n = do
  d <- ioctl h C'VIDIOC_ENUM_FMT . (\s->s{ c'v4l2_fmtdesc'index = fromIntegral n, c'v4l2_fmtdesc'type = toBufferType t }) =<< zero
  return FormatDescription
    { formatDescription = fromString $ c'v4l2_fmtdesc'description d
    , formatPixelFormat = fromPixelFormat $ c'v4l2_fmtdesc'pixelformat d
    , formatFlags = fromFormatFlag $ c'v4l2_fmtdesc'flags d
    }

{- |  Enumerate supported buffer formats.

      Exceptions:

        * InvalidArgument - buffer type not supported or index out of range
-}     
queryFormats :: Device -> BufferType -> IO (Map FormatID FormatDescription)
queryFormats = enumfmts' 0

enumfmts' :: FormatID -> Device -> BufferType -> IO (Map FormatID FormatDescription)
enumfmts' n h t = do
  mi <- (Just `fmap` enumfmt h t n) `E.catch` (\e -> case ioe_type e of
    InvalidArgument -> return Nothing
    _ -> throwIO e)
  case mi of
    Just i -> M.insert n i `fmap` enumfmts' (n + 1) h t
    Nothing -> return M.empty

{- |  Enumerate supported frame sizes. -}
queryFrameSizes :: Device -> PixelFormat -> IO FrameSizes
queryFrameSizes h p = do
  fs <- enumframesizes0 h p `E.catch` (\e -> case ioe_type e of
    InvalidArgument -> return (DiscreteSizes S.empty)
    _ -> throwIO e)
  case fs of
    DiscreteSizes f | not $ S.null f -> do
      fs' <- enumframesizesD 1 h p
      return . DiscreteSizes $ f `S.union` fs'
    _ -> return fs

enumframesizes0 :: Device -> PixelFormat -> IO FrameSizes
enumframesizes0 h p = do
  f <- ioctl h C'VIDIOC_ENUM_FRAMESIZES . (\s->s{ c'v4l2_frmsizeenum'index = 0, c'v4l2_frmsizeenum'pixel_format = toPixelFormat p }) =<< zero
  case c'v4l2_frmsizeenum'type f of
    x | x == c'V4L2_FRMSIZE_TYPE_DISCRETE -> return . DiscreteSizes . S.singleton . decodeFrameSize . c'v4l2_frmsizeenum_u'discrete . c'v4l2_frmsizeenum'u $ f
      | x == c'V4L2_FRMSIZE_TYPE_STEPWISE ||
        x == c'V4L2_FRMSIZE_TYPE_CONTINUOUS -> do
          let s = c'v4l2_frmsizeenum_u'stepwise . c'v4l2_frmsizeenum'u $ f
          return StepwiseSizes
            { stepwiseMinWidth = fromIntegral $ c'v4l2_frmsize_stepwise'min_width s
            , stepwiseMaxWidth = fromIntegral $ c'v4l2_frmsize_stepwise'max_width s
            , stepwiseStepWidth = fromIntegral $ c'v4l2_frmsize_stepwise'step_width s
            , stepwiseMinHeight = fromIntegral $ c'v4l2_frmsize_stepwise'min_height s
            , stepwiseMaxHeight = fromIntegral $ c'v4l2_frmsize_stepwise'max_height s
            , stepwiseStepHeight = fromIntegral $ c'v4l2_frmsize_stepwise'step_height s
            }
      | otherwise -> error "err"

enumframesizesD :: Word32 -> Device -> PixelFormat -> IO (Set FrameSize)
enumframesizesD n h p = do
  f <- (do
    f' <- ioctl h C'VIDIOC_ENUM_FRAMESIZES . (\s->s{ c'v4l2_frmsizeenum'index = n, c'v4l2_frmsizeenum'pixel_format = toPixelFormat p }) =<< zero
    when (c'v4l2_frmsizeenum'type f' /= c'V4L2_FRMSIZE_TYPE_DISCRETE) $ error "err"
    return (Just f')) `E.catch` (\e -> case ioe_type e of
    InvalidArgument -> return Nothing
    _ -> throwIO e)
  case f of
    Nothing -> return S.empty
    Just fs -> S.insert (decodeFrameSize . c'v4l2_frmsizeenum_u'discrete . c'v4l2_frmsizeenum'u $ fs) `fmap` enumframesizesD (n + 1) h p

{- |  Single frame size. -}
data FrameSize = FrameSize
  { frameWidth
  , frameHeight :: Int
  }
  deriving (Eq, Ord, Read, Show, Data, Typeable)

{- |  Discrete and continuous frame sizes. -}
data FrameSizes
  = DiscreteSizes
      { discreteSizes :: Set FrameSize }
  | StepwiseSizes
      { stepwiseMinWidth
      , stepwiseMaxWidth
      , stepwiseStepWidth
      , stepwiseMinHeight
      , stepwiseMaxHeight
      , stepwiseStepHeight :: Int
      }
  deriving (Eq, Ord, Read, Show, Data, Typeable)

decodeFrameSize :: C'v4l2_frmsize_discrete -> FrameSize
decodeFrameSize d = FrameSize
  { frameWidth = fromIntegral $ c'v4l2_frmsize_discrete'width  d
  , frameHeight = fromIntegral $ c'v4l2_frmsize_discrete'height d
  }

{- |  Enumerate frame intervals. -}
queryFrameIntervals :: Device -> PixelFormat -> FrameSize -> IO FrameIntervals
queryFrameIntervals h p f = do
  fi <- enumframeintervals0 h p f `E.catch` (\e -> case ioe_type e of
    InvalidArgument -> return (DiscreteIntervals S.empty)
    _ -> throwIO e)
  case fi of
    DiscreteIntervals i | not $ S.null i -> do
      is <- enumframeintervalsD 1 h p f
      return . DiscreteIntervals $ i `S.union` is
    _ -> return fi

enumframeintervals0 :: Device -> PixelFormat -> FrameSize -> IO FrameIntervals
enumframeintervals0 h p f = do
  i <- ioctl h C'VIDIOC_ENUM_FRAMEINTERVALS . (\s->s{ c'v4l2_frmivalenum'index = 0, c'v4l2_frmivalenum'pixel_format = toPixelFormat p, c'v4l2_frmivalenum'width = fromIntegral (frameWidth f), c'v4l2_frmivalenum'height = fromIntegral (frameHeight f) }) =<< zero
  case c'v4l2_frmivalenum'type i of
    x | x == c'V4L2_FRMIVAL_TYPE_DISCRETE -> return . DiscreteIntervals . S.singleton . fromFraction . c'v4l2_frmivalenum_u'discrete . c'v4l2_frmivalenum'u $ i
      | x == c'V4L2_FRMIVAL_TYPE_STEPWISE ||
        x == c'V4L2_FRMIVAL_TYPE_CONTINUOUS -> do
          let s = c'v4l2_frmivalenum_u'stepwise . c'v4l2_frmivalenum'u $ i
          return StepwiseIntervals
            { stepwiseMin = fromFraction $ c'v4l2_frmival_stepwise'min s
            , stepwiseMax = fromFraction $ c'v4l2_frmival_stepwise'max s
            , stepwiseStep = fromFraction $ c'v4l2_frmival_stepwise'step s
            }
      | otherwise -> error "err"

enumframeintervalsD :: Word32 -> Device -> PixelFormat -> FrameSize -> IO (Set Fraction)
enumframeintervalsD n h p f = do
  i <- (do
    f' <- ioctl h C'VIDIOC_ENUM_FRAMEINTERVALS . (\s->s{ c'v4l2_frmivalenum'index = n, c'v4l2_frmivalenum'pixel_format = toPixelFormat p, c'v4l2_frmivalenum'width = fromIntegral (frameWidth f), c'v4l2_frmivalenum'height = fromIntegral (frameHeight f) }) =<< zero
    when (c'v4l2_frmivalenum'type f' /= c'V4L2_FRMIVAL_TYPE_DISCRETE) $ error "err"
    return (Just f')) `E.catch` (\e -> case ioe_type e of
    InvalidArgument -> return Nothing
    _ -> throwIO e)
  case i of
    Nothing -> return S.empty
    Just fs -> S.insert (fromFraction . c'v4l2_frmivalenum_u'discrete . c'v4l2_frmivalenum'u $ fs) `fmap` enumframeintervalsD (n + 1) h p f

{- |  Discrete and continuous frame intervals. -}
data FrameIntervals
  = DiscreteIntervals
      { discreteIntervals :: Set Fraction }
  | StepwiseIntervals
      { stepwiseMin, stepwiseMax, stepwiseStep :: Fraction }
  deriving (Eq, Ord, Read, Show, Data, Typeable)