# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

from typing import List, Optional, Tuple

# The number of words to accumulate
ACC_LEN = 8

# The maximum of beats we allow between when we get the last word and are told
# that CDC has finished.
MAX_CDC_WAIT = 5


class EdnClient:
    def __init__(self) -> None:
        # An accumulator of 32-bit values that we've read so far. This is
        # non-None if we're in the middle of reading a value from the EDN. This
        # should always have length <= ACC_LEN (sending an extra beat of data
        # causes an error).
        self._acc = None  # type: Optional[List[int]]

        # A counter of the number of beats we've been waiting since self._acc
        # became full before being told that CDC was done. This is None unless
        # self._acc contains a list of ACC_LEN values.
        self._cdc_counter = None  # type: Optional[int]

        # If true, the next transaction that completes will be discarded.
        self._poisoned = False

        # If true, self._poisoned should also be true. In this case, after we
        # discard the next transaction that completes, we should re-try it.
        self._retry = False

        self._fips_err = False
        self._rep_err = False

        self._last_word = None  # type: Optional[int]

    def request(self) -> None:
        '''Start a request if there isn't one pending'''
        if self._acc is None:
            assert self._cdc_counter is None
            self._acc = []
        elif self._poisoned:
            # This is a request when there was a RND fetch pending (from the
            # previous OTBN run), but we now actually want the results.
            self._retry = True

    def poison(self) -> None:
        '''Mark any current request as "poisoned" and clear the retry flag'''
        if self._acc is not None:
            self._poisoned = True
            self._retry = False
            self._fips_err = False
            self._rep_err = False

    def forget(self) -> None:
        '''Clear the retry flag, if set.

        This is used at the end of an operation to model the situation where a
        previous run had started a prefetch that we're still waiting for. In
        the meantime, we started an operation that also requested a prefetch.
        Normally, we'd want to make sure to send out *that* request as soon as
        the first (ignored) result came in. But maybe we've actually finished
        this run as well! In that case, we don't care about the prefetch that
        was pending and should just discard it.
        '''
        self._retry = False

    def take_word(self, word: int, fips_err: bool) -> None:
        '''
        Take a 32-bit data word, which we requested.

        If there is a reset in between an EDN transaction, request flag will
        drop. In that case, incoming word will be ignored. Otherwise, append
        it to the internal `_acc` list.
        '''
        assert 0 <= word < (1 << 32)

        if self._acc is None:
            return
        else:
            assert len(self._acc) < ACC_LEN
            assert self._cdc_counter is None
            # Set the FIPS error for each received word.
            self._fips_err = fips_err | self._fips_err

            # If the length of accumulated words is nonzero we check for
            # repetition between accumulated and received
            self._rep_err = (self._last_word == word) | self._rep_err

            self._acc.append(word)
            self._last_word = word
            if len(self._acc) == ACC_LEN:
                self._cdc_counter = 0

    def edn_reset(self) -> None:
        '''Called on a reset signal on the EDN clock domain'''
        self._acc = None
        self._cdc_counter = None
        self._poisoned = False
        self._retry = False
        self._fips_err = False
        self._rep_err = False
        self._last_word = None

    def cdc_complete(self) -> Tuple[Optional[int], bool, bool, bool]:
        '''Called when CDC completes for a transfer

        This returns a pair (data, retry). In normal operation (where the
        client hasn't been poisoned), data is the 256b word we received and
        retry is False.

        If the client was poisoned then data is None (meaning that we should
        discard the result). In this case, retry might be True, which means
        that we are going to retry the operation.
        '''
        assert self._acc is not None
        assert len(self._acc) == ACC_LEN
        assert self._cdc_counter is not None
        assert self._cdc_counter <= MAX_CDC_WAIT

        poisoned = self._poisoned
        retry = self._retry

        if poisoned:
            data = None
            fips_err = False
            rep_err = False
        else:
            # Assemble the ACC_LEN words into a single integer "little-endian"
            # (so the first word that came in is the bottom 32 bits).
            data = 0
            for word in reversed(self._acc):
                data = (data << 32) | word
            fips_err = self._fips_err
            rep_err = self._rep_err

        self._acc = None
        self._cdc_counter = None
        self._poisoned = False
        self._retry = False
        self._fips_err = False
        self._rep_err = False

        if retry:
            assert poisoned
            self.request()

        return (data, retry, fips_err, rep_err)

    def step(self) -> None:
        '''Called on each main clock cycle. Increment and check CDC counter'''
        if self._cdc_counter is not None:
            assert self._acc is not None
            assert len(self._acc) == ACC_LEN

            self._cdc_counter += 1
            assert self._cdc_counter <= MAX_CDC_WAIT
