module Language.Egison.Variables where
import Language.Egison.Types
import Control.Monad.Error
import Data.IORef
import qualified Data.Map

-- |Show the contents of an environment
printEnv :: Env         -- ^Environment
         -> IO String   -- ^Contents of the env as a string
printEnv env = do
  bindings <- liftIO $ readIORef $ topFrameRef env
  l <- mapM showBind $ Data.Map.toList bindings 
  return $ unlines l
 where 
  showBind ((name, nums), objRef) = do
    obj <- liftIO $ readIORef objRef
    return $ name ++ unwordsNums nums ++ ": " ++ show obj

-- |Create a copy of an environment
copyEnv :: Env      -- ^ Source environment
        -> IO Env   -- ^ A copy of the source environment
copyEnv env = do
  bindings <- liftIO $ readIORef $ topFrameRef env
  bindingListT <- mapM addBinding $ Data.Map.toList bindings
  bindingList <- newIORef $ Data.Map.fromList bindingListT
  return $ Environment (parentEnv env) bindingList
 where addBinding (var, val) = do --ref <- newIORef $ liftIO $ readIORef val
                                  x <- liftIO $ readIORef val
                                  ref <- newIORef x
                                  return (var, ref)

-- |Extend given environment by binding a series of values to a new environment.
extendEnv :: Env -- ^ Environment
          -> [(Var, ObjectRef)] -- ^ Extensions to the environment
          -> IO Env -- ^ Extended environment
extendEnv env abindings = do bindinglist <- newIORef $ Data.Map.fromList abindings
                             return $ Environment (Just env) bindinglist

makeLetRecFrame :: Env
                -> [(String, EgisonExpr)]
                -> IO FrameRef
makeLetRecFrame env abindings = do
  newEnv <- extendLetRec env abindings
  return $ topFrameRef newEnv
                             
-- |Extend given environment by binding a series of values to a new environment for letrec.
extendLetRec :: Env -- ^ Environment 
             -> [(String, EgisonExpr)] -- ^ Extensions to the environment
             -> IO Env -- ^ Extended environment
extendLetRec env abindings = do
  bindinglistT <- (mapM addDummyBinding abindings) -- >>= newIORef
  bindinglist <- newIORef $ Data.Map.fromList $ map (\(name,objRef) -> ((name,[]),objRef)) bindinglistT
  let newEnv = Environment (Just env) bindinglist
  mapM (replaceWithNewEnv newEnv) bindinglistT
  return newEnv
 where addDummyBinding (name, expr) = do dummy <- nullEnv
                                         objRef <- makeClosure dummy expr
                                         return (name, objRef)
       replaceWithNewEnv newEnv (_, objRef) = do obj <- readIORef objRef
                                                 case obj of
                                                   Closure _ cExpr -> writeIORef objRef (Closure newEnv cExpr)

-- |Recursively search environments to find one that contains the given variable.
findEnv 
    :: Env      -- ^Environment to begin the search; 
                --  parent env's will be searched as well.
    -> Var      -- ^Variable
    -> IO (Maybe Env) -- ^Environment, or Nothing if there was no match.
findEnv envRef var = do
  found <- liftIO $ isBound envRef var
  if found
     then return (Just envRef)
     else case parentEnv envRef of
               (Just par) -> findEnv par var
               Nothing -> return Nothing

-- |Determine if a variable is bound
isBound 
    :: Env      -- ^ Environment
    -> Var   -- ^ Variable
    -> IO Bool  -- ^ True if the variable is bound
isBound envRef var = 
    (readIORef $ topFrameRef envRef) >>= return . Data.Map.member var

-- |Determine if a variable is bound
--  or a parent of the given environment.
isRecBound 
    :: Env      -- ^ Environment
    -> Var   -- ^ Variable
    -> IO Bool  -- ^ True if the variable is bound
isRecBound envRef var = do
  env <- findEnv envRef var
  case env of
    (Just e) -> isBound e var
    Nothing -> return False

getVarFromFrame :: Frame -> Var -> IOThrowsError ObjectRef
getVarFromFrame frame var = do
  case Data.Map.lookup var frame of
    (Just a) -> return a
    Nothing -> throwError $ UnboundVar "Getting an unbound variable" (showVar var)
    
-- |Retrieve the value of a variable defined
getVar :: Env     -- ^ Environment
       -> Var  -- ^ Variable
       -> IOThrowsError ObjectRef -- ^ Contents of the variable
getVar envRef
       var = do binds <- liftIO $ readIORef $ topFrameRef envRef
                case Data.Map.lookup var binds of
                  (Just a) -> return a
                  Nothing -> case parentEnv envRef of
                               (Just par) -> getVar par var
                               Nothing -> (throwError $ UnboundVar "Getting an unbound variable" (showVar var))

-- |Bind a variable
defineVar
    :: Env      -- ^ Environment 
    -> Var   -- ^ Variable
    -> ObjectRef  -- ^ Value
    -> IOThrowsError ()   -- ^ Result
defineVar envRef
          var objRef = do
  liftIO $ do
    env <- readIORef $ topFrameRef envRef
    writeIORef (topFrameRef envRef) (Data.Map.insert var objRef env)
    return ()