{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module: Optics.At
-- Description: Optics for 'Data.Map.Map' and 'Data.Set.Set'-like containers.
--
-- This module provides optics for 'Data.Map.Map' and 'Data.Set.Set'-like
-- containers, including an 'AffineTraversal' to traverse a key in a map or an
-- element of a sequence:
--
-- >>> preview (ix 1) ['a','b','c']
-- Just 'b'
--
-- a 'Lens' to get, set or delete a key in a map:
--
-- >>> set (at 0) (Just 'b') (Map.fromList [(0, 'a')])
-- fromList [(0,'b')]
--
-- and a 'Lens' to insert or remove an element of a set:
--
-- >>> IntSet.fromList [1,2,3,4] & contains 3 .~ False
-- fromList [1,2,4]
--
-- This module includes the core definitions from "Optics.At.Core" along with
-- extra (orphan) instances.
--
module Optics.At
  (
    -- * Type families
    Index
  , IxValue

    -- * Ixed
  , Ixed(..)
  , ixAt

    -- * At
  , At(..)
  , at'
  , sans

  -- * Contains
  , Contains(..)
  ) where

import qualified Data.ByteString as StrictB
import qualified Data.ByteString.Lazy as LazyB
import Data.HashMap.Lazy (HashMap)
import qualified Data.HashMap.Lazy as HashMap
import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import Data.Hashable (Hashable)
import Data.Int (Int64)
import qualified Data.Text as StrictT
import qualified Data.Text.Lazy as LazyT
import qualified Data.Vector as Vector hiding (indexed)
import qualified Data.Vector.Primitive as Prim
import qualified Data.Vector.Storable as Storable
import qualified Data.Vector.Unboxed as Unboxed hiding (indexed)
import Data.Word (Word8)

import Optics.Core

type instance Index (HashSet a) = a
type instance Index (HashMap k a) = k
type instance Index (Vector.Vector a) = Int
type instance Index (Prim.Vector a) = Int
type instance Index (Storable.Vector a) = Int
type instance Index (Unboxed.Vector a) = Int
type instance Index StrictT.Text = Int
type instance Index LazyT.Text = Int64
type instance Index StrictB.ByteString = Int
type instance Index LazyB.ByteString = Int64

-- Contains

instance (Eq a, Hashable a) => Contains (HashSet a) where
  contains k = lensVL $ \f s -> f (HashSet.member k s) <&> \b ->
    if b then HashSet.insert k s else HashSet.delete k s
  {-# INLINE contains #-}

-- Ixed

type instance IxValue (HashMap k a) = a
-- Default implementation uses HashMap.alterF
instance (Eq k, Hashable k) => Ixed (HashMap k a)

type instance IxValue (HashSet k) = ()
instance (Eq k, Hashable k) => Ixed (HashSet k) where
  ix k = atraversalVL $ \point f m ->
    if HashSet.member k m
    then f () <&> \() -> HashSet.insert k m
    else point m
  {-# INLINE ix #-}

type instance IxValue (Vector.Vector a) = a
instance Ixed (Vector.Vector a) where
  ix i = atraversalVL $ \point f v ->
    if 0 <= i && i < Vector.length v
    then f (v Vector.! i) <&> \a -> v Vector.// [(i, a)]
    else point v
  {-# INLINE ix #-}

type instance IxValue (Prim.Vector a) = a
instance Prim.Prim a => Ixed (Prim.Vector a) where
  ix i = atraversalVL $ \point f v ->
    if 0 <= i && i < Prim.length v
    then f (v Prim.! i) <&> \a -> v Prim.// [(i, a)]
    else point v
  {-# INLINE ix #-}

type instance IxValue (Storable.Vector a) = a
instance Storable.Storable a => Ixed (Storable.Vector a) where
  ix i = atraversalVL $ \point f v ->
    if 0 <= i && i < Storable.length v
    then f (v Storable.! i) <&> \a -> v Storable.// [(i, a)]
    else point v
  {-# INLINE ix #-}

type instance IxValue (Unboxed.Vector a) = a
instance Unboxed.Unbox a => Ixed (Unboxed.Vector a) where
  ix i = atraversalVL $ \point f v ->
    if 0 <= i && i < Unboxed.length v
    then f (v Unboxed.! i) <&> \a -> v Unboxed.// [(i, a)]
    else point v
  {-# INLINE ix #-}

type instance IxValue StrictT.Text = Char
instance Ixed StrictT.Text where
  ix e = atraversalVL $ \point f s ->
    case StrictT.splitAt e s of
      (l, mr) -> case StrictT.uncons mr of
        Nothing      -> point s
        Just (c, xs) -> f c <&> \d -> StrictT.concat [l, StrictT.singleton d, xs]
  {-# INLINE ix #-}

type instance IxValue LazyT.Text = Char
instance Ixed LazyT.Text where
  ix e = atraversalVL $ \point f s ->
    case LazyT.splitAt e s of
      (l, mr) -> case LazyT.uncons mr of
        Nothing      -> point s
        Just (c, xs) -> f c <&> \d -> LazyT.append l (LazyT.cons d xs)
  {-# INLINE ix #-}

type instance IxValue StrictB.ByteString = Word8
instance Ixed StrictB.ByteString where
  ix e = atraversalVL $ \point f s ->
    case StrictB.splitAt e s of
      (l, mr) -> case StrictB.uncons mr of
        Nothing      -> point s
        Just (c, xs) -> f c <&> \d -> StrictB.concat [l, StrictB.singleton d, xs]
  {-# INLINE ix #-}

type instance IxValue LazyB.ByteString = Word8
instance Ixed LazyB.ByteString where
  -- TODO: we could be lazier, returning each chunk as it is passed
  ix e = atraversalVL $ \point f s ->
    case LazyB.splitAt e s of
      (l, mr) -> case LazyB.uncons mr of
        Nothing      -> point s
        Just (c, xs) -> f c <&> \d -> LazyB.append l (LazyB.cons d xs)
  {-# INLINE ix #-}

-- At

instance (Eq k, Hashable k) => At (HashMap k a) where
#if MIN_VERSION_unordered_containers(0,2,10)
  at k = lensVL $ \f -> HashMap.alterF f k
#else
  at k = lensVL $ \f m ->
    let mv = HashMap.lookup k m
    in f mv <&> \r -> case r of
      Nothing -> maybe m (const (HashMap.delete k m)) mv
      Just v' -> HashMap.insert k v' m
#endif
  {-# INLINE at #-}

instance (Eq k, Hashable k) => At (HashSet k) where
  at k = lensVL $ \f m ->
    let mv = if HashSet.member k m
             then Just ()
             else Nothing
    in f mv <&> \r -> case r of
      Nothing -> maybe m (const (HashSet.delete k m)) mv
      Just () -> HashSet.insert k m
  {-# INLINE at #-}

-- $setup
-- >>> import qualified Data.IntSet as IntSet
-- >>> import qualified Data.Map as Map
