{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}

-- |
-- Module      : Database.Relational.Query.ProjectableExtended
-- Copyright   : 2013 Kei Hibino
-- License     : BSD3
--
-- Maintainer  : ex8k.hibino@gmail.com
-- Stability   : experimental
-- Portability : unknown
--
-- This module defines operators on various polymorphic projections
-- which needs extended GHC features.
module Database.Relational.Query.ProjectableExtended (
  -- * Projection for nested 'Maybe's
  ProjectableFlattenMaybe (flatten),

  flattenPiMaybe,

  -- * Get narrower projections
  (!), (?!), (?!?), (!??),

  (.!), (.?),

  -- * Aggregate functions
  unsafeAggregateOp,
  count,
  sum', sumMaybe, avg, avgMaybe,
  max', maxMaybe, min', minMaybe,
  every, any', some',

  -- * Zipping projection type trick
  ProjectableIdZip (leftId, rightId),
  ProjectableRunIdsZip (runIds), flattenPh
  -- generalizedZip', (>?<)
  )  where

import Prelude hiding (pi)
import Data.Int (Int64)
import Data.Monoid ((<>))

import qualified Language.SQL.Keyword as SQL

import Database.Relational.Query.Context (Flat, Aggregated, OverWindow)
import Database.Relational.Query.Expr (Expr, fromJust)
import Database.Relational.Query.Projection (Projection)
import qualified Database.Relational.Query.Projection as Projection
import Database.Relational.Query.Projectable
  (expr, PlaceHolders, unsafeUniOp,
   ProjectableMaybe (flattenMaybe), ProjectableIdZip (leftId, rightId),
   SqlProjectable)
import Database.Relational.Query.Pi (Pi)


-- | Projection interface.
class Projectable p0 p1 where
  -- | Project from projection type 'p0' into weaken projection types 'p1'.
  project :: p0 c a -> p1 c a


class AggregatedContext ac
instance AggregatedContext Aggregated
instance AggregatedContext OverWindow

-- | Unsafely make aggregation uni-operator from SQL keyword.
unsafeAggregateOp :: (AggregatedContext ac, SqlProjectable (p ac))
                  => SQL.Keyword -> Projection Flat a -> p ac b
unsafeAggregateOp op = unsafeUniOp ((op <>) . SQL.paren)

-- | Aggregation function COUNT.
count :: (AggregatedContext ac, SqlProjectable (p ac))
      => Projection Flat a -> p ac Int64
count =  unsafeAggregateOp SQL.COUNT

-- | Aggregation function SUM.
sumMaybe :: (Num a, AggregatedContext ac, SqlProjectable (p ac))
         => Projection Flat (Maybe a) -> p ac (Maybe a)
sumMaybe  =  unsafeAggregateOp SQL.SUM

-- | Aggregation function SUM.
sum' :: (Num a, AggregatedContext ac, SqlProjectable (p ac))
     => Projection Flat a -> p ac (Maybe a)
sum'  =  sumMaybe . Projection.just

-- | Aggregation function AVG.
avgMaybe :: (Num a, Fractional b, AggregatedContext ac, SqlProjectable (p ac))
         => Projection Flat (Maybe a) -> p ac (Maybe b)
avgMaybe   =  unsafeAggregateOp SQL.AVG

-- | Aggregation function AVG.
avg :: (Num a, Fractional b, AggregatedContext ac, SqlProjectable (p ac))
    => Projection Flat a -> p ac (Maybe b)
avg =  avgMaybe . Projection.just

-- | Aggregation function MAX.
maxMaybe :: (Ord a, AggregatedContext ac, SqlProjectable (p ac))
         => Projection Flat (Maybe a) -> p ac (Maybe a)
maxMaybe  =  unsafeAggregateOp SQL.MAX

-- | Aggregation function MAX.
max' :: (Ord a, AggregatedContext ac, SqlProjectable (p ac))
     => Projection Flat a -> p ac (Maybe a)
max' =  maxMaybe . Projection.just

-- | Aggregation function MIN.
minMaybe :: (Ord a, AggregatedContext ac, SqlProjectable (p ac))
         => Projection Flat (Maybe a) -> p ac (Maybe a)
minMaybe  =  unsafeAggregateOp SQL.MIN

-- | Aggregation function MIN.
min' :: (Ord a, AggregatedContext ac, SqlProjectable (p ac))
     => Projection Flat a -> p ac (Maybe a)
min' =  minMaybe . Projection.just

-- | Aggregation function EVERY.
every :: (AggregatedContext ac, SqlProjectable (p ac))
      => Projection Flat (Maybe Bool) -> p ac (Maybe Bool)
every =  unsafeAggregateOp SQL.EVERY

-- | Aggregation function ANY.
any' :: (AggregatedContext ac, SqlProjectable (p ac))
     => Projection Flat (Maybe Bool) -> p ac (Maybe Bool)
any'  =  unsafeAggregateOp SQL.ANY

-- | Aggregation function SOME.
some' :: (AggregatedContext ac, SqlProjectable (p ac))
      => Projection Flat (Maybe Bool) -> p ac (Maybe Bool)
some' =  unsafeAggregateOp SQL.SOME

-- | Project from 'Projection' into 'Projection'.
instance Projectable Projection Projection where
  project = id

-- | Project from 'Projection' into 'Expr' 'Projection'.
instance Projectable Projection Expr where
  project = expr

projectPi :: Projectable Projection p1 => Projection c a -> Pi a b -> p1 c b
projectPi p = project . Projection.pi p

projectPiMaybe :: Projectable Projection p1 => Projection c (Maybe a) -> Pi a b -> p1 c (Maybe b)
projectPiMaybe p = project . Projection.piMaybe p

projectPiMaybe' :: Projectable Projection p1 => Projection c (Maybe a) -> Pi a (Maybe b) -> p1 c (Maybe b)
projectPiMaybe' p = project . Projection.piMaybe' p

-- | Get narrower projection along with projection path
--   and project into result projection type.
(!) :: Projectable Projection p
    => Projection c a   -- ^ Source projection
    -> Pi a b -- ^ Projection path
    -> p c b   -- ^ Narrower projected object
(!) =  projectPi

-- | Get narrower projection along with projection path
--   and project into result projection type.
--   'Maybe' phantom type is propagated.
(?!) :: Projectable Projection p
     => Projection c (Maybe a) -- ^ Source 'Projection'. 'Maybe' type
     -> Pi a b       -- ^ Projection path
     -> p c (Maybe b) -- ^ Narrower projected object. 'Maybe' type result
(?!) =  projectPiMaybe

-- | Get narrower projection along with projection path
--   and project into result projection type.
--   'Maybe' phantom type is propagated. Projection path leaf is 'Maybe' case.
(?!?) :: Projectable Projection p
      => Projection c (Maybe a)   -- ^ Source 'Projection'. 'Maybe' phantom type
      -> Pi a (Maybe b) -- ^ Projection path. 'Maybe' type leaf
      -> p c (Maybe b)   -- ^ Narrower projected object. 'Maybe' phantom type result
(?!?) =  projectPiMaybe'

-- | Get narrower projected expression along with projectino path
--   and strip 'Maybe' phantom type off.
(.!) :: Projection c (Maybe a) -- ^ Source projection type 'p'. 'Maybe' phantom type
     -> Pi a b      -- ^ Projection path
     -> Expr c b    -- ^ Narrower projected expression. 'Maybe' phantom type is stripped off
(.!) p = fromJust . projectPiMaybe p

-- | Get narrower projected expression along with projectino path
--   and strip 'Maybe' phantom type off.
--   Projection path leaf is 'Maybe' case.
(.?) :: Projection c (Maybe a)    -- ^ Source projection type 'p'. 'Maybe' phantom type
     -> Pi a (Maybe b) -- ^ Projection path. 'Maybe' type leaf
     -> Expr c b       -- ^ Narrower projected expression. 'Maybe' phantom type is stripped off
(.?) p = fromJust . projectPiMaybe' p


-- | Interface to compose phantom 'Maybe' nested type.
class ProjectableFlattenMaybe a b where
  flatten :: ProjectableMaybe p => p a -> p b

-- | Compose 'Maybe' type in projection phantom type.
instance ProjectableFlattenMaybe (Maybe a) b
         => ProjectableFlattenMaybe (Maybe (Maybe a)) b where
  flatten = flatten . flattenMaybe

-- | Not 'Maybe' type is not processed.
instance ProjectableFlattenMaybe (Maybe a) (Maybe a) where
  flatten = id

-- | Get narrower projection with flatten leaf phantom Maybe types along with projection path.
flattenPiMaybe :: (ProjectableMaybe (Projection cont), ProjectableFlattenMaybe (Maybe b) c)
               => Projection cont (Maybe a) -- ^ Source 'Projection'. 'Maybe' phantom type
               -> Pi a b               -- ^ Projection path
               -> Projection cont c         -- ^ Narrower 'Projection'. Flatten 'Maybe' phantom type
flattenPiMaybe p = flatten . Projection.piMaybe p

projectFlattenPiMaybe :: (ProjectableMaybe (Projection cont),
                          Projectable Projection p1, ProjectableFlattenMaybe (Maybe b) c)
                      => Projection cont (Maybe a) -- ^ Source 'Projection'. 'Maybe' phantom type
                      -> Pi a b                    -- ^ Projection path
                      -> p1 cont c                 -- ^ Narrower 'Projection'. Flatten 'Maybe' phantom type
projectFlattenPiMaybe p = project . flattenPiMaybe p

-- | Get narrower projection with flatten leaf phantom Maybe types along with projection path
--   and project into result projection type.
(!??) :: (ProjectableFlattenMaybe (Maybe b) c,
          Projectable Projection p, ProjectableMaybe (p cont))
      => Projection cont (Maybe a) -- ^ Source 'Projection'. 'Maybe' phantom type
      -> Pi a b                    -- ^ Projection path
      -> p cont c                  -- ^ Narrower flatten and projected object.
(!??) =  projectFlattenPiMaybe


-- | Interface to run recursively identity element laws.
class ProjectableRunIdsZip a b where
  runIds :: ProjectableIdZip p => p a -> p b

-- | Run left identity element law.
instance ProjectableRunIdsZip a b => ProjectableRunIdsZip ((), a) b where
  runIds = runIds . leftId

-- | Run right identity element law.
instance ProjectableRunIdsZip a b => ProjectableRunIdsZip (a, ()) b where
  runIds = runIds . rightId

-- | Base case definition to run recursively identity element laws.
instance ProjectableRunIdsZip a a where
  runIds = id

-- | Specialize 'runIds' for 'PlaceHolders' type.
flattenPh :: ProjectableRunIdsZip a b => PlaceHolders a -> PlaceHolders b
flattenPh =  runIds

-- -- | Binary operator the same as 'generalizedZip'.
-- (>?<) :: (ProjectableIdZip p, ProjectableRunIdsZip (a, b) c)
--       => p a -> p b -> p c
-- (>?<) =  generalizedZip'

infixl 8 !, ?!, ?!?, !??, .!, .?
-- infixl 1 >?<