{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}

-- |
-- Module      : Database.HDBC.Record.InternalTH
-- Copyright   : 2013,2014,2016 Kei Hibino
-- License     : BSD3
--
-- Maintainer  : ex8k.hibino@gmail.com
-- Stability   : experimental
-- Portability : unknown
--
-- This module provides internal definitions used from DB-record templates.
module Database.HDBC.Record.InternalTH (
  -- * Persistable instances along with 'Convertible' instances
  derivePersistableInstancesFromConvertibleSqlValues
  ) where

import Data.Maybe (catMaybes)
import Data.Set (Set)
import qualified Data.Set as Set

import Language.Haskell.TH
  (Q, Dec, Type(AppT, ConT),
   Info (ClassI), reify)
import Language.Haskell.TH.Compat.Data (unInstanceD)
import Data.Convertible (Convertible)
import Database.HDBC (SqlValue)
import Database.HDBC.SqlValueExtra ()
import Database.Record (PersistableWidth)
import Database.Record.TH (deriveNotNullType)
import Database.Record.Instances ()
import Database.Relational.TH (defineScalarDegree)

import Database.HDBC.Record.TH (derivePersistableInstanceFromConvertible)


-- | Wrapper type which represents type constructor.
newtype TypeCon = TypeCon { unTypeCon :: Type } deriving Eq

-- | Ord instance for type constructor.
instance Ord TypeCon  where
  TypeCon (ConT an) `compare` TypeCon (ConT bn)    = an `compare` bn
  TypeCon (ConT _)  `compare` TypeCon _            = LT
  TypeCon _         `compare` TypeCon (ConT _)     = GT
  a                 `compare` b       | a == b     = EQ
                                      | otherwise  = EQ

-- | Set of 'TypeCon'.
type TConSet = Set TypeCon

-- | From 'Type' list into 'TConSet'.
fromList :: [Type] -> TConSet
fromList =  Set.fromList . map TypeCon

-- | From 'TConSet' into 'Type' list.
toList :: TConSet -> [Type]
toList =  map unTypeCon . Set.toList


-- | 'SqlValue' type 'Q'.
sqlValueType :: Q Type
sqlValueType =  [t| SqlValue |]

-- | 'Convertble' pairs with 'SqlValue'.
convertibleSqlValues' :: Q [(Type, Type)]
convertibleSqlValues' =  cvInfo >>= d0  where
  cvInfo = reify ''Convertible
  unknownDeclaration =
    fail . ("convertibleSqlValues: Unknown declaration pattern: " ++)
  d0 (ClassI _ is) = fmap catMaybes $ mapM (d1 . unInstanceD) is  where
    d1 (Just (_cxt, (AppT (AppT (ConT _n) a) b), _ds))
      = do qvt <- sqlValueType
           return
             $ if qvt == a || qvt == b
               then case (a, b) of
                 (ConT _, ConT _) -> Just (a, b)
                 _                -> Nothing
               else Nothing
    d1  _
      =    unknownDeclaration $ show is
  d0 cls           = unknownDeclaration $ show cls

-- | Get types which are 'Convertible' with.
convertibleSqlValues :: Q TConSet
convertibleSqlValues =  do
  qvt <- sqlValueType
  vs  <- convertibleSqlValues'
  let from = fromList . map snd . filter ((== qvt) . fst) $ vs
      to   = fromList . map fst . filter ((== qvt) . snd) $ vs
  return $ Set.intersection from to

-- | Get types which are instance of 'PersistableWith'.
persistableWidthTypes :: Q TConSet
persistableWidthTypes =  cvInfo >>= d0  where
  cvInfo = reify ''PersistableWidth
  unknownDeclaration =
    fail . ("persistableWidthTypes: Unknown declaration pattern: " ++)
  d0 (ClassI _ is) = fmap fromList $ mapM (d1 . unInstanceD) is  where
    d1 (Just (_cxt, (AppT (ConT _n) a), _ds))  = return a
    d1  _                                      = unknownDeclaration $ show is
  d0 cls           = unknownDeclaration $ show cls

-- | Map instance declarations.
mapInstanceD :: (Q Type -> Q [Dec]) -- ^ Template to declare instances from a type
             -> [Type]              -- ^ Types
             -> Q [Dec]             -- ^ Result declaration template.
mapInstanceD fD = fmap concat . mapM (fD . return)

-- | Template to declare HDBC instances of DB-record along with 'Convertible' instances.
derivePersistableInstancesFromConvertibleSqlValues :: Q [Dec]
derivePersistableInstancesFromConvertibleSqlValues =  do
  wds <- persistableWidthTypes
  svs <- convertibleSqlValues
  ws <- mapInstanceD deriveNotNullType (toList $ Set.difference svs wds)
  let svl = toList svs
  ps <- mapInstanceD derivePersistableInstanceFromConvertible svl
  ss <- mapInstanceD defineScalarDegree svl
  return $ ws ++ ps ++ ss
