------------------------------------------------------------------------------
-- |
-- Module      :  Util
-- Copyright   :  (c) Mads N Noe 2009
-- Maintainer  :  mntnoe (@) gmail.com
-- License     :  as-is
--
-- Utility functions for XMonad.
--
------------------------------------------------------------------------------

module Util where

-- Haskell modules
import Control.Monad (unless, when)
import Control.Monad.Trans (lift)
import Data.List
import qualified Data.Map as M
import Data.Monoid (Endo(Endo))
import System.Posix.Unistd(getSystemID, nodeName)
import System.IO.Error (isDoesNotExistError)

-- XMonad modules
import XMonad
import XMonad.Actions.CycleWS
import XMonad.Actions.WindowGo
import qualified XMonad.StackSet as W
import XMonad.Util.WorkspaceCompare (getSortByTag)

-- | Perform k x if x return a 'Just' value.
(?+:: (Monad m) => m (Maybe a) -> (a -> m ()) -> m ()
?+ k = x >>= maybe (return ()) k
infixr 1 ?+

-- | Helper function for use with monads.
if_ :: t -> t -> Bool -> t
if_ t f c = if c 
                  then t
                  else f

-- | Return a string that launches xterm with the given 'title', 'appName' and
--   command to execute.
xterm :: String -> String -> String
xterm a e = concat ["xterm -wf -title '", e,  "' -name '", a, "' -e '", e, "'"]

-- | Shift a window to a workspace and switch to that workspace in one
--   operation.
shiftView :: WorkspaceId -> WindowSet -> WindowSet
shiftView ws w = W.greedyView ws $ W.shift ws w

-- | Perform a workspace transformation on the next workspace in 'WSDirection'
--   of type 'WSType'.
doWithWS :: (String -> (WindowSet -> WindowSet)) -> WSDirection -> WSType -> X ()
doWithWS f dir wstype = do
    i <- findWorkspace getSortByTag dir wstype 1
    windows $ f i

-- | Is the current workspace empty?
isCurrentWsEmpty :: X Bool
isCurrentWsEmpty = withWindowSet $ \-> do
    let l = W.integrate' $ W.stack $ W.workspace $ W.current s
    return $ null l

-- | Modify the 'WindowSet' with a non-pure function.  Counterpart to 'doF'.
doX :: (Window -> X (WindowSet -> WindowSet)) -> ManageHook
doX f = ask >>= Query . lift . fmap Endo . f

-- | Ensure that a window always starts on an empty workspace.  If a window
--   satisfying the query exists, focus it.  Otherwise run the specified
--   command, swithing to an empty workspace if the current one is not empty.
reqEmptyWS :: Query Bool -> X () -> X ()
reqEmptyWS q f = do
    raiseNextMaybe (reqEmptyWS' >> f) q
  where
    reqEmptyWS' = do
        empty <- isCurrentWsEmpty
        i <- findWorkspace getSortByTag Next EmptyWS 1
        unless empty $ windows $ W.greedyView i

-- | Kill the focused window. If the window satisfies the query, return to the
--   previously displayed workspace.
killAndReturn q = withFocused $ \-> do
    qr <- runQuery q w
    kill
    when qr toggleWS

-- | Perform a 'WindowSet' transformation on the workspace with the given
--   index.
withNthWorkspace :: (String -> WindowSet -> WindowSet) -> Int -> X ()
withNthWorkspace job wnum = nthWorkspaceTag wnum ?+ windows . job
  where
    nthWorkspaceTag :: Int -> X (Maybe String)
    nthWorkspaceTag wnum = do 
        sort <- getSortByTag
        ws <- gets (map W.tag . sort . W.workspaces . windowset)
        case drop wnum ws of
            (w:_) -> return $ Just w
            [] -> return Nothing

-- | Is the focused window the \"master window\" of the current workspace?
isMaster :: Query Bool
isMaster = ask >>= (\-> liftX $ withWindowSet $ \ws -> return $ Just w == master ws)
  where
    master :: WindowSet -> Maybe Window
    master ws = 
        case W.integrate' $ W.stack $ W.workspace $ W.current ws of
             [] -> Nothing
             (x:xs) -> Just x

-- | Is the focused window a floating window?
isFloat :: Query Bool
isFloat = ask >>= (\-> liftX $ withWindowSet $ \ws -> return $ M.member w $ W.floating ws)

-- | Swap the focused window with the last window in the stack.
swapBottom :: W.StackSet i l a s sd -> W.StackSet i l a s sd
swapBottom = W.modify' $ \-> case c of
    W.Stack _ _ [] -> c    -- already bottom.
    W.Stack t ls rs -> W.Stack t (xs ++ x : ls) [] where (x:xs) = reverse rs

-- | Swap the focused window with the following window, or if the window is
--   floating, lower it to the bottom.
swapOrLower :: X ()
swapOrLower = withFocused $ \->
    runQuery isFloat w >>= if_ (windows swapBottom) (windows W.swapDown)

-- | Swap the focused window with the preceding window, or if the window is
--   floating, raise it to the top.
swapOrRaise :: X ()
swapOrRaise = withFocused $ \->
    runQuery isFloat w >>= if_ (windows W.swapMaster) (windows W.swapUp)

-- | Determine the host.
getHost = do 
    host <- getSystemID
    case nodeName host of
         "mntnoe-desktop" -> return Desktop
         "mntnoe-laptop"  -> return Laptop
         _                -> return Desktop

-- | For use in cross host configutions.
data Host = Desktop | Laptop deriving Eq