{-# LANGUAGE ApplicativeDo #-}
{-# OPTIONS_GHC -fconstraint-solver-iterations=10 #-}

-- | Circuits and utilities for working with Standard-mode Wishbone circuits.
module Protocols.Experimental.Wishbone.Standard where

import Clash.Prelude
import Data.Bifunctor qualified as B
import Protocols
import Protocols.Experimental.Wishbone
import Prelude hiding (head, not, repeat, (!!), (&&), (||))

-- | Distribute requests amongst N slave circuits
roundrobin ::
  forall n dom addressBits dataBytes.
  ( KnownNat n
  , HiddenClockResetEnable dom
  , KnownNat addressBits
  , KnownNat dataBytes
  , 1 <= n
  ) =>
  Circuit
    (Wishbone dom 'Standard addressBits dataBytes)
    (Vec n (Wishbone dom 'Standard addressBits dataBytes))
roundrobin = Circuit $ \(m2s, s2ms) -> B.first head $ fn (singleton m2s, s2ms)
 where
  Circuit fn = sharedBus selectFn
  selectFn (unbundle -> (mIdx, sIdx, _)) =
    liftA2 (,) mIdx (satSucc SatWrap <$> sIdx)

{- | General-purpose shared-bus with N masters and M slaves.

  A selector signal is used to compute the next M-S pair.
-}
sharedBus ::
  forall n m dom addressBits dataBytes.
  ( KnownNat n
  , KnownNat m
  , HiddenClockResetEnable dom
  , KnownNat addressBits
  , KnownNat dataBytes
  ) =>
  -- | Function used to select which M-S pair should be connected next.
  ( Signal
      dom
      ( Index n
      , Index m
      , Vec n (WishboneM2S addressBits dataBytes)
      ) ->
    Signal dom (Index n, Index m)
  ) ->
  Circuit
    (Vec n (Wishbone dom 'Standard addressBits dataBytes))
    (Vec m (Wishbone dom 'Standard addressBits dataBytes))
sharedBus selectFn = Circuit go
 where
  go (bundle -> m2ss0, bundle -> s2ms0) = (unbundle s2ms1, unbundle m2ss1)
   where
    mIdx0 = regEn (0 :: Index n) acceptIds mIdx1
    sIdx0 = regEn (0 :: Index m) acceptIds sIdx1

    (mIdx1, sIdx1) = unbundle $ selectFn (liftA3 (,,) mIdx0 sIdx0 m2ss0)

    m2s = liftA2 (!!) m2ss0 mIdx0
    s2m = liftA2 (!!) s2ms0 sIdx0

    acceptIds = (not . busCycle <$> m2s) .&&. (not . lock <$> m2s)

    m2ss1 = liftA3 replace sIdx0 m2s $ pure (repeat emptyWishboneM2S)
    s2ms1 = liftA3 replace mIdx0 s2m $ pure (repeat emptyWishboneS2M)

-- | Crossbar switch allowing N masters to be routed dynamically to M slaves.
crossbarSwitch ::
  forall n m dom addressBits dataBytes.
  ( KnownNat n
  , KnownNat m
  , KnownDomain dom
  , KnownNat addressBits
  , KnownNat dataBytes
  ) =>
  Circuit
    ( CSignal dom (Vec n (Index m)) -- route
    , Vec n (Wishbone dom 'Standard addressBits dataBytes) -- masters
    )
    (Vec m (Wishbone dom 'Standard addressBits dataBytes)) -- slaves
crossbarSwitch = Circuit go
 where
  go ((route, bundle -> m2ss0), bundle -> s2ms0) =
    (((), unbundle s2ms1), unbundle m2ss1)
   where
    m2ss1 = scatter @_ @_ @_ @_ @0 (repeat emptyWishboneM2S) <$> route <*> m2ss0
    s2ms1 = gather <$> s2ms0 <*> route

-- | State used to guarantee correct response timing in 'memoryWb'.
data MemoryDelayState = Wait | AckRead
  deriving (Generic, NFDataX)

{- | Memory component circuit using a specific RAM function

  This circuit uses 'Standard' mode and only supports the classic cycle type.
  Because of this, the data rate is limited by the one-cycle delay of the RAM
  function when reading and the inserted stall-cycle.

  The data rate could be increased by using registered feedback cycles or
  by using a pipelined circuit which would eliminate one wait cycle.

  Since the underlying block RAM operates on the whole bitvector, the only
  accepted bus selector value is 'maxBound'. All other bus selector values
  will cause an ERR response.

  TODO create pipelined memory circuit
-}
memoryWb ::
  forall dom addressBits dataBytes.
  ( KnownDomain dom
  , KnownNat addressBits
  , KnownNat dataBytes
  , HiddenClockResetEnable dom
  ) =>
  ( Signal dom (BitVector addressBits) ->
    Signal dom (Maybe (BitVector addressBits, BitVector (dataBytes * 8))) ->
    Signal dom (BitVector (dataBytes * 8))
  ) ->
  Circuit (Wishbone dom 'Standard addressBits dataBytes) ()
memoryWb ram = Circuit go
 where
  go (m2s, ()) = (s2m1, ())
   where
    (readAddr, write, s2m0) = unbundle $ mealy fsm Wait m2s
    s2m1 = (\s2m dat -> s2m{readData = dat}) <$> s2m0 <*> readValue
    readValue = ram readAddr write

  fsm st (m2s :: WishboneM2S addressBits dataBytes)
    -- Manager must be active if we're in this state
    | AckRead <- st = (Wait, (0, Nothing, noS2M{acknowledge = True}))
    -- Stay in Wait for invalid transactions
    | isError = (Wait, (0, Nothing, noS2M{err = True}))
    -- write requests can be ACKed directly
    | isWrite = (Wait, (0, write, noS2M{acknowledge = True}))
    -- For read requests we go to AckRead state
    | isRead = (AckRead, (m2s.addr, Nothing, noS2M))
    | otherwise = (Wait, (0, Nothing, noS2M))
   where
    noS2M = emptyWishboneS2M :: WishboneS2M dataBytes
    managerActive = m2s.busCycle && m2s.strobe
    isError = managerActive && (m2s.busSelect /= maxBound)
    isWrite = managerActive && m2s.writeEnable
    isRead = managerActive && not (m2s.writeEnable)
    write = Just (m2s.addr, m2s.writeData)