package com.beaglebuddy.mp3;

import java.io.File;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.HttpURLConnection;
import java.util.List;

import com.beaglebuddy.id3.enums.ID3TagVersion;
import com.beaglebuddy.id3.v1.ID3v1Tag;
import com.beaglebuddy.id3.v23.ID3v23Tag;
import com.beaglebuddy.id3.v23.ID3v23TagHeader;
import com.beaglebuddy.id3.v24.ID3v24Tag;
import com.beaglebuddy.id3.v24.ID3v24TagHeader;
import com.beaglebuddy.mp3.exception.TagNotFoundException;




/**
 * <table class="logos_width">
 *    <tbody>
 *       <tr>
 *          <td                               ><img src="../../../resources/id3v2.gif"                     width="56"  height="54"  alt="ID3 logo"        /></td>
 *          <td class="logos_horz_align_right"><img src="../../../resources/beaglebuddy_software_logo.gif" width="340" height="110" alt="Beaglebuddy logo"/></td>
 *       </tr>
 *    </tbody>
 * </table>
 * <p class="beaglebuddy">
 * This base class provides some underlying methods that help make the derived MP3 class's interface much cleaner and easier to understand.
 * Thus, this class provides lower level methods that are used by the MP3 class, but would otherwise clutter the MP3 class's public interface.
 * </p>
 */
public class MP3Base extends MP3BaseID3v24
{
   // data members
                                      /** if the mp3 file is loaded from a local file, then this data member will contain the path to the .mp3 file. Otherwise, this data member will be null. */
   protected File      mp3File;       /** if the mp3 file is loaded from a URL,        then this data member will contain the URL to the .mp3 file.  Otherwise, this data member will be null. */
   protected URL       mp3Url;        /** size (in bytes) of the .mp3 file.                                                                                                                    */
   protected long      fileSize;      /** size (in bytes) of the ID3v2.x tag.                                                                                                                  */
   protected long      tagSize;       /** size (in bytes) of the audio portion of the .mp3 file.  That is, the number of bytes comprising the actual sound data for the song.                  */
   protected long      audioSize;



   /**
    * constructor.
    * @param mp3File   path to the .mp3 file from which to get information.
    * @throws IOException                   if there is a problem reading the .mp3 file.
    */
   public MP3Base(String mp3File) throws IOException
   {
      this(new File(mp3File));
   }

   /**
    * constructor.
    * @param mp3File   .mp3 file from which to get information.
    * @throws IOException   if there is a problem reading the .mp3 file.
    */
   public MP3Base(File mp3File) throws IOException
   {
      this.mp3File = mp3File;
      readID3Tag(new BufferedInputStream(new FileInputStream(mp3File)));
      this.fileSize  = mp3File.length();
      this.tagSize   = id3v23Tag != null ? id3v23Tag.getSize() : id3v24Tag.getSize();
      this.audioSize = fileSize - tagSize;

      if (id3v24Tag != null)
         setV24AudioSize((int)audioSize);
   }

   /**
    * constructor.
    * @param mp3Url   URL of an .mp3 file from which to get information.
    * @throws IOException   if there is a problem reading the .mp3 file.
    */
   public MP3Base(URL mp3Url) throws IOException
   {
      this.mp3Url = mp3Url;
      HttpURLConnection httpConnection = (HttpURLConnection)this.mp3Url.openConnection();
      httpConnection.connect();
      readID3Tag(new BufferedInputStream(mp3Url.openStream()));
      this.fileSize  = httpConnection.getContentLength();
      this.tagSize   = id3v23Tag != null ? id3v23Tag.getSize() : id3v24Tag.getSize();
      this.audioSize = fileSize - tagSize;
      httpConnection.disconnect();
   }

   /**
    * reads in the ID3v2.x tag from the .mp3 file.  If the .mp3 file does not have an ID3v2.x tag, then an ID3v1 tag is searched for, and if found, values from it
    * are used to create an ID3v2.3 tag.
    * @param inputStream   input stream pointing to the beginning of an .mp3 file
    * @throws IOException  if there is a problem reading the .mp3 file.
    */
   protected void readID3Tag(InputStream inputStream) throws IOException
   {
      this.id3v23Tag = null;
      this.id3v24Tag = null;

      try
      {
         ID3TagVersion id3TagVersion = ID3TagVersion.readVersion(inputStream);

         switch (id3TagVersion)
         {
            case NONE:
                 // the .mp3 file does not contain an ID3v2.x tag.  create one.
                 this.id3v23Tag = new ID3v23Tag();
                 this.tagSize   = 0;
                 this.id3v23Tag.setPadding(0);

                 // if the .mp3 file is being read from a local file (on the user's hard drive for example), see if it has an ID3v1 tag
                 // since the ID3v1 tag is found at the very end of an .mp3 file, you don't want to download the entire .mp3 file just to
                 // read the ID3v1 tag if .mp3 file is being read from a URL.
                 if (mp3File != null)
                 {
                    try
                    {
                       // skip to the ID3v1 tag at the end of the file
                       // note: we read in the bytes for the ID3v2.3 tag header, so subtract those bytes as well
                       long tagOffset = mp3File.length() - ID3TagVersion.NUM_ID_BYTES - ID3v1Tag.ID3v1_TAG_SIZE;
                       if (skip(inputStream, tagOffset) == tagOffset)
                       {
                          ID3v1Tag id3v1Tag = new ID3v1Tag(inputStream, getPath());
                          if (id3v1Tag.getAlbum().length()         != 0)
                             setV23Album(id3v1Tag.getAlbum());
                          if (id3v1Tag.getArtist().length()        != 0)
                             setV23Band(id3v1Tag.getArtist());
                          if (id3v1Tag.getTitle().length()         != 0)
                             setV23Title(id3v1Tag.getTitle());
                          if (id3v1Tag.getTrack()                  != 0)
                             setV23Track(id3v1Tag.getTrack());
                          if (id3v1Tag.getGenreAsString().length() != 0)
                             setV23MusicType("(" + (id3v1Tag.getGenre() & 0xFF )+ ")");                  // java treats byte as a signed value, while the ID3v2.3 spec treats bytes as unsigned.
                                                                                                         // this necessitates converting a byte to a larger value (integer)
                          if (id3v1Tag.getYear().length()          == 4)
                          {
                        	 try
                        	 {
                        	    int year = Integer.parseInt(id3v1Tag.getYear());
                        	    setV23Year(year);
                        	 }
                        	 catch (Exception ex)
                        	 {
                        		setV23Year(1);
                        	 }
                          }

                          audioSize = mp3File.length();
                          inputStream.close();                                                           // otherwise, the save() method can't rename the temp file to the .mp3 file
                          saveID3v23();                                                                  // save the newly converted ID3v1 -> ID3v2.3 tag to the .mp3 file
                       }
                    }
                    catch (Exception e)
                    {
                       // nothing to do.  hey, at least we tried.
                    }
                 }
            break;
            case ID3V2_2:
                 throw new IOException("An ID3v2.2 tag was found in the mp3 file but that version is not currently supported.");
            case ID3V2_3:
                 this.id3v23Tag = new ID3v23Tag(inputStream);
            break;
            case ID3V2_4:
                 this.id3v24Tag = new ID3v24Tag(inputStream);
            break;
         }
      }
      finally
      {
         if (inputStream != null)
            try {inputStream.close();} catch (Exception ioex) { /* nothing can be done */ }
      }
   }

   /**
    * determines if the mp3 file had any errors in the information stored in the frames of the ID3v2.x tag when it was read in.  If any of the frames were invalid, then they are
    * removed from the ID3v2.x tag.
    * @return whether any invalid frames were encountered while reading in the ID3v2.x tag from the .mp3 file.
    */
   public boolean hasErrors()
   {
      return id3v23Tag != null ? hasV23Errors() : hasV24Errors();
   }

   /**
    * gets a list of any errors encountered while reading in the ID3v2.x tag.
    * @return a list of errors that occurred while reading in the ID3v2.x tag.
    */
   public List<String> getErrors()
   {
      return id3v23Tag != null ? getV23Errors() : getV24Errors();
   }

   /**
    * displays any errors found while reading in the ID3v2.x tag from the .mp3 file.  If no errors were found, then nothing is written to the print stream.
    * @param printStream   print stream used to write out the errors.
    */
   public void displayErrors(PrintStream printStream)
   {
      List<String> errors = getErrors();

      if (errors.size() != 0)
      {
         printStream.println(getPath() + " had " + errors.size() + " invalid frames");
         for(String error : errors)
            printStream.println("   " + error);
      }
   }

   /**
    * gets the read only error message to display to users when they try to modify an .mp3 file loaded from a URL.
    * @return the read only error message to display to users when they try to modify an .mp3 file loaded from a URL.
    */
   protected String getReadOnlyErrorMessage()
   {
      return "The mp3 song " + (id3v23Tag != null ? getV23Title() : getV24Title()) + " was loaded from a URL, " + getPath() + ", and is therefore read only.";
   }

   /**
    * gets the path to the .mp3 file.
    * <br/><br/><b>Example:</b><code><pre class="beaglebuddy">
    *     // load the song "Take Hold of the Flame" from Queensryche's album, "The Warning".
    *     MP3 mp3 = new MP3("c:/mp3/queensryche/the warning/take hold of the flame.mp3");
    *     System.out.println("the mp3 file was loaded from " + mp3.getPath());
    *
    *     // load the song "Take Hold of the Flame" from Queensryche's album, "The Warning".
    *     MP3 mp3 = new MP3("http://www.beaglebuddy.com/mp3/queensryche/the warning/take hold of the flame.mp3");
    *     System.out.println("the mp3 file was loaded from " + mp3.getPath());</pre></code>
    * @return the path to the .mp3 file.
    */
   public String getPath()
   {
      return mp3File == null ? mp3Url.toExternalForm() : mp3File.getPath();
   }

   /**
    * returns the size (in bytes) of the .mp3 file.
    * @return the size (in bytes) of the .mp3 file.
    */
   public long getFileSize()
   {
      return fileSize;
   }

   /**
    * the bit rate in kbps (kilobits per second) at which the mp3 file was created.
    * see the wikipedia definition of <a href="http://en.wikipedia.org/wiki/Bit_rates">kbps</a>.
    * @return the bit rate in kbps (kilobits per second) at which the mp3 file was created.
    */
/* public int getBitRate()
   {
      int[] bitRates      = {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320, 384};   // allowed bit rates
      int   bitRate       = 0;                                                                                      // bit rate at which the mp3 file was created
      int   audioDuration = getTextAsInteger(FrameType.LENGTH) / 1000;                                              // length of song in seconds

      if (audioDuration != 0)
      {
         int kbps    = (int)Math.round(audioSize * 8.0 / audioDuration / 1000.0);   // (number of bytes in audio portion of mp3 file) * (8 bits/byte) / (number of seconds in song) / 1000;
         int minDiff = Integer.MAX_VALUE;

         for(int i=0; i<bitRates.length; ++i)
         {
            int diff = Math.abs(bitRates[i] - kbps);
            if (diff < minDiff)
            {
               bitRate = bitRates[i];
               minDiff = diff;
            }
         }
      }
      return bitRate;
   }
*/
   /**
    * renames the temporary file name to the current .mp3 filename.
    * @param tempFile   temporary .mp3 file which will be renamed to the .mp3 file.
    * @param mp3File    current .mp3 file which will be deleted.
    * @throws IOException if the current .mp3 file can not be deleted or if the temporary .mp3 file can not be renamed to the current .mp3 file.
    */
   private static void rename(File tempFile, File mp3File) throws IOException
   {
      if (!mp3File.delete())
         throw new IOException("Unable to delete the file " + mp3File.getPath());
      if (!tempFile.renameTo(mp3File))
         throw new IOException("Unable to rename the file " + tempFile.getPath() + " to " + mp3File.getPath() + ".");
   }

   /**
    * determines whether the .mp3 file has an ID3v1 tag at the end of the .mp3 file.
    * @return    whether the .mp3 file has an ID3v1 tag at the end of the .mp3 file.
    * @throws IOException              if there is a problem reading the .mp3 file.
    * @throws IllegalStateException    if the mp3 song was loaded from a URL and therefore will take a considerable amount of time to reach the ID3v1 tag at the end of the .mp3 file.
    */
   public boolean hasID3v1Tag() throws IOException, IllegalStateException
   {
      if (mp3File == null)
        throw new IllegalStateException(getReadOnlyErrorMessage());

      InputStream inputStream = null;
      boolean     hasId3v1Tag = false;

      try
      {
         inputStream = new BufferedInputStream(new FileInputStream(mp3File));

         // see if there is an ID3v1 tag at the end of the .mp3 file
         long tagOffset = mp3File.length() - ID3v1Tag.ID3v1_TAG_SIZE;
         if (skip(inputStream, tagOffset) == tagOffset)
         {
            try
            {
               new ID3v1Tag(inputStream, getPath());
               hasId3v1Tag = true;
            }
            catch (TagNotFoundException ex)
            {  // the .mp3 file does not have an ID3v1 tag
               hasId3v1Tag = false;
            }
         }
      }
      finally
      {
         if (inputStream != null)
            try {inputStream.close(); } catch (Exception ex) { /* nothing can be done */  }
      }
      return hasId3v1Tag;
   }

   /**
    * remove the ID3v1 tag at the end of the .mp3 file.
    * <br/><br/>
    * @throws IOException              if there was an error truncating ID3v1 tag from the end of the .mp3 file.
    * @throws IllegalStateException    if the mp3 song was loaded from a URL and is therefore considered to be read only and thus may not be modified.
    */
   public void removeID3v1Tag() throws IOException
   {
      if (mp3File == null)
        throw new IllegalStateException(getReadOnlyErrorMessage());

      RandomAccessFile file = new RandomAccessFile(mp3File, "rwd");
      file.setLength(fileSize - ID3v1Tag.ID3v1_TAG_SIZE);              // truncate the last 128 bytes from the .mp3 file
      file.close();

      audioSize -= ID3v1Tag.ID3v1_TAG_SIZE;
      fileSize   = mp3File.length();

      // if the mp3 file has an ID3v2.3 tag, then save the new audio size to the audio size frame
      if (id3v23Tag != null)
      {
         setV23AudioSize((int)audioSize);    // set the size (in bytes) of the audio portion of the .mp3 in a TSIZ frame
         saveID3v23();                       // save the id3v2.3 tag back to the .mp3 file
      }
   }

   /**
    * save the ID3v2.3 tag to the .mp3 file.
    * This is a very messy method, and you really have to understand the ID3v2.3 structure to understand this method.
    * So, if you can, by all means, avoid reading the code in this method.
    * <br/><br/>
    * @throws IOException              if there was an error writing the ID3v2.3 tag to the .mp3 file.
    */
   protected void saveID3v23() throws IOException
   {
      setV23AudioSize((int)audioSize);                                           // set the size (in bytes) of the audio portion of the .mp3 in a TSIZ frame
      long            oldTagSize     = tagSize;                                  // get the size of the tag before any changes were made
      long            oldPaddingSize = id3v23Tag.getPadding().length;            // get the size of the padding in the tag before any changes were made
                                       id3v23Tag.setBuffer();                    // save any changes to the tag's internal byte buffer
      long            newTagSize     = id3v23Tag.getSize() - oldPaddingSize;     // get the size of the tag after any changes were made - this size is without any padding
      ID3v23TagHeader header         = id3v23Tag.getHeader();                    // header in ID3v2.3 tag where the tag size - sizeof(header) is stored

      // if the new tag is smaller than the old tag, then we can just re-use the old tag's space and adjust the padding
      if (newTagSize < oldTagSize)
      {
         id3v23Tag.setPadding((int)(oldTagSize - newTagSize));                   // set the new padding size
         header.setTagSize((int)(oldTagSize - ID3v23TagHeader.TAG_HEADER_SIZE)); // new tag size is same as the old tag size, just the amount of padding has been reduced
         header.setBuffer();                                                     // clear the dirty flag in the header
         RandomAccessFile file = new RandomAccessFile(mp3File, "rwd");           // open the mp3 file for writing
         id3v23Tag.save(file);                                                   // write the ID3v2.3 tag to the beginning of the .mp3 file
         file.close();
      }
      // otherwise, we need to re-write the whole .m3 file so that we have enough space to accommodate the tag's new larger size.
      else
      {
         File             tempFile     = new File(mp3File.getPath() + ".tmp");   // name of the temporary .mp3 file
         FileOutputStream tempMp3File  = new FileOutputStream(tempFile);         // output stream used to write the bytes to the temp    .mp3 file
         FileInputStream  audioFile    = new FileInputStream (mp3File);          // input  stream used to read  the audio of the current .mp3 file
         byte[]           audio        = new byte[2048];                         // buffer used to read the audio bytes from the current .mp3 file to the temp .mp3 file

         id3v23Tag.setPadding(ID3v23Tag.DEFAULT_PADDING_SIZE);                   // set the padding to a default size
         newTagSize += ID3v23Tag.DEFAULT_PADDING_SIZE;                           // add the new padding to the total tag size
         header.setTagSize((int)(newTagSize - ID3v23TagHeader.TAG_HEADER_SIZE)); // new tag size is larger than the old tag size
         header.setBuffer();                                                     // clear the dirty flag in the header
         id3v23Tag.save(tempMp3File);                                            // save the new ID3v2.3 tag to the beginning of the new .mp3 file

         // copy the audio portion of the old .mp3 file to the new one
         int n = 0;
         int audioSizeWritten = 0;
         skip(audioFile, oldTagSize);                                            // skip to the audio portion of the current .mp3 file

         while ((n = audioFile.read(audio)) != -1)
         {
            audioSizeWritten+=n;
            tempMp3File.write(audio, 0, n);
         }
         tempMp3File.close();
         audioFile.close();

         if (audioSizeWritten != audioSize)
             throw new IOException("Error saving the audio portion.  Expected " + audioSize + " bytes, but saved " + audioSizeWritten + " bytes.");

         rename(tempFile, mp3File);
      }
      tagSize  = id3v23Tag.getSize();
      fileSize = mp3File.length();
   }

   /**
    * save the ID3v2.4 tag to the .mp3 file.
    * This is a very messy method, and you really have to understand the ID3v2.4 structure to understand this method.
    * So, if you can, by all means, avoid reading the code in this method.
    * <br/><br/>
    * @throws IOException  if there was an error writing the ID3v2.4 tag to the .mp3 file.
    */
   protected void saveID3v24() throws IOException
   {
      ID3v24TagHeader header         = id3v24Tag.getHeader();                       // header in ID3v2.4 tag where the tag size - sizeof(header) is stored
      boolean         footerPresent  = header.isFooterPresent();                    // does the ID3v2.4 tag have a footer
      long            oldTagSize     = tagSize;                                     // get the size of the tag before any changes were made
      long            oldPaddingSize = id3v24Tag.getPadding().length;               // get the size of the padding in the tag before any changes were made
                                       id3v24Tag.setBuffer();                       // save any changes to the tag's internal byte buffer
      long            newTagSize     = id3v24Tag.getSize() -                        // get the size of the tag after any changes were made - this size is without any padding
                                      (header.isFooterPresent() ? 0 : oldPaddingSize);

      // if the new tag is smaller than the old tag, and doesn't have a footer, then we can just re-use the old tag's space and adjust the padding
      if (newTagSize < oldTagSize && !footerPresent)
      {
         id3v24Tag.setPadding((int)(oldTagSize - newTagSize));
         header.setTagSize((int)(oldTagSize - ID3v24TagHeader.TAG_HEADER_SIZE));    // new tag size is same as the old tag size, just the amount of padding has been reduced
         header.setBuffer();                                                        // clear the dirty flag in the header
         RandomAccessFile file = new RandomAccessFile(mp3File, "rwd");              // open the mp3 file for writing
         id3v24Tag.save(file);                                                      // write the ID3v2.4 tag to the beginning of the .mp3 file
         file.close();
      }
      // otherwise, we need to re-write the whole .m3 file so that we have enough space to accommodate the tag's new larger size.
      else
      {
         File             tempFile     = new File(mp3File.getPath() + ".tmp");      // name of the temporary .mp3 file
         FileOutputStream tempMp3File  = new FileOutputStream(tempFile);            // output stream used to write the bytes to the temp    .mp3 file
         FileInputStream  audioFile    = new FileInputStream (mp3File);             // input  stream used to read  the audio of the current .mp3 file
         byte[]           audio        = new byte[2048];                            // buffer used to read the audio bytes from the current .mp3 file to the temp .mp3 file

         id3v24Tag.setPadding(footerPresent ? 0 :  ID3v24Tag.DEFAULT_PADDING_SIZE); // set the padding to a default size
         id3v24Tag.save(tempMp3File);                                               // save the new ID3v2.4 tag to the beginning of the new .mp3 file

         // copy the audio portion of the old .mp3 file to the new one
         int n = 0;
         int audioSizeWritten = 0;
         skip(audioFile, oldTagSize);                                               // skip to the audio portion of the current .mp3 file

         while ((n = audioFile.read(audio)) != -1)
         {
            audioSizeWritten+=n;
            tempMp3File.write(audio, 0, n);
         }
         tempMp3File.close();
         audioFile.close();

         if (audioSizeWritten != audioSize)
             throw new IOException("Error saving the audio portion.  Expected " + audioSize + " bytes, but saved " + audioSizeWritten + " bytes.");

         rename(tempFile, mp3File);
      }
      tagSize  = id3v24Tag.getSize();
      fileSize = mp3File.length();
   }

   /**
    * this is a more robust implementation of the InputStream class's skip() method.
    * note: see http://bugs.sun.com/view_bug.do?bug_id=6204246
    * @return the number of bytes actually skipped.
    * @param numBytes the number of bytes to skip.
    * @throws IOException  if an error occurs while skipping over the bytes in the input stream.
    */
   private static long skip(InputStream inputStream, long numBytes) throws IOException
   {
      long totalNumBytesSkipped = 0L;

      while (numBytes > 0)
      {
         long bytesSkipped = inputStream.skip(numBytes);
         if (bytesSkipped > 0)
         {
             numBytes -= bytesSkipped;
             totalNumBytesSkipped += bytesSkipped;
         }
         else if (bytesSkipped == 0)
         {
             // should we retry? lets read one byte
             if (inputStream.read() == -1)  // EOF
                break;
             else
                numBytes--;
             totalNumBytesSkipped++;
         }
         else
         {
            throw new IOException("skip() returned a negative value, " + bytesSkipped + ".");
         }
      }
      return totalNumBytesSkipped;
   }

   /**
    * @return string representation of the mp3 file.
    */
   @Override
   public String toString()
   {
      StringBuffer buffer = new StringBuffer();
      buffer.append("mp3 file.....: " + getPath()     + "\n");
      buffer.append("mp3 file size: " + fileSize      + " bytes\n");
      buffer.append("audio size...: " + audioSize     + " bytes\n");
      if (id3v23Tag != null)
         buffer.append("ID3v2.3 tag..: " + id3v23Tag + "\n");
      else
         buffer.append("ID3v2.4 tag..: " + id3v24Tag + "\n");

      return buffer.toString();
   }
}
