/*
 * File    : JavaSoundAudioPlayer.java
 * Created : 23-jun-2004 11:29
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * 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 (see the LICENSE file).
 */

package edu.xtec.jclic.media;

import edu.xtec.jclic.bags.MediaBag;
import edu.xtec.util.ExtendedByteArrayInputStream;
import edu.xtec.util.StreamIO;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.tritonus.applet.AppletMpegSPIWorkaround;
import org.tritonus.applet.AppletVorbisSPIWorkaround;

/**
 * @author Francesc Busquets (fbusquets@xtec.cat)
 * @version 13.08.28
 */
public class JavaSoundAudioPlayer implements AudioPlayer {

  private Clip clip;
  protected AudioInputStream ais;
  protected boolean isMpeg;
  protected boolean isOgg;
  protected boolean isWav;

  static final int INTERNAL_BUFFER_SIZE = 1024;
  static final int CHECK_BUFFER_SIZE = 0x60;
  static final int BIT_SAMPLE_SIZE = 16;
  static final boolean BIG_ENDIAN = true;

  /** Creates a new instance of JavaSoundAudioPlayer */
  public JavaSoundAudioPlayer() {
  }

  public boolean setDataSource(Object source) throws Exception {

    close();

    InputStream is = null;
    javax.sound.sampled.AudioFileFormat m_audioFileFormat = null;

    if (source instanceof ExtendedByteArrayInputStream) {
      is = checkInputStream((InputStream) source, ((ExtendedByteArrayInputStream) source).getName());
    } else if (source instanceof InputStream) {
      is = checkInputStream((InputStream) source, null);
    } else if (source instanceof File) {
      is = checkInputStream(new java.io.FileInputStream((File) source), ((File) source).getName());
    } else {
      java.net.URL url = null;
      if (source instanceof java.net.URL)
        url = (java.net.URL) source;
      else if (source instanceof String) {
        url = new java.net.URL((String) source);
      }
      if (url != null) {
        is = checkInputStream(url.openStream(), source.toString());
      }
    }

    if (is != null) {
      if (isMpeg || (!isWav && !isOgg)) {
        try {
          m_audioFileFormat = AppletMpegSPIWorkaround.getAudioFileFormat(is);
          ais = AppletMpegSPIWorkaround.getAudioInputStream(is);
          isMpeg = true;
        } catch (IOException ex) {
          throw ex;
        } catch (UnsupportedAudioFileException ex) {
          isMpeg = false;
        }
      }
      if (isOgg || (!isMpeg && !isWav)) {
        try {
          m_audioFileFormat = AppletVorbisSPIWorkaround.getAudioFileFormat(is);
          ais = AppletVorbisSPIWorkaround.getAudioInputStream(is);
          isOgg = true;
        } catch (IOException ex) {
          throw ex;
        } catch (UnsupportedAudioFileException ex) {
          isOgg = false;
        }
      }
      if (isWav || (!isMpeg && !isOgg)) {
        m_audioFileFormat = AudioSystem.getAudioFileFormat(is);
        ais = AudioSystem.getAudioInputStream(is);
      }
    }

    if (ais != null) {
      AudioFormat af = ais.getFormat();
      DataLine.Info info = new DataLine.Info(SourceDataLine.class, af, INTERNAL_BUFFER_SIZE);
      if (!AudioSystem.isLineSupported(info)) {
        AudioFormat sourceFormat = af;
        AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(),
            BIT_SAMPLE_SIZE, sourceFormat.getChannels(), sourceFormat.getChannels() * (BIT_SAMPLE_SIZE / 8),
            sourceFormat.getSampleRate(), BIG_ENDIAN);

        if (isMpeg)
          ais = AppletMpegSPIWorkaround.getAudioInputStream(targetFormat, ais);
        else if (isOgg)
          ais = AppletVorbisSPIWorkaround.getAudioInputStream(targetFormat, ais);
        else
          ais = AudioSystem.getAudioInputStream(targetFormat, ais);
      }
    }
    return ais != null;
  }

  public Clip getClip() throws Exception {
    if (clip == null && ais != null) {
      clip = (Clip) AudioSystem.getLine(new DataLine.Info(Clip.class, ais.getFormat(), INTERNAL_BUFFER_SIZE));
    }
    return clip;
  }

  public void realize(String fileName, MediaBag mediaBag) throws Exception {
    if (fileName != null)
      setDataSource(mediaBag.getMediaDataSource(fileName));
    if (ais != null && getClip() != null) {
      clip.open(ais);
    }
  }

  // Modified to avoid unexpected exceptions like
  // "IllegalStateException: line already closed"
  // launched by org.classpath.icedtea.pulseaudio.PulseAudioClip.close
  public void close() {
    if (clip != null && clip.isOpen()) {
      if (clip.isRunning())
        clip.stop();
      clip.close();
    }
    clip = null;
    ais = null;
  }

  public void play() {
    try {
      stop();
      if (getClip() != null) {
        clip.setFramePosition(0);
        clip.start();
      }
    } catch (Exception ex) {
      System.err.println("Error playing sound:\n" + ex);
    }
  }

  public void stop() {
    if (clip != null && clip.isActive())
      clip.stop();
  }

  protected InputStream checkInputStream(InputStream is, String name) throws Exception {
    String s = (name == null ? null : name.toLowerCase());
    if (s != null) {
      if (s.endsWith(".wav"))
        isWav = true;
      else if (s.endsWith(".ogg"))
        isOgg = true;
      else if (s.endsWith(".mp3"))
        isMpeg = true;
    }

    if (s == null || isWav) {
      byte[] data;
      if (!is.markSupported()) {
        data = StreamIO.readInputStream(is);
        is = new ByteArrayInputStream(data);
      }
      is.mark(CHECK_BUFFER_SIZE);
      byte[] b = new byte[CHECK_BUFFER_SIZE];
      is.read(b);
      is.reset();
      if (b[0x00] == 'R' && b[0x01] == 'I' && b[0x02] == 'F' && b[0x03] == 'F' && b[0x08] == 'W' && b[0x09] == 'A'
          && b[0x0A] == 'V' && b[0x0B] == 'E' && b[0x0C] == 'f' && b[0x0D] == 'm' && b[0x0E] == 't' && b[0x0F] == ' '
          && b[0x14] == 0x55 && b[0x15] == 0x00) {
        for (int p = 0x11; p < CHECK_BUFFER_SIZE - 6; p++) {
          if (b[p] == 'd' && b[p + 1] == 'a' && b[p + 2] == 't' && b[p + 3] == 'a') {
            int offset = p + 4 + 4;
            is.skip(offset);
            isWav = false;
            isMpeg = true;
            break;
          }
        }
      }
    }
    return is;
  }
}
