-- | This module provides a service for alerts related to battery levels.
module Navi.Services.Battery.Percentage
  ( BatteryPercentageToml,
    toEvent,
  )
where

import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Map qualified as Map
import Navi.Data.NaviNote (NaviNote (MkNaviNote))
import Navi.Data.PollInterval (PollInterval (MkPollInterval))
import Navi.Event.Toml qualified as EventToml
import Navi.Event.Types
  ( AnyEvent (MkAnyEvent),
    ErrorNote,
    Event
      ( MkEvent,
        errorNote,
        name,
        pollInterval,
        raiseAlert,
        repeatEvent,
        serviceType
      ),
    RepeatEvent,
  )
import Navi.Prelude
import Navi.Services.Battery.Percentage.Toml
  ( BatteryPercentageNoteToml,
    BatteryPercentageToml,
    PercentageData (PercentageExact, PercentageRange),
  )
import Navi.Services.Types (ServiceType (BatteryPercentage))
import Pythia.Services.Battery
  ( Battery,
    BatteryApp,
    BatteryStatus (Discharging),
  )

-- | Transforms toml configuration data into an 'AnyEvent'.
toEvent :: (MonadIORef m) => BatteryPercentageToml -> m AnyEvent
toEvent :: forall (m :: Type -> Type).
MonadIORef m =>
BatteryPercentageToml -> m AnyEvent
toEvent BatteryPercentageToml
toml = do
  RepeatEvent PercentageData
repeatEvent <- (Percentage -> PercentageData)
-> Maybe (MultiRepeatEventToml Percentage)
-> m (RepeatEvent PercentageData)
forall (m :: Type -> Type) b a.
(MonadIORef m, Ord b) =>
(a -> b) -> Maybe (MultiRepeatEventToml a) -> m (RepeatEvent b)
EventToml.mMultiRepeatEventTomlToVal Percentage -> PercentageData
PercentageExact (Maybe (MultiRepeatEventToml Percentage)
 -> m (RepeatEvent PercentageData))
-> Maybe (MultiRepeatEventToml Percentage)
-> m (RepeatEvent PercentageData)
forall a b. (a -> b) -> a -> b
$ BatteryPercentageToml
toml BatteryPercentageToml
-> Optic'
     A_Lens
     NoIx
     BatteryPercentageToml
     (Maybe (MultiRepeatEventToml Percentage))
-> Maybe (MultiRepeatEventToml Percentage)
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic'
  A_Lens
  NoIx
  BatteryPercentageToml
  (Maybe (MultiRepeatEventToml Percentage))
#repeatEvent
  ErrorNote
errorNote <- Maybe ErrorNoteToml -> m ErrorNote
forall (m :: Type -> Type).
MonadIORef m =>
Maybe ErrorNoteToml -> m ErrorNote
EventToml.mErrorNoteTomlToVal (Maybe ErrorNoteToml -> m ErrorNote)
-> Maybe ErrorNoteToml -> m ErrorNote
forall a b. (a -> b) -> a -> b
$ BatteryPercentageToml
toml BatteryPercentageToml
-> Optic' A_Lens NoIx BatteryPercentageToml (Maybe ErrorNoteToml)
-> Maybe ErrorNoteToml
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageToml (Maybe ErrorNoteToml)
#errorNote
  let evt :: Event Battery PercentageData
evt = NonEmpty (PercentageData, NaviNote)
-> BatteryApp
-> PollInterval
-> RepeatEvent PercentageData
-> ErrorNote
-> Event Battery PercentageData
mkBatteryEvent NonEmpty (PercentageData, NaviNote)
percentNoteList BatteryApp
app PollInterval
pi RepeatEvent PercentageData
repeatEvent ErrorNote
errorNote
  AnyEvent -> m AnyEvent
forall a. a -> m a
forall (f :: Type -> Type) a. Applicative f => a -> f a
pure (AnyEvent -> m AnyEvent) -> AnyEvent -> m AnyEvent
forall a b. (a -> b) -> a -> b
$ Event Battery PercentageData -> AnyEvent
forall trigger result.
(Ord trigger, Show result, Show trigger) =>
Event result trigger -> AnyEvent
MkAnyEvent Event Battery PercentageData
evt
  where
    percentNoteList :: NonEmpty (PercentageData, NaviNote)
percentNoteList = BatteryPercentageNoteToml -> (PercentageData, NaviNote)
tomlToNote (BatteryPercentageNoteToml -> (PercentageData, NaviNote))
-> NonEmpty BatteryPercentageNoteToml
-> NonEmpty (PercentageData, NaviNote)
forall (f :: Type -> Type) a b. Functor f => (a -> b) -> f a -> f b
<$> BatteryPercentageToml
toml BatteryPercentageToml
-> Optic'
     A_Lens
     NoIx
     BatteryPercentageToml
     (NonEmpty BatteryPercentageNoteToml)
-> NonEmpty BatteryPercentageNoteToml
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic'
  A_Lens
  NoIx
  BatteryPercentageToml
  (NonEmpty BatteryPercentageNoteToml)
#alerts
    app :: BatteryApp
app = BatteryPercentageToml
toml BatteryPercentageToml
-> Optic' A_Lens NoIx BatteryPercentageToml BatteryApp
-> BatteryApp
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageToml BatteryApp
#app
    pi :: PollInterval
pi = PollInterval -> Maybe PollInterval -> PollInterval
forall a. a -> Maybe a -> a
fromMaybe (Natural -> PollInterval
MkPollInterval Natural
30) (BatteryPercentageToml
toml BatteryPercentageToml
-> Optic' A_Lens NoIx BatteryPercentageToml (Maybe PollInterval)
-> Maybe PollInterval
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageToml (Maybe PollInterval)
#pollInterval)
{-# INLINEABLE toEvent #-}

tomlToNote :: BatteryPercentageNoteToml -> (PercentageData, NaviNote)
tomlToNote :: BatteryPercentageNoteToml -> (PercentageData, NaviNote)
tomlToNote BatteryPercentageNoteToml
toml =
  ( PercentageData
percentage,
    Text
-> Maybe Text -> Maybe UrgencyLevel -> Maybe Timeout -> NaviNote
MkNaviNote
      Text
summary
      Maybe Text
forall a. Maybe a
Nothing
      (BatteryPercentageNoteToml
toml BatteryPercentageNoteToml
-> Optic'
     A_Lens NoIx BatteryPercentageNoteToml (Maybe UrgencyLevel)
-> Maybe UrgencyLevel
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageNoteToml (Maybe UrgencyLevel)
#urgency)
      (BatteryPercentageNoteToml
toml BatteryPercentageNoteToml
-> Optic' A_Lens NoIx BatteryPercentageNoteToml (Maybe Timeout)
-> Maybe Timeout
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageNoteToml (Maybe Timeout)
#mTimeout)
  )
  where
    percentage :: PercentageData
percentage = BatteryPercentageNoteToml
toml BatteryPercentageNoteToml
-> Optic' A_Lens NoIx BatteryPercentageNoteToml PercentageData
-> PercentageData
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx BatteryPercentageNoteToml PercentageData
#percentage
    summary :: Text
summary = Text
"Battery Percentage"

-- NOTE: [Battery Percentage Result/Trigger]
--
-- A battery percentage event has result Battery and trigger
-- PercentageData. There are two reasons why these are not the same type.
--
-- 1. We should only raise an alert when the battery status is discharging,
--    hence we need the status. But we do not need to save this in the
--    trigger for repeat-events, as we can assume only discharging percentages
--    were sent.
--
-- 2. We want to store PercentageData, not Percentage. A battery reading
--    corresponds to exactly one percentage, hence Percentage. But the
--    trigger is based on PercentageData, therefore this is what we need to
--    store for correct block-repeat semantics.
--
-- Suppose we have a battery percentage range r = [0, 10] and querying the
-- service yields {discharging, 8}. We will send off a notif, since it is
-- in range.
--
-- Now suppose we read {discharging, 6}. Should we send off a notif? It depends
-- entirely on how repeat-events was configured.
--
--   - If repeats are allowed for this range (either repeat-events = true or
--     repeat-events = [..., 0 ,...]), then we need to know 6 corresponds to an
--     allowed range, hence storing the range [0, 10], not the last value 8.
--
--   - If repeats are not allowed, then we still need to know that 6
--     corresponds to the previous range [0, 10], and block it. Relying on the
--     actual value (6) would produce the wrong result.

mkBatteryEvent ::
  NonEmpty (PercentageData, NaviNote) ->
  BatteryApp ->
  PollInterval ->
  RepeatEvent PercentageData ->
  ErrorNote ->
  Event Battery PercentageData
mkBatteryEvent :: NonEmpty (PercentageData, NaviNote)
-> BatteryApp
-> PollInterval
-> RepeatEvent PercentageData
-> ErrorNote
-> Event Battery PercentageData
mkBatteryEvent NonEmpty (PercentageData, NaviNote)
percentNoteList BatteryApp
batteryProgram PollInterval
pollInterval RepeatEvent PercentageData
repeatEvent ErrorNote
errorNote =
  MkEvent
    { name :: Text
name = Text
"battery-percentage",
      serviceType :: ServiceType Battery
serviceType = BatteryApp -> ServiceType Battery
BatteryPercentage BatteryApp
batteryProgram,
      PollInterval
pollInterval :: PollInterval
pollInterval :: PollInterval
pollInterval,
      raiseAlert :: Battery -> Maybe (PercentageData, NaviNote)
raiseAlert = \Battery
b ->
        -- Though the trigger is based on the range, we want to display the
        -- actual percentage i.e. param b.
        Optic
  An_AffineTraversal
  NoIx
  (Maybe (PercentageData, NaviNote))
  (Maybe (PercentageData, NaviNote))
  (Maybe Text)
  (Maybe Text)
-> Maybe Text
-> Maybe (PercentageData, NaviNote)
-> Maybe (PercentageData, NaviNote)
forall k (is :: IxList) s t a b.
Is k A_Setter =>
Optic k is s t a b -> b -> s -> t
set' (Prism
  (Maybe (PercentageData, NaviNote))
  (Maybe (PercentageData, NaviNote))
  (PercentageData, NaviNote)
  (PercentageData, NaviNote)
forall a b. Prism (Maybe a) (Maybe b) a b
_Just Prism
  (Maybe (PercentageData, NaviNote))
  (Maybe (PercentageData, NaviNote))
  (PercentageData, NaviNote)
  (PercentageData, NaviNote)
-> Optic
     A_Lens
     NoIx
     (PercentageData, NaviNote)
     (PercentageData, NaviNote)
     NaviNote
     NaviNote
-> Optic
     An_AffineTraversal
     NoIx
     (Maybe (PercentageData, NaviNote))
     (Maybe (PercentageData, NaviNote))
     NaviNote
     NaviNote
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
       b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% Optic
  A_Lens
  NoIx
  (PercentageData, NaviNote)
  (PercentageData, NaviNote)
  NaviNote
  NaviNote
forall s t a b. Field2 s t a b => Lens s t a b
_2 Optic
  An_AffineTraversal
  NoIx
  (Maybe (PercentageData, NaviNote))
  (Maybe (PercentageData, NaviNote))
  NaviNote
  NaviNote
-> Optic A_Lens NoIx NaviNote NaviNote (Maybe Text) (Maybe Text)
-> Optic
     An_AffineTraversal
     NoIx
     (Maybe (PercentageData, NaviNote))
     (Maybe (PercentageData, NaviNote))
     (Maybe Text)
     (Maybe Text)
forall k l m (is :: IxList) (js :: IxList) (ks :: IxList) s t u v a
       b.
(JoinKinds k l m, AppendIndices is js ks) =>
Optic k is s t u v -> Optic l js u v a b -> Optic m ks s t a b
% Optic A_Lens NoIx NaviNote NaviNote (Maybe Text) (Maybe Text)
#body) (Text -> Maybe Text
forall a. a -> Maybe a
Just (Text -> Maybe Text) -> Text -> Maybe Text
forall a b. (a -> b) -> a -> b
$ Percentage -> Text
forall a. Display a => a -> Text
display (Percentage -> Text) -> Percentage -> Text
forall a b. (a -> b) -> a -> b
$ Battery
b Battery -> Optic' A_Lens NoIx Battery Percentage -> Percentage
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx Battery Percentage
#percentage)
          (Maybe (PercentageData, NaviNote)
 -> Maybe (PercentageData, NaviNote))
-> Maybe (PercentageData, NaviNote)
-> Maybe (PercentageData, NaviNote)
forall a b. (a -> b) -> a -> b
$ Map PercentageData NaviNote
-> Battery -> Maybe (PercentageData, NaviNote)
lookupPercent Map PercentageData NaviNote
percentNoteMap Battery
b,
      RepeatEvent PercentageData
repeatEvent :: RepeatEvent PercentageData
repeatEvent :: RepeatEvent PercentageData
repeatEvent,
      ErrorNote
errorNote :: ErrorNote
errorNote :: ErrorNote
errorNote
    }
  where
    percentNoteMap :: Map PercentageData NaviNote
percentNoteMap = [(PercentageData, NaviNote)] -> Map PercentageData NaviNote
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList ([(PercentageData, NaviNote)] -> Map PercentageData NaviNote)
-> [(PercentageData, NaviNote)] -> Map PercentageData NaviNote
forall a b. (a -> b) -> a -> b
$ NonEmpty (PercentageData, NaviNote) -> [(PercentageData, NaviNote)]
forall a. NonEmpty a -> [a]
NE.toList NonEmpty (PercentageData, NaviNote)
percentNoteList

lookupPercent :: Map PercentageData NaviNote -> Battery -> Maybe (PercentageData, NaviNote)
lookupPercent :: Map PercentageData NaviNote
-> Battery -> Maybe (PercentageData, NaviNote)
lookupPercent Map PercentageData NaviNote
percentNoteMap Battery
state = case Battery
state Battery
-> Optic' A_Lens NoIx Battery BatteryStatus -> BatteryStatus
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx Battery BatteryStatus
#status of
  -- lookupLE so we can attempt to find ranges as well. Note that this can
  -- behave unexpectedly when ranges overlap. E.g. suppose our map contains
  -- keys:
  --
  --   - [40, 60]
  --   - 50
  --
  -- And we receive 55. Arguably the [40, 60] range _should_ apply here, but
  -- the lookup will find exact 50 first, then fail the equality check.
  -- Handling this would be complicated for little gain, so we make a note
  -- of it here, and advise that percentages should not overlap.
  BatteryStatus
Discharging -> case PercentageData
-> Map PercentageData NaviNote -> Maybe (PercentageData, NaviNote)
forall k v. Ord k => k -> Map k v -> Maybe (k, v)
Map.lookupLE (Percentage -> PercentageData
PercentageExact Percentage
p) Map PercentageData NaviNote
percentNoteMap of
    Maybe (PercentageData, NaviNote)
Nothing -> Maybe (PercentageData, NaviNote)
forall a. Maybe a
Nothing
    -- Found exact key, need to check equality.
    Just (r :: PercentageData
r@(PercentageExact Percentage
q), NaviNote
note)
      -- Exact match, send off note.
      | Percentage
p Percentage -> Percentage -> Bool
forall a. Eq a => a -> a -> Bool
== Percentage
q -> (PercentageData, NaviNote) -> Maybe (PercentageData, NaviNote)
forall a. a -> Maybe a
Just (PercentageData
r, NaviNote
note)
      -- Not a match, do nothing.
      | Bool
otherwise -> Maybe (PercentageData, NaviNote)
forall a. Maybe a
Nothing
    -- Found a range, need to check bounds.
    Just (r :: PercentageData
r@(PercentageRange Percentage
low Percentage
high), NaviNote
note)
      | Percentage
p Percentage -> Percentage -> Bool
forall a. Ord a => a -> a -> Bool
>= Percentage
low Bool -> Bool -> Bool
&& Percentage
p Percentage -> Percentage -> Bool
forall a. Ord a => a -> a -> Bool
< Percentage
high -> (PercentageData, NaviNote) -> Maybe (PercentageData, NaviNote)
forall a. a -> Maybe a
Just (PercentageData
r, NaviNote
note)
      | Bool
otherwise -> Maybe (PercentageData, NaviNote)
forall a. Maybe a
Nothing
  BatteryStatus
_ -> Maybe (PercentageData, NaviNote)
forall a. Maybe a
Nothing
  where
    p :: Percentage
p = Battery
state Battery -> Optic' A_Lens NoIx Battery Percentage -> Percentage
forall k s (is :: IxList) a.
Is k A_Getter =>
s -> Optic' k is s a -> a
^. Optic' A_Lens NoIx Battery Percentage
#percentage