{-| This module provides a variant of the vector, equipped with @push_back@ operation.
IOVector here are supposed to be used in single thread situation.
-}
module Data.Vector.Mutable.PushBack where

import Prelude hiding (length, read)
import Control.Monad
import Data.IORef
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as VM
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import System.IO.Unsafe

-- | @IOVector@ consists of (1) pointer to the underlying vector (2) length
-- While 'Data.Vector' has the underlying array itself, this type only has the pointer.
-- This means read/write should be slower than the original vector.
data IOVector a = IOVector !(IORef (VM.IOVector a)) !(VUM.IOVector Int)

-- Allocate (p + 10)-element vector, which might be more efficient than allocating just a small size of vector, like 1-element or 2-element.
new :: Int -> IO (IOVector a)
new p = new' (p + 10)
  where new' p = IOVector <$> (newIORef =<< VM.new p) <*> (VUM.replicate 1 0)

read :: IOVector a -> Int -> IO a
read (IOVector vref _) k = readIORef vref >>= \vec -> VM.read vec k

-- | Get the position of the last cell in the @IOVector@. This operation is not safe because of the 'unsafePerformIO'.
safeLength :: IOVector a -> IO Int
safeLength (IOVector _ uvec) = VUM.read uvec 0

length :: IOVector a -> Int
length pvec = unsafePerformIO $ safeLength pvec

safeCapacity :: IOVector a -> IO Int
safeCapacity (IOVector vref _) = fmap VM.length $ readIORef vref

-- | Get the capacity of the @IOVector@. This operation is not safe because of the 'unsafePerformIO'.
capacity :: IOVector a -> Int
capacity pvec = unsafePerformIO $ safeCapacity pvec

write :: IOVector a -> Int -> a -> IO ()
write (IOVector vref _) i v = do
  vec <- readIORef vref
  VM.write vec i v

-- | /O(n)/ Insert a value into any place. This is a slow operation.
insert
  :: IOVector a  -- ^ The vector should have positive (non-zero) length
  -> Int
  -> a
  -> IO ()
insert pvec i v = do
  len <- safeLength pvec

  read pvec (len - 1) >>= push pvec
  forM_ (reverse [i .. len - 2]) $ \j -> read pvec j >>= write pvec (j + 1)
  write pvec i v

-- | /O(n)/ This is a slow operation. This also throws an exception if the specified index does not exist.
delete :: IOVector a -> Int -> IO ()
delete pvec@(IOVector _ uvec) i = do
  len <- safeLength pvec
  forM_ [i + 1 .. len - 1] $ \j -> read pvec j >>= write pvec (j - 1)
  VUM.modify uvec (\x -> x - 1) 0

push :: IOVector a -> a -> IO ()
push pvec@(IOVector vref uvec) v = do
  vec <- readIORef vref
  len <- safeLength pvec
  cap <- safeCapacity pvec
  when (len == cap) $ do
    vec' <- VM.grow vec cap
    writeIORef vref vec'

  write      pvec len   v
  VUM.modify uvec (+ 1) 0

fromList :: [a] -> IO (IOVector a)
fromList xs = do
  vec  <- V.thaw $ V.fromList xs
  vec' <- VM.grow vec ((VM.length vec + 5) * 2)
  vref <- newIORef vec'
  uvec <- VU.thaw $ VU.fromList [VM.length vec]
  return $ IOVector vref uvec

asIOVector :: IOVector a -> IO (VM.IOVector a)
asIOVector pvec@(IOVector vref _) = do
  len <- safeLength pvec
  readIORef vref >>= \vec -> return (VM.slice 0 len vec)

asUnsafeIOVector :: IOVector a -> VM.IOVector a
asUnsafeIOVector pvec = unsafePerformIO $ asIOVector pvec