-- | Extraction and unification of AutoType's @Type@ from Aeson @Value@.
module Data.Aeson.AutoType.Extract(valueSize, valueTypeSize,
                                   valueDepth, Dict(..),
                                   Type(..), emptyType,
                                   extractType, unifyTypes,
                                   typeCheck) where

import           Control.Exception               (assert)
import           Data.Aeson.AutoType.Type
import qualified Data.HashMap.Strict      as Map
import           Data.HashMap.Strict             (HashMap)
import qualified Data.Set                 as Set
import qualified Data.Vector              as V
import           Data.Aeson
import           Data.Text                       (Text)
import           Data.Set                        (Set )
import           Data.List                       (foldl1')

--import           Debug.Trace

-- | Compute total number of nodes (and leaves) within the value tree.
-- Each simple JavaScript type (including String) is counted as of size 1,
-- whereas both Array or object types are counted as 1+sum of the sizes
-- of their member values.
valueSize :: Value -> Int
valueSize  Null      = 1
valueSize (Bool   _) = 1
valueSize (Number _) = 1
valueSize (String _) = 1
valueSize (Array  a) = V.foldl' (+) 1 $ V.map valueSize a
valueSize (Object o) = (1+) . sum . map valueSize . Map.elems $ o

-- | Compute total size of the type of the @Value@.
-- For:
-- * simple types it is always 1, 
-- * for arrays it is just 1+_maximum_ size of the (single) element type,
-- * for objects it is _sum_ of the sizes of fields (since each field type
--   is assumed to be different.)
valueTypeSize :: Value -> Int
valueTypeSize  Null      = 1
valueTypeSize (Bool   _) = 1
valueTypeSize (Number _) = 1
valueTypeSize (String _) = 1
valueTypeSize (Array  a) = (1+) . V.foldl' max 0 $ V.map valueTypeSize a
valueTypeSize (Object o) = (1+) . sum . map valueTypeSize . Map.elems $ o

-- | Compute total depth of the value.
-- For:
-- * simple types it is 1
-- * for either Array or Object, it is 1 + maximum of depths of their members
valueDepth :: Value -> Int
valueDepth  Null      = 1
valueDepth (Bool   _) = 1
valueDepth (Number _) = 1
valueDepth (String _) = 1
valueDepth (Array  a) = (1+) . V.foldl' max 0 $ V.map valueDepth a
valueDepth (Object o) = (1+) . maximum . (0:) . map valueDepth . Map.elems $ o

-- | Extract @Type@ from the JSON @Value@.
-- Unifying types of array elements, if necessary.
extractType                             :: Value -> Type
extractType (Object o)                   = TObj $ Dict $ Map.map extractType o
extractType  Null                        = TNull
extractType (Bool   _)                   = TBool
extractType (Number _)                   = TNum
extractType (String _)                   = TString
extractType (Array  a) | V.null a        = TArray   emptyType
extractType (Array  a)                   = TArray $ V.foldl1' unifyTypes $ traceShow $ V.map extractType a
  where
    --traceShow a = trace (show a) a
    traceShow a = a

-- | Type check the value with the derived type.
typeCheck :: Value -> Type -> Bool
typeCheck  Null          TNull            = True
typeCheck  v            (TUnion  u)       = typeCheck v `any` Set.toList u
typeCheck (Bool   _)     TBool            = True
typeCheck (Number _)     TNum             = True
typeCheck (String _)     TString          = True
typeCheck (Array  elts) (TArray  eltType) = (`typeCheck` eltType) `all` V.toList elts
typeCheck (Object d)    (TObj    e      ) = typeCheckKey `all` keysOfBoth
  where
    typeCheckKey k = getValue k d `typeCheck` get k e
    getValue   :: Text -> HashMap Text Value -> Value
    getValue    = Map.lookupDefault Null
    keysOfBoth :: [Text]
    keysOfBoth  =  Set.toList $ Set.fromList (Map.keys d) `Set.union` keys e
typeCheck         _     (TLabel  _      ) = error "Cannot typecheck labels without environment!"
typeCheck         a      b                = {-trace msg $-} False
  where
    msg = "Mismatch: " ++ show a ++ " :: " ++ show b

d `allKeys` e = Set.toList (keys d `Set.union` keys e)

-- | Standard unification procedure on @Type@s,
-- with inclusion of @Type@ unions.
unifyTypes :: Type -> Type -> Type
unifyTypes  TBool      TBool     = TBool
unifyTypes  TNum       TNum      = TNum
unifyTypes  TString    TString   = TString
unifyTypes  TNull      TNull     = TNull
unifyTypes (TObj   d) (TObj   e) = TObj newDict 
  where
    newDict :: Dict
    newDict = Dict $ Map.fromList [(k, get k d `unifyTypes`
                                        get k e) | k <- allKeys d e ]
unifyTypes (TArray u) (TArray v) = TArray $ u `unifyTypes` v
unifyTypes t           s         = typeAsSet t `unifyUnion` typeAsSet s

-- | Unify sets of types (sets are union types of alternatives).
unifyUnion :: Set Type -> Set Type -> Type
unifyUnion u v = assertions $
                   union $ uSimple        `Set.union`
                           vSimple        `Set.union`
                           unifiedObjects `Set.union`
                           Set.singleton unifiedArray
  where
    -- We partition our types for easier unification into simple and compound
    (uSimple, uCompound) = Set.partition isSimple u
    (vSimple, vCompound) = Set.partition isSimple v
    assertions = assert (Set.null $ Set.filter (not . isArray) uArr) .
                 assert (Set.null $ Set.filter (not . isArray) vArr)
    -- then we partition compound typs into objects and arrays.
    -- Note that there should be no TUnion here, since we are inside a TUnion already.
    -- (That is reduced by @union@ smart costructor as superfluous.)
    (uObj, uArr)   = Set.partition isObject uCompound
    (vObj, vArr)   = Set.partition isObject vCompound
    unifiedObjects = Set.fromList $ if null objects
                                       then []
                                       else [foldl1' unifyTypes objects]
    objects = Set.toList $ uObj `Set.union` vObj
    arrayElts :: [Type]
    arrayElts  = map (\(TArray ty) -> ty) $
                   Set.toList $
                     uArr `Set.union` vArr
    unifiedArray = TArray $ if null arrayElts
                               then emptyType
                               else foldl1' unifyTypes arrayElts

-- | Smart constructor for union types.
union ::  Set Type -> Type
union = simplifyUnion . TUnion

-- | Simplify TUnion's so there is no TUnion directly inside TUnion.
-- If there is only one element of the set, then return this single
-- element as a type.
simplifyUnion :: Type -> Type
simplifyUnion (TUnion s) | Set.size s == 1 = head $ Set.toList s
simplifyUnion (TUnion s)                   = TUnion $ Set.unions $ map elements $ Set.toList s
  where
    elements (TUnion elems) = elems
    elements s              = Set.singleton s