{-
    Copyright 2015-2017 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 FlexibleInstances #-}
{-# LANGUAGE CPP #-}
{-|
Module      : Monky.Utility
Description : Provides utility functions
Maintainer  : ongy, moepi
Stability   : testing
Portability : Linux

This module provides utility functions used in monky modules
-}
module Monky.Utility
    ( readValue
    , readValueI
    , readValues
    , fopen
    , fclose
    , File
    , readLine
    , readContent
    , findLine
    , splitAtEvery
    , maybeOpenFile
    , sdivBound
    , sdivUBound
    , listDirectory
    , C.UName
    , getUname
    , getKernelVersion
    )
where

import Data.Word (Word)
import qualified Monky.CUtil as C
import System.IO
import System.IO.Unsafe (unsafePerformIO)
import Data.List (isPrefixOf)

import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.Read as T
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS

#if MIN_VERSION_base(4,9,0)
import System.Directory (listDirectory)
#else
import System.Directory (getDirectoryContents)

listDirectory :: String -> IO [String]
listDirectory = fmap (filter (not . ("." `isPrefixOf`))) . getDirectoryContents
#endif


class LineReadable a where
  hGetReadable :: Handle -> IO a

instance LineReadable String where
  hGetReadable = hGetLine

instance LineReadable ByteString where
  hGetReadable = BS.hGetLine

class FileReadable a where
  hGetFile :: Handle -> IO [a]

instance FileReadable String where
  hGetFile = readStringLines

instance FileReadable ByteString where
  hGetFile = readBSLines



-- |type alias to distinguish system functions from utility
newtype File = File Handle deriving (Show, Eq)

-- |Find a line in a list of Strings
findLine :: Eq a => [a] -> [[a]] -> Maybe [a]
findLine y (x:xs) = if y `isPrefixOf` x
  then Just x
  else findLine y xs
findLine _ [] = Nothing

-- |Read the first line of the file and convert it into an 'Int'
readValue :: File -> IO Int
readValue (File h) = do
  hSeek h AbsoluteSeek 0
  line <- hGetReadable h
  let value = fmap fst $ BS.readInt line
  return . fromMaybe (error ("Failed to read value from file:" ++ show h)) $ value

-- |Read the first line of the file and convert it into an 'Integer'
readValueI :: File -> IO Integer
readValueI (File h) = do
  hSeek h AbsoluteSeek 0
  line <- hGetReadable h
  let value = fmap fst $ BS.readInteger line
  return . fromMaybe (error ("Failed to read value from file:" ++ show h)) $ value


-- |Read the first line of the file and convert the words in it into 'Int's
readValues :: File -> IO [Int]
readValues (File h) = do
  hSeek h AbsoluteSeek 0
  line <- hGetReadable h
  let value = mapM (fmap fst . BS.readInt) $ BS.words line
  return . fromMaybe (error ("Failed to read values from file:" ++ show h)) $ value

-- |Read the first line of the file
readLine :: LineReadable a => File -> IO a
readLine (File h) = do
  hSeek h AbsoluteSeek 0
  hGetReadable h

-- |Read a File as String line by line
readStringLines :: Handle -> IO [String]
readStringLines h = do
  eof <- hIsEOF h
  if eof
    then return []
    else do
      l <- hGetReadable h
      fmap (l:) $ readStringLines h

-- |Read a File as ByteString line by line
readBSLines :: Handle -> IO [ByteString]
readBSLines h = fmap (BS.lines . BS.concat) $ readLines' []
  where
    readLines' ls = do
      ret <- BS.hGet h 512
      if ret == BS.empty
        then return $ reverse ls
        else readLines' (ret:ls)

-- |Rewind the file descriptor and read the complete file as lines
readContent :: FileReadable a => File -> IO [a]
readContent (File h) = do
  hSeek h AbsoluteSeek 0
  hGetFile h

-- |open a file read only
fopen :: String -> IO File
fopen = fmap File . flip openFile ReadMode

-- |Close a file opened by 'fopen'
fclose :: File -> IO ()
fclose (File h) = hClose h

-- |Split ys at every occurence of xs
splitAtEvery :: String -> String -> [String]
splitAtEvery s str = splitAtEvery' s str []
  where splitAtEvery' _ [] zs = [zs]
        splitAtEvery' xs (y:ys) zs = if xs `isPrefixOf` (y:ys)
          then zs:splitAtEvery' xs (cut ys) []
          else splitAtEvery' xs ys (zs ++ [y])
          where cut = drop (length xs -1)

-- |fmap fopen would give Maybe (IO File), this fixes that
maybeOpenFile :: Maybe String -> IO (Maybe File)
maybeOpenFile Nothing = return Nothing
maybeOpenFile (Just x) = fmap Just . fopen $ x

-- |0 save divide, uses maxbound for default value
sdivBound :: (Integral a, Bounded a) => a -> a -> a
sdivBound _ 0 = maxBound
sdivBound x y = x `div` y

-- |0 save divide, uses default value
sdivUBound :: Integral a => a -> a -> a -> a
sdivUBound _ 0 d = d
sdivUBound x y _ = x `div` y

infixl 7 `sdivBound`, `sdivUBound`

-- This should never change while we are on the same kernel
-- | Get the kernel name/version/release and so forth
getUname :: C.UName
getUname = unsafePerformIO C.uname

-- | Get the kernel version as (major, minor)
getKernelVersion :: (Word, Word)
getKernelVersion =
    let txt = T.splitOn (T.pack ".") . C._uRelease $ getUname
        (Right major: Right minor:_) = fmap T.decimal txt
     in (fst major, fst minor)
