-- {-# OPTIONS_GHC -cpp -DDEBUG #-} 
{-# OPTIONS_GHC -cpp #-}

-----------------------------------------------------------------------------
-- |
-- Module      : Berp.Base.Object
-- Copyright   : (c) 2010 Bernie Pope
-- License     : BSD-style
-- Maintainer  : florbitous@gmail.com
-- Stability   : experimental
-- Portability : ghc
--
-- Primitive operations on Objects.
--
-----------------------------------------------------------------------------

#include "BerpDebug.h"

module Berp.Base.Object 
   (lookupAttribute, lookupSpecialAttribute, lookupAttributeMaybe, 
    typeOf, identityOf, objectEquality, dictOf, dir) where

import Berp.Base.Truth (truth)
import Berp.Base.Prims (callMethod, showObject)
import Data.List (nub)
import Control.Monad (zipWithM)
import Control.Applicative ((<$>))
import Data.Maybe (isJust, catMaybes)
import Berp.Base.SemanticTypes (Object (..), Eval)
import Berp.Base.Mangle (deMangle)
import Berp.Base.Identity (Identity)
import Berp.Base.Hash (Hashed)
import Berp.Base.StdNames (eqName, cmpName)
import Berp.Base.LiftedIO as LIO (MonadIO)
#ifdef DEBUG
import Berp.Base.LiftedIO as LIO (putStrLn)
#endif
import {-# SOURCE #-} Berp.Base.HashTable (stringLookup, keys)
import {-# SOURCE #-} Berp.Base.StdTypes.Integer (intClass, int)
import {-# SOURCE #-} Berp.Base.StdTypes.Bool (boolClass)
import {-# SOURCE #-} Berp.Base.StdTypes.Tuple (tupleClass, getTupleElements)
import {-# SOURCE #-} Berp.Base.StdTypes.Function (functionClass)
import {-# SOURCE #-} Berp.Base.StdTypes.String (stringClass)
import {-# SOURCE #-} Berp.Base.StdTypes.None (noneClass, noneIdentity)
import {-# SOURCE #-} Berp.Base.StdTypes.Dictionary (dictionaryClass)
import {-# SOURCE #-} Berp.Base.StdTypes.List (listClass, list)
import {-# SOURCE #-} Berp.Base.StdTypes.Generator (generatorClass)
import {-# SOURCE #-} Berp.Base.StdTypes.String (string)

-- needed for overloaded numeric literals
instance Num Object where
    fromInteger = int
    (+) = undefined
    (*) = undefined
    abs = undefined
    signum = undefined

-- Python allows the type of an object to change in a limited set of circumstances.
-- But we will ignore that for the moment and make it a pure function.
typeOf :: Object -> Object
typeOf obj@(Object {}) = object_type obj 
typeOf obj@(Type {}) = object_type obj
typeOf (Integer {}) = intClass 
typeOf (Bool {}) = boolClass
typeOf (Tuple {}) = tupleClass
typeOf (List {}) = listClass 
typeOf (Function {}) = functionClass
typeOf (String {}) = stringClass
typeOf (None {}) = noneClass
typeOf (Dictionary {}) = dictionaryClass
typeOf (Generator {}) = generatorClass

-- The identity of an object should never change so this can be a pure function.
identityOf :: Object -> Identity
identityOf None = noneIdentity 
identityOf object = object_identity object

dictOf :: Object -> Maybe Object 
dictOf obj@(Object {}) = Just $ object_dict obj
dictOf obj@(Type {}) = Just $ object_dict obj
dictOf obj@(Function {}) = Just $ object_dict obj
dictOf _other = Nothing

lookupAttribute :: Object -> Hashed String -> Eval Object
lookupAttribute obj ident = do
   lookupResult <- lookupAttributeMaybe obj ident
   checkLookup obj ident lookupResult 

lookupSpecialAttribute :: Object -> Hashed String -> Eval Object
lookupSpecialAttribute obj ident = do
   lookupResult <- lookupSpecialAttributeMaybe obj ident
   checkLookup obj ident lookupResult 

checkLookup :: Object -> Hashed String -> Maybe Object -> Eval Object
checkLookup obj (_, identStr) lookupResult =
   case lookupResult of
      -- XXX This should raise a proper catchable exception 
      Nothing -> do
         objStr <- showObject obj
         fail $ objStr ++ " has no attribute called " ++ deMangle identStr
      Just attributeObj -> do
         case attributeObj of
            -- XXX this should return a bound method object
            Function { object_procedure = proc, object_arity = arity } -> 
               return attributeObj { object_procedure = \args -> proc (obj:args), object_arity = arity - 1 }
            _other -> do
               return attributeObj 

-- XXX does not handle descriptors or getattr/getattribute.
-- XXX Hack: If the result of the lookup is a function, then 
--     turn it into a bound method on return, by supplying the
--     object as the first argument. This is not ideal, but
--     it will work until descriptors are supported

lookupSpecialAttributeMaybe :: MonadIO m => Object -> Hashed String -> m (Maybe Object)
lookupSpecialAttributeMaybe object ident = do
   BELCH("Looking for special attribute: " ++ show ident ++ " in: " ++ show object)
   lookupAttributeType object ident

lookupAttributeMaybe :: MonadIO m => Object -> Hashed String -> m (Maybe Object)
lookupAttributeMaybe object ident = do
   BELCH("Looking for: " ++ show ident ++ " in: " ++ show object)
   BELCH("Looking in dictionary of object")
   case dictOf object of
      -- The object does have a dictionary; look in there.
      Just dict -> do
         BELCH("Object has a dictionary")
         dictResult <- stringLookup ident $ object_hashTable dict 
         case dictResult of
            -- The ident was not found in the object, look in the type, then the bases.
            Nothing -> do
               BELCH("Ident not found in dictionary of object")
               lookupAttributeType object ident
            -- The ident was found in the object; return it.
            Just _ -> do
               BELCH("Ident was found in dictionary of object")
               return dictResult 
      -- The object does not have a dictionary; look in the type, then the bases.
      Nothing -> do
         BELCH("Object does not have a dictionary")
         lookupAttributeType object ident

lookupAttributeType :: MonadIO m => Object -> Hashed String -> m (Maybe Object)
lookupAttributeType object ident = do
   BELCH("Looking in dict of the type: " ++ show objectType)
   let mroList = getTupleElements $ object_mro objectType
   searchMRO mroList
   where
   objectType :: Object
   objectType = typeOf object
   searchMRO :: MonadIO m => [Object] -> m (Maybe Object)
   searchMRO [] = do
      BELCH("Ident was not found in the mro of the type of the object")
      return Nothing
   searchMRO (klass:rest) = do
      BELCH("Looking in the dict of the type: " ++ show klass)
      case dictOf klass of
         Nothing -> do
            BELCH("Type does not have a dictionary")
            searchMRO rest 
         Just dict -> do
            BELCH("Type does have a dictionary")
            dictResult <- stringLookup ident $ object_hashTable dict
            case dictResult of
               Nothing -> do
                  BELCH("Ident not found in dictionary of type")
                  searchMRO rest 
               Just _ -> do
                  BELCH("Ident was found in dictionary of type")
                  return dictResult

hasAttribute :: (Functor m, MonadIO m) => Object -> Hashed String -> m Bool
hasAttribute object ident = isJust <$> lookupAttributeMaybe object ident

-- | Check if two objects are equal. For some objects we might have
--   to call the __eq__ (or __cmp__) method on the objects. This means
--   the result must be in the Eval monad.
objectEquality :: Object -> Object -> Eval Bool
objectEquality obj1@(Integer {}) obj2@(Integer {})
   = return (object_integer obj1 == object_integer obj2)
objectEquality obj1@(Bool {}) obj2@(Bool {})
   = return (object_bool obj1 == object_bool obj2)
objectEquality obj1@(Tuple {}) obj2@(Tuple {})
   | object_identity obj1 == object_identity obj2 = return True
   | object_length obj1 == object_length obj2 =
        and <$> zipWithM objectEquality (object_tuple obj1) (object_tuple obj2)
   | otherwise = return False
objectEquality obj1@(String {}) obj2@(String {})
   = return (object_string obj1 == object_string obj2)
objectEquality None None = return True
objectEquality obj1 obj2 
   | object_identity obj1 == object_identity obj2 = return True
   | otherwise = do
        canEq <- hasAttribute obj1 eqName
        if canEq
           then truth <$> callMethod obj1 eqName [obj2]
           else do
              canCmp <- hasAttribute obj1 cmpName
              if canCmp
                 then do
                    cmpResult <- callMethod obj1 cmpName [obj2]
                    case cmpResult of
                       Integer {} -> return $ object_integer cmpResult == 0
                       _other -> fail $ "__cmp__ method on object does not return an integer: " ++ show obj1
                 else return False -- XXX should this raise an exception?

dir :: Object -> Eval Object
dir object = do
   let maybeObjDict = dictOf object
   let objectBasesDicts = map dictOf $ getTupleElements $ object_mro $ typeOf object
   let allDicts = catMaybes (maybeObjDict : objectBasesDicts)
   let hashTables = map object_hashTable allDicts 
   keyObjects <- concat <$> mapM keys hashTables
   let keyStrings = nub $ map (deMangle . object_string) keyObjects
   list $ map string keyStrings