/*
 * Copyright (C) 2012 Jacquet Wong
 *
 * 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 com.musicg.fingerprint;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import com.musicg.dsp.Resampler;
import com.musicg.processor.TopManyPointsProcessorChain;
import com.musicg.properties.FingerprintProperties;
import com.musicg.wave.Wave;
import com.musicg.wave.WaveHeader;
import com.musicg.wave.extension.Spectrogram;

/**
 * Audio fingerprint manager, handle fingerprint operations
 * 
 * @author jacquet
 *
 */
public class FingerprintManager{
	
	private FingerprintProperties fingerprintProperties=FingerprintProperties.getInstance();
	private int sampleSizePerFrame=fingerprintProperties.getSampleSizePerFrame();
	private int overlapFactor=fingerprintProperties.getOverlapFactor();
	private int numRobustPointsPerFrame=fingerprintProperties.getNumRobustPointsPerFrame();
	private int numFilterBanks=fingerprintProperties.getNumFilterBanks();
	
	/**
	 * Constructor
	 */
	public FingerprintManager(){
		
	}

	/**
	 * Extract fingerprint from Wave object
	 * 
	 * @param wave	Wave Object to be extracted fingerprint
	 * @return fingerprint in bytes
	 */
	public byte[] extractFingerprint(Wave wave){

		int[][] coordinates;	// coordinates[x][0..3]=y0..y3
		byte[] fingerprint=new byte[0];
				
		// resample to target rate
		Resampler resampler=new Resampler();
		int sourceRate = wave.getWaveHeader().getSampleRate();
        int targetRate = fingerprintProperties.getSampleRate();

       	byte[] resampledWaveData=resampler.reSample(wave.getBytes(), wave.getWaveHeader().getBitsPerSample(), sourceRate, targetRate);
		
        // update the wave header
        WaveHeader resampledWaveHeader=wave.getWaveHeader();
        resampledWaveHeader.setSampleRate(targetRate);
        
        // make resampled wave
        Wave resampledWave=new Wave(resampledWaveHeader,resampledWaveData);
        // end resample to target rate
        
		// get spectrogram's data
		Spectrogram spectrogram=resampledWave.getSpectrogram(sampleSizePerFrame, overlapFactor);
		double[][] spectorgramData=spectrogram.getNormalizedSpectrogramData();
		
		List<Integer>[] pointsLists=getRobustPointList(spectorgramData);
		int numFrames=pointsLists.length;
				
		// prepare fingerprint bytes
		coordinates=new int[numFrames][numRobustPointsPerFrame];
			
		for (int x=0; x<numFrames; x++){
			if (pointsLists[x].size()==numRobustPointsPerFrame){
				Iterator<Integer> pointsListsIterator=pointsLists[x].iterator();
				for (int y=0; y<numRobustPointsPerFrame; y++){
					coordinates[x][y]=pointsListsIterator.next();
				}
			}
			else{		
				// use -1 to fill the empty byte
				for (int y=0; y<numRobustPointsPerFrame; y++){
					coordinates[x][y]=-1;
				}
			}
		}		
		// end make fingerprint
			
		// for each valid coordinate, append with its intensity
		List<Byte> byteList=new LinkedList<Byte>();
		for (int i=0; i<numFrames; i++){
			for (int j=0; j<numRobustPointsPerFrame; j++){
				if (coordinates[i][j]!=-1){
					// first 2 bytes is x
					int x=i;
					byteList.add((byte)(x>>8));
					byteList.add((byte)x);
					
					// next 2 bytes is y
					int y=coordinates[i][j];
					byteList.add((byte)(y>>8));
					byteList.add((byte)y);
					
					// next 4 bytes is intensity
					int intensity=(int)(spectorgramData[x][y]*Integer.MAX_VALUE);	// spectorgramData is ranged from 0~1
					byteList.add((byte)(intensity>>24));
					byteList.add((byte)(intensity>>16));
					byteList.add((byte)(intensity>>8));
					byteList.add((byte)intensity);
				}
			}
		}
		// end for each valid coordinate, append with its intensity
			
		fingerprint=new byte[byteList.size()];
		Iterator<Byte> byteListIterator=byteList.iterator();
		int pointer=0;
		while(byteListIterator.hasNext()){
			fingerprint[pointer++]=byteListIterator.next();
		}

		return fingerprint;
	}

	/**
	 * Get bytes from fingerprint file
	 * 
	 * @param fingerprintFile	fingerprint filename
	 * @return fingerprint in bytes
	 */
	public byte[] getFingerprintFromFile(String fingerprintFile){
		byte[] fingerprint=null;
		try {
			InputStream fis=new FileInputStream(fingerprintFile);
			fingerprint=getFingerprintFromInputStream(fis);
			fis.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return fingerprint;
	}
	
	/**
	 * Get bytes from fingerprint inputstream
	 * 
	 * @param fingerprintFile	fingerprint inputstream
	 * @return fingerprint in bytes
	 */
	public byte[] getFingerprintFromInputStream(InputStream inputStream){		
		byte[] fingerprint=null;
		try {
			fingerprint = new byte[inputStream.available()];
			inputStream.read(fingerprint);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return fingerprint;
	}
	
	/**
	 * Save fingerprint to a file
	 * 
	 * @param fingerprint	fingerprint bytes
	 * @param filename		fingerprint filename
	 * @see	fingerprint file saved
	 */
	public void saveFingerprintAsFile(byte[] fingerprint, String filename){

        FileOutputStream fileOutputStream;
		try {
			fileOutputStream = new FileOutputStream(filename);
			fileOutputStream.write(fingerprint);
			fileOutputStream.close();
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	// robustLists[x]=y1,y2,y3,...
	private List<Integer>[] getRobustPointList(double[][] spectrogramData){
		
		int numX=spectrogramData.length;
		int numY=spectrogramData[0].length;
		
		double[][] allBanksIntensities=new double[numX][numY];		
		int bandwidthPerBank=numY/numFilterBanks;
		
		for (int b=0; b<numFilterBanks; b++){
			
			double[][] bankIntensities=new double[numX][bandwidthPerBank];
			
			for (int i=0; i<numX; i++){
				for (int j=0; j<bandwidthPerBank; j++){
					bankIntensities[i][j]=spectrogramData[i][j+b*bandwidthPerBank];
				}
			}
			
			// get the most robust point in each filter bank
			TopManyPointsProcessorChain processorChain=new TopManyPointsProcessorChain(bankIntensities,1);
			double[][] processedIntensities=processorChain.getIntensities();
			
			for (int i=0; i<numX; i++){
				for (int j=0; j<bandwidthPerBank; j++){
					allBanksIntensities[i][j+b*bandwidthPerBank]=processedIntensities[i][j];
				}
			}
		}
		
		List<int[]> robustPointList=new LinkedList<int[]>();
		
		// find robust points
		for (int i=0; i<allBanksIntensities.length; i++){
			for (int j=0; j<allBanksIntensities[i].length; j++){	
				if (allBanksIntensities[i][j]>0){
					
					int[] point=new int[]{i,j};
					//System.out.println(i+","+frequency);
					robustPointList.add(point);
				}
			}
		}
		// end find robust points

		List<Integer>[] robustLists=new LinkedList[spectrogramData.length];
		for (int i=0; i<robustLists.length; i++){
			robustLists[i]=new LinkedList<Integer>();
		}
		
		// robustLists[x]=y1,y2,y3,...
		Iterator<int[]> robustPointListIterator=robustPointList.iterator();
		while (robustPointListIterator.hasNext()){
			int[] coor=robustPointListIterator.next();
			robustLists[coor[0]].add(coor[1]);
		}
		
		// return the list per frame
		return robustLists;
	}

	/**
	 * Number of frames in a fingerprint
	 * Each frame lengths 8 bytes
	 * Usually there is more than one point in each frame, so it cannot simply divide the bytes length by 8
	 * Last 8 byte of thisFingerprint is the last frame of this wave
	 * First 2 byte of the last 8 byte is the x position of this wave, i.e. (number_of_frames-1) of this wave	 
	 * 
	 * @param fingerprint	fingerprint bytes
	 * @return number of frames of the fingerprint
	 */
	public static int getNumFrames(byte[] fingerprint){
		
		if (fingerprint.length<8){
			return 0;
		}
		
		// get the last x-coordinate (length-8&length-7)bytes from fingerprint
		int numFrames=((int)(fingerprint[fingerprint.length-8]&0xff)<<8 | (int)(fingerprint[fingerprint.length-7]&0xff))+1;
		return numFrames;
	}
}