# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A file-like object that decrypts the data it reads.

It reads the ciphertext from a given other file-like object, and decrypts it.
"""

import io
from typing import BinaryIO

from tink import core
from tink.cc.pybind import tink_bindings
from tink.streaming_aead import _file_object_adapter


class RawDecryptingStream(io.RawIOBase):
  """A file-like object which decrypts reads from an underlying object.

  It reads the ciphertext from the wrapped file-like object, and decrypts it.
  """

  def __init__(self, stream_aead: tink_bindings.StreamingAead,
               ciphertext_source: BinaryIO, associated_data: bytes, *,
               close_ciphertext_source: bool):
    """Create a new RawDecryptingStream.

    Args:
      stream_aead: C++ StreamingAead primitive from which a C++ DecryptingStream
        will be obtained.
      ciphertext_source: A readable file-like object from which ciphertext bytes
        will be read.
      associated_data: The associated data to use for decryption.
      close_ciphertext_source: Whether ciphertext_source should be closed when
        close() is called.
    """
    super().__init__()
    self._ciphertext_source = ciphertext_source
    self._close_ciphertext_source = close_ciphertext_source
    if not ciphertext_source.readable():
      raise ValueError('ciphertext_source must be readable')
    cc_ciphertext_source = _file_object_adapter.FileObjectAdapter(
        ciphertext_source)
    self._input_stream_adapter = self._get_input_stream_adapter(
        stream_aead, associated_data, cc_ciphertext_source)

  @staticmethod
  @core.use_tink_errors
  def _get_input_stream_adapter(cc_primitive, aad, source):
    """Implemented as a separate method to ensure correct error transform."""
    return tink_bindings.new_cc_decrypting_stream(
        cc_primitive, aad, source)

  @core.use_tink_errors
  def _read_from_input_stream_adapter(self, size: int) -> bytes:
    """Implemented as a separate method to ensure correct error transform."""
    return self._input_stream_adapter.read(size)

  def read(self, size=-1) -> bytes:
    """Read and return up to size bytes, where size is an int.

    It blocks until at least one byte can be returned.

    Args:
      size: Maximum number of bytes to read. As a convenience, if size is
      unspecified or -1, all bytes until EOF are returned.

    Returns:
      Bytes read. If b'' is returned and size was not 0, this indicates EOF.

    Raises:
      TinkError if there was a permanent error.
    """
    if self.closed:  # pylint:disable=using-constant-test
      raise ValueError('read on closed file.')
    if size is None:
      size = -1
    if size < 0:
      return self.readall()
    try:
      # _input_stream_adapter may return an empty string when there is currently
      # no data is available. In Python (in blocking mode), read is expected to
      # block until some data is available.
      # https://docs.python.org/3/library/io.html also mentions a
      # non-blocking mode, but according to https://bugs.python.org/issue13322
      # that mode is not properly implemented and not really used.
      while True:
        data = self._read_from_input_stream_adapter(size)
        if data:
          return data
    except tink_bindings.PythonTinkStreamFinishedException:
      return b''

  def readinto(self, b: bytearray) -> int:
    """Read bytes into a pre-allocated bytes-like object b.

    Args:
      b: Bytes-like object to which data will be read.

    Returns:
      Number of bytes read. It returns 0 if EOF is reached, and None if no data
      is available at the moment.

    Raises:
      TinkError if there was a permanent error.
    """
    data = self.read(len(b))
    n = len(data)
    b[:n] = data
    return n

  def close(self) -> None:
    """Close the stream. Has no effect on a closed stream."""
    if self.closed:  # pylint:disable=using-constant-test
      return
    if self._close_ciphertext_source:
      self._ciphertext_source.close()
    super().close()

  def readable(self) -> bool:
    """Return True if the stream can be read from."""
    return True

  def write(self, b: bytes) -> int:
    raise io.UnsupportedOperation()
