-- | EEPROMex library for arduino-copilot.
--
-- This module is designed to be imported qualified.
--
-- This is an interface to the C EEPROMex library, which will need to be
-- installed in your Arduino development environment.
-- https://playground.arduino.cc/Code/EEPROMex/

{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}

module Copilot.Arduino.Library.EEPROMex (
        -- * Configuration
        maxAllowedWrites,
        memPool,
        StartAddress(..),
        EndAddress(..),
        EEPROMable,
        -- * Single values
        alloc,
        alloc',
        Location,
        -- * Ranges
        Range,
        RangeIndex,
        allocRange,
        sweepRange,
        sweepRange',
        RangeWrites(..),
) where

import Copilot.Arduino
import Copilot.Arduino.Internals
import Control.Monad.Writer
import Data.Proxy

-- | Set the maximum number of writes to EEPROM that can be made while
-- the Arduino is running. When too many writes have been made,
-- it will no longer write to the EEPROM, and will send warning
-- messages to the serial port.
--
-- This is a strongly encouraged safety measure to use, because the
-- Arduino's EEPROM can only endure around 100,000 writes, and
-- a Sketch that's constantly writing to EEPROM, without a well chosen 
-- `:@` rate limit or `delay`, could damage your hardware in just a few
-- minutes.
--
-- Note that this module uses EEPROMex's update facility for all writes
-- to EEPROM. That avoids writing a byte when its current value is
-- the same as what needed to be written. That prevents excessive wear
-- in some cases, but you should still use maxAllowedWrites too.
--
-- For this to work, CPPFLAGS needs to include "-D_EEPROMEX_DEBUG" when the
-- EEPROMex C library gets built. When you use this, it generates C
-- code that makes sure that is enabled.
maxAllowedWrites :: Word16 -> Sketch ()
maxAllowedWrites n = tell [(return (), f)]
  where
        f = mempty
                { earlySetups =
                        [ CLine "#ifdef _EEPROMEX_DEBUG"
                        , CLine $ "EEPROM.setMaxAllowedWrites(" <> show n <> ");"
                        , CLine "#else"
                        , CLine "#error \"maxAllowedWrites cannot be checked because _EEPROMEX_DEBUG is not set.\""
                        , CLine "#endif"
                        ]
                , defines = [ includeCLine ]
                }

-- | The address of the first byte of the EEPROM that will be allocated
-- by `alloc`. The default is to start allocation from 0. 
--
-- Picking a different StartAddress can avoid overwriting the 
-- part of the EEPROM that is used by some other program.
newtype StartAddress = StartAddress Word16
        deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real)

-- | The address of the last byte of the EEPROM that can be allocated
-- by `alloc`. The default is to allow allocating 512 bytes.
--
-- Modules for particular boards, such as Copilot.Arduino.Library.Uno,
-- each define a sizeOfEEPROM value, so to use the entire EEPROM, use:
--
-- > EndAddress sizeOfEEPROM
newtype EndAddress = EndAddress Word16
        deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real)

-- | Configure the EEPROM memory pool that the program can use.
--
-- > EEPROM.memPool 0 (EndAddress sizeOfEEPROM)
memPool :: StartAddress -> EndAddress -> Sketch ()
memPool (StartAddress start) (EndAddress end) = tell [(return (), f)]
  where
        f = mempty
                -- setMemPool() has to come before any
                -- getAddress(), so do it in earlySetups.
                { earlySetups =
                        [ CLine $ "EEPROM.setMemPool("
                                <> show start
                                <> ", "
                                <> show end
                                <> ");"
                        ]
                , defines = [ includeCLine ]
                }

-- | Allocates a location in the EEPROM. 
--
-- Two things are returned; first a `Behavior` which contains a value
-- read from the EEPROM at boot; and secondly a `Location` that can be
-- written to later on.
--
-- Here's an example of using it, to remember the maximum value read from
-- a1, persistently across power cycles.
--
-- > EEPROM.maxAllowedWrites 100
-- > (bootval, eepromloc) <- EEPROM.alloc
-- > currval <- input a1 :: Sketch (Behavior ADC)
-- > let maxval = [maxBound] ++ if currval > bootval then currval else bootval
-- > eepromloc =: currval @: (currval > maxval)
-- > delay =: MilliSeconds (constant 10000)
--
-- Of course, the EEPROM could already contain any value at the allocated
-- location to start with (either some default value or something written
-- by another program). So you'll need to first boot up a sketch that zeros
-- it out before using the sketch above. Here's a simple sketch that zeros
-- two values:
--
-- > EEPROM.maxAllowedWrites 100
-- > (_, eepromloc1) <- EEPROM.alloc
-- > (_, eepromloc2) <- EEPROM.alloc
-- > eepromloc1 =: constant (0 :: ADC) :@ firstIteration
-- > eepromloc2 =: constant (0 :: Word16) :@ firstIteration
-- 
-- `alloc` can be used as many times as you want, storing multiple values
-- in the EEPROM, up to the limit of the size of the EEPROM.
-- (Which is not statically checked.)
--
-- Do note that the EEPROM layout is controlled by the order of calls to
-- `alloc`, so take care when reordering or deleting calls.
alloc :: forall t. (EEPROMable t) => Sketch (Behavior t, Location t)
alloc = alloc' (factoryValue (Proxy @t))

-- | Same as `alloc'`, but with a value which will be used
-- as the EEPROM's boot value when interpreting the Sketch,
-- instead of the default of acting as if all bits of the EEPROM are
-- set.
alloc' :: forall t. (EEPROMable t) => t -> Sketch (Behavior t, Location t)
alloc' interpretval = do
        i <- getUniqueId
        let addrvarname = "eeprom_address" <> show i
        let bootvarname = "eeprom_boot_val" <> show i
        let writername = "eeprom_write" <> show i
        let proxy = Proxy @t
        bootval <- mkInput $ InputSource
                { defineVar =
                        [ includeCLine
                        , CLine $ "int " <> addrvarname <> ";"
                        , CLine $ showCType proxy <> " " <> bootvarname <> ";"
                        , CLine $ "void " <> writername
                                <> "(" <> showCType proxy <> " value) {"
                        , CLine $ "  EEPROM." <> writeValue proxy
                                <> "(" <> addrvarname <> ", value);"
                        , CLine "}"
                        ]
                , setupInput =
                        [ CLine $ addrvarname <>
                                " = EEPROM.getAddress(sizeof("
                                <> showCType proxy <> "));"
                        , CLine $ bootvarname <>
                                " = EEPROM."
                                <> readValue proxy <> "(" <> addrvarname <> ");"
                        ]
                , readInput = []
                , inputStream = extern bootvarname (Just (repeat interpretval))
                , inputPinmode = mempty
                }
        return (bootval, Location writername)

data Location t = Location String

instance EEPROMable t => Output (Location t) (Event () (Stream t)) where
        Location writername =: (Event v c) =
                tell [(trigger writername c [arg v], mempty)]

-- | A range of values in the EEPROM.
data Range t = Range
        { rangeStart :: Location (Range t)
        , rangeSize :: Word16 -- ^ number of `t` values in the Range
        }

-- | Allocates a Range in the EEPROM, which stores the specified number of
-- items of some type.
--
-- This is an example of using a range of the EEPROM as a ring buffer,
-- to store the last 100 values read from a1.
--
-- > EEPROM.maxAllowedWrites 1000
-- > range <- EEPROM.allocRange 100 :: Sketch (EEPROM.Range ADC)
-- > currval <- input a1 :: Sketch (Behavior ADC)
-- > range =: EEPROM.sweepRange 0 currval
-- > delay =: MilliSeconds (constant 10000)
--
-- `allocRange` can be used as many times as you want, and combined with
-- uses of `alloc`, up to the size of the EEPROM. 
-- (Which is not statically checked.)
--
-- Do note that the EEPROM layout is controlled by the order of calls to
-- `allocRange` and `alloc`, so take care when reordering or deleting calls.
allocRange :: forall t. (EEPROMable t) => Word16 -> Sketch (Range t)
allocRange sz = do
        i <- getUniqueId
        let startaddrvarname = "eeprom_range_address" <> show i
        let writername = "eeprom_range_write" <> show i
        let proxy = Proxy @t
        let f = Framework
                { defines =
                        [ includeCLine
                        , CLine $ "int " <> startaddrvarname <> ";"
                        , CLine $ "void " <> writername
                                <> "(" <> showCType proxy <> " value"
                                <> ", " <> showCType (Proxy @Word16) <> " offset"
                                <> ") {"
                        , CLine $ "  EEPROM." <> writeValue proxy
                                <> "(" <> startaddrvarname
                                        <> " + offset*sizeof("
                                                <> showCType proxy
                                                <> ")"
                                <> ", value);"
                        , CLine "}"
                        ]
                , setups =
                        [ CLine $ startaddrvarname <> " = EEPROM.getAddress"
                                <> "(sizeof(" <> showCType proxy <> ")"
                                <> " * " <> show sz
                                <> ");"
                        ]
                , earlySetups = []
                , pinmodes = mempty
                , loops = mempty
                }
        tell [(return (), f)]
        return (Range (Location writername) sz)

-- | Description of writes made to a Range.
--
-- This can be turned into an `Event` by using `@:` with it, and that's
-- what the Behavior Bool is needed for. Consider this example,
-- that ignores the Behavior Bool, and just uses a counter for the
-- `RangeIndex`:
--
-- > range =: EEPROM.RangeWrites (\_ -> counter) (constant 0) @: blinking
--
-- The use of `@:` `blinking` makes an `Event` that only occurs on every other
-- iteration of the Sketch. But, the counter increases on every
-- iteration of the Sketch. So that will only write to every other value
-- in the `Range`.
--
-- The Behavior Bool is only True when an event occurs, and so
-- it can be used to avoid incrementing the counter otherwise. See
--`sweepRange'` for an example.
data RangeWrites t = RangeWrites
        (Behavior Bool -> Behavior RangeIndex)
        (Behavior t)

-- | An index into a Range. 0 is the first value in the Range.
--
-- Indexes larger than the size of the Range will not overflow it,
-- instead they loop back to the start of the Range.
type RangeIndex = Word16

instance EEPROMable t => Output (Range t) (RangeWrites t) where
        (=:) = writeRange true

instance EEPROMable t => Output (Range t) (Event () (RangeWrites t)) where
        range =: Event ws c =
                writeRange c range ws

instance EEPROMable t => IsBehavior (RangeWrites t) where
        (@:) = Event

type instance BehaviorToEvent (RangeWrites t) = Event () (RangeWrites t)

writeRange :: EEPROMable t => Behavior Bool -> Range t -> RangeWrites t -> Sketch()
writeRange c range (RangeWrites idx v) =
        tell [(trigger writername c [arg idx', arg v], mempty)]
  where
        Location writername = rangeStart range
        idx' = idx c `mod` constant (rangeSize range)

-- | Treat the range as a ring buffer, and starting with the specified
-- `RangeIndex`, sweep over the range writing values from the
-- `Behavior`.
sweepRange :: RangeIndex -> Behavior t -> RangeWrites t
sweepRange start = RangeWrites (sweepRange' start)

-- | This is actually just a simple counter that increments on each write
-- to the range. That's sufficient, because writes that overflow the
-- end of the range wrap back to the start.
sweepRange' :: RangeIndex -> Behavior Bool -> Behavior RangeIndex
sweepRange' start c = cnt
  where
        cnt = [start] ++ rest
        rest = if c then cnt + 1 else cnt

class (ShowCType t, Typed t) => EEPROMable t where
        readValue :: Proxy t -> String
        writeValue :: Proxy t -> String
        factoryValue :: Proxy t -> t
        -- ^ The EEPROM comes from the factory with all bits set,
        -- this follows suite. Eg, maxBound for unsigned ints,
        -- minBound for signed ints since the sign bit being set
        -- makes it negative, and NaN for floats.

-- | This instance is not efficient; a whole byte is read/written
-- rather than a single bit.
instance EEPROMable Bool where
        readValue _ = "readByte"
        writeValue _ = "updateByte"
        factoryValue _ = True

instance EEPROMable Int8 where
        readValue _ = "readByte"
        writeValue _ = "updateByte"
        factoryValue _ = minBound

instance EEPROMable Int16 where
        readValue _ = "readInt"
        writeValue _ = "updateInt"
        factoryValue _= minBound

instance EEPROMable Int32 where
        readValue _ = "readLong"
        writeValue _ = "updateLong"
        factoryValue _= minBound

instance EEPROMable Word8 where
        readValue _ = "readByte"
        writeValue _ = "updateByte"
        factoryValue _ = maxBound

instance EEPROMable Word16 where
        readValue _ = "readInt"
        writeValue _ = "updateInt"
        factoryValue _ = maxBound

instance EEPROMable Word32 where
        readValue _ = "readLong"
        writeValue _ = "updateLong"
        factoryValue _ = maxBound

instance EEPROMable Float where
        readValue _ = "readFloat"
        writeValue _ = "updateFloat"
        factoryValue _ = 0/0 -- NaN

instance EEPROMable Double where
        readValue _ = "readDouble"
        writeValue _ = "updateDOuble"
        factoryValue _ = 0/0 -- NaN

includeCLine :: CLine
includeCLine = CLine "#include <EEPROMex.h>"