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

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

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


-}

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

{-# OPTIONS -fglasgow-exts #-}
module Holumbus.FileSystem.FileSystem
(
-- * Datatypes
  S.FileId
, S.FileContent
, S.getContentLength
, S.FileData(..)
, FileSystem

-- * Configuration
, FSStandaloneConf(..)
, defaultFSStandaloneConfig
, FSControllerConf(..)
, defaultFSControllerConfig
, FSNodeConf(..)
, defaultFSNodeConfig
, FSClientConf(..)
, defaultFSClientConfig

-- * Creation and Destruction
, mkStandaloneFileSystem
, mkFileSystemController
, mkFileSystemNode
, mkFileSystemClient
, closeFileSystem

-- * Operations
, getMySiteId
, getFileSites
, getNearestNodePortWithFile
, getNearestNodePortForFile
, containsFile

, createFile
, createFiles
, appendFile
, deleteFile

, getFileContent
, getMultiFileContent
, getFileData
, isFileLocal

)
where

import           Prelude hiding (appendFile)

import           Control.Concurrent
import           Control.Monad


import qualified Data.List as L
import qualified Data.Set as Set
import qualified Data.Map as Map
import           System.Log.Logger

import           Holumbus.Common.Debug

import qualified Holumbus.FileSystem.Controller                as C
import qualified Holumbus.FileSystem.Controller.ControllerData as CD
import qualified Holumbus.FileSystem.Controller.ControllerPort as CP

import qualified Holumbus.FileSystem.Node                      as N
import qualified Holumbus.FileSystem.Node.NodeData             as ND
import qualified Holumbus.FileSystem.Node.NodePort             as NP

import qualified Holumbus.FileSystem.Storage                   as S
import qualified Holumbus.FileSystem.Storage.FileStorage       as FST

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

import Holumbus.FileSystem.Messages (ClientPortMap)

localLogger :: String
localLogger = "Holumbus.FileSystem.FileSystem"
 
-- ---------------------------------------------------------------------------
-- Datatypes
-- ---------------------------------------------------------------------------

data FileSystemData = 
   forall c n. (C.ControllerClass c, N.NodeClass n, Debug c, Debug n) =>
   FileSystemData SiteId c (Maybe n)
--    fsd_siteId     :: SiteId
--  , fsd_controller :: MVar c
--  , fsd_nodep      :: forall n. (N.Node n) => MVar (Maybe n)
--}

data FileSystem = FileSystem (MVar FileSystemData)

instance Show FileSystem where
  show _ = "FileSystem"



-- ---------------------------------------------------------------------------
-- Configurations
-- ---------------------------------------------------------------------------


data FSStandaloneConf = FSStandaloneConf {
    fstc_StreamName       :: StreamName
  , fstc_StoragePath      :: FilePath
  , fstc_StorageFile      :: FilePath
  } 
  
defaultFSStandaloneConfig :: FSStandaloneConf
defaultFSStandaloneConfig = FSStandaloneConf "FSController" "storage/" "directory"


data FSControllerConf = FSControllerConf {
    fcoc_StreamName       :: StreamName
  , fcoc_PortNumber       :: Maybe PortNumber
  }
   
defaultFSControllerConfig :: FSControllerConf
defaultFSControllerConfig = FSControllerConf "FSController" Nothing


data FSNodeConf = FSNodeConf {
    fnoc_StreamName       :: StreamName
  , fnoc_SocketId         :: Maybe SocketId
  , fnoc_StoragePath      :: FilePath
  , fnoc_StorageFile      :: FilePath
  }
   
defaultFSNodeConfig :: FSNodeConf
defaultFSNodeConfig = FSNodeConf "FSController" Nothing "storage/" "directory"


data FSClientConf = FSClientConf {
    fclc_StreamName       :: StreamName
  , fclc_SocketId         :: Maybe SocketId
  }
   
defaultFSClientConfig :: FSClientConf
defaultFSClientConfig = FSClientConf "FSController" Nothing




-- ---------------------------------------------------------------------------
-- Creation and Destruction
-- ---------------------------------------------------------------------------


mkStandaloneFileSystem
  :: FSStandaloneConf 
  -> IO (FileSystem)
mkStandaloneFileSystem conf
  = do
    controller <- CD.newController (fstc_StreamName conf) Nothing
    let storage = FST.newFileStorage (fstc_StoragePath conf) (fstc_StorageFile conf) 
    node <- ND.newNode (fstc_StreamName conf) Nothing storage
    fs <- newFileSystem controller (Just node)
    return fs


mkFileSystemController
  :: FSControllerConf
  -> IO (FileSystem)
mkFileSystemController conf
  = do
    sid <- getSiteId
    infoM localLogger $ "initialising controller on site " ++ show sid  
    controller <- CD.newController (fcoc_StreamName conf) (fcoc_PortNumber conf)
    newFileSystem controller (Nothing::Maybe NP.NodePort)
  

mkFileSystemNode
  :: FSNodeConf
  -> IO (FileSystem)
mkFileSystemNode conf
  = do
    sid <- getSiteId
    infoM localLogger $ "initialising node on site " ++ show sid  
    cp <- CP.newControllerPort (fnoc_StreamName conf) (fnoc_SocketId conf)
    infoM localLogger "creating filestorage"
    let storage = FST.newFileStorage (fnoc_StoragePath conf) (fnoc_StorageFile conf) 
    node <- ND.newNode (fnoc_StreamName conf) (fnoc_SocketId conf) storage
    newFileSystem cp (Just node)
  

mkFileSystemClient
  :: FSClientConf
  -> IO (FileSystem)
mkFileSystemClient conf
  = do
    sid <- getSiteId
    infoM localLogger $ "initialising client on site " ++ show sid  
    cp <- CP.newControllerPort (fclc_StreamName conf) (fclc_SocketId conf)
    newFileSystem cp (Nothing::Maybe NP.NodePort)


-- | Closes the filesystem.
closeFileSystem :: FileSystem -> IO ()
closeFileSystem (FileSystem fs) 
  = do
    debugM localLogger "beginning to close filesystem"
    withMVar fs $
      \(FileSystemData _ c n) ->
      do
      case n of
        (Just n') -> 
          do
          debugM localLogger "closing Node"
          N.closeNode n'
        (Nothing) -> 
          do
          debugM localLogger "no Node to close"
          return ()
      debugM localLogger "closing Controller"
      C.closeController c
      debugM localLogger "filesystem closed"



-- ---------------------------------------------------------------------------
-- private helper-functions
-- ---------------------------------------------------------------------------


-- | Creates a new FileSystem with a controller and (maybe) a node.
newFileSystem :: (C.ControllerClass c, N.NodeClass n, Debug c, Debug n) => c -> Maybe n -> IO (FileSystem)
newFileSystem c n
  = do
    sid <- getSiteId
    fs <- newMVar (FileSystemData sid c n)
    return (FileSystem fs)


-- | get a NodePort from a NodeRequestPort
createNodePort :: (Maybe ClientPort) -> (Maybe NP.NodePort)
createNodePort Nothing = Nothing
createNodePort (Just p) = Just $ NP.newNodePort p


-- ---------------------------------------------------------------------------
-- public functions
-- ---------------------------------------------------------------------------

getMySiteId :: FileSystem -> IO (SiteId)
getMySiteId (FileSystem fs)
  = do 
    withMVar fs $
      \(FileSystemData s _ _) -> return s

-- | Get a set of all sites the file exists.
getFileSites :: S.FileId -> FileSystem -> IO (Set.Set SiteId)
getFileSites f (FileSystem fs)
  = do
    withMVar fs $
      \(FileSystemData _ c _) -> 
      C.getFileSites f c


-- | gets the nearest NodePort with our fileId
getNearestNodePortWithFile :: S.FileId -> FileSystem -> IO (Maybe NP.NodePort)
getNearestNodePortWithFile f (FileSystem fs)
  = withMVar fs $
      \(FileSystemData sid c _) -> 
      (liftM createNodePort) $ C.getNearestNodePortWithFile f sid c
    
-- | gets the nearest NodePorts with list of fileId
getNearestNodePortWithFiles :: [S.FileId] -> FileSystem -> IO [(NP.NodePort,[S.FileId])]
getNearestNodePortWithFiles l (FileSystem fs)
  = withMVar fs $
      \(FileSystemData sid c _) -> do
        infoM localLogger $ "Searching for nearest ports with files"
        ports <- C.getNearestNodePortWithFiles l sid c
        infoM localLogger $ ">>>>>>>>>>>>>>> " ++ show ports
        let result = createPortList ports
        debugM localLogger $ "Found ports" ++ show ports
        return result


-- | gets the nearest NodePort on which we can create our fileId. we need the
--   content-size to get a node with enough space.
getNearestNodePortForFile :: S.FileId -> Integer -> FileSystem -> IO (Maybe NP.NodePort)
getNearestNodePortForFile f l (FileSystem fs)
  = withMVar fs $
      \(FileSystemData sid c _) ->
      (liftM createNodePort) $ C.getNearestNodePortForFile f l sid c

-- | gets the nearest NodePort on which we can create our files. Iterate through the list and give back a result, where each nodeport is assosiated with the list of files it can handle. If we have an empty Storage system, one pair is returned. If there are existing files in the list, the files are stored on the node, they exists on.
getNearestNodePortForFiles :: [(S.FileId,Integer)] -> FileSystem -> IO [(NP.NodePort,[S.FileId])]
getNearestNodePortForFiles l (FileSystem fs)
  = withMVar fs $
      \(FileSystemData sid c _) -> do
      infoM localLogger $ "Searching for nearest ports"
      ports <- C.getNearestNodePortForFiles l sid c
      let result = createPortList ports
      debugM localLogger $ "Found ports" ++ show ports
      return result

createPortList :: ClientPortMap -> [(NP.NodePort,[S.FileId])]
createPortList [] = []
createPortList ((p,fids):ps) = case createNodePort (Just p) of
                        (Just p') -> ( (p',fids) : (createPortList ps) )
                        Nothing -> createPortList ps


-- | Checks if a file is in the filesystem
containsFile :: S.FileId -> FileSystem -> IO Bool
containsFile f (FileSystem fs)
  = do
    withMVar fs $
      \(FileSystemData _ c _) ->
      C.containsFile f c


-- | Creates a file in the filesystem.
createFile :: S.FileId -> S.FileContent -> FileSystem -> IO ()
createFile f c fs
  = do
    infoM localLogger $ "creating file: " ++ show f
    np <- getNearestNodePortForFile f (S.getContentLength c) fs
    case np of
      (Nothing) -> return ()
      (Just np') -> 
        do 
        N.createFile f c np'
        return ()

-- | Creates a list of files in the filesystem.
createFiles :: [(S.FileId,S.FileContent)] -> FileSystem -> IO ()
createFiles l fs = do
  infoM localLogger $ "creating files: " ++ (show . map fst $ l)
  nps <- getNearestNodePortForFiles (map (\(fid,c) -> (fid,S.getContentLength c)) l) fs
  debugM localLogger $ "Nodes to files map:" ++ show nps
  mapM_ (\(node,fids) -> N.createFiles (filesToStore fids) node) nps
    where
    asMap :: Map.Map S.FileId S.FileContent
    asMap = Map.fromList l

    filesToStore :: [S.FileId] -> [(S.FileId,S.FileContent)]
    filesToStore = L.foldl' addFile [] 

    addFile :: [(S.FileId,S.FileContent)] -> S.FileId -> [(S.FileId,S.FileContent)]
    addFile l' fid = case Map.lookup fid asMap of
                      Nothing -> l'
                      (Just c) -> ((fid,c):l')

-- | Appends a file in the fileSystem.
appendFile :: S.FileId -> S.FileContent -> FileSystem -> IO ()
appendFile f c fs
  = do
    infoM localLogger $ "appending file: " ++ show f
    np <- getNearestNodePortForFile f (S.getContentLength c) fs
    debugM localLogger $ "nearest port: " ++ show np
    case np of
      (Nothing) -> return ()
      (Just np') ->
        do
        N.appendFile f c np'
        return ()


-- | Deletes a file from the filesystem.
deleteFile :: S.FileId -> FileSystem -> IO ()
deleteFile f fs
  = do
    infoM localLogger $ "deleting file: " ++ show f
    np <- getNearestNodePortWithFile f fs
    case np of
      (Nothing) -> return ()
      (Just np') ->
        do
        -- start a delete cascade... (True)
        N.deleteFile f True np'
        return ()


-- | Gets the file content from the nearest site whitch holds the file
getFileContent :: S.FileId -> FileSystem -> IO (Maybe S.FileContent)
getFileContent f fs
  = do
    infoM localLogger $ "getting file: " ++ show f
    np <- getNearestNodePortWithFile f fs
    case np of
      (Nothing) -> return Nothing
      (Just np') ->
        do
        N.getFileContent f np' 

-- | Gets the file content from the nearest site whitch holds the file
getMultiFileContent :: [S.FileId] -> FileSystem -> IO [(S.FileId,S.FileContent)]
getMultiFileContent l fs
  = do
    infoM localLogger $ "getting files: " ++ (show l)
    portmap <- getNearestNodePortWithFiles l fs
    infoM localLogger $ "ports with files: " ++ show portmap
    res <- mapM f portmap
    return . concat $ res
    where
    f :: (NP.NodePort,[S.FileId]) -> IO [(S.FileId,S.FileContent)]
    f (np,files) = N.getMultiFileContent files np

-- | Gets the file data from the nearest site whitch holds the file
getFileData :: S.FileId -> FileSystem -> IO (Maybe S.FileData)
getFileData f fs 
  = do
    np <- getNearestNodePortWithFile f fs
    case np of
      (Nothing) -> return Nothing
      (Just np') ->
        do
        N.getFileData f np' 


-- | Tests, if the local storage (if one exists) holds the file
isFileLocal :: S.FileId -> FileSystem -> IO Bool
isFileLocal f (FileSystem fs)
  = do
    withMVar fs $
      \(FileSystemData _ _ n) ->
      maybe (return False) (\n' -> N.containsFile f n') n


instance Debug FileSystem where  
  printDebug (FileSystem fs)
    = do
      withMVar fs $
        \(FileSystemData s c n) ->
        do
        putStrLn "--------------------------------------------------------"
        putStrLn "FileSystem - internal data\n"
        putStrLn "--------------------------------------------------------"
        putStrLn "SiteId:"
        putStrLn $ show s
        putStrLn "--------------------------------------------------------"
        putStrLn "Controller:"
        printDebug c
        putStrLn "--------------------------------------------------------"
        putStrLn "Node:"
        maybe (putStrLn "NOTHING") (\n' -> printDebug n') n
        putStrLn "--------------------------------------------------------"
  getDebug (FileSystem fs)
    = do
      tmp <- withMVar fs $
        \(FileSystemData s c n) ->
        do
        tmp <-  getDebug c
        tmp2 <- maybe (return "NOTHING") (\n' -> getDebug n') n
        let line = "--------------------------------------------------------"
        return (line
          ++"\n"++ "FileSystem - internal data\n"
          ++"\n"++ line
          ++"\n"++ "SiteId:"
          ++"\n"++ show s
          ++"\n"++ line
          ++"\n"++ "Controller:"
          ++"\n"++ tmp
          ++"\n"++ line
          ++"\n"++ "Node:"
          ++"\n"++ tmp2
          ++"\n"++ line ++"\n")
      return tmp