-- ----------------------------------------------------------------------------

{- |
  Module     : Holumbus.FileSystem.Controller.ControllerData
  Copyright  : Copyright (C) 2008 Stefan Schmidt
  License    : MIT

  Maintainer : Stefan Schmidt (stefanschmidt@web.de)
  Stability  : experimental
  Portability: portable
  Version    : 0.1


-}

-- ----------------------------------------------------------------------------

module Holumbus.FileSystem.Controller.ControllerData
(
-- * datatypes
  ControllerData
  
-- * creation and destruction
, newController
)
where

import           Prelude hiding (appendFile)

import           Control.Concurrent

import           Data.Maybe
import qualified Data.List                              as List
import qualified Data.Map                               as Map
import qualified Data.Set                               as Set

import           System.Log.Logger

import           Holumbus.Common.Debug
import           Holumbus.Common.Utils                  ( handleAll )

import qualified Holumbus.FileSystem.Controller         as C
import qualified Holumbus.FileSystem.Node               as N
import qualified Holumbus.FileSystem.Node.NodePort      as NP
import qualified Holumbus.FileSystem.Messages           as M
import qualified Holumbus.FileSystem.Storage            as S

import           Holumbus.Network.Site
import           Holumbus.Network.Communication


import Control.Monad (foldM)
import System.Random


localLogger :: String
localLogger = "Holumbus.FileSystem.Controller"

-- ----------------------------------------------------------------------------
--
-- ----------------------------------------------------------------------------

type FileToNodeMap = Map.Map S.FileId (Set.Set M.NodeId)

data FileControllerData = FileControllerData {
    cm_FileToNodeMap :: ! FileToNodeMap
  }

type FileController = MVar FileControllerData


data ControllerData = ControllerData {
    cd_Server         :: Server
  , cd_FileController :: FileController
  }

-- ----------------------------------------------------------------------------
--
-- ----------------------------------------------------------------------------
newFileController :: IO FileController
newFileController
  = do
    let fc = FileControllerData Map.empty
    newMVar fc


newController :: StreamName -> Maybe PortNumber -> IO ControllerData
newController sn pn
  = do
    -- initialise the server
    c <- newEmptyMVar
    server <- newServer sn pn (dispatch c) (Just $ registerNode c) (Just $ unregisterNode c)
    -- initialize values
    fc <- newFileController
    let con = ControllerData server fc
    putMVar c con    
    return con
  

dispatch 
  :: MVar ControllerData 
  -> M.ControllerRequestMessage 
  -> IO (Maybe M.ControllerResponseMessage)
dispatch c msg
  = do
    cd <-readMVar c
    case msg of
      (M.CReqGetFileSites f) ->
        do
        s <- C.getFileSites f cd
        return $ Just $ M.CRspGetFileSites s
      (M.CReqContains f) ->
        do
        b <- C.containsFile f cd
        return $ Just $ M.CRspContains b
      (M.CReqGetNearestNodePortWithFile f sid) ->
        do
        p <- C.getNearestNodePortWithFile f sid cd
        return $ Just $ M.CRspGetNearestNodePortWithFile p
      (M.CReqGetNearestNodePortWithFiles l sid) ->
        do
        portmap <- C.getNearestNodePortWithFiles l sid cd
        return $ Just $ M.CRspGetNearestNodePortWithFiles portmap
      (M.CReqGetNearestNodePortForFile f l sid) ->
        do
        p <- C.getNearestNodePortForFile f l sid cd
        return $ Just $ M.CRspGetNearestNodePortForFile p
      (M.CReqGetNearestNodePortForFiles l sid) ->
        do
        p <- C.getNearestNodePortForFiles l sid cd
        return $ Just $ M.CRspGetNearestNodePortForFiles p
      (M.CReqCreate f n) ->
        do
        C.createFile f n cd
        return $ Just $ M.CRspSuccess
      (M.CReqCreateS l) ->
        do
        C.createFiles l cd
        return $ Just $ M.CRspSuccess
      (M.CReqAppend f n) -> 
        do
        C.appendFile f n cd
        return $ Just $ M.CRspSuccess
      (M.CReqDelete f n) ->
        do
        C.deleteFile f n cd
        return $ Just $ M.CRspSuccess
      _ -> return Nothing


registerNode :: MVar ControllerData -> IdType -> ClientPort -> IO ()
registerNode c i cp
  = do
    let np = NP.newNodePort cp
    fids <- N.getFileIds np
    cd <- readMVar c
    modifyMVar (cd_FileController cd) $
      \fc ->
      do
      let fc' = addFilesToController fids i fc
      return (fc', ())


unregisterNode :: MVar ControllerData -> IdType -> ClientPort -> IO ()
unregisterNode c i _
  = do
    debugM localLogger "unregisterNode: start"
    cd <- readMVar c
    modifyMVar (cd_FileController cd) $
      \fc ->
      do
      let fc' = deleteFilesFromController i fc
      return (fc', ())
    debugM localLogger "unregisterNode: end"
    
    
-- ----------------------------------------------------------------------------
--
-- ----------------------------------------------------------------------------


{-

nodes2sites :: ControllerMaps -> [M.NodeId] ->  [SiteId]
nodes2sites (ControllerMaps _ nsm _ _ _) nids = lookupList nsm nids 
 
sites2ports :: ControllerMaps -> [SiteId] ->  [NP.NodePort]
sites2ports (ControllerMaps _ _ spm _ _) sids = lookupList spm sids 


-- | gets all nodessites
getNodesList :: ControllerMaps -> [M.NodeId]
getNodesList (ControllerMaps _ nsm _ _ _) = Map.keys nsm


-- TODO
getNodesWithSpace :: S.FileId -> Integer -> ControllerMaps -> [SiteId]
getNodesWithSpace _ _ cm = getSiteList cm


getOtherFileNodes :: S.FileId -> M.NodeId -> ControllerMaps -> [M.NodeId]
getOtherFileNodes f nid (ControllerMaps fnm _ _ _ _)
  = Set.toList $ Set.delete nid allNids
  where
    allNids = Map.findWithDefault (Set.empty) f fnm   

-- | the minimal occurrence of a file on the nodes
copyingLimit :: Int
copyingLimit = 2


-- | Get a list of all fileIds that should be copied.
--   That means that the file has not enough copies
-- TODO we only compare the nodeid... we should look for sites or hosts
getFilesForCopying :: FileControllerData -> [S.FileId]
getFilesForCopying cm = (fst . unzip) filteredList 
  where
    fnm = cm_FileToNodeMap cm
    filteredList = filter setSelector (Map.toList fnm)
    setSelector (_,s) = Set.size s < copyingLimit
-}


-- | Adds the files of a node to the global directory.
addFilesToController :: [S.FileId] -> M.NodeId -> FileControllerData -> FileControllerData
addFilesToController fids nid cm 
  = cm { cm_FileToNodeMap = fnm' }
  where
    fnm = cm_FileToNodeMap cm
    fnm' = Map.unionWith combine fnm newMap
    newMap = Map.fromList $ zip fids (repeat $ Set.singleton nid)
    combine s1 s2 = Set.union s1 s2


-- | Deletes the files of a node from the global directory.
deleteFilesFromController :: M.NodeId -> FileControllerData -> FileControllerData
deleteFilesFromController nid cm
  = cm { cm_FileToNodeMap = fnm' }
  where 
    fnm = cm_FileToNodeMap cm 
    fnm' = Map.fromList filteredList
    filteredList = filter (\(_,s) -> s /= Set.empty) list
    list = map (\(k,s) -> (k, Set.delete nid s)) (Map.toList fnm)


addFileToController :: S.FileId -> M.NodeId -> FileControllerData -> FileControllerData
--addFileToController fid nid cm = addFilesToController [fid] nid cm
addFileToController fid nid cm = cm { cm_FileToNodeMap = fnm' }
  where
    fnm = cm_FileToNodeMap cm
    fnm' = Map.insert fid nid' fnm 
    nid' = Set.singleton nid


deleteFileFromController :: S.FileId -> FileControllerData -> FileControllerData
deleteFileFromController fid cm
  = cm { cm_FileToNodeMap = fnm' }
  where 
    fnm = cm_FileToNodeMap cm 
    fnm' = Map.delete fid fnm


-- | gets the List of all sites the file is located on...
getFileClientInfoList :: S.FileId -> Server -> FileControllerData -> IO [ClientInfo]
getFileClientInfoList f s cm
  = do
    let fnm = cm_FileToNodeMap cm
    let is = Set.toList $ maybe Set.empty id (Map.lookup f fnm)
    mbDats <- mapM (\i -> getClientInfo i s) is
    return $ catMaybes mbDats  

shuffle :: [a] -> IO [a]
shuffle l' = shuffle' l' []
  where
    shuffle' [] acc = return acc
    shuffle' l acc = do
      k <- randomRIO (0, length l - 1)
      let (lead, x:xs) = splitAt k l
      shuffle' (lead ++ xs) (x:acc)

lookupNearestPortWithFile :: S.FileId -> SiteId -> Server -> FileControllerData -> IO (Maybe ClientPort)
lookupNearestPortWithFile f sid s cm
  = do
    dats <- getFileClientInfoList f s cm
    let sids'  = map (\ci -> ci_Site ci) dats
    sids <- shuffle sids'
    let mbns  = nearestId sid sids
        mbdat = maybe Nothing (\ns -> List.find (\ci -> (ci_Site ci) == ns) dats) mbns
        mbnp  = maybe Nothing (\ci -> Just $ ci_Port ci) mbdat 
    return mbnp

-- | gets the List of all sites the files are located on...
{-
trude :: SiteId -> [S.FileId] -> Server -> FileControllerData -> IO (Maybe SiteId)
trude currentSite files s cm
  = do
    let file2node       = cm_FileToNodeMap cm;
        nodeIdsWithFiles = concatMap (\file -> Set.toList $ maybe Set.empty id (Map.lookup file file2node)) files
    sitesIdsWithFiles <- foldM f ([],[],[]) nodeIdsWithFiles
    nearestId' sitesIdsWithFiles
    where

    nearestId' :: ([SiteId],[SiteId],[SiteId]) -> IO (Maybe SiteId)
    nearestId' ([],  [],  [])  = return Nothing                      -- no site id found
    nearestId' ([],  [],  xs)  = do 
      xs' <- shuffle xs
      return . Just . head $ xs' -- only others ids, shuffle and return the first
    nearestId' ([],  x:_, _)   = return $ Just x                     -- return the hosts stie
    nearestId' (x:_, _,   _)   = return $ Just x                     -- return the procs site

    f :: ([SiteId],[SiteId],[SiteId]) -> M.NodeId -> IO ([SiteId],[SiteId],[SiteId])
    f (procs,hosts,others) i = do
      cli <- getClientInfo i s
      case cli of
        Nothing -> return (procs,hosts,others)
        (Just clientInfo) -> insert (procs,hosts,others) (ci_Site clientInfo)
    
    insert :: ([SiteId],[SiteId],[SiteId]) -> SiteId -> IO ([SiteId],[SiteId],[SiteId])
    insert (procs,hosts,others) thisSite = if isSameProcess thisSite currentSite
                       then return (thisSite:procs,hosts,others)
                       else if isSameHost thisSite currentSite
                              then return (procs,thisSite:hosts,others)
                              else return (procs,hosts,thisSite:others)
-}

lookupNearestPortWithFiles :: [S.FileId] -> SiteId -> Server -> FileControllerData -> IO M.ClientPortMap
lookupNearestPortWithFiles l sid s cm = do
  infoM localLogger $  "Getting nearest ports with: " ++ show l
  res <- foldM f [] l
  infoM localLogger $  "Clientportmap is: " ++ show res
  return res
  where

  f :: M.ClientPortMap -> S.FileId -> IO M.ClientPortMap
  f theMap fid = do

    infoM localLogger $  "Getting nearest ports with: " ++ fid
    maybeport <- lookupNearestPortWithFile fid sid s cm

    debugM localLogger $  "Nearest ports: " ++ show maybeport
    case maybeport of
      (Just port) -> return (ins port fid theMap)
      Nothing -> return theMap

-- lookupNearestPortWithFileAndSpace :: S.FileId -> Integer -> SiteId -> Server -> FileControllerData -> IO (Maybe ClientPort)
-- lookupNearestPortWithFileAndSpace f _size sid s cm
--   = lookupNearestPortWithFile f sid s cm
{-    dats <- getFileClientInfoList f s cm
    -- TODO add Capacity
    let sids  = map (\ci -> ci_Site ci) $ filter (...) dats
        mbns  = nearestId sid sids
        mbdat = maybe Nothing (\ns -> List.find (\(s,_,_) -> s == ns) dats) mbns
        mbnp  = maybe Nothing (\(_,np,_) -> Just np) mbdat
    return mbnp 
-}

-- TODO add capacity
lookupNearestPortWithSpace :: Integer -> SiteId -> Server -> FileControllerData -> IO (Maybe ClientPort)
lookupNearestPortWithSpace _size sid s _cm
  = do
    dats <- getAllClientInfos s 
    let sids'  = map (\ci -> ci_Site ci) dats
    sids <- shuffle sids'
    let mbns  = nearestId sid sids
        mbdat = maybe Nothing (\ns -> List.find (\ci -> (ci_Site ci) == ns) dats) mbns
        mbnp  = maybe Nothing (\ci -> Just $ ci_Port ci) mbdat 
    return mbnp


lookupPortWithoutFile :: S.FileId -> Server -> FileControllerData -> IO (Maybe ClientPort)
lookupPortWithoutFile f s cm
  = do
    -- get all sites with the files
    fileCis <- getFileClientInfoList f s cm
    -- get all sites
    allCis  <- getAllClientInfos s
    let fileNids = map ci_Id fileCis
        allNids  = map ci_Id allCis
        nonNids  = Set.toList $ Set.difference (Set.fromList allNids) (Set.fromList fileNids)
    if (null nonNids)
      then return Nothing
      else do
        let i = head nonNids
        mbCi <- getClientInfo i s
        case mbCi of
          (Just ci) -> return $ Just $ ci_Port ci
          (Nothing) -> return Nothing 


lookupNearestPortForFile :: S.FileId -> Integer -> SiteId -> Server -> FileControllerData -> IO (Maybe ClientPort)
lookupNearestPortForFile _ size sid s cm
  -- if file exists, get nearest node, else the closest with space
  = do
--    nodeWithFile    <- lookupNearestPortWithFileAndSpace f size sid s cm
    nodeWithoutFile <- lookupNearestPortWithSpace size sid s cm   
    let mbnp = maybe Nothing (\np -> Just np) nodeWithoutFile
{-  debugM localLogger $ "lookupNearestPortForFile: file:            " ++ show f    
    debugM localLogger $ "lookupNearestPortForFile: size:            " ++ show size
    debugM localLogger $ "lookupNearestPortForFile: site:            " ++ show sid    
    debugM localLogger $ "lookupNearestPortForFile: nodeWithFile:    " ++ show nodeWithFile
    debugM localLogger $ "lookupNearestPortForFile: nodeWithoutFile: " ++ show nodeWithoutFile
    debugM localLogger $ "lookupNearestPortForFile: result:          " ++ show mbnp
-}  return mbnp
    

{-  = do
    dats <- getAllClientInfos s :: [ClientInfo]
    let sids'  = map (\ci -> ci_Site ci) dats
    sids <- shuffle sids'
    let mbns  = nearestId sid sids
        mbdat = maybe Nothing (\ns -> List.find (\ci -> (ci_Site ci) == ns) dats) mbns
        mbnp  = maybe Nothing (\ci -> Just $ ci_Port ci) mbdat
    return mbnp-}

lookupNearestPortForFiles :: [(S.FileId,Integer)] -> SiteId -> Server -> FileControllerData -> IO M.ClientPortMap
lookupNearestPortForFiles l sid s cm = do
  nearestPortWithSpace <- lookupNearestPortWithSpace 0 sid s cm
  case nearestPortWithSpace of
    Nothing -> return []
    (Just p) -> return [(p,map fst l)]
--  infoM localLogger $  "Getting nearest ports for: " ++ show l  
--  res <- foldM f [] l
--  infoM localLogger $  "Clientportmap is: " ++ show res
--  return res
--  where
--
--  f :: M.ClientPortMap -> (S.FileId,Integer) -> IO M.ClientPortMap
--  f theMap (fid,len) = do
--    infoM localLogger $  "Getting nearest ports for: " ++ fid
--    mp <- lookupNearestPortForFile fid len sid s cm
--    debugM localLogger $  "Nearest ports: " ++ show mp
--    cpm <- case mp of
--      (Just port) -> return (ins port fid theMap)
--      Nothing -> return theMap
--    return cpm

ins  :: ClientPort -> S.FileId -> M.ClientPortMap -> M.ClientPortMap
ins port fid []            = [(port,[fid])]
ins port fid ((p,fids):[]) = if (p==port)
                               then [(p,(fid:fids))]
                               else [(port,[fid]),(p,fids)]
ins port fid ((p,fids):ps) = if (p==port)
                               then ((p,fid:fids):ps) 
                               else (p,fids):(ins port fid ps)
  --   

getOtherFilePorts :: S.FileId -> IdType -> Server -> FileControllerData -> IO [ClientInfo]
getOtherFilePorts f nid s cm
  = do
    let fnm = cm_FileToNodeMap cm
    -- get all nodes which hold the file without the given node
    let otherids = Set.toList $ Set.delete nid $ maybe Set.empty id (Map.lookup f fnm)
    mbDats <- mapM (\i -> getClientInfo i s) otherids    
    return $ catMaybes mbDats

  
deleteFileFromNodes :: S.FileId -> [NP.NodePort] -> IO ()
deleteFileFromNodes fid nps = sequence_ $ map deleteFileFromNode nps
  where
    deleteFileFromNode np 
      = do
        handleAll (\e -> putStrLn $ show e) $
          do 
          -- send a delete-request to the node 
          -- but don't inform the controller again (False)
          N.deleteFile fid False np
          return ()
          
-- ----------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------- 

instance C.ControllerClass ControllerData where

  closeController cd 
    = do
      debugM localLogger "closing Server"
      closeServer (cd_Server cd)
      debugM localLogger "server closed"


  
  -- getFileSites :: S.FileId -> Controller -> IO (Set.Set SiteId) 
  getFileSites f cd
    = withMVar (cd_FileController cd) $
        \fc ->
        do
        dats <- getFileClientInfoList f (cd_Server cd) fc 
        let sids = map (\ci -> ci_Site ci) dats
        return (Set.fromList sids)

  
  -- containsFile :: S.FileId -> Controller -> IO Bool
  containsFile f cd
    = withMVar (cd_FileController cd) $
        \fc -> return $ Map.member f (cm_FileToNodeMap fc)
  
      
  -- getNearestNodePortWithFile :: S.FileId -> SiteId -> c -> IO (Maybe M.NodeRequestPort)
  getNearestNodePortWithFile f sid cd
    = withMVar (cd_FileController cd) $
        \fc -> lookupNearestPortWithFile f sid (cd_Server cd) fc 

  -- getNearestNodePortWithFiles :: [S.FileId] -> SiteId -> c -> IO (Maybe M.NodeRequestPort)
  getNearestNodePortWithFiles l sid cd
    = withMVar (cd_FileController cd) $
        \fc -> lookupNearestPortWithFiles l sid (cd_Server cd) fc 

  -- getNearestNodePortForFile :: S.FileId -> Integer -> SiteId -> c -> IO (Maybe M.NodeRequestPort)
  getNearestNodePortForFile f c sid cd
    = withMVar (cd_FileController cd) $
        \fc -> lookupNearestPortForFile f c sid (cd_Server cd) fc

  -- getNearestNodePortForFiles :: [(S.FileId,Integer)] -> SiteId -> c -> IO (ClientPortMap)
  getNearestNodePortForFiles l sid cd
    = withMVar (cd_FileController cd) $
        \fc -> lookupNearestPortForFiles l sid (cd_Server cd) fc
-- ----------------------------------------------------------------------------
-- used by the nodes
-- ----------------------------------------------------------------------------


--createFile :: S.FileId -> M.NodeId -> ControllerData -> IO ControllerData
  createFile f nid cd
    = modifyMVar (cd_FileController cd) $
        \fc ->
        do
        mbCi <- getClientInfo nid (cd_Server cd)
        case mbCi of
          (Just _) ->
            do
            let fc' = addFileToController f nid fc
            -- copy file to one other node
            mpCp <- lookupPortWithoutFile f (cd_Server cd) fc
            case mpCp of
              (Just _) -> 
                do 
                return ()
                -- let np = NP.newNodePort cp
                -- N.copyFile f (ci_Port ci) np
              (Nothing) -> return ()                  
            return (fc', ())
          (Nothing) -> return (fc,())

--createFiles :: [(S.FileId,M.NodeId)] -> ControllerData -> IO ControllerData
  createFiles l cd
    = modifyMVar (cd_FileController cd) $
        \fc ->
        do
        fc'' <- foldM f fc l
        return  (fc'',())
        where
        f :: FileControllerData -> (S.FileId,M.NodeId) -> IO FileControllerData
        f filecontroller (fid,nid) = do
          mbCi <- getClientInfo nid (cd_Server cd)
          case mbCi of
            (Just _) ->
              do
              let fc' = addFileToController fid nid filecontroller
            -- copy file to one other node
--            mpCp <- lookupPortWithoutFile f (cd_Server cd) fc
--            case mpCp of
--              (Just _) ->
--                do
--                return ()
                -- let np = NP.newNodePort cp
                -- N.copyFile f (ci_Port ci) np
--              (Nothing) -> return ()
              return fc'
            (Nothing) -> return filecontroller


--appendFile :: S.FileId -> M.NodeId -> ControllerData -> IO ControllerData
  appendFile f nid cd
    = modifyMVar (cd_FileController cd) $
        \fc ->
        do
        mbCi <- getClientInfo nid (cd_Server cd)
        case mbCi of
          (Just ci) ->
            do
            -- renew file entry
            let fc' = addFileToController f nid fc
            -- get other nodes with this file
            cps <- getOtherFilePorts f nid (cd_Server cd) fc
            let nps = map (\i -> NP.newNodePort (ci_Port i)) cps
            -- order them to copy the file from the first node
            _ <- mapM (N.copyFile f (ci_Port ci)) nps
            return (fc', ())
          (Nothing) -> return (fc,())


--deleteFile :: S.FileId -> M.NodeId -> ControllerData -> IO ControllerData
  deleteFile f nid cd
    = modifyMVar (cd_FileController cd) $
        \fc ->
        do
        -- inform all other nodes to delete node
        cps <- getOtherFilePorts f nid (cd_Server cd) fc
        let nps = map (\ci -> NP.newNodePort (ci_Port ci)) cps
        deleteFileFromNodes f nps
        -- delete file from Controller
        let fc' = deleteFileFromController f fc        
        return (fc', ())
        
        

instance Debug ControllerData where
  printDebug cd
    = do
      putStrLn "Controller-Object (full)"
      putStrLn "--------------------------------------------------------"        
      putStrLn "Server"
      printDebug (cd_Server cd)
      putStrLn "--------------------------------------------------------"
      putStrLn "FileToNodeMap:"
      withMVar (cd_FileController cd) $
        \fc -> do
        putStrLn $ show (cm_FileToNodeMap $ fc)
  getDebug cd
    = do
      let line = "--------------------------------------------------------"
      tmp <- getDebug (cd_Server cd)
      tmp2 <- withMVar (cd_FileController cd) $
          \fc -> do
          return $ show (cm_FileToNodeMap $ fc)
      return ( "Controller-Object (full)"
        ++"\n"++ line
        ++"\n"++ "Server"
        ++"\n"++ tmp
        ++"\n"++ line
        ++"\n"++ "FileToNodeMap:"
        ++"\n"++tmp2++"\n")