import io
import os
import mimetypes
import json

from googleapiclient import errors
from googleapiclient.http import MediaIoBaseUpload
from googleapiclient.http import MediaIoBaseDownload
from googleapiclient.http import DEFAULT_CHUNK_SIZE
from functools import wraps

from .apiattr import ApiAttribute
from .apiattr import ApiAttributeMixin
from .apiattr import ApiResource
from .apiattr import ApiResourceList
from .auth import LoadAuth

BLOCK_SIZE = 1024
# Usage: MIME_TYPE_TO_BOM['<Google Drive mime type>']['<download mimetype>'].
MIME_TYPE_TO_BOM = {
    "application/vnd.google-apps.document": {"text/plain": "\ufeff".encode()}
}


class FileNotUploadedError(RuntimeError):
    """Error trying to access metadata of file that is not uploaded."""


class ApiRequestError(IOError):
    def __init__(self, http_error):
        assert isinstance(http_error, errors.HttpError)
        content = json.loads(http_error.content.decode("utf-8"))
        self.error = content.get("error", {}) if content else {}

        # Initialize args for backward compatibility
        super().__init__(http_error)

    def GetField(self, field):
        """Returns the `field` from the first error"""
        return self.error.get("errors", [{}])[0].get(field, "")


class FileNotDownloadableError(RuntimeError):
    """Error trying to download file that is not downloadable."""


def LoadMetadata(decoratee):
    """Decorator to check if the file has metadata and fetches it if not.

    :raises: ApiRequestError, FileNotUploadedError
    """

    @wraps(decoratee)
    def _decorated(self, *args, **kwargs):
        if not self.uploaded:
            self.FetchMetadata()
        return decoratee(self, *args, **kwargs)

    return _decorated


class GoogleDriveFileList(ApiResourceList):
    """Google Drive FileList instance.

    Equivalent to Files.list() in Drive APIs.
    """

    def __init__(self, auth=None, param=None):
        """Create an instance of GoogleDriveFileList."""
        super().__init__(auth=auth, metadata=param)

    @LoadAuth
    def _GetList(self):
        """Overwritten method which actually makes API call to list files.

        :returns: list -- list of pydrive2.files.GoogleDriveFile.
        """
        # Teamdrive support
        self["supportsAllDrives"] = True
        self["includeItemsFromAllDrives"] = True

        try:
            self.metadata = (
                self.auth.service.files()
                .list(**dict(self))
                .execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)

        result = []
        for file_metadata in self.metadata["items"]:
            tmp_file = GoogleDriveFile(
                auth=self.auth, metadata=file_metadata, uploaded=True
            )
            result.append(tmp_file)
        return result


class IoBuffer:
    """Lightweight retention of one chunk."""

    def __init__(self, encoding):
        self.encoding = encoding
        self.chunk = None

    def write(self, chunk):
        self.chunk = chunk

    def read(self):
        return (
            self.chunk.decode(self.encoding)
            if self.chunk and self.encoding
            else self.chunk
        )


class MediaIoReadable:
    def __init__(
        self,
        request,
        encoding=None,
        pre_buffer=True,
        remove_prefix=b"",
        chunksize=DEFAULT_CHUNK_SIZE,
    ):
        """File-like wrapper around MediaIoBaseDownload.

        :param pre_buffer: Whether to read one chunk into an internal buffer
        immediately in order to raise any potential errors.
        :param remove_prefix: Bytes prefix to remove from internal pre_buffer.
        :raises: ApiRequestError
        """
        self.done = False
        self._fd = IoBuffer(encoding)
        self.downloader = MediaIoBaseDownload(
            self._fd, request, chunksize=chunksize
        )
        self.size = None
        self._pre_buffer = False
        if pre_buffer:
            self.read()
            if remove_prefix:
                chunk = io.BytesIO(self._fd.chunk)
                GoogleDriveFile._RemovePrefix(chunk, remove_prefix)
                self._fd.chunk = chunk.getvalue()
            self._pre_buffer = True

    def read(self):
        """
        :returns: bytes or str -- chunk (or None if done)
        :raises: ApiRequestError
        """
        if self._pre_buffer:
            self._pre_buffer = False
            return self._fd.read()
        if self.done:
            return None
        try:
            status, self.done = self.downloader.next_chunk()
            self.size = status.total_size
        except errors.HttpError as error:
            raise ApiRequestError(error)
        return self._fd.read()

    def __iter__(self):
        """
        :raises: ApiRequestError
        """
        while True:
            chunk = self.read()
            if chunk is None:
                break
            yield chunk

    def __len__(self):
        return self.size


class GoogleDriveFile(ApiAttributeMixin, ApiResource):
    """Google Drive File instance.

    Inherits ApiResource which inherits dict.
    Can access and modify metadata like dictionary.
    """

    content = ApiAttribute("content")
    uploaded = ApiAttribute("uploaded")
    metadata = ApiAttribute("metadata")

    def __init__(self, auth=None, metadata=None, uploaded=False):
        """Create an instance of GoogleDriveFile.

        :param auth: authorized GoogleAuth instance.
        :type auth: pydrive2.auth.GoogleAuth
        :param metadata: file resource to initialize GoogleDriveFile with.
        :type metadata: dict.
        :param uploaded: True if this file is confirmed to be uploaded.
        :type uploaded: bool.
        """
        ApiAttributeMixin.__init__(self)
        ApiResource.__init__(self)
        self.metadata = {}
        self.dirty = {"content": False}
        self.auth = auth
        self.uploaded = uploaded
        if uploaded:
            self.UpdateMetadata(metadata)
        elif metadata:
            self.update(metadata)
        self.has_bom = True

    def __getitem__(self, key):
        """Overwrites manner of accessing Files resource.

        If this file instance is not uploaded and id is specified,
        it will try to look for metadata with Files.get().

        :param key: key of dictionary query.
        :type key: str.
        :returns: value of Files resource
        :raises: KeyError, FileNotUploadedError
        """
        try:
            return dict.__getitem__(self, key)
        except KeyError as e:
            if self.uploaded:
                raise KeyError(e)
            if self.get("id"):
                self.FetchMetadata()
                return dict.__getitem__(self, key)
            else:
                raise FileNotUploadedError()

    def SetContentString(self, content, encoding="utf-8"):
        """Set content of this file to be a string.

        Creates io.BytesIO instance of utf-8 encoded string.
        Sets mimeType to be 'text/plain' if not specified and file id is not
        set (means that we are uploading this file for the first time).

        :param encoding: The encoding to use when setting the content of this file.
        :type encoding: str
        :param content: content of the file in string.
        :type content: str
        """
        self.content = io.BytesIO(content.encode(encoding))
        if self.get("mimeType") is None and self.get("id") is None:
            self["mimeType"] = "text/plain"

    def SetContentFile(self, filename):
        """Set content of this file from a file.

        Opens the file specified by this method.
        Will be read, uploaded, and closed by Upload() method.
        Sets metadata 'title' and 'mimeType' automatically if not specified and
        the file is uploaded for the first time (id is not set).

        :param filename: name of the file to be uploaded.
        :type filename: str.
        """
        self.content = open(filename, "rb")
        if self.get("id") is None:
            if self.get("title") is None:
                self["title"] = os.path.basename(filename)
            if self.get("mimeType") is None:
                self["mimeType"] = mimetypes.guess_type(filename)[0]

    def GetContentString(
        self, mimetype=None, encoding="utf-8", remove_bom=False
    ):
        """Get content of this file as a string.

        :param mimetype: The mimetype of the content string.
        :type mimetype: str

        :param encoding: The encoding to use when decoding the byte string.
        :type encoding: str

        :param remove_bom: Whether to strip a known BOM.
        :type remove_bom: bool

        :returns: str -- utf-8 decoded content of the file
        :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
        """
        if (
            self.content is None
            or not isinstance(self.content, io.BytesIO)
            or self.has_bom == remove_bom
        ):
            self.FetchContent(mimetype, remove_bom)
        return self.content.getvalue().decode(encoding)

    @LoadAuth
    def GetContentFile(
        self,
        filename,
        mimetype=None,
        remove_bom=False,
        callback=None,
        chunksize=DEFAULT_CHUNK_SIZE,
        acknowledge_abuse=False,
    ):
        """Save content of this file as a local file.

        :param filename: name of the file to write to.
        :type filename: str
        :param mimetype: mimeType of the file.
        :type mimetype: str
        :param remove_bom: Whether to remove the byte order marking.
        :type remove_bom: bool
        :param callback: passed two arguments: (total transferred, file size).
        :type param: callable
        :param chunksize: chunksize in bytes (standard 100 MB(1024*1024*100))
        :type chunksize: int
        :param acknowledge_abuse: Acknowledging the risk and download file
            identified as abusive.
        :type acknowledge_abuse: bool
        :raises: ApiRequestError, FileNotUploadedError
        """
        files = self.auth.service.files()
        file_id = self.metadata.get("id") or self.get("id")
        if not file_id:
            raise FileNotUploadedError()

        def download(fd, request):
            downloader = MediaIoBaseDownload(
                fd, self._WrapRequest(request), chunksize=chunksize
            )
            done = False
            while done is False:
                status, done = downloader.next_chunk()
                if callback:
                    callback(status.resumable_progress, status.total_size)

        with open(filename, mode="w+b") as fd:
            # Should use files.export_media instead of files.get_media if
            # metadata["mimeType"].startswith("application/vnd.google-apps.").
            # But that would first require a slow call to FetchMetadata().
            # We prefer to try-except for speed.
            try:
                download(
                    fd,
                    files.get_media(
                        fileId=file_id, acknowledgeAbuse=acknowledge_abuse
                    ),
                )
            except errors.HttpError as error:
                exc = ApiRequestError(error)
                if (
                    exc.error["code"] != 403
                    or exc.GetField("reason") != "fileNotDownloadable"
                ):
                    raise exc
                mimetype = mimetype or "text/plain"
                fd.seek(0)  # just in case `download()` modified `fd`
                try:
                    download(
                        fd,
                        files.export_media(fileId=file_id, mimeType=mimetype),
                    )
                except errors.HttpError as error:
                    raise ApiRequestError(error)

            if mimetype == "text/plain" and remove_bom:
                fd.seek(0)
                bom = self._GetBOM(mimetype)
                if bom:
                    self._RemovePrefix(fd, bom)

    @LoadAuth
    def GetContentIOBuffer(
        self,
        mimetype=None,
        encoding=None,
        remove_bom=False,
        chunksize=DEFAULT_CHUNK_SIZE,
        acknowledge_abuse=False,
    ):
        """Get a file-like object which has a buffered read() method.

        :param mimetype: mimeType of the file.
        :type mimetype: str
        :param encoding: The encoding to use when decoding the byte string.
        :type encoding: str
        :param remove_bom: Whether to remove the byte order marking.
        :type remove_bom: bool
        :param chunksize: default read()/iter() chunksize.
        :type chunksize: int
        :param acknowledge_abuse: Acknowledging the risk and download file
            identified as abusive.
        :type acknowledge_abuse: bool
        :returns: MediaIoReadable -- file-like object.
        :raises: ApiRequestError, FileNotUploadedError
        """
        files = self.auth.service.files()
        file_id = self.metadata.get("id") or self.get("id")
        if not file_id:
            raise FileNotUploadedError()

        # Should use files.export_media instead of files.get_media if
        # metadata["mimeType"].startswith("application/vnd.google-apps.").
        # But that would first require a slow call to FetchMetadata().
        # We prefer to try-except for speed.
        try:
            request = self._WrapRequest(
                files.get_media(
                    fileId=file_id, acknowledgeAbuse=acknowledge_abuse
                )
            )
            return MediaIoReadable(
                request, encoding=encoding, chunksize=chunksize
            )
        except ApiRequestError as exc:
            if (
                exc.error["code"] != 403
                or exc.GetField("reason") != "fileNotDownloadable"
            ):
                raise exc
            mimetype = mimetype or "text/plain"
            request = self._WrapRequest(
                files.export_media(fileId=file_id, mimeType=mimetype)
            )
            remove_prefix = (
                self._GetBOM(mimetype)
                if mimetype == "text/plain" and remove_bom
                else b""
            )
            return MediaIoReadable(
                request,
                encoding=encoding,
                remove_prefix=remove_prefix,
                chunksize=chunksize,
            )

    @LoadAuth
    def FetchMetadata(self, fields=None, fetch_all=False):
        """Download file's metadata from id using Files.get().

        :param fields: The fields to include, as one string, each entry separated
                       by commas, e.g. 'fields,labels'.
        :type fields: str

        :param fetch_all: Whether to fetch all fields.
        :type fetch_all: bool

        :raises: ApiRequestError, FileNotUploadedError
        """
        file_id = self.metadata.get("id") or self.get("id")

        if fetch_all:
            fields = "*"

        if file_id:
            try:
                metadata = (
                    self.auth.service.files()
                    .get(
                        fileId=file_id,
                        fields=fields,
                        # Teamdrive support
                        supportsAllDrives=True,
                    )
                    .execute(http=self.http)
                )
            except errors.HttpError as error:
                raise ApiRequestError(error)
            else:
                self.uploaded = True
                self.UpdateMetadata(metadata)
        else:
            raise FileNotUploadedError()

    @LoadMetadata
    def FetchContent(self, mimetype=None, remove_bom=False):
        """Download file's content from download_url.

        :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
        """
        download_url = self.metadata.get("downloadUrl")
        export_links = self.metadata.get("exportLinks")
        if download_url:
            self.content = io.BytesIO(self._DownloadFromUrl(download_url))
            self.dirty["content"] = False

        elif export_links and export_links.get(mimetype):
            self.content = io.BytesIO(
                self._DownloadFromUrl(export_links.get(mimetype))
            )
            self.dirty["content"] = False

        else:
            raise FileNotDownloadableError(
                "No downloadLink/exportLinks for mimetype found in metadata"
            )

        if mimetype == "text/plain" and remove_bom:
            self._RemovePrefix(
                self.content, MIME_TYPE_TO_BOM[self["mimeType"]][mimetype]
            )
            self.has_bom = not remove_bom

    def Upload(self, param=None):
        """Upload/update file by choosing the most efficient method.

        :param param: additional parameter to upload file.
        :type param: dict.
        :raises: ApiRequestError
        """
        if self.uploaded or self.get("id") is not None:
            if self.dirty["content"]:
                self._FilesUpdate(param=param)
            else:
                self._FilesPatch(param=param)
        else:
            self._FilesInsert(param=param)

    def Trash(self, param=None):
        """Move a file to the trash.

        :raises: ApiRequestError
        """
        self._FilesTrash(param=param)

    def UnTrash(self, param=None):
        """Move a file out of the trash.
        :param param: Additional parameter to file.
        :type param: dict.
        :raises: ApiRequestError
        """
        self._FilesUnTrash(param=param)

    def Delete(self, param=None):
        """Hard-delete a file.

        :param param: additional parameter to file.
        :type param: dict.
        :raises: ApiRequestError
        """
        self._FilesDelete(param=param)

    @LoadAuth
    def Copy(self, target_folder=None, new_title=None, param=None):
        """Creates a copy of this file. Folders cannot be copied.

        :param target_folder: Folder where the file will be copied.
        :type target_folder: GoogleDriveFile, optional
        :param new_title: Name of the new file.
        :type new_title: str, optional
        :param param: addition parameters to pass.
        :type param: dict, optional
        :raises: ApiRequestError
        :return: the copied file
        :rtype: GoogleDriveFile
        """

        if param is None:
            param = {}

        param["fileId"] = self["id"]
        param["supportsAllDrives"] = True
        param["body"] = {}

        if target_folder:
            param["body"]["parents"] = [{"id": target_folder["id"]}]
        param["body"]["title"] = new_title

        new_file = None
        try:
            new_file = (
                self.auth.service.files().copy(**param).execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)

        return GoogleDriveFile(self.auth, new_file)

    def InsertPermission(self, new_permission, param=None):
        """Insert a new permission. Re-fetches all permissions after call.

        :param new_permission: The new permission to insert, please see the
                               official Google Drive API guide on permissions.insert
                               for details.
        :type new_permission: object

        :param param: addition parameters to pass
        :type param: dict

        :return: The permission object.
        :rtype: object
        """
        if param is None:
            param = {}
        param["fileId"] = self.metadata.get("id") or self["id"]
        param["body"] = new_permission
        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            permission = (
                self.auth.service.permissions()
                .insert(**param)
                .execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            self.GetPermissions()  # Update permissions field.

        return permission

    @LoadAuth
    def GetPermissions(self):
        """Get file's or shared drive's permissions.

        For files in a shared drive, at most 100 results will be returned.
        It doesn't paginate and collect all results.

        :return: A list of the permission objects.
        :rtype: object[]
        """
        file_id = self.metadata.get("id") or self.get("id")

        # We can't do FetchMetada call (which would nicely update
        # local metada cache, etc) here since it  doesn't return
        # permissions for the team drive use case.
        permissions = (
            self.auth.service.permissions()
            .list(
                fileId=file_id,
                # Teamdrive support
                supportsAllDrives=True,
            )
            .execute(http=self.http)
        ).get("items")

        if permissions:
            self["permissions"] = permissions
            self.metadata["permissions"] = permissions

        return permissions

    def DeletePermission(self, permission_id):
        """Deletes the permission specified by the permission_id.

        :param permission_id: The permission id.
        :type permission_id: str
        :return: True if it succeeds.
        :rtype: bool
        """
        return self._DeletePermission(permission_id)

    def _WrapRequest(self, request):
        """Replaces request.http with self.http.

        Ensures thread safety. Similar to other places where we call
        `.execute(http=self.http)` to pass a client from the thread local storage.
        """
        if self.http:
            request.http = self.http
        return request

    @LoadAuth
    def _FilesInsert(self, param=None):
        """Upload a new file using Files.insert().

        :param param: additional parameter to upload file.
        :type param: dict.
        :raises: ApiRequestError
        """
        if param is None:
            param = {}
        param["body"] = self.GetChanges()

        # teamdrive support
        param["supportsAllDrives"] = True

        try:
            if self.dirty["content"]:
                param["media_body"] = self._BuildMediaBody()
            metadata = (
                self.auth.service.files()
                .insert(**param)
                .execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            self.uploaded = True
            self.dirty["content"] = False
            self.UpdateMetadata(metadata)

    @LoadAuth
    def _FilesUnTrash(self, param=None):
        """Un-delete (Trash) a file using Files.UnTrash().
        :param param: additional parameter to file.
        :type param: dict.
        :raises: ApiRequestError
        """
        if param is None:
            param = {}
        param["fileId"] = self.metadata.get("id") or self["id"]

        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            self.auth.service.files().untrash(**param).execute(http=self.http)
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            if self.metadata:
                self.metadata["labels"]["trashed"] = False
            return True

    @LoadAuth
    def _FilesTrash(self, param=None):
        """Soft-delete (Trash) a file using Files.Trash().

        :param param: additional parameter to file.
        :type param: dict.
        :raises: ApiRequestError
        """
        if param is None:
            param = {}
        param["fileId"] = self.metadata.get("id") or self["id"]

        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            self.auth.service.files().trash(**param).execute(http=self.http)
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            if self.metadata:
                self.metadata["labels"]["trashed"] = True
            return True

    @LoadAuth
    def _FilesDelete(self, param=None):
        """Delete a file using Files.Delete()
        (WARNING: deleting permanently deletes the file!)

        :param param: additional parameter to file.
        :type param: dict.
        :raises: ApiRequestError
        """
        if param is None:
            param = {}
        param["fileId"] = self.metadata.get("id") or self["id"]

        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            self.auth.service.files().delete(**param).execute(http=self.http)
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            return True

    @LoadAuth
    def _FilesUpdate(self, param=None):
        """Update metadata and/or content using Files.Update().

        :param param: additional parameter to upload file.
        :type param: dict.
        :raises: ApiRequestError, FileNotUploadedError
        """
        if param is None:
            param = {}
        param["body"] = self.GetChanges()
        param["fileId"] = self.metadata.get("id") or self.get("id")

        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            if self.dirty["content"]:
                param["media_body"] = self._BuildMediaBody()
            metadata = (
                self.auth.service.files()
                .update(**param)
                .execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            self.uploaded = True
            self.dirty["content"] = False
            self.UpdateMetadata(metadata)

    @LoadAuth
    def _FilesPatch(self, param=None):
        """Update metadata using Files.Patch().

        :param param: additional parameter to upload file.
        :type param: dict.
        :raises: ApiRequestError, FileNotUploadedError
        """
        if param is None:
            param = {}
        param["body"] = self.GetChanges()
        param["fileId"] = self.metadata.get("id") or self.get("id")

        # Teamdrive support
        param["supportsAllDrives"] = True

        try:
            metadata = (
                self.auth.service.files()
                .patch(**param)
                .execute(http=self.http)
            )
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            self.UpdateMetadata(metadata)

    def _BuildMediaBody(self):
        """Build MediaIoBaseUpload to get prepared to upload content of the file.

        Sets mimeType as 'application/octet-stream' if not specified.

        :returns: MediaIoBaseUpload -- instance that will be used to upload content.
        """
        if self.get("mimeType") is None:
            self["mimeType"] = "application/octet-stream"
        return MediaIoBaseUpload(
            self.content, self["mimeType"], resumable=True
        )

    @LoadAuth
    def _DownloadFromUrl(self, url):
        """Download file from url using provided credential.

        :param url: link of the file to download.
        :type url: str.
        :returns: str -- content of downloaded file in string.
        :raises: ApiRequestError
        """
        resp, content = self.http.request(url)
        if resp.status != 200:
            raise ApiRequestError(errors.HttpError(resp, content, uri=url))
        return content

    @LoadAuth
    def _DeletePermission(self, permission_id):
        """Deletes the permission remotely, and from the file object itself.

        :param permission_id: The ID of the permission.
        :type permission_id: str

        :return: The permission
        :rtype: object
        """
        file_id = self.metadata.get("id") or self["id"]
        try:
            self.auth.service.permissions().delete(
                fileId=file_id, permissionId=permission_id
            ).execute()
        except errors.HttpError as error:
            raise ApiRequestError(error)
        else:
            if "permissions" in self and "permissions" in self.metadata:
                permissions = self["permissions"]
                is_not_current_permission = (
                    lambda per: per["id"] == permission_id
                )
                permissions = list(
                    filter(is_not_current_permission, permissions)
                )
                self["permissions"] = permissions
                self.metadata["permissions"] = permissions
            return True

    @staticmethod
    def _GetBOM(mimetype):
        """Based on download mime type (ignores Google Drive mime type)"""
        for bom in MIME_TYPE_TO_BOM.values():
            if mimetype in bom:
                return bom[mimetype]

    @staticmethod
    def _RemovePrefix(file_object, prefix, block_size=BLOCK_SIZE):
        """Deletes passed prefix by shifting content of passed file object by to
        the left. Operation is in-place.

        Args:
          file_object (obj): The file object to manipulate.
          prefix (str): The prefix to insert.
          block_size (int): The size of the blocks which are moved one at a time.
        """
        prefix_length = len(prefix)
        # Detect if prefix exists in file.
        content_start = file_object.read(prefix_length)

        if content_start == prefix:
            # Shift content left by prefix length, by copying 1KiB at a time.
            block_to_write = file_object.read(block_size)
            current_block_length = len(block_to_write)

            # Read and write location in separate variables for simplicity.
            read_location = prefix_length + current_block_length
            write_location = 0

            while current_block_length > 0:
                # Write next block.
                file_object.seek(write_location)
                file_object.write(block_to_write)
                # Set write location to the next block.
                write_location += len(block_to_write)

                # Read next block of input.
                file_object.seek(read_location)
                block_to_write = file_object.read(block_size)
                # Update the current block length and read_location.
                current_block_length = len(block_to_write)
                read_location += current_block_length

            # Truncate the file to its, now shorter, length.
            file_object.truncate(read_location - prefix_length)

    @staticmethod
    def _InsertPrefix(file_object, prefix, block_size=BLOCK_SIZE):
        """Inserts the passed prefix in the beginning of the file, operation is
        in-place.

        Args:
          file_object (obj): The file object to manipulate.
          prefix (str): The prefix to insert.
        """
        # Read the first two blocks.
        first_block = file_object.read(block_size)
        second_block = file_object.read(block_size)
        # Pointer to the first byte of the next block to be read.
        read_location = block_size * 2

        # Write BOM.
        file_object.seek(0)
        file_object.write(prefix)
        # {read|write}_location separated for readability.
        write_location = len(prefix)

        # Write and read block alternatingly.
        while len(first_block):
            # Write first block.
            file_object.seek(write_location)
            file_object.write(first_block)
            # Increment write_location.
            write_location += block_size

            # Move second block into first variable.
            first_block = second_block

            # Read in the next block.
            file_object.seek(read_location)
            second_block = file_object.read(block_size)
            # Increment read_location.
            read_location += block_size
