import io
import os
from nkms.storage.header import Header
from nkms.storage.constants import NONCE_COUNTER_BYTE_SIZE, PADDING_LENGTH
# from nkms.crypto import default_algorithm, symmetric_from_algorithm


class EncryptedFile(object):
    def __init__(self, key, path, header_path):
        """
        Creates an EncryptedFile object that allows the user to encrypt or
        decrypt data into a file defined at `path`.

        An EncryptedFile object actually is composed of two files:
        1) The ciphertext -- This is the chunked and encrypted ciphertext
        2) The header -- This contains the metadata of the ciphertext that
            tells us how to decrypt it, or add more data.

        :param bytes key: Symmetric key to use for encryption/decryption
        :param bytes path: Path of ciphertext file to open
        :param bytes header_path: Path of header file
        """
        cipher = symmetric_from_algorithm(default_algorithm)
        self.cipher = cipher(key)

        # Opens the header file and parses it, if it exists. If not, creates it
        self.header_path = header_path
        self.header_obj = Header(self.header_path)

        self.path = path

        # Always seek the beginning of the file on first open
        self.file_obj = open(self.path, mode='a+b')
        self.file_obj.seek(0)

    @property
    def header(self):
        return self.header_obj.header

    def _read_chunk(self, chunk_size, nonce):
        """
        Reads a chunk and decrypts/authenticates it.

        :param int chunk_size: Size of chunk to read from self.file_obj
        :param bytes nonce: Nonce to use during decryption

        :return: Decrypted/Authenticated chunk
        :rtype: Bytes
        """
        ciphertext = self.file_obj.read(chunk_size + PADDING_LENGTH)
        return self.cipher.decrypt(ciphertext, nonce=nonce)

    def read(self, num_chunks=0):
        """
        Reads num_chunks of encrypted ciphertext and decrypt/authenticate it.

        :param int num_chunks: Number of chunks to read. When set to 0, it will
            read the all the chunks and decrypt them.

        :return: List of decrypted/authenticated ciphertext chunks
        :rtype: List
        """
        if not num_chunks:
            num_chunks = self.header[b'num_chunks']

        chunks = []
        for chunk_num in range(num_chunks):
            nonce = (self.header[b'nonce']
                     + chunk_num.to_bytes(NONCE_COUNTER_BYTE_SIZE,
                                          byteorder='big'))
            chunks.append(self._read_chunk(self.header[b'chunk_size'], nonce))
        return chunks

    def write(self, data):
        """
        Writes encrypted data to self.file_obj.

        :param bytes data: Data to encrypt and write
        :param int chunk_num: Chunk number to start writing at

        :return: Number of chunks written
        :rtype: int
        """
        # Always start writing at the end of the file, never overwrite.
        self.file_obj.seek(0, os.SEEK_END)

        # Start off at the last chunk_num
        chunk_num = self.header[b'num_chunks']

        buf_data = io.BytesIO(data)

        chunks_written = 0
        plaintext = buf_data.read(self.header[b'chunk_size'])
        while len(plaintext) > 0:
            nonce = (self.header[b'nonce']
                     + chunk_num.to_bytes(NONCE_COUNTER_BYTE_SIZE,
                                          byteorder='big'))
            enc_data = self.cipher.encrypt(plaintext, nonce=nonce)
            self.file_obj.write(enc_data.ciphertext)
            chunks_written += 1

            plaintext = buf_data.read(self.header[b'chunk_size'])
            chunk_num += 1
        self.header_obj.update_header({b'num_chunks': chunk_num})
        return chunks_written

    def close(self):
        """
        Writes the header to the filesystem and closes the file_obj.
        """
        self.header_obj.update_header()
        self.file_obj.close()
