{-|
Module      : System.GPIO.Linux.Sysfs.Util
Description : Useful low-level Linux @sysfs@ functions
Copyright   : (c) 2018, Quixoftic, LLC
License     : BSD3
Maintainer  : Drew Hess <dhess-src@quixoftic.com>
Stability   : experimental
Portability : non-portable

Useful low-level Linux @sysfs@ functions.

-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Safe #-}

module System.GPIO.Linux.Sysfs.Util
       ( -- * Paths and file names
         sysfsPath
       , exportFileName
       , unexportFileName
       , pinDirName
       , pinActiveLowFileName
       , pinDirectionFileName
       , pinEdgeFileName
       , pinValueFileName
         -- * Convert Haskell types to/from their @sysfs@ representation
         --
         -- | A note on newlines: a Linux GPIO pin's /attributes/
         -- (i.e., the @sysfs@ files representing a pin's state) are
         -- read and written as 'ByteString's. When reading their
         -- contents, the attribute files always return their
         -- (ASCII-encoded) value followed by a newline character
         -- (@\\n@). When writing their contents, the attribute files
         -- will accept their (ASCII-encoded) new value either with or
         -- without a trailing newline character. For consistency (and
         -- for the sake of isomorphic conversions back-and-forth),
         -- these functions always use a trailing newline when
         -- encoding the ASCII value from the Haskell value.
       , pinDirectionToBS
       , pinDirectionValueToBS
       , bsToPinDirection
       , sysfsEdgeToBS
       , bsToSysfsEdge
       , pinValueToBS
       , bsToPinValue
       , activeLowToBS
       , bsToActiveLow
       , intToBS
       , bsToInt
       ) where

import Protolude
import qualified Data.ByteString as BS (empty)
import Data.ByteString.Builder (toLazyByteString, intDec)
import qualified Data.ByteString.Char8 as C8 (readInt)
import System.FilePath ((</>))

import System.GPIO.Types (Pin(..), PinDirection(..), PinValue(..))
import System.GPIO.Linux.Sysfs.Types (SysfsEdge(..))

-- | The base path to Linux's @sysfs@ GPIO filesystem.
--
-- >>> sysfsPath
-- "/sys/class/gpio"
sysfsPath :: FilePath
sysfsPath = "/sys/class/gpio"

-- | The name of the control file used to export GPIO pins via
-- @sysfs@.
--
-- >>> exportFileName
-- "/sys/class/gpio/export"
exportFileName :: FilePath
exportFileName = sysfsPath </> "export"

-- | The name of the control file used to "unexport" GPIO pins via
-- @sysfs@.
--
-- >>> unexportFileName
-- "/sys/class/gpio/unexport"
unexportFileName :: FilePath
unexportFileName = sysfsPath </> "unexport"

-- | Exporting a GPIO pin via @sysfs@ creates a control directory
-- corresponding to that pin. 'pinDirName' gives the name of that
-- directory for a given 'Pin'.
--
-- >>> pinDirName (Pin 16)
-- "/sys/class/gpio/gpio16"
pinDirName :: Pin -> FilePath
pinDirName (Pin n) = sysfsPath </> ("gpio" ++ show n)

-- | The name of the attribute file used to read and write the pin's
-- @active_low@ value.
--
-- >>> pinActiveLowFileName (Pin 16)
-- "/sys/class/gpio/gpio16/active_low"
pinActiveLowFileName :: Pin -> FilePath
pinActiveLowFileName p = pinDirName p </> "active_low"

-- | Pins whose direction can be controlled via @sysfs@ provide a
-- @direction@ attribute file. 'pinDirectionFileName' gives the name
-- of that file for a given 'Pin'. Note that some pins' direction
-- cannot be set. In these cases, the file named by this function does
-- not actually exist.
--
-- >>> pinDirectionFileName (Pin 16)
-- "/sys/class/gpio/gpio16/direction"
pinDirectionFileName :: Pin -> FilePath
pinDirectionFileName p = pinDirName p </> "direction"

-- | Pins that can be configured as interrupt-generating inputs
-- provide an @edge@ attribute file. 'pinEdgeFileName' gives the name
-- of that file for a given 'Pin'. Note that some pins' edge
-- configuration cannot be set. In these cases, the file named by this
-- function does not actually exist.
--
-- >>> pinEdgeFileName (Pin 16)
-- "/sys/class/gpio/gpio16/edge"
pinEdgeFileName :: Pin -> FilePath
pinEdgeFileName p = pinDirName p </> "edge"

-- | The name of the attribute file used to read and write the pin's
-- logical signal value.
--
-- >>> pinValueFileName (Pin 16)
-- "/sys/class/gpio/gpio16/value"
pinValueFileName :: Pin -> FilePath
pinValueFileName p = pinDirName p </> "value"

-- | Convert a 'PinDirection' value to the corresponding 'ByteString'
-- value expected by a pin's @direction@ attribute in the @sysfs@ GPIO
-- filesystem.
--
-- >>> pinDirectionToBS In
-- "in\n"
-- >>> pinDirectionToBS Out
-- "out\n"
pinDirectionToBS :: PinDirection -> ByteString
pinDirectionToBS In = "in\n"
pinDirectionToBS Out = "out\n"

-- | Convert a 'PinValue' value to the corresponding 'ByteString'
-- value expected by a pin's @direction@ attribute in the @sysfs@
-- GPIO, which can be used to configure the pin for output and
-- simultaneously set the pin's (physical) signal level; see the
-- <https://www.kernel.org/doc/Documentation/gpio/sysfs.txt Linux kernel documentation>
-- for details.
--
-- >>> pinDirectionValueToBS Low
-- "low\n"
-- >>> pinDirectionValueToBS High
-- "high\n"
pinDirectionValueToBS :: PinValue -> ByteString
pinDirectionValueToBS Low = "low\n"
pinDirectionValueToBS High = "high\n"

-- | When writing a pin's @direction@ attribute in the @sysfs@ GPIO
-- filesystem with a 'ByteString' value, @in\\n@ configures the pin
-- for input, and @out\\n@ configures the pin for output while also
-- initializing the pin's (physical) signal level to a low value.
--
-- Furthermore, you may write @low\\n@ or @high\\n@ to the
-- @direction@ attribute to configure the pin for output and
-- simulataneously set the pin's physical value.
--
-- Therefore, writing a pin's @direction@ attribute affects not only
-- its direction, but also (potentially) its value. This function's
-- return type reflects that possibility.
--
-- See the
-- <https://www.kernel.org/doc/Documentation/gpio/sysfs.txt Linux kernel documentation>
-- for details.
--
-- This function converts a @direction@ attribute value, encoded as a
-- strict 'ByteString', to its corresponding 'PinDirection' and
-- (possible) 'PinValue' pair; or 'Nothing' if the attribute encoding
-- is invalid.
--
-- >>> :set -XOverloadedStrings
-- >>> bsToPinDirection "in\n"
-- Just (In,Nothing)
-- >>> bsToPinDirection "out\n"
-- Just (Out,Just Low)
-- >>> bsToPinDirection "low\n"
-- Just (Out,Just Low)
-- >>> bsToPinDirection "high\n"
-- Just (Out,Just High)
-- >>> bsToPinDirection "foo\n"
-- Nothing
bsToPinDirection :: ByteString -> Maybe (PinDirection, Maybe PinValue)
bsToPinDirection "in\n" = Just (In, Nothing)
bsToPinDirection "out\n" = Just (Out, Just Low)
bsToPinDirection "low\n" = Just (Out, Just Low)
bsToPinDirection "high\n" = Just (Out, Just High)
bsToPinDirection _ = Nothing

-- | Convert a 'SysfsEdge' value to the 'ByteString' value expected by
-- a pin's @edge@ attribute in the @sysfs@ GPIO filesystem.
--
-- >>> sysfsEdgeToBS None
-- "none\n"
-- >>> sysfsEdgeToBS Rising
-- "rising\n"
-- >>> sysfsEdgeToBS Falling
-- "falling\n"
-- >>> sysfsEdgeToBS Both
-- "both\n"
sysfsEdgeToBS :: SysfsEdge -> ByteString
sysfsEdgeToBS None = "none\n"
sysfsEdgeToBS Rising = "rising\n"
sysfsEdgeToBS Falling = "falling\n"
sysfsEdgeToBS Both = "both\n"

-- | Inverse of 'sysfsEdgeToBS'.
--
-- >>> :set -XOverloadedStrings
-- >>> bsToSysfsEdge "none\n"
-- Just None
-- >>> bsToSysfsEdge "rising\n"
-- Just Rising
-- >>> bsToSysfsEdge "falling\n"
-- Just Falling
-- >>> bsToSysfsEdge "both\n"
-- Just Both
-- >>> bsToSysfsEdge "foo\n"
-- Nothing
bsToSysfsEdge :: ByteString -> Maybe SysfsEdge
bsToSysfsEdge "none\n" = Just None
bsToSysfsEdge "rising\n" = Just Rising
bsToSysfsEdge "falling\n" = Just Falling
bsToSysfsEdge "both\n" = Just Both
bsToSysfsEdge _ = Nothing

-- | Convert a 'PinValue' to the 'ByteString' value expected by a
-- pin's @value@ attribute in the @sysfs@ GPIO filesystem.
--
-- >>> pinValueToBS Low
-- "0\n"
-- >>> pinValueToBS High
-- "1\n"
pinValueToBS :: PinValue -> ByteString
pinValueToBS Low = "0\n"
pinValueToBS High = "1\n"

-- | Convert a @value@ attribute value, encoded as a strict
-- 'ByteString', to its corresponding 'PinValue'.
--
-- Note that the @sysfs@ @value@ attribute is quite liberal: a
-- 'ByteString' value of @0\\n@ will set the pin's (logical) signal
-- level to low, but any other (non-empty) 'ByteString' value will set
-- it to high.
--
-- >>> :set -XOverloadedStrings
-- >>> bsToPinValue "0\n"
-- Just Low
-- >>> bsToPinValue "1\n"
-- Just High
-- >>> bsToPinValue "high\n"
-- Just High
-- >>> bsToPinValue "low\n" -- nota bene!
-- Just High
-- >>> bsToPinValue "foo\n"
-- Just High
-- >>> bsToPinValue ""
-- Nothing
bsToPinValue :: ByteString -> Maybe PinValue
bsToPinValue "0\n" = Just Low
bsToPinValue bs
  | bs == BS.empty = Nothing
  | otherwise = Just High

-- | Convert a 'Bool' to the 'ByteString' value expected by a pin's
-- @active_low@ attribute in the @sysfs@ GPIO filesystem.
--
-- >>> activeLowToBS False
-- "0\n"
-- >>> activeLowToBS True
-- "1\n"
activeLowToBS :: Bool -> ByteString
activeLowToBS False = "0\n"
activeLowToBS True = "1\n"

-- | Convert an @active_low@ attribute value, encoded as a strict
-- 'ByteString', to its corresponding 'Bool' value.
--
-- Note that the @sysfs@ @active_low@ attribute is quite liberal: a
-- 'ByteString' value of @0\\n@ returns 'False' and any other
-- (non-empty) 'ByteString' value returns 'True'.
--
-- >>> :set -XOverloadedStrings
-- >>> bsToActiveLow "0\n"
-- Just False
-- >>> bsToActiveLow "1\n"
-- Just True
-- >>> bsToActiveLow "high\n"
-- Just True
-- >>> bsToActiveLow "low\n" -- nota bene!
-- Just True
-- >>> bsToActiveLow "foo\n"
-- Just True
-- >>> bsToActiveLow ""
-- Nothing
bsToActiveLow :: ByteString -> Maybe Bool
bsToActiveLow "0\n" = Just False
bsToActiveLow bs
  | bs == BS.empty = Nothing
  | otherwise = Just True

-- | Convert an 'Int' to a decimal ASCII encoding in a strict
-- 'ByteString'.
--
-- >>> intToBS 37
-- "37"
intToBS :: Int -> ByteString
intToBS = toS . toLazyByteString . intDec

-- | Convert a strict decimal ASCII 'ByteString' encoding of an
-- integer to an 'Int' (maybe). If there are any extraneous trailing
-- characters after the decimal ASCII encoding, other than a single
-- newline character, this is treated as a failure (unlike
-- 'C8.readInt', which returns the remaining string).
--
-- >>> :set -XOverloadedStrings
-- >>> bsToInt "37"
-- Just 37
-- >>> bsToInt "37\n"
-- Just 37
-- >>> bsToInt "37abc"
-- Nothing
-- >>> bsToInt "37 a"
-- Nothing
bsToInt :: ByteString -> Maybe Int
bsToInt = go . C8.readInt
  where
    go :: Maybe (Int, ByteString) -> Maybe Int
    go (Just (n, bs))
      | bs == BS.empty = Just n
      | bs == "\n" = Just n
      | otherwise = Nothing
    go _ = Nothing