/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.
 */
package org.modeshape.sequencer.audio;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioHeader;
import org.jaudiotagger.audio.asf.AsfFileReader;
import org.jaudiotagger.audio.flac.FlacFileReader;
import org.jaudiotagger.audio.mp3.MP3AudioHeader;
import org.jaudiotagger.audio.mp3.MP3FileReader;
import org.jaudiotagger.audio.mp4.Mp4AudioHeader;
import org.jaudiotagger.audio.mp4.Mp4FileReader;
import org.jaudiotagger.audio.ogg.OggFileReader;
import org.jaudiotagger.audio.wav.WavFileReader;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.datatype.Artwork;
import org.modeshape.common.util.IoUtil;

/**
 * Utility for extracting metadata from audio files.
 * 
 * @since 5.1
 */
public class AudioMetadata {

    /**
     * Return value of {@link #getFormat()} for MP3 streams. AudioMetadata can extract metadata and audio information from
     * MP3 streams.
     */
    public static final int FORMAT_MP3 = 0;

    /**
     * Return value of {@link #getFormat()} for MP4 streams. AudioMetadata can extract metadata and audio information from
     * MP4 streams.
     */
    public static final int FORMAT_MP4 = 1;

    /**
     * Return value of {@link #getFormat()} for Vorbis streams. AudioMetadata can extract metadata and audio information from
     * Vorbis stream and Ogg container.
     */
    public static final int FORMAT_VORBIS = 2;

    /**
     * Return value of {@link #getFormat()} for FLAC streams. AudioMetadata can extract metadata and audio information from
     * FLAC stream and Ogg container.
     */
    public static final int FORMAT_FLAC = 3;

    /**
     * Return value of {@link #getFormat()} for WMA streams. AudioMetadata can extract metadata and audio information from
     * WMA stream and ASF container.
     */
    public static final int FORMAT_WMA = 4;

    /**
     * Return value of {@link #getFormat()} for WAVE streams. AudioMetadata can extract metadata and audio information from
     * WAVE stream and ASF container.
     */
    public static final int FORMAT_WAV = 5;


    public static final int FORMAT_UNSUPPORTED = -1;


    /**
     * The names of the MIME types for all supported file formats.
     */
    static final String[] MIME_TYPE_STRINGS = {"audio/mpeg",
                                               "audio/mp4", "video/mp4", "video/quicktime",
                                               "audio/vorbis", "audio/x-vorbis", "audio/ogg",
                                               "audio/flac", "audio/x-flac",
                                               "audio/vnd.ms-asf", "audio/x-ms-wma", "audio/x-ms-asf",
                                               "audio/x-wav", "audio/wav", "audio/wave"};

    /**
     * The extensions of all supported file formats. The FORMAT_xyz int constants can be used as index values for
     * this array.
     */
    static final String[] FORMAT_NAMES = {"mp3", "mp4", "ogg", "flac", "wma", "wav", "real"};

    private int format;
    private InputStream in;
    private AudioFile audioFile;

    private Long bitrate;
    private Integer sampleRate;
    private Double duration;
    private String channels;

    private String title;
    private String artist;
    private String album;
    private String year;
    private String comment;
    private String track;
    private String genre;

    private List<AudioMetadataArtwork> artwork;

    private String mimeType;

    public AudioMetadata( InputStream inputStream,
                          final String mimeType ) {
        this.in = inputStream;
        this.mimeType = mimeType;
    }

    /*
     * Check that given file is supported by this sequencer and find out the format.
     */
    public boolean check() throws Exception {
        format = FORMAT_UNSUPPORTED;
        // create a temporary copy from input
        File fileCopy = File.createTempFile("modeshape-sequencer-audio", ".tmp");
        IoUtil.write(in, new BufferedOutputStream(new FileOutputStream(fileCopy)));

        if (mimeType.startsWith("audio/mpeg")) {
            format = FORMAT_MP3;
            audioFile = new MP3FileReader().read(fileCopy);
        } else if (mimeType.startsWith("audio/vorbis") || mimeType.startsWith("audio/x-vorbis") || mimeType.startsWith("audio/ogg")) {
            format = FORMAT_VORBIS;
            audioFile = new OggFileReader().read(fileCopy);
        } else if (mimeType.startsWith("audio/flac") || mimeType.startsWith("audio/x-flac")) {
            format = FORMAT_FLAC;
            audioFile = new FlacFileReader().read(fileCopy);
        } else if (mimeType.equals("audio/mp4") || mimeType.equals("video/mp4") || mimeType.equals("video/quicktime")) {
            format = FORMAT_MP4;
            audioFile = new Mp4FileReader().read(fileCopy);
        } else if (mimeType.equals("audio/x-ms-wma")) {
            format = FORMAT_WMA;
            audioFile = new AsfFileReader().read(fileCopy);
        } else if (mimeType.startsWith("audio/x-wav") || mimeType.startsWith("audio/wav")) {
            format = FORMAT_WAV;
            audioFile = new WavFileReader().read(fileCopy);
        }

        // try to delete the file immediately or on JVM exit
        boolean deleted = false;
        try {
            deleted = fileCopy.delete();
        } catch (SecurityException e) {
            // ignore
        }
        if (!deleted) {
            fileCopy.deleteOnExit();
        }
        return checkSupportedAudio();
    }

    /**
     * Parse tags common for all audio files.
     */
    private boolean checkSupportedAudio() {
        AudioHeader header = audioFile.getAudioHeader();
        bitrate = header.getBitRateAsNumber();
        sampleRate = header.getSampleRateAsNumber();
        channels = header.getChannels();
        if (header.getChannels().toLowerCase().contains("stereo")) {
            channels = "2";
        }

        if (header instanceof MP3AudioHeader) {
            duration = ((MP3AudioHeader) header).getPreciseTrackLength();
        } else if (header instanceof Mp4AudioHeader) {
            duration = (double) ((Mp4AudioHeader) header).getPreciseLength();
        } else {
            duration = (double) header.getTrackLength();
        }

        // generic frames
        Tag tag = audioFile.getTag();
        artist = tag.getFirst(FieldKey.ARTIST);
        album = tag.getFirst(FieldKey.ALBUM);
        title = tag.getFirst(FieldKey.TITLE);
        comment = tag.getFirst(FieldKey.COMMENT);
        year = tag.getFirst(FieldKey.YEAR);
        track = tag.getFirst(FieldKey.TRACK);
        genre = tag.getFirst(FieldKey.GENRE);

        artwork = new ArrayList<>();
        for (Artwork a : tag.getArtworkList()) {
            AudioMetadataArtwork ama = new AudioMetadataArtwork();
            ama.setMimeType(a.getMimeType());
            if (a.getPictureType() >= 0) {
                ama.setType(a.getPictureType());
            }
            ama.setData(a.getBinaryData());

            artwork.add(ama);
        }

        return true;
    }

    /**
     * If {@link #check()} was successful, returns the audio format as one of the FORMAT_xyz constants from this class. Use
     * {@link #getFormatName()} to get a textual description of the file format.
     *
     * @return file format as a FORMAT_xyz constant
     */
    public int getFormat() {
        return format;
    }

    /**
     * If {@link #check()} was successful, returns the audio format's name. Use {@link #getFormat()} to get a unique number.
     *
     * @return file format name
     */
    public String getFormatName() {
        if (format >= 0 && format < FORMAT_NAMES.length) {
            return FORMAT_NAMES[format];
        }
        return "?";
    }

    public Long getBitrate() {
        return bitrate;
    }

    public Integer getSampleRate() {
        return sampleRate;
    }

    public String getChannels() {
        return channels;
    }

    public Double getDuration() {
        return duration;
    }

    public String getTitle() {
        return title;
    }

    public String getArtist() {
        return artist;
    }

    public String getAlbum() {
        return album;
    }

    public String getYear() {
        return year;
    }

    public String getComment() {
        return comment;
    }

    public String getTrack() {
        return track;
    }

    public String getGenre() {
        return genre;
    }

    public List<AudioMetadataArtwork> getArtwork() {
        return artwork;
    }
}
