module Generics.SOP.Lens.Computed (
    -- * Abstract lenses
    AbstractLens(..)
  , abstractId
  , afterGLens
    -- * Getters and setters
  , get
  , set
  , modify
  , getM
  , setM
  , modifyM
    -- * Computing lenses
  , Path
  , CLens
  , lens
    -- * Manually constructing lenses
  , emptyPathOnly
    -- * Configuration
  , LensOptions(..)
  , defaultLensOptions
  ) where

import Prelude hiding (id, (.))
import Control.Arrow
import Control.Category
import Control.Monad
import Data.Functor.Identity
import Data.Maybe (catMaybes)

import Generics.SOP
import Generics.SOP.Lens (GLens)
import qualified Generics.SOP.Lens as GLens

{-------------------------------------------------------------------------------
  Abstract lenses
-------------------------------------------------------------------------------}

-- | An abstract lens qualifies existentially over the target type of the lens
--
-- Sadly, abstract lenses do not form a category, so we provide special
-- identity and composition functions.
data AbstractLens r w c a =
  forall x. c x => AbstractLens (GLens r w a x)

-- | Identity abstract lens
abstractId :: (ArrowApply r, ArrowApply w, c a) => AbstractLens r w c a
abstractId = AbstractLens id

-- | Compose with a pointwise lens on the right
afterGLens :: (ArrowApply r, ArrowApply w)
           => AbstractLens r w c   a -- ^ @a -> x@
           -> GLens        r w   b a -- ^ @b -> a@
           -> AbstractLens r w c b   -- ^ @b -> x@
afterGLens (AbstractLens l) l' = AbstractLens (l . l')

{-------------------------------------------------------------------------------
  Getters and setters (mostly just for convenience)
-------------------------------------------------------------------------------}

-- | Getter for computed lenses
--
-- > get l == runIdentity . getM l . Identity
get :: Category r => AbstractLens r w c a -> (forall x. c x => r a x -> b) -> b
get l f = runIdentity $ getM l (Identity . f)

-- | Setter for computed lenses
--
-- > set l == runIdentity . setM l . Identity
set :: Arrow w => AbstractLens r w c a -> (forall x. c x => x) -> w a a
set l x = runIdentity $ setM l (Identity x)

-- | Modifier for computed lenses
modify :: Arrow w => AbstractLens r w c a -> (forall x. c x => w x x) -> w a a
modify l f = runIdentity $ modifyM l (Identity f)

-- | Getter with possibility for "compile time" failure
getM :: (Monad m, Category r)
     => AbstractLens r w c a
     -> (forall x. c x => r a x -> m b)
     -> m b
getM (AbstractLens l) k = k (GLens.get l)

-- | Setter with possibility for "compile time" failure
setM :: (Monad m, Arrow w)
     => AbstractLens r w c a -> (forall x. c x => m x) -> m (w a a)
setM (AbstractLens l) mx =
  mx >>= \x -> return $ GLens.set l . arr (\a -> (x, a))

-- | Modifier with possibility for "compile time" failure
modifyM :: (Monad m, Arrow w)
        => AbstractLens r w c a -> (forall x. c x => m (w x x)) -> m (w a a)
modifyM (AbstractLens l) mf =
  mf >>= \f -> return $ GLens.modify l . arr (\a -> (f, a))

{-------------------------------------------------------------------------------
  Paths
-------------------------------------------------------------------------------}

-- | A path is a series of field names. For instance, given
--
-- > data T1 = T1 { a :: Int, b :: Int } deriving Generic
-- > data T2 = T2 { c :: T1,  d :: Int } deriving Generic
--
-- valid paths on T2 are
--
-- > []
-- > ["c"]
-- > ["d"]
-- > ["c", "a"]
-- > ["c", "b"]
type Path = [String]

{-------------------------------------------------------------------------------
  Top-level generic function
-------------------------------------------------------------------------------}

-- | Compute a lens for a given type and path
--
-- The @Either@ is used to indicate "compile time" failure of the computation
-- of the lens (for instance, when this path is invalid for this data type).
--
-- Some lenses may of course be themselves effectful, depending on the category.
-- However, the lenses returned by the generic computation are pure and total
-- (as is evident from the type of glens).
class CLens r w c a where
  default lens :: ( Generic a
                  , HasDatatypeInfo a
                  , ArrowApply r
                  , ArrowApply w
                  , c a
                  , Code a ~ '[xs]
                  , All (CLens r w c) xs
                  )
               => LensOptions -> Path -> Either String (AbstractLens r w c a)

  lens :: LensOptions -> Path -> Either String (AbstractLens r w c a)
  lens = glens

{-------------------------------------------------------------------------------
  Instances

  We don't provide any instances here, because applications might want to
  implement special kinds of semantics for certain paths for types that we
  normally cannot "look into".
-------------------------------------------------------------------------------}

-- | A lens for abstract types (supports empty paths only)
--
-- Useful for defining CLens instances for types such as Int, Bool,
-- Text, etc.
--
-- > instance CLens c Int where lens = emptyPathOnly
emptyPathOnly :: (ArrowApply r, ArrowApply w, c a)
              => LensOptions -> Path -> Either String (AbstractLens r w c a)
emptyPathOnly _ [] = Right $ abstractId
emptyPathOnly _ _  = Left "Trying to look inside abstract type"

{-------------------------------------------------------------------------------
  Lens options
-------------------------------------------------------------------------------}

data LensOptions = LensOptions {
    -- | Match a selector against a path component
    lensOptionsMatch :: DatatypeName -> FieldName -> String -> Bool
  }

-- | Default match just compares field names
defaultLensOptions :: LensOptions
defaultLensOptions = LensOptions {
    lensOptionsMatch = const (==)
  }

{-------------------------------------------------------------------------------
  The actual generic function
-------------------------------------------------------------------------------}

glens :: forall r w a c xs.
         ( ArrowApply r
         , ArrowApply w
         , Generic a
         , HasDatatypeInfo a
         , c a
         , Code a ~ '[xs]
         , All (CLens r w c) xs
         )
      => LensOptions -> Path -> Either String (AbstractLens r w c a)
glens _    []     = Right $ abstractId
glens opts (p:ps) = liftM (`afterGLens` (GLens.sop . GLens.rep))
                         . glens' opts p ps
                         $ datatypeInfo (Proxy :: Proxy a)

glens' :: ( ArrowApply r
          , ArrowApply w
          , All (CLens r w c) xs
          )
       => LensOptions -> String -> Path
       -> DatatypeInfo '[xs]
       -> Either String (AbstractLens r w c (NP I xs))
glens' opts p ps d =
  glens'' opts ps (datatypeName d) p (hd (constructorInfo d))

glens'' :: forall r w c xs.
           ( ArrowApply r
           , ArrowApply w
           , All (CLens r w c) xs
           )
        => LensOptions -> Path
        -> DatatypeName -> String
        -> ConstructorInfo xs
        -> Either String (AbstractLens r w c (NP I xs))
glens'' _ _ _ _ (Constructor _) =
    Left $ "Cannot compute lenses for non-record types"
glens'' _ _ _ _ (Infix _ _ _) =
    Left $ "Cannot compute lenses for non-record types"
glens'' opts ps d p (Record _ fs) =
    case matchingLenses of
      []  -> Left $ "Unknown field " ++ show p ++ " of datatype " ++ show d
      [l] -> l
      _   -> Left $ "Invalid metadata for datatype " ++ show d
  where
    matchingLenses :: [Either String (AbstractLens r w c (NP I xs))]
    matchingLenses = catMaybes . hcollapse $ hcliftA2 pl aux fs GLens.np

    aux :: forall a. CLens r w c a
        => FieldInfo a
        -> GLens r w (NP I xs) a
        -> K (Maybe (Either String (AbstractLens r w c (NP I xs)))) a
    aux (FieldInfo f) l = K $
      if lensOptionsMatch opts d f p
        then Just $ ((`afterGLens` l) `liftM` lens opts ps)
        else Nothing

    pl :: Proxy (CLens r w c)
    pl = Proxy
