content: {-|
  Module: Squeal.PostgreSQL.Expression.Window
  Description: window functions, arguments and definitions
  Copyright: (c) Eitan Chatav, 2019
  Maintainer: eitan@morphism.tech
  Stability: experimental
  
  window functions, arguments and definitions
  -}
  
  {-# LANGUAGE
      DataKinds
    , DeriveGeneric
    , DerivingStrategies
    , FlexibleContexts
    , FlexibleInstances
    , GADTs
    , GeneralizedNewtypeDeriving
    , KindSignatures
    , LambdaCase
    , MultiParamTypeClasses
    , OverloadedStrings
    , PatternSynonyms
    , RankNTypes
    , ScopedTypeVariables
    , TypeApplications
    , TypeOperators
    , UndecidableInstances
  #-}
  
  module Squeal.PostgreSQL.Expression.Window
    ( -- * Window Definition
      WindowDefinition (..)
    , partitionBy
      -- * Window Function
      -- ** Types
    , WindowFunction (..)
    , WindowArg (..)
    , pattern Window
    , pattern Windows
    , WinFun0
    , type (-#->)
    , type (--#->)
      -- ** Functions
    , rank
    , rowNumber
    , denseRank
    , percentRank
    , cumeDist
    , ntile
    , lag
    , lead
    , firstValue
    , lastValue
    , nthValue
    , unsafeWindowFunction1
    , unsafeWindowFunctionN
    ) where
  
  import Control.DeepSeq
  import Data.ByteString (ByteString)
  
  import qualified GHC.Generics as GHC
  import qualified Generics.SOP as SOP
  
  import Squeal.PostgreSQL.Type.Alias
  import Squeal.PostgreSQL.Expression
  import Squeal.PostgreSQL.Expression.Aggregate
  import Squeal.PostgreSQL.Expression.Logic
  import Squeal.PostgreSQL.Expression.Sort
  import Squeal.PostgreSQL.Type.List
  import Squeal.PostgreSQL.Render
  import Squeal.PostgreSQL.Type.Schema
  
  instance Aggregate (WindowArg grp) (WindowFunction grp) where
    countStar = UnsafeWindowFunction "count(*)"
    count = unsafeWindowFunction1 "count"
    sum_ = unsafeWindowFunction1 "sum"
    arrayAgg = unsafeWindowFunction1 "array_agg"
    jsonAgg = unsafeWindowFunction1 "json_agg"
    jsonbAgg = unsafeWindowFunction1 "jsonb_agg"
    bitAnd = unsafeWindowFunction1 "bit_and"
    bitOr = unsafeWindowFunction1 "bit_or"
    boolAnd = unsafeWindowFunction1 "bool_and"
    boolOr = unsafeWindowFunction1 "bool_or"
    every = unsafeWindowFunction1 "every"
    max_ = unsafeWindowFunction1 "max"
    min_ = unsafeWindowFunction1 "min"
    avg = unsafeWindowFunction1 "avg"
    corr = unsafeWindowFunctionN "corr"
    covarPop = unsafeWindowFunctionN "covar_pop"
    covarSamp = unsafeWindowFunctionN "covar_samp"
    regrAvgX = unsafeWindowFunctionN "regr_avgx"
    regrAvgY = unsafeWindowFunctionN "regr_avgy"
    regrCount = unsafeWindowFunctionN "regr_count"
    regrIntercept = unsafeWindowFunctionN "regr_intercept"
    regrR2 = unsafeWindowFunctionN "regr_r2"
    regrSlope = unsafeWindowFunctionN "regr_slope"
    regrSxx = unsafeWindowFunctionN "regr_sxx"
    regrSxy = unsafeWindowFunctionN "regr_sxy"
    regrSyy = unsafeWindowFunctionN "regr_syy"
    stddev = unsafeWindowFunction1 "stddev"
    stddevPop = unsafeWindowFunction1 "stddev_pop"
    stddevSamp = unsafeWindowFunction1 "stddev_samp"
    variance = unsafeWindowFunction1 "variance"
    varPop = unsafeWindowFunction1 "var_pop"
    varSamp = unsafeWindowFunction1 "var_samp"
  
  -- | A `WindowDefinition` is a set of table rows that are somehow related
  -- to the current row
  data WindowDefinition grp lat with db params from where
    WindowDefinition
      :: SOP.SListI bys
      => NP (Expression grp lat with db params from) bys
         -- ^ `partitionBy` clause
      -> [SortExpression grp lat with db params from]
         -- ^ `Squeal.PostgreSQL.Expression.Sort.orderBy` clause
      -> WindowDefinition grp lat with db params from
  
  instance OrderBy (WindowDefinition grp) grp where
    orderBy sortsR (WindowDefinition parts sortsL)
      = WindowDefinition parts (sortsL ++ sortsR)
  
  instance RenderSQL (WindowDefinition grp lat with db params from) where
    renderSQL (WindowDefinition part ord) =
      renderPartitionByClause part <> renderSQL ord
      where
        renderPartitionByClause = \case
          Nil -> ""
          parts -> "PARTITION" <+> "BY" <+> renderCommaSeparated renderExpression parts
  
  {- |
  The `partitionBy` clause within `Squeal.PostgreSQL.Query.Over` divides the rows into groups,
  or partitions, that share the same values of the `partitionBy` `Expression`(s).
  For each row, the window function is computed across the rows that fall into
  the same partition as the current row.
  -}
  partitionBy
    :: SOP.SListI bys
    => NP (Expression grp lat with db params from) bys -- ^ partitions
    -> WindowDefinition grp lat with db params from
  partitionBy bys = WindowDefinition bys []
  
  {- |
  A window function performs a calculation across a set of table rows
  that are somehow related to the current row. This is comparable to the type
  of calculation that can be done with an aggregate function.
  However, window functions do not cause rows to become grouped into a single
  output row like non-window aggregate calls would.
  Instead, the rows retain their separate identities.
  Behind the scenes, the window function is able to access more than
  just the current row of the query result.
  -}
  newtype WindowFunction
    (grp :: Grouping)
    (lat :: FromType)
    (with :: FromType)
    (db :: SchemasType)
    (params :: [NullType])
    (from :: FromType)
    (ty :: NullType)
      = UnsafeWindowFunction { renderWindowFunction :: ByteString }
      deriving stock (GHC.Generic,Show,Eq,Ord)
      deriving newtype (NFData)
  
  {- |
  `WindowArg`s are used for the input of `WindowFunction`s.
  -}
  data WindowArg
    (grp :: Grouping)
    (args :: [NullType])
    (lat :: FromType)
    (with :: FromType)
    (db :: SchemasType)
    (params :: [NullType])
    (from :: FromType)
      = WindowArg
      { windowArgs :: NP (Expression grp lat with db params from) args
        -- ^ `Window` or `Windows`
      , windowFilter :: [Condition grp lat with db params from]
        -- ^ `filterWhere`
      } deriving stock (GHC.Generic)
  
  instance (HasUnique tab (Join from lat) row, Has col row ty)
    => IsLabel col (WindowArg 'Ungrouped '[ty] lat with db params from) where
      fromLabel = Window (fromLabel @col)
  instance (Has tab (Join from lat) row, Has col row ty)
    => IsQualified tab col (WindowArg 'Ungrouped '[ty] lat with db params from) where
      tab ! col = Window (tab ! col)
  instance (HasUnique tab (Join from lat) row, Has col row ty, GroupedBy tab col bys)
    => IsLabel col (WindowArg ('Grouped bys) '[ty] lat with db params from) where
      fromLabel = Window (fromLabel @col)
  instance (Has tab (Join from lat) row, Has col row ty, GroupedBy tab col bys)
    => IsQualified tab col (WindowArg ('Grouped bys) '[ty] lat with db params from) where
      tab ! col = Window (tab ! col)
  
  instance SOP.SListI args
    => RenderSQL (WindowArg grp args lat with db params from) where
      renderSQL (WindowArg args filters) =
        parenthesized (renderCommaSeparated renderSQL args)
        & renderFilters filters
        where
          renderFilter wh = "FILTER" <+> parenthesized ("WHERE" <+> wh)
          renderFilters = \case
            [] -> id
            wh:whs -> (<+> renderFilter (renderSQL (foldr (.&&) wh whs)))
  
  instance FilterWhere (WindowArg grp) grp where
    filterWhere wh (WindowArg args filters) = WindowArg args (wh : filters)
  
  -- | `Window` invokes a `WindowFunction` on a single argument.
  pattern Window
    :: Expression grp lat with db params from arg
    -- ^ argument
    -> WindowArg grp '[arg] lat with db params from
  pattern Window x = Windows (x :* Nil)
  
  -- | `Windows` invokes a `WindowFunction` on multiple argument.
  pattern Windows
    :: NP (Expression grp lat with db params from) args
    -- ^ arguments
    -> WindowArg grp args lat with db params from
  pattern Windows xs = WindowArg xs []
  
  instance RenderSQL (WindowFunction grp lat with db params from ty) where
    renderSQL = renderWindowFunction
  
  {- |
  A @RankNType@ for window functions with no arguments.
  -}
  type WinFun0 x
    = forall grp lat with db params from
    . WindowFunction grp lat with db params from x
      -- ^ cannot reference aliases
  
  {- |
  A @RankNType@ for window functions with 1 argument.
  -}
  type (-#->) x y
    =  forall grp lat with db params from
    .  WindowArg grp '[x] lat with db params from
       -- ^ input
    -> WindowFunction grp lat with db params from y
       -- ^ output
  
  {- | A @RankNType@ for window functions with a fixed-length
  list of heterogeneous arguments.
  Use the `*:` operator to end your argument lists.
  -}
  type (--#->) xs y
    =  forall grp lat with db params from
    .  WindowArg grp xs lat with db params from
       -- ^ inputs
    -> WindowFunction grp lat with db params from y
       -- ^ output
  
  -- | escape hatch for defining window functions
  unsafeWindowFunction1 :: ByteString -> x -#-> y
  unsafeWindowFunction1 fun x
    = UnsafeWindowFunction $ fun <> renderSQL x
  
  -- | escape hatch for defining multi-argument window functions
  unsafeWindowFunctionN :: SOP.SListI xs => ByteString -> xs --#-> y
  unsafeWindowFunctionN fun xs = UnsafeWindowFunction $ fun <> renderSQL xs
  
  {- | rank of the current row with gaps; same as `rowNumber` of its first peer
  
  >>> printSQL rank
  rank()
  -}
  rank :: WinFun0 ('NotNull 'PGint8)
  rank = UnsafeWindowFunction "rank()"
  
  {- | number of the current row within its partition, counting from 1
  
  >>> printSQL rowNumber
  row_number()
  -}
  rowNumber :: WinFun0 ('NotNull 'PGint8)
  rowNumber = UnsafeWindowFunction "row_number()"
  
  {- | rank of the current row without gaps; this function counts peer groups
  
  >>> printSQL denseRank
  dense_rank()
  -}
  denseRank :: WinFun0 ('NotNull 'PGint8)
  denseRank = UnsafeWindowFunction "dense_rank()"
  
  {- | relative rank of the current row: (rank - 1) / (total partition rows - 1)
  
  >>> printSQL percentRank
  percent_rank()
  -}
  percentRank :: WinFun0 ('NotNull 'PGfloat8)
  percentRank = UnsafeWindowFunction "percent_rank()"
  
  {- | cumulative distribution: (number of partition rows
  preceding or peer with current row) / total partition rows
  
  >>> printSQL cumeDist
  cume_dist()
  -}
  cumeDist :: WinFun0 ('NotNull 'PGfloat8)
  cumeDist = UnsafeWindowFunction "cume_dist()"
  
  {- | integer ranging from 1 to the argument value,
  dividing the partition as equally as possible
  
  >>> printSQL $ ntile (Window 5)
  ntile((5 :: int4))
  -}
  ntile :: 'NotNull 'PGint4 -#-> 'NotNull 'PGint4
  ntile = unsafeWindowFunction1 "ntile"
  
  {- | returns value evaluated at the row that is offset rows before the current
  row within the partition; if there is no such row, instead return default
  (which must be of the same type as value). Both offset and default are
  evaluated with respect to the current row.
  -}
  lag :: '[ty, 'NotNull 'PGint4, ty] --#-> ty
  lag = unsafeWindowFunctionN "lag"
  
  {- | returns value evaluated at the row that is offset rows after the current
  row within the partition; if there is no such row, instead return default
  (which must be of the same type as value). Both offset and default are
  evaluated with respect to the current row.
  -}
  lead :: '[ty, 'NotNull 'PGint4, ty] --#-> ty
  lead = unsafeWindowFunctionN "lead"
  
  {- | returns value evaluated at the row that is the
  first row of the window frame
  -}
  firstValue :: ty -#-> ty
  firstValue = unsafeWindowFunction1 "first_value"
  
  {- | returns value evaluated at the row that is the
  last row of the window frame
  -}
  lastValue :: ty -#-> ty
  lastValue = unsafeWindowFunction1 "last_value"
  
  {- | returns value evaluated at the row that is the nth
  row of the window frame (counting from 1); null if no such row
  -}
  nthValue :: '[null ty, 'NotNull 'PGint4] --#-> 'Null ty
  nthValue = unsafeWindowFunctionN "nth_value"
  