{-
    Copyright 2015 Markus Ongyerth, Stephan Guenther

    This file is part of Monky.

    Monky is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Monky is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Monky.  If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE CPP #-}
{-|
Module      : Monky.CPU
Description : Allows access to information about the systems cpu
Maintainer  : ongy
Stability   : testing
Portability : Linux

-}
module Monky.CPU
  ( CPUHandle
  , TempHandle
  , FreqHandle
  , NumaHandle(..)
  , getCPUHandle
  , getCPUPercent
  , getNumaPercent
  , getCPUTemp
  , getCPUMaxScalingFreq
  , ScalingType(..)
  , getNumaHandles

  , guessThermalZone
  , getThermalZone
  , getThermalZones

  , getFreqHandle
  , getFreqNuma
  )
where

import Data.Char (isDigit)
import System.IO.Unsafe (unsafePerformIO)
import System.Directory (getDirectoryContents)
import Monky.Utility
import Data.List (isPrefixOf)
import Data.Maybe (listToMaybe, fromMaybe)
import Data.IORef

import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS (readInt, words, unpack)

#if MIN_VERSION_base(4,8,0)
#else
import Control.Applicative ((<$>))
#endif


{- Stat work all-}
-- |The handle exported by this module
data CPUHandle = CPUH File (IORef [Int]) (IORef [Int])
-- |The handle for themperature
newtype TempHandle = TH (Maybe File)
-- |The handle for cpu frequency
newtype FreqHandle = FH [File]

-- Wrapper around numa nodes for stat reading
-- |Numa aware version of 'CPUHandle'
data NumaHandle = NumaHandle
  { numaCpus :: [String]
  , numaHandle :: CPUHandle
  }


-- |Which values should be returned by getCPUFreq
data ScalingType
  = ScalingMax -- ^Use the maximum frequencie allowed
  | ScalingCur -- ^Use the current frequencie used
  | ScalingNone -- ^Don't open any files for scaling type


pathStat :: String
pathStat = "/proc/stat"

pathCPUBase :: String
pathCPUBase = "/sys/devices/system/cpu"

thermalBaseP :: String
thermalBaseP = "/sys/class/thermal/"

pathTemp :: String -> String
pathTemp zone = thermalBaseP ++ zone ++ "/temp"

pathMaxScaling :: String -> String
pathMaxScaling str = "/sys/devices/system/cpu/" ++ str ++ "/cpufreq/scaling_max_freq"

pathCurScaling :: String -> String
pathCurScaling str = "/sys/devices/system/cpu/" ++ str ++ "/cpufreq/scaling_cur_freq"

pathNumaBase :: String
pathNumaBase = "/sys/devices/system/node/"

getCPUFreqsCur :: [String] -> IO [File]
getCPUFreqsCur = mapM (fopen . pathCurScaling)

getCPUFreqsMax :: [String] -> IO [File]
getCPUFreqsMax = mapM (fopen . pathMaxScaling)


-- |Check if thermal zone is x86_pkg_temp
isX86PkgTemp :: String -> Bool
isX86PkgTemp xs = unsafePerformIO $do
  str <- readFile (thermalBaseP ++ xs ++ "/type")
  return (str == "x86_pkg_temp\n")

-- |Try to guess the thermal zones
guessThermalZones :: IO [String]
guessThermalZones = do
  filter isX86PkgTemp . filter ("thermal_zone" `isPrefixOf`) <$> tzones
  where tzones = getDirectoryContents thermalBaseP

-- |Tries to guess the thermal zone based on type
guessThermalZone :: IO (Maybe String)
guessThermalZone = fmap listToMaybe guessThermalZones

-- |Calcualate the work done by the cores (all, work)
calculateWork :: [[Int]] -> ([Int], [Int])
calculateWork xs =
  let work = map (sum . take 3) xs
      sall  = zipWith (\x y -> x + sum y) work (map (drop 3) xs) in
    (sall, work)


calculatePercent :: [Int] -> [Int] -> [Int] -> [Int] -> [Int]
calculatePercent sall work owork oall =
  let cwork = zipWith (-) work owork
      call  = zipWith (-) sall oall in
    zipWith (sdivBound . (* 100)) cwork call


readVals :: [ByteString] -> [Int]
readVals = map (fst . fromMaybe (error "CPUModule: Something in /proc/stat was unexpted") . BS.readInt) . tail


getPercent :: ([String] -> Bool) -> CPUHandle -> IO [Int]
getPercent f (CPUH file aref wref) = do
  content <- map BS.words <$> readContent file
  let cpus = filter (f . map BS.unpack) content
  let d = map readVals cpus
  let (sall, work) = calculateWork d
  a <- readIORef aref
  w <- readIORef wref
  writeIORef wref work
  writeIORef aref sall
  return $ calculatePercent sall work w a

-- |Get the cpu usage in percent for each (virtual) cpu
getCPUPercent :: CPUHandle -> IO [Int]
getCPUPercent = getPercent (\(x:_) -> "cpu" `isPrefixOf` x && length x > 3)

-- |Read node information
getNumaPercent :: NumaHandle -> IO [Int]
getNumaPercent (NumaHandle cpus h) =
  getPercent (\xs -> head xs `elem` cpus) h

-- |get current CPU temperature
getCPUTemp :: TempHandle -> IO Int
getCPUTemp (TH Nothing) = return (-1)
getCPUTemp (TH (Just f)) = do
  temp <- readValue f
  return (temp `div` 1000)


getMax :: [Int] -> Int
getMax = foldr max (-1)

{- |This function returns a frequency according the 'ScalingType' of the handle.

The returned valued will be the max of all (virtual) proceessors on the system.
-}
getCPUMaxScalingFreq :: FreqHandle -> IO Float
getCPUMaxScalingFreq (FH files) = do
  vals <- mapM readValue files
  return (fromIntegral (getMax vals) / 1000000)

-- open the files to read the frequency from
getCPUFreqs :: ScalingType -> [String] -> IO [File]
getCPUFreqs ScalingMax = getCPUFreqsMax
getCPUFreqs ScalingCur = getCPUFreqsCur
getCPUFreqs ScalingNone = (\_ -> return [])


getCPUs :: String -> IO [String]
getCPUs = fmap (filter isCPU) . getDirectoryContents
  where isCPU ys = "cpu" `isPrefixOf` ys && all isDigit (drop 3 ys)


-- |Get a frequency handle by type
getFreqHandle :: ScalingType -> IO FreqHandle
getFreqHandle t = do
  cpus <- getCPUs pathCPUBase
  FH <$> getCPUFreqs t cpus

-- |Get a frequency handle limited to the cpus the numa handle uses
getFreqNuma :: ScalingType -> NumaHandle -> IO FreqHandle
getFreqNuma t (NumaHandle cpus _) =
  FH <$> getCPUFreqs t cpus


getHandle :: String -> IO NumaHandle
getHandle path = do
  getNumaHandle =<< getCPUs path

-- |Raw-ish access to numa handle, this can be (ab)used to make your own filter on cpus
getNumaHandle
  :: [String] -- ^CPU "names" to include ([cpu0, cpu1, cpu2, ..])
  -> IO NumaHandle
getNumaHandle cpus = do
  workref <- newIORef ([0] :: [Int])
  allref <- newIORef ([0] :: [Int])
  stat <- fopen pathStat
  return $ NumaHandle cpus (CPUH stat allref workref)

-- |Create an 'CPUHandle'
getCPUHandle :: IO CPUHandle
getCPUHandle = numaHandle <$> getHandle pathCPUBase

-- |Get the CPUs thermal zone
getThermalZone :: String -> IO TempHandle
getThermalZone = fmap (TH . Just) . fopen . pathTemp

-- |Get the CPUs thermal zones, will be same order as numa nodes (hopefully)
getThermalZones :: IO [TempHandle]
getThermalZones = do
  real <- mapM (fmap (TH . Just) . fopen . pathTemp) =<< guessThermalZones
  return (real ++ repeat (TH Nothing))

-- |Get the Numa aware handle
getNumaHandles :: IO [NumaHandle]
getNumaHandles = do
  nodes <- filter isNode <$> getDirectoryContents pathNumaBase
  mapM (getHandle . (pathNumaBase ++)) nodes
  where isNode = ("node" `isPrefixOf`)