# Copyright (C) 2005  Michael Urman
#               2006  Lukas Lalinsky
#               2013  Christoph Reiter
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

import struct

import mutagen
from mutagen._util import insert_bytes, delete_bytes, enum, \
    loadfile, convert_error, read_full
from mutagen._tags import PaddingInfo

from ._util import error, ID3NoHeaderError, ID3UnsupportedVersionError, \
    BitPaddedInt
from ._tags import ID3Tags, ID3Header, ID3SaveConfig
from ._id3v1 import MakeID3v1, find_id3v1


@enum
class ID3v1SaveOptions(object):

    REMOVE = 0
    """ID3v1 tags will be removed"""

    UPDATE = 1
    """ID3v1 tags will be updated but not added"""

    CREATE = 2
    """ID3v1 tags will be created and/or updated"""


class ID3(ID3Tags, mutagen.Metadata):
    """ID3(filething=None)

    A file with an ID3v2 tag.

    If any arguments are given, the :meth:`load` is called with them. If no
    arguments are given then an empty `ID3` object is created.

    ::

        ID3("foo.mp3")
        # same as
        t = ID3()
        t.load("foo.mp3")

    Arguments:
        filething (filething): or `None`

    Attributes:
        version (tuple[int]): ID3 tag version as a tuple
        unknown_frames (list[bytes]): raw frame data of any unknown frames
            found
        size (int): the total size of the ID3 tag, including the header
    """

    __module__ = "mutagen.id3"

    PEDANTIC = True
    """`bool`:

    .. deprecated:: 1.28

        Doesn't have any effect
    """

    filename = None

    def __init__(self, *args, **kwargs):
        self._header = None
        self._version = (2, 4, 0)
        super(ID3, self).__init__(*args, **kwargs)

    @property
    def version(self):
        if self._header is not None:
            return self._header.version
        return self._version

    @version.setter
    def version(self, value):
        self._version = value

    @property
    def f_unsynch(self):
        if self._header is not None:
            return self._header.f_unsynch
        return False

    @property
    def f_extended(self):
        if self._header is not None:
            return self._header.f_extended
        return False

    @property
    def size(self):
        if self._header is not None:
            return self._header.size
        return 0

    def _pre_load_header(self, fileobj):
        # XXX: for aiff to adjust the offset..
        pass

    @convert_error(IOError, error)
    @loadfile()
    def load(self, filething, known_frames=None, translate=True, v2_version=4,
             load_v1=True):
        """Load tags from a filename.

        Args:
            filename (filething): filename or file object to load tag data from
            known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
                IDs to Frame objects
            translate (bool): Update all tags to ID3v2.3/4 internally. If you
                intend to save, this must be true or you have to
                call update_to_v23() / update_to_v24() manually.
            v2_version (int): if update_to_v23 or update_to_v24 get called
                (3 or 4)
            load_v1 (bool): Load tags from ID3v1 header if present. If both
                ID3v1 and ID3v2 headers are present, combine the tags from
                the two, with ID3v2 having precedence.

                .. versionadded:: 1.42

        Example of loading a custom frame::

            my_frames = dict(mutagen.id3.Frames)
            class XMYF(Frame): ...
            my_frames["XMYF"] = XMYF
            mutagen.id3.ID3(filename, known_frames=my_frames)
        """

        fileobj = filething.fileobj

        if v2_version not in (3, 4):
            raise ValueError("Only 3 and 4 possible for v2_version")

        self.unknown_frames = []
        self._header = None
        self._padding = 0

        self._pre_load_header(fileobj)

        try:
            self._header = ID3Header(fileobj)
        except (ID3NoHeaderError, ID3UnsupportedVersionError):
            if not load_v1:
                raise

            frames, offset = find_id3v1(fileobj, v2_version, known_frames)
            if frames is None:
                raise

            self.version = ID3Header._V11
            for v in frames.values():
                if len(self.getall(v.HashKey)) == 0:
                    self.add(v)
        else:
            # XXX: attach to the header object so we have it in spec parsing..
            if known_frames is not None:
                self._header._known_frames = known_frames

            data = read_full(fileobj, self.size - 10)
            remaining_data = self._read(self._header, data)
            self._padding = len(remaining_data)

            if load_v1:
                v1v2_ver = 4 if self.version[1] == 4 else 3
                frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames)
                if frames:
                    for v in frames.values():
                        if len(self.getall(v.HashKey)) == 0:
                            self.add(v)

        if translate:
            if v2_version == 3:
                self.update_to_v23()
            else:
                self.update_to_v24()

    def _prepare_data(self, fileobj, start, available, v2_version, v23_sep,
                      pad_func):

        if v2_version not in (3, 4):
            raise ValueError("Only 3 or 4 allowed for v2_version")

        config = ID3SaveConfig(v2_version, v23_sep)
        framedata = self._write(config)

        needed = len(framedata) + 10

        fileobj.seek(0, 2)
        trailing_size = fileobj.tell() - start

        info = PaddingInfo(available - needed, trailing_size)
        new_padding = info._get_padding(pad_func)
        if new_padding < 0:
            raise error("invalid padding")
        new_size = needed + new_padding

        new_framesize = BitPaddedInt.to_str(new_size - 10, width=4)
        header = struct.pack(
            '>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize)

        data = header + framedata
        assert new_size >= len(data)
        data += (new_size - len(data)) * b'\x00'
        assert new_size == len(data)

        return data

    @convert_error(IOError, error)
    @loadfile(writable=True, create=True)
    def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
             padding=None):
        """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)

        Save changes to a file.

        Args:
            filething (filething):
                Filename to save the tag to. If no filename is given,
                the one most recently loaded is used.
            v1 (ID3v1SaveOptions):
                if 0, ID3v1 tags will be removed.
                if 1, ID3v1 tags will be updated but not added.
                if 2, ID3v1 tags will be created and/or updated
            v2 (int):
                version of ID3v2 tags (3 or 4).
            v23_sep (text):
                the separator used to join multiple text values
                if v2_version == 3. Defaults to '/' but if it's None
                will be the ID3v2v2.4 null separator.
            padding (:obj:`mutagen.PaddingFunction`)

        Raises:
            mutagen.MutagenError

        By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3
        tags, you must call method update_to_v23 before saving the file.

        The lack of a way to update only an ID3v1 tag is intentional.
        """

        f = filething.fileobj

        try:
            header = ID3Header(filething.fileobj)
        except ID3NoHeaderError:
            old_size = 0
        else:
            old_size = header.size

        data = self._prepare_data(
            f, 0, old_size, v2_version, v23_sep, padding)
        new_size = len(data)

        if (old_size < new_size):
            insert_bytes(f, new_size - old_size, old_size)
        elif (old_size > new_size):
            delete_bytes(f, old_size - new_size, new_size)
        f.seek(0)
        f.write(data)

        self.__save_v1(f, v1)

    def __save_v1(self, f, v1):
        tag, offset = find_id3v1(f)
        has_v1 = tag is not None

        f.seek(offset, 2)
        if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \
                v1 == ID3v1SaveOptions.CREATE:
            f.write(MakeID3v1(self))
        else:
            f.truncate()

    @loadfile(writable=True)
    def delete(self, filething=None, delete_v1=True, delete_v2=True):
        """delete(filething=None, delete_v1=True, delete_v2=True)

        Remove tags from a file.

        Args:
            filething (filething): A filename or `None` to use the one used
                when loading.
            delete_v1 (bool): delete any ID3v1 tag
            delete_v2 (bool): delete any ID3v2 tag

        If no filename is given, the one most recently loaded is used.
        """

        delete(filething, delete_v1, delete_v2)
        self.clear()


@convert_error(IOError, error)
@loadfile(method=False, writable=True)
def delete(filething, delete_v1=True, delete_v2=True):
    """Remove tags from a file.

    Args:
        delete_v1 (bool): delete any ID3v1 tag
        delete_v2 (bool): delete any ID3v2 tag

    Raises:
        mutagen.MutagenError: In case deleting failed
    """

    f = filething.fileobj

    if delete_v1:
        tag, offset = find_id3v1(f)
        if tag is not None:
            f.seek(offset, 2)
            f.truncate()

    # technically an insize=0 tag is invalid, but we delete it anyway
    # (primarily because we used to write it)
    if delete_v2:
        f.seek(0, 0)
        idata = f.read(10)
        try:
            id3, vmaj, vrev, flags, insize = struct.unpack('>3sBBB4s', idata)
        except struct.error:
            pass
        else:
            insize = BitPaddedInt(insize)
            if id3 == b'ID3' and insize >= 0:
                delete_bytes(f, insize + 10, 0)


class ID3FileType(mutagen.FileType):
    """ID3FileType(filething, ID3=None, **kwargs)

    An unknown type of file with ID3 tags.

    Args:
        filething (filething): A filename or file-like object
        ID3 (ID3): An ID3 subclass to use for tags.

    Raises:
        mutagen.MutagenError: In case loading the file failed

    Load stream and tag information from a file.

    A custom tag reader may be used in instead of the default
    mutagen.id3.ID3 object, e.g. an EasyID3 reader.
    """

    __module__ = "mutagen.id3"

    ID3 = ID3

    class _Info(mutagen.StreamInfo):
        length = 0

        def __init__(self, fileobj, offset):
            pass

        @staticmethod
        def pprint():
            return u"Unknown format with ID3 tag"

    @staticmethod
    def score(filename, fileobj, header_data):
        return header_data.startswith(b"ID3")

    def add_tags(self, ID3=None):
        """Add an empty ID3 tag to the file.

        Args:
            ID3 (ID3): An ID3 subclass to use or `None` to use the one
                that used when loading.

        A custom tag reader may be used in instead of the default
        `ID3` object, e.g. an `mutagen.easyid3.EasyID3` reader.
        """

        if ID3 is None:
            ID3 = self.ID3
        if self.tags is None:
            self.ID3 = ID3
            self.tags = ID3()
        else:
            raise error("an ID3 tag already exists")

    @loadfile()
    def load(self, filething, ID3=None, **kwargs):
        # see __init__ for docs

        fileobj = filething.fileobj

        if ID3 is None:
            ID3 = self.ID3
        else:
            # If this was initialized with EasyID3, remember that for
            # when tags are auto-instantiated in add_tags.
            self.ID3 = ID3

        try:
            self.tags = ID3(fileobj, **kwargs)
        except ID3NoHeaderError:
            self.tags = None

        if self.tags is not None:
            try:
                offset = self.tags.size
            except AttributeError:
                offset = None
        else:
            offset = None

        self.info = self._Info(fileobj, offset)
