/*
 * JBoss, the OpenSource J2EE webOS
 * 
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package org.jboss.media.format.audio.mpeg;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.HashMap;
import java.util.Map;

import javax.emb.FormatSyntaxException;
import javax.emb.MediaException;
import javax.emb.MediaHeader;

/**
 * Represents an MPEG Audio Header
 * 
 * @version <tt>$Revision 1.1 $</tt>
 * @author <a href="mailto:ogreen@users.sourceforge.net">Owen Green</a>
 */
public class MpegAudioHeader implements MediaHeader
{
   /*
    * Lookups for bitrates at various combinations of MPEG version and layer
    */
   //Version 1, Layer I
   private final static int[] v1L1BitRates =
      { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 };

   //Version 1, Layer II
   private final static int[] v1L2BitRates =
      { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 };

   //Version 1, Layer III
   private final static int[] v1L3BitRates =
      { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 };

   //Version 2 and 2.5, Layer I
   private final static int[] v2L1BitRates =
      { 0, 32, 48, 56, 64, 80, 96, 112, 128, 411, 460, 476, 192, 224, 256 };

   //Version 2 and 2.5, Layer II and III
   private final static int[] v2L23BitRates =
      { 0, 8, 16, 24, 32, 40, 28, 56, 64, 80, 96, 112, 128, 144, 160 };

   /*
    * Lookups for sampling rates for each version
    */
   private final static int[] v1SampleRates = { 44100, 48000, 32000 };
   private final static int[] v2SampleRates = { 22050, 24000, 16000 };
   private final static int[] v25SampleRates = { 11025, 12000, 8000 };

   /*
    * field name keys for fields map
    */
   private final static String VERSION_KEY = "version";
   private final static String LAYER_KEY = "layer";
   private final static String BITRATE_KEY = "bitRate";
   private final static String SAMPLERATE_KEY = "samplingRate";
   private final static String CHANNELMODE_KEY = "channelMode";
   private final static String COPYRIGHT_KEY = "copyright";
   private final static String ORGINAL_KEY = "original";

   //Map to hold fields in
   private final Map fieldMap = new HashMap(7);

   private final PushbackInputStream content;

   /**
    * Constructs a new instance and attempts to extract an MPEG audio header
    * from <code>mediaObject</code>
    * 
    * @param mediaObject the media to extract an MPEG Audio header from
    * @throws MediaException if an there is an error accessing the media
    *         content
    * @throws FormatSyntaxException if invalid data for this format is
    *         encoutered
    * @throws RemoteException if an RMI error occurs
    */
   public MpegAudioHeader(InputStream data)
      throws MediaException, FormatSyntaxException
   {
      try
      {
         content = new PushbackInputStream(data, 4);
         int headerPos = findHeaderStart(content, 0);

         if (headerPos != -1)
         {
            byte[] header = new byte[4];
            content.read(header);
            readHeader(header);
         }
         else
         {
            throw new FormatSyntaxException("MPEG Audio header could not be found; this is not valid MPEG Audio data");
         }
      }
      catch (IOException ex)
      {
         throw new MediaException(ex.getMessage());
      }
   }

   /**
    * Returns the bit rate for this media in kbps.
    * 
    * @return int the bit rate for this media
    */
   public int getBitrate()
   {
      return ((Integer) fieldMap.get(BITRATE_KEY)).intValue();
   }

   /**
    * Returns the channel mode for this MPEG audio.
    * 
    * @return MpegAudioFormat.ChannelMode the channel mode for this MPEG audio
    * @see MpegAudioFormat.ChannelMode
    */
   public MpegAudioFormat.ChannelMode getChannelMode()
   {
      return (MpegAudioFormat.ChannelMode) fieldMap.get(CHANNELMODE_KEY);
   }

   /**
    * Indicates whether this is copyrighted material or not
    * 
    * @return boolean <code>true</code> if this is copyrighted material
    */
   public boolean isCopyright()
   {
      return ((Boolean) fieldMap.get(COPYRIGHT_KEY)).booleanValue();
   }

   /**
    * Returns the MPEG layer of this media
    * 
    * @return MpegAudioFormat.Layer the MPEG audio layer of this media
    * @see MpegAudioFormat
    * @see MpegAudioFormat.Layer
    */
   public MpegAudioFormat.Layer getLayer()
   {
      return (MpegAudioFormat.Layer) fieldMap.get(LAYER_KEY);
   }

   /**
    * Indicates whether this is the orginal media, or a copy
    * 
    * @return boolean <code>true</code> if this is the orginal media
    */
   public boolean isOriginal()
   {
      return ((Boolean) fieldMap.get(ORGINAL_KEY)).booleanValue();
   }

   /**
    * Returns the sampling rate of this audio in Hz
    * 
    * @return int the sampling rate of this audio
    */
   public int getSamplerate()
   {
      return ((Integer) fieldMap.get(SAMPLERATE_KEY)).intValue();
   }

   /**
    * Returns the MPEG version of this media
    * 
    * @return MpegAudioFormat.Version the MPEG version of this media
    * @see MpegAudioFormat.Version
    */
   public MpegAudioFormat.Version getVersion()
   {
      return (MpegAudioFormat.Version) fieldMap.get(VERSION_KEY);
   }

   /**
    * Provides a list of the field names in this header
    * 
    * @return a list of the field names in this header
    * @see javax.emb.MediaHeader#getFieldNames()
    */
   public String[] getFieldNames()
   {
      return (String[]) fieldMap.keySet().toArray(new String[0]);
   }

   /**
    * Retreives a field by name. In the case of primitive fields, the object
    * equivalent is returned (e.g. <code>int</code> becomes <code>java.lang.Integer</code>
    * 
    * @param fieldname the name of the field to access
    * @return the requested field, or <code>null</code> if the field name
    *         doesn't exist
    * @see javax.emb.MediaHeader#getField(String)
    */
   public Object getField(String fieldname)
   {
      return fieldMap.get(fieldname);
   }

   /**
    * Finds the start of the next MPEG audio header after <code>offset</code>,
    * skipping any ID3 tag.
    * 
    * @param mediaObject the {@link javax.emb.Media}to search for a header
    * @param the offset to start searching at
    * @return the position in the <code>Media</code> of the next header, or
    *         <code>-1</code> if no header is found
    */
   private int findHeaderStart(PushbackInputStream content, int offset)
      throws MediaException
   {
      try
      {
         if (offset == 0)
         {
            byte[] id3 = new byte[4];
            content.read(id3);
            String id3Test = new String(id3);
            content.unread(id3);
            //see if we've got an ID3 tag at the begining
            if (id3Test.equals("ID3"))
            {
               ID3Tag id3Tag = new ID3Tag(content);
               offset += id3Tag.getSize();
            }
         }

         byte[] headerTest = new byte[2];

         while ((content.read(headerTest)) != -1)
         {
            //check for frame sync block - 1111 1111 111
            if (((headerTest[0] & 0xFF) == 0xFF)
               && ((headerTest[1] & 0xE0) == 0xE0))
            {
               content.unread(headerTest);
               return offset;
            }
            content.unread(headerTest[1]);
            offset++;
         }
      }
      catch (IOException e)
      {
         throw new MediaException(e);
      }

      return -1;
   }

   /**
    * Reads and interprets an MPEG audio header chunk
    * 
    * @param header the header data - at least 4 bytes long
    * @throws FormatSyntaxException if illegal data is encountered
    * @throws IllegalArgumentException if <code>header</code> is less than 4
    *         elements long
    */
   private void readHeader(byte[] header) throws FormatSyntaxException
   {
      if (header.length < 4)
      {
         throw new IllegalArgumentException("Not enough header data");
      }

      fieldMap.put(VERSION_KEY, getVersion(header[1]));
      fieldMap.put(LAYER_KEY, getLayer(header[1]));
      fieldMap.put(BITRATE_KEY, new Integer(getBitrate(header[2])));
      fieldMap.put(SAMPLERATE_KEY, new Integer(getSampleRate(header[2])));
      fieldMap.put(CHANNELMODE_KEY, getChannelMode(header[3]));
      fieldMap.put(COPYRIGHT_KEY, new Boolean(getCopyright(header[3])));
      fieldMap.put(ORGINAL_KEY, new Boolean(getOriginal(header[3])));
   }

   /**
    * Extracts the MPEG Version from an MPEG header
    * 
    * @return the MPEG version
    * @param versionByte the byte of the header that contains the version
    *        information
    * @throws FormatSyntaxException if an illegal version ID is encountered
    * @see MpegAudioFormat.Version
    */
   private MpegAudioFormat.Version getVersion(byte versionByte)
      throws FormatSyntaxException
   {
      switch (versionByte & 0x18) //mask with 00011000
      {
         case 0x00 :
            return MpegAudioFormat.Version.MPEG25;
         case 0x10 :
            return MpegAudioFormat.Version.MPEG2;
         case 0x18 :
            return MpegAudioFormat.Version.MPEG1;
         default :
            throw new FormatSyntaxException("Could not determine MPEG Audio version");
      }
   }

   /**
    * Extracts the MPEG Layer from an MPEG header byte
    * 
    * @return the MPEG Audio Layer
    * @param layerByte the byte that contains layer information
    * @throws FormatSyntaxException if an illegal Layer value is encountered
    * @see MpegAudioFormat.Layer
    */
   private MpegAudioFormat.Layer getLayer(byte layerByte)
      throws FormatSyntaxException
   {
      switch (layerByte & 0x06) //mask with 0000 0110
      {
         case 0x02 :
            return MpegAudioFormat.Layer.LAYERIII;
         case 0x04 :
            return MpegAudioFormat.Layer.LAYERII;
         case 0x06 :
            return MpegAudioFormat.Layer.LAYERI;
         default :
            throw new FormatSyntaxException("Could not determine MPEG Audio version");
      }
   }

   /**
    * Estabilshes whether this MPEG frame has a CRC checksum after the header
    * (not currently used)
    * 
    * @param crcByte the header byte containing the CRC indicator bit
    */
   private boolean getCRC(byte crcByte)
   {
      return (crcByte & 0x1) == 0;
   }

   /**
    * Extracts the MPEG bit rate from an MPEG header byte
    * 
    * @param bitrateByte the header byte containing bit rate information
    * @throws FormatSyntaxException if an illegal bit rate value is encountered
    * @throws IllegalStateException if the Version and Layer have not yet been
    *         extracted
    */
   private int getBitrate(byte bitrateByte) throws FormatSyntaxException
   {
      int bitrateKey = (bitrateByte & 0xF0) >>> 4;

      if (0xF == bitrateKey)
      {
         throw new FormatSyntaxException("Illegal bitrate specified");
      }

      if (MpegAudioFormat.Version.MPEG1 == getVersion())
      {
         if (MpegAudioFormat.Layer.LAYERI == getLayer())
         {
            return v1L1BitRates[bitrateKey];
         }

         if (MpegAudioFormat.Layer.LAYERII == getLayer())
         {
            return v1L2BitRates[bitrateKey];
         }

         if (MpegAudioFormat.Layer.LAYERIII == getLayer())
         {
            return v1L3BitRates[bitrateKey];
         }
      }

      if ((MpegAudioFormat.Version.MPEG2 == getVersion())
         || (MpegAudioFormat.Version.MPEG25 == getVersion()))
      {
         if (MpegAudioFormat.Layer.LAYERI == getLayer())
         {
            return v2L1BitRates[bitrateKey];
         }

         if ((MpegAudioFormat.Layer.LAYERII == getLayer())
            || (MpegAudioFormat.Layer.LAYERIII == getLayer()))
         {
            return v2L23BitRates[bitrateKey];
         }
      }

      throw new IllegalStateException("Version and layer must be determined before extracting bitrate");
   }

   /**
    * Extracts the MPEG sample rate from an MPEG header byte
    * 
    * @param samplerateByte the header byte containing sample rate information
    * @throws FormatSyntaxException if an illegal sample rate value is
    *         encountered
    * @throws IllegalStateException if the Version and Layer have not yet been
    *         extracted
    */
   private int getSampleRate(byte samplerateByte) throws FormatSyntaxException
   {
      int samplerateKey = (samplerateByte & 0xC) >>> 2;

      if (samplerateKey > 2)
      {
         throw new FormatSyntaxException("Illegal sampling rate specified");
      }

      if (MpegAudioFormat.Version.MPEG1 == getVersion())
      {
         return v1SampleRates[samplerateKey];
      }

      if (MpegAudioFormat.Version.MPEG2 == getVersion())
      {
         return v2SampleRates[samplerateKey];
      }

      if (MpegAudioFormat.Version.MPEG25 == getVersion())
      {
         return v25SampleRates[samplerateKey];
      }

      throw new IllegalStateException("Version must be determined before extracting sample rate");
   }

   /**
    * Extracts the MPEG channel mode from a MPEG header byte
    * 
    * @param channelModeByte the byte containing channel mode information
    * @throws FormatSyntaxException if an illegal channel mode value is
    *         encountered
    */
   private MpegAudioFormat.ChannelMode getChannelMode(byte channelModeByte)
      throws FormatSyntaxException
   {
      switch (channelModeByte & 0xC0)
      {
         case 0x00 :
            return MpegAudioFormat.ChannelMode.STEREO;
         case 0x40 :
            return MpegAudioFormat.ChannelMode.JOINT_STEREO;
         case 0x80 :
            return MpegAudioFormat.ChannelMode.DUAL_CHANNEL;
         case 0xC0 :
            return MpegAudioFormat.ChannelMode.SINGLE_CHANNEL;
         default :
            throw new FormatSyntaxException("Illegal Channel Mode encountered");
      }
   }

   /**
    * Extracts the copyright bit from an MPEG header byte
    */
   private boolean getCopyright(byte copyrightByte)
   {
      return (copyrightByte & 8) != 0;
   }

   private boolean getOriginal(byte originalByte)
   {
      return (originalByte & 4) != 0;
   }
}