{-# LANGUAGE CPP, TypeOperators #-}
-- | A 'Frame' is a finite 'Int'-indexed collection of rows.
module Frames.Frame where
import Data.Foldable
#if __GLASGOW_HASKELL__ < 800
import Data.Monoid
#endif
import qualified Data.Vector as V
import Data.Vinyl.TypeLevel
import Frames.Rec (Record)
import Frames.RecF (rappend)

-- | A 'Frame' is a finite collection of rows indexed by 'Int'.
data Frame r = Frame { frameLength :: !Int
                     , frameRow    :: Int -> r }

-- | A 'Frame' whose rows are 'Record' values.
type FrameRec rs = Frame (Record rs)

instance Functor Frame where
  fmap f (Frame len g) = Frame len (f . g)

-- | Build a 'Frame' from any 'Foldable'. This simply uses a boxed
-- 'V.Vector' to hold each row. If you have a collection of 'Record's,
-- consider using 'Frames.InCore.toFrame'.
boxedFrame :: Foldable f => f r -> Frame r
boxedFrame xs = Frame (V.length v) (v V.!)
  where v = V.fromList (toList xs)

-- | The 'Monoid' instance for 'Frame' provides a mechanism for
-- vertical concatenation of 'Frame's. That is, @f1 <> f2@ will return
-- a new 'Frame' with the rows of @f1@ followed by the rows of @f2@.
instance Monoid (Frame r) where
  mempty = Frame 0 (const $ error "index out of bounds (empty frame)")
  Frame l1 f1 `mappend` Frame l2 f2 = Frame (l1+l2) $ \i ->
                                      if i < l1 then f1 i else f2 (i - l1)

instance Foldable Frame where
  foldMap f (Frame n row) = foldMap (f . row) [0..n-1]
  {-# INLINE foldMap #-}
  foldl' f z (Frame n row) = foldl' ((. row) . f) z [0..n-1]
  {-# INLINE foldl' #-}

instance Applicative Frame where
  -- | A frame of 'maxBound' rows, each of which is the given value.
  pure x = Frame maxBound (const x)
  -- | Zips two 'Frame's together, applying the rows of the first to
  -- those of the second. The result has as many rows as the smaller
  -- of the two argument 'Frame's.
  Frame l1 f1 <*> Frame l2 f2 = Frame (min l1 l2) $ ($) <$> f1 <*> f2

instance Monad Frame where
  -- | A frame of 'maxBound' rows, each of which is the given value.
  return = pure
  -- | Like 'concatMap' for lists.
  Frame l f >>= fb = foldMap (fb . f) [0 .. l - 1]

-- | Horizontal 'Frame' concatenation. That is, @zipFrames f1 f2@ will
-- return a 'Frame' with as many rows as the smaller of @f1@ and @f2@
-- whose rows are the result of appending the columns of @f2@ to those
-- of @f1@.
zipFrames :: FrameRec rs -> FrameRec rs' -> FrameRec (rs ++ rs')
zipFrames (Frame l1 f1) (Frame l2 f2) =
    Frame (min l1 l2) $ rappend <$> f1 <*> f2