/* JOrbisComment -- pure Java Ogg Vorbis Comment Editor
 *
 * Copyright (C) 2000 ymnk, JCraft,Inc.
 *
 * Written by: 2000 ymnk<ymnk@jcaft.com>
 *
 * Many thanks to 
 *   Monty <monty@xiph.org> and 
 *   The XIPHOPHORUS Company http://www.xiph.org/ .
 * JOrbis has been based on their awesome works, Vorbis codec and
 * JOrbisPlayer depends on JOrbis.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/**
 * modified heavily to not be standalane program for
 * inlusion in the limewire code.
 */
package com.limegroup.gnutella.metadata;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.jcraft.jogg.Packet;
import com.jcraft.jogg.Page;
import com.jcraft.jogg.StreamState;
import com.jcraft.jogg.SyncState;
import com.jcraft.jorbis.Comment;
import com.jcraft.jorbis.Info;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.util.FileUtils;


public class JOrbisComment {
	
 private static final	Log LOG = LogFactory.getLog(JOrbisComment.class);
	
  private State state=null;

  
  
  /**
   * updates the given ogg file with the new Comment field
   * @param comment the <tt>com.jcraft.jorbis.Comment</tt> object to 
   * put in the file
   * @param file the .ogg file to be updated
   */
  public void update(Comment comment, File file) throws IOException{
  	InputStream in =null;
  	OutputStream out = null;
  	File tempFile = null;
  	
  	try {
  		state =new State();
    	in =new BufferedInputStream(new FileInputStream(file));
    
    	read(in);
    
    	//update the comment
    	state.vc=comment;
    
    	//copy the newly created file in a temp folder
    	tempFile=null;
    
    	try {
    		tempFile = File.createTempFile(file.getName(),"tmp");
    	}catch(IOException e) {
    		//sometimes either the temp path is messed up or
    		//	there isn't enough space on that partition.
    		//try to create a temp file on the same folder as the
    		//original.  It will not be around long enough to get shared
    	
    		//if an exception is thrown, let it propagate
    		LOG.debug("couldn't create temp file in $TEMP, trying elsewhere");
    	
    		tempFile = new File(file.getAbsolutePath(),
				file.getName()+".tmp");
    	}
    	out=new BufferedOutputStream(new FileOutputStream(tempFile));
    
    
    	LOG.debug("about to write ogg file");
    
    	write(out);
    
    	out.flush();
    	
    }finally {
  		if (out!=null)
  		try {out.close(); }catch(IOException ignored){}
  		if (in!=null)
  	  		try {in.close(); }catch(IOException ignored){}
  	}
    
	if (tempFile.length() == 0)
	    throw new IOException("writing of file failed");
	
	//rename fails on some rare filesystem setups
	if (!FileUtils.forceRename(tempFile,file))
		//something's seriously wrong
		throw new IOException("couldn't rename file");
    
    
  }

  private static int CHUNKSIZE=4096;


  void read(InputStream in) throws IOException{
    state.in=in;

    Page og=new Page();

    int index;
    byte[] buffer;
    int bytes=0;

    state.oy=new SyncState();
    state.oy.init();
    
    index=state.oy.buffer(CHUNKSIZE);
    buffer=state.oy.data;
    bytes=state.in.read(buffer, index, CHUNKSIZE); 
    
    state.oy.wrote(bytes);
    
    if(state.oy.pageout(og)!=1)
    	throw new IOException("input truncated, empty or not an ogg");
    
    state.serial=og.serialno();
    state.os= new StreamState();
    state.os.init(state.serial);
//  os.reset();

    state.vi=new Info();
    state.vi.init();

    state.vc=new Comment();
    state.vc.init();

    if(state.os.pagein(og)<0) 
     throw new IOException ("Error reading first page of Ogg bitstream data.");
      

    Packet header_main = new Packet();

    if(state.os.packetout(header_main)!=1)
    	throw new IOException("Error reading initial header packet.");


    if(state.vi.synthesis_headerin(state.vc, header_main)<0) 
      throw new IOException("This Ogg bitstream does not contain Vorbis data.");


    state.mainlen=header_main.bytes;
    state.mainbuf=new byte[state.mainlen];
    System.arraycopy(header_main.packet_base, header_main.packet, 
		     state.mainbuf, 0, state.mainlen);

    int i=0;
    Packet header;
    Packet header_comments=new Packet();
    Packet header_codebooks=new Packet();

    header=header_comments;
    while(i<2) {
      while(i<2) {
        int result = state.oy.pageout(og);
  	if(result == 0) break; /* Too little data so far */
   	else if(result == 1){
          state.os.pagein(og);
          while(i<2){
	    result = state.os.packetout(header);
	    if(result == 0) break;
   	    if(result == -1)
	      throw new IOException("Corrupt secondary header.");

            state.vi.synthesis_headerin(state.vc, header);
	    if(i==1) {
	      state.booklen=header.bytes;
	      state.bookbuf=new byte[state.booklen];
              System.arraycopy(header.packet_base, header.packet, 
			       state.bookbuf, 0, header.bytes);
	    }
	    i++;
  	    header = header_codebooks;
	  }
        }
      }

      index=state.oy.buffer(CHUNKSIZE);
      buffer=state.oy.data; 
      bytes=state.in.read(buffer, index, CHUNKSIZE); 
      

      if(bytes == 0 && i < 2)
        throw new IOException("EOF before end of vorbis headers.");

      state.oy.wrote(bytes);
    }

    //System.out.println(state.vi);
  }

  int write(OutputStream out) throws IOException{
    StreamState streamout=new StreamState();
    Packet header_main=new Packet();
    Packet header_comments=new Packet();
    Packet header_codebooks=new Packet();

    Page ogout=new Page();

    Packet op=new Packet();
    long granpos = 0;

    int result;

    int index;
    byte[] buffer;

    int bytes, eosin=0;
    int needflush=0, needout=0;

    header_main.bytes = state.mainlen;
    header_main.packet_base= state.mainbuf;
    header_main.packet = 0;
    header_main.b_o_s = 1;
    header_main.e_o_s = 0;
    header_main.granulepos = 0;

    header_codebooks.bytes = state.booklen;
    header_codebooks.packet_base = state.bookbuf;
    header_codebooks.packet = 0;
    header_codebooks.b_o_s = 0;
    header_codebooks.e_o_s = 0;
    header_codebooks.granulepos = 0;

    streamout.init(state.serial);

    state.vc.header_out(header_comments);

    streamout.packetin(header_main);
    streamout.packetin(header_comments);
    streamout.packetin(header_codebooks);

//System.out.println("%1");

    while((result=streamout.flush(ogout))!=0){
//System.out.println("result="+result);
      
        out.write(ogout.header_base, ogout.header, ogout.header_len);
        out.flush();
      
      
        out.write(ogout.body_base, ogout.body,ogout.body_len);
        out.flush();
    }

//System.out.println("%2");

    while(state.fetch_next_packet(op)!=0){
      int size=state.blocksize(op);
      granpos+=size;
//System.out.println("#1");
      if(needflush!=0){ 
//System.out.println("##1");
        if(streamout.flush(ogout)!=0){
          
            out.write(ogout.header_base,ogout.header,ogout.header_len);
            out.flush();
          
          
            out.write(ogout.body_base,ogout.body,ogout.body_len);
            out.flush();
          
        }
      }
//System.out.println("%2 eosin="+eosin);
      else if(needout!=0){
//System.out.println("##2");
        if(streamout.pageout(ogout)!=0){
       
            out.write(ogout.header_base,ogout.header,ogout.header_len);
            out.flush();
       
       
            out.write(ogout.body_base,ogout.body,ogout.body_len);
            out.flush();
       
        }
      }

//System.out.println("#2");

      needflush=needout=0;

      if(op.granulepos==-1){
        op.granulepos=granpos;
        streamout.packetin(op);
      }
      else{
        if(granpos>op.granulepos){
          granpos=op.granulepos;
          streamout.packetin(op);
          needflush=1;
	}
        else{
          streamout.packetin(op);
          needout=1;
	}
      }
//System.out.println("#3");
    }

//System.out.println("%3");

    streamout.e_o_s=1;
    while(streamout.flush(ogout)!=0){
      
        out.write(ogout.header_base,ogout.header,ogout.header_len);
        out.flush();
      
      
        out.write(ogout.body_base,ogout.body,ogout.body_len);
        out.flush();
      
    }

//System.out.println("%4");

    state.vi.clear();
//System.out.println("%3 eosin="+eosin);

//System.out.println("%5");

    eosin=0; /* clear it, because not all paths to here do */
    while(eosin==0){ /* We reached eos, not eof */
      /* We copy the rest of the stream (other logical streams)
	 * through, a page at a time. */
      while(true){
        result=state.oy.pageout(ogout);
//System.out.println(" result4="+result);
	if(result==0) break;
	if(result<0){
	  if (LOG.isDebugEnabled()) 
	  	LOG.debug("Corrupt or missing data, continuing...");
	}
	else{
          /* Don't bother going through the rest, we can just 
           * write the page out now */
      
            out.write(ogout.header_base,ogout.header,ogout.header_len);
            out.flush();
	  
      
            out.write(ogout.body_base,ogout.body,ogout.body_len);
            out.flush();
	  
	}
      }

      index=state.oy.buffer(CHUNKSIZE);
      buffer=state.oy.data;
      bytes=state.in.read(buffer, index, CHUNKSIZE); 
      
      
//System.out.println("bytes="+bytes);
      state.oy.wrote(bytes);

      if(bytes == 0 || bytes==-1) {
        eosin = 1;
	break;
      }
    }

    /*
cleanup:
	ogg_stream_clear(&streamout);
	ogg_packet_clear(&header_comments);

	free(state->mainbuf);
	free(state->bookbuf);

	jorbiscomment_clear_internals(state);
	if(!eosin)
	{
		state->lasterror =  
			"Error writing stream to output. "
			"Output stream may be corrupted or truncated.";
		return -1;
	}

	return 0;
       }
    */
    return 0;
  }
  
  class State{
    private final int CHUNKSIZE=4096;
    SyncState oy;
    StreamState os;
    Comment vc;
    Info vi;

    InputStream in;
    int  serial;
    byte[] mainbuf;
    byte[] bookbuf;
    int mainlen;
    int booklen;
    String lasterror;

    int prevW;

    int blocksize(Packet p){
      int _this = vi.blocksize(p);
      int ret = (_this + prevW)/4;

      if(prevW==0){
        prevW=_this;
	return 0;
      }

      prevW = _this;
      return ret;
    }

    Page og=new Page();
    int fetch_next_packet(Packet p){
      int result;
      byte[] buffer;
      int index;
      int bytes;

      result = os.packetout(p);

      if(result > 0){
	return 1;
      }

      while(oy.pageout(og) <= 0){
        index=oy.buffer(CHUNKSIZE);
        buffer=oy.data; 
        try{ bytes=in.read(buffer, index, CHUNKSIZE); }
        catch(Exception e){
          ErrorService.error(e);
          return 0;
        }
        if(bytes>0)
          oy.wrote(bytes);
	if(bytes==0 || bytes==-1) {
          return 0;
	}
      }
      os.pagein(og);

      return fetch_next_packet(p);
    }
}

}