#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011  Brian Langenberger

#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.

#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.

#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA


from audiotools import (AudioFile, InvalidFile, PCMReader, Con,
                        transfer_data, InvalidFormat,
                        __capped_stream_reader__, BUFFER_SIZE,
                        FILENAME_FORMAT, EncodingError, DecodingError,
                        ChannelMask)
import audiotools.pcm
import gettext

gettext.install("audiotools", unicode=True)


class InvalidAU(InvalidFile):
    pass


#######################
#Sun AU
#######################


class AuReader(PCMReader):
    """A subclass of PCMReader for reading Sun AU file contents."""

    def __init__(self, au_file, data_size,
                 sample_rate, channels, channel_mask, bits_per_sample):
        """au_file is a file, data_size is an integer byte count.

        sample_rate, channels, channel_mask and bits_per_sample are ints.
        """

        PCMReader.__init__(self,
                           file=au_file,
                           sample_rate=sample_rate,
                           channels=channels,
                           channel_mask=channel_mask,
                           bits_per_sample=bits_per_sample)
        self.data_size = data_size

    def read(self, bytes):
        """Try to read a pcm.FrameList of size "bytes"."""

        #align bytes downward if an odd number is read in
        bytes -= (bytes % (self.channels * self.bits_per_sample / 8))
        bytes = max(bytes, self.channels * self.bits_per_sample / 8)
        pcm_data = self.file.read(bytes)
        if ((len(pcm_data) == 0) and (self.data_size > 0)):
            raise IOError("data ends prematurely")
        else:
            self.data_size -= len(pcm_data)

        try:
            return audiotools.pcm.FrameList(pcm_data,
                                            self.channels,
                                            self.bits_per_sample,
                                            True,
                                            True)
        except ValueError:
            raise IOError("data ends prematurely")


class AuAudio(AudioFile):
    """A Sun AU audio file."""

    SUFFIX = "au"
    NAME = SUFFIX

    AU_HEADER = Con.Struct('header',
                           Con.Const(Con.String('magic_number', 4), '.snd'),
                           Con.UBInt32('data_offset'),
                           Con.UBInt32('data_size'),
                           Con.UBInt32('encoding_format'),
                           Con.UBInt32('sample_rate'),
                           Con.UBInt32('channels'))

    def __init__(self, filename):
        AudioFile.__init__(self, filename)

        try:
            f = file(filename, 'rb')
        except IOError, msg:
            raise InvalidAU(str(msg))
        try:
            header = AuAudio.AU_HEADER.parse_stream(f)

            if (header.encoding_format not in (2, 3, 4)):
                raise InvalidFile(_(u"Unsupported Sun AU encoding format"))

            self.__bits_per_sample__ = {2: 8, 3: 16, 4: 24}[
                header.encoding_format]
            self.__channels__ = header.channels
            self.__sample_rate__ = header.sample_rate
            self.__total_frames__ = header.data_size / \
                (self.__bits_per_sample__ / 8) / \
                self.__channels__
            self.__data_offset__ = header.data_offset
            self.__data_size__ = header.data_size
        except Con.ConstError:
            raise InvalidFile(_(u"Invalid Sun AU header"))
        except Con.FieldError:
                raise InvalidAU(_(u"Invalid Sun AU header"))

    @classmethod
    def is_type(cls, file):
        """Returns True if the given file object describes this format.

        Takes a seekable file pointer rewound to the start of the file."""

        return file.read(4) == ".snd"

    def lossless(self):
        """Returns True."""

        return True

    def bits_per_sample(self):
        """Returns an integer number of bits-per-sample this track contains."""

        return self.__bits_per_sample__

    def channels(self):
        """Returns an integer number of channels this track contains."""

        return self.__channels__

    def channel_mask(self):
        """Returns a ChannelMask object of this track's channel layout."""

        if (self.channels() <= 2):
            return ChannelMask.from_channels(self.channels())
        else:
            return ChannelMask(0)

    def sample_rate(self):
        """Returns the rate of the track's audio as an integer number of Hz."""

        return self.__sample_rate__

    def total_frames(self):
        """Returns the total PCM frames of the track as an integer."""

        return self.__total_frames__

    def to_pcm(self):
        """Returns a PCMReader object containing the track's PCM data."""

        f = file(self.filename, 'rb')
        f.seek(self.__data_offset__, 0)

        return AuReader(au_file=f,
                        data_size=self.__data_size__,
                        sample_rate=self.sample_rate(),
                        channels=self.channels(),
                        channel_mask=int(self.channel_mask()),
                        bits_per_sample=self.bits_per_sample())

    @classmethod
    def from_pcm(cls, filename, pcmreader, compression=None):
        """Encodes a new file from PCM data.

        Takes a filename string, PCMReader object
        and optional compression level string.
        Encodes a new audio file from pcmreader's data
        at the given filename with the specified compression level
        and returns a new AuAudio object."""

        if (pcmreader.bits_per_sample not in (8, 16, 24)):
            raise InvalidFormat(
                _(u"Unsupported bits per sample %s") % (
                    pcmreader.bits_per_sample))

        bytes_per_sample = pcmreader.bits_per_sample / 8

        header = Con.Container(magic_number='.snd',
                               data_offset=0,
                               data_size=0,
                               encoding_format={8: 2, 16: 3, 24: 4}[
                pcmreader.bits_per_sample],
                               sample_rate=pcmreader.sample_rate,
                               channels=pcmreader.channels)

        try:
            f = file(filename, 'wb')
        except IOError, err:
            raise EncodingError(str(err))
        try:
            #send out a dummy header
            f.write(AuAudio.AU_HEADER.build(header))
            header.data_offset = f.tell()

            #send our big-endian PCM data
            #d will be a list of ints, so we can't use transfer_data
            try:
                framelist = pcmreader.read(BUFFER_SIZE)
                while (len(framelist) > 0):
                    bytes = framelist.to_bytes(True, True)
                    f.write(bytes)
                    header.data_size += len(bytes)
                    framelist = pcmreader.read(BUFFER_SIZE)
            except (IOError, ValueError), err:
                cls.__unlink__(filename)
                raise EncodingError(str(err))
            except Exception, err:
                cls.__unlink__(filename)
                raise err

            #send out a complete header
            f.seek(0, 0)
            f.write(AuAudio.AU_HEADER.build(header))
        finally:
            f.close()

        try:
            pcmreader.close()
        except DecodingError, err:
            raise EncodingError(err.error_message)

        return AuAudio(filename)

    @classmethod
    def track_name(cls, file_path, track_metadata=None, format=None,
                   suffix=None):
        """Constructs a new filename string.

        Given a plain string to an existing path,
        a MetaData-compatible object (or None),
        a UTF-8-encoded Python format string
        and an ASCII-encoded suffix string (such as "mp3")
        returns a plain string of a new filename with format's
        fields filled-in and encoded as FS_ENCODING.
        Raises UnsupportedTracknameField if the format string
        contains invalid template fields."""

        if (format is None):
            format = "track%(track_number)2.2d.au"
        return AudioFile.track_name(file_path, track_metadata, format,
                                    suffix=cls.SUFFIX)
