/***************************************************************************
*                                                                          *
* Panako - acoustic fingerprinting                                         *
* Copyright (C) 2014 - 2017 - Joren Six / IPEM                             *
*                                                                          *
* This program is free software: you can redistribute it and/or modify     *
* it under the terms of the GNU Affero General Public License as           *
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.                      *
*                                                                          *
* You should have received a copy of the GNU Affero General Public License *
* along with this program.  If not, see <http://www.gnu.org/licenses/>     *
*                                                                          *
****************************************************************************
*    ______   ________   ___   __    ________   ___   ___   ______         *
*   /_____/\ /_______/\ /__/\ /__/\ /_______/\ /___/\/__/\ /_____/\        *
*   \:::_ \ \\::: _  \ \\::\_\\  \ \\::: _  \ \\::.\ \\ \ \\:::_ \ \       *
*    \:(_) \ \\::(_)  \ \\:. `-\  \ \\::(_)  \ \\:: \/_) \ \\:\ \ \ \      *
*     \: ___\/ \:: __  \ \\:. _    \ \\:: __  \ \\:. __  ( ( \:\ \ \ \     *
*      \ \ \    \:.\ \  \ \\. \`-\  \ \\:.\ \  \ \\: \ )  \ \ \:\_\ \ \    *
*       \_\/     \__\/\__\/ \__\/ \__\/ \__\/\__\/ \__\/\__\/  \_____\/    *
*                                                                          *
****************************************************************************
*                                                                          *
*                              Panako                                      *
*                       Acoustic Fingerprinting                            *
*                                                                          *
****************************************************************************/

package be.panako.strategy.nfft.storage.redisson;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;

import org.redisson.Redisson;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import be.panako.strategy.nfft.NFFTFingerprint;
import be.panako.strategy.nfft.storage.NFFTFingerprintHit;
import be.panako.strategy.nfft.storage.NFFTFingerprintQueryMatch;
import be.panako.strategy.nfft.storage.Storage;
import be.panako.util.FileUtils;
import be.panako.util.StopWatch;

public class NFFTRedisStorage implements Storage {
	
	private final static Logger LOG = Logger.getLogger(NFFTRedisStorage.class.getName());
	
	/**
	 * The single instance of the storage.
	 */
	private static Storage instance;

	/**
	 * A mutex for synchronization purposes
	 */
	private static Object mutex = new Object();

	/**
	 * @return Returns or creates a storage instance. This should be a thread
	 *         safe operation.
	 */
	public synchronized static Storage getInstance() {
		if (instance == null) {
			synchronized (mutex) {
				if (instance == null) {
					instance = new NFFTRedisStorage();
				}
			}
		}
		return instance;
	}
	
	private final RMap<Integer, List<Integer>> fingerprintMap;
	private final RMap<Integer, String> metaDataMap;
	private final RAtomicLong secondsStored;
	//private final Random rnd; 
	
	public NFFTRedisStorage() {
		Config config = new Config();
		config.useSingleServer().setAddress("127.0.0.1:6379");
		//config.useSingleServer().setAddress("157.193.92.74:6379");

		final RedissonClient redisson = Redisson.create(config);
		fingerprintMap = redisson.getMap("integerMap");
		metaDataMap = redisson.getMap("descriptionMap");
		
		secondsStored = redisson.getAtomicLong("secondsStoredAtomicLong");
				
		Runtime.getRuntime().addShutdownHook(new Thread(new Runnable(){
			@Override
			public void run() {
				redisson.shutdown();
			}}));
		
		//rnd = new Random();
	}
	
	public void clearDatabase(){
		metaDataMap.clear();
		fingerprintMap.clear();
		secondsStored.set(0);
	}

	@Override
	public void addAudio(int identifier, String description) {
		metaDataMap.put(identifier, description);
	}

	@Override
	public void audioObjectAdded(int numberOfSeconds) {
		secondsStored.addAndGet(numberOfSeconds);
	}

	@Override
	public int getNumberOfFingerprints() {
		return fingerprintMap.size();
	}

	@Override
	public String getAudioDescription(int identifier) {
		return metaDataMap.get(identifier);
	}

	@Override
	public int getNumberOfAudioObjects() {
		return metaDataMap.size();
	}

	@Override
	public double getNumberOfSeconds() {
		return secondsStored.get();
	}

	@Override
	public boolean hasDescription(String description) {
		int indentifier = FileUtils.getIdentifier(description);
		return description.equals(getAudioDescription(indentifier));
	}

	@Override
	public float addFingerprint(int identifier, int time, int landmarkHash) {
		List<Integer> list;
		if(fingerprintMap.containsKey(landmarkHash)){
			list = fingerprintMap.get(landmarkHash);
		}else{
			list = new ArrayList<Integer>();
		}
		//store identifier first, then time
		list.add(identifier);
		list.add(time);
		if(list.size()>1500){
			//an even index between 0 and 1498
			//remove? item?
		}
		fingerprintMap.put(landmarkHash,list);
		return 0;
	}

	@Override
	public List<NFFTFingerprintQueryMatch> getMatches(
			List<NFFTFingerprint> fingerprints, int size) {
		StopWatch w = new StopWatch();
		Set<NFFTFingerprintHit> allHits = new HashSet<NFFTFingerprintHit>();	
		try{
		    for(NFFTFingerprint fingerprint: fingerprints){
		    	int hash = fingerprint.hash();
		    	List<Integer> data = fingerprintMap.get(hash);
		    	for(int i = 0 ; i < data.size() -1 ; i+=2){
		    		NFFTFingerprintHit lh = new NFFTFingerprintHit();
					int queryTime = fingerprint.t1;//queryTimeForHash.get(landmarkHash);
					lh.identifier = data.get(i);
					lh.matchTime = data.get(i+1);
					lh.timeDifference = lh.matchTime - queryTime;
					lh.queryTime = queryTime;
					allHits.add(lh);
		    	}
		    }
		}catch(Exception e){
			System.out.println(e.getMessage());
		}
	
	    LOG.info(String.format("Redis answered to query of %d hashes in %s and found %d hits.", fingerprints.size(),w.formattedToString(),allHits.size()));
		
	   
	    HashMap<Integer,List<NFFTFingerprintHit>> hitsPerIdentifer = new HashMap<Integer, List<NFFTFingerprintHit>>();
		for(NFFTFingerprintHit hit : allHits){
			if(!hitsPerIdentifer.containsKey(hit.identifier)){
				hitsPerIdentifer.put(hit.identifier, new ArrayList<NFFTFingerprintHit>());
			}
			List<NFFTFingerprintHit> hitsForIdentifier = hitsPerIdentifer.get(hit.identifier);
			hitsForIdentifier.add(hit);
		}
		
		//This could be done in an SQL where clause also (with e.g. a group by identifier /having count(identifier) >= 5 clause)
		//removes random chance hash hits.
		int minMatchingLandmarksThreshold = 3;
		for(Integer identifier: new HashSet<Integer>(hitsPerIdentifer.keySet())){
			if(hitsPerIdentifer.get(identifier).size() < minMatchingLandmarksThreshold){
				hitsPerIdentifer.remove(identifier);
			}
		}
		
		//Holds the maximum number of aligned offsets per identifier
		//The key is the number of aligned offsets. The list contains a list of identifiers. 
		//The list will most of the time only contain one entry.
		//The most common offset will be at the top of the list (reversed integer order).
		TreeMap<Integer,List<Integer>> scorePerIdentifier = new TreeMap<Integer,List<Integer>>(reverseIntegerOrder);
		//A map that contains the most popular offset per identifier
		HashMap<Integer,Integer> offsetPerIdentifier = new HashMap<Integer,Integer>();

		//iterate every list per identifier and count the most popular offsets
		for(Integer identifier: hitsPerIdentifer.keySet()){
			//use this hash table to count the most popular offsets 
			HashMap<Integer,Integer> popularOffsetsPerIdentifier = new HashMap<Integer, Integer>();
			//the final score for the identifier
			int maxAlignedOffsets = 0;
			
			//add the offsets for each landmark hit 
			for(NFFTFingerprintHit hit : hitsPerIdentifer.get(identifier)){
				if(!popularOffsetsPerIdentifier.containsKey(hit.timeDifference)){
					popularOffsetsPerIdentifier.put(hit.timeDifference, 0);	
				}
				int numberOfAlignedOffsets = 1 + popularOffsetsPerIdentifier.get(hit.timeDifference);
				popularOffsetsPerIdentifier.put(hit.timeDifference,numberOfAlignedOffsets);
				if(numberOfAlignedOffsets > maxAlignedOffsets){
					maxAlignedOffsets = numberOfAlignedOffsets;
					offsetPerIdentifier.put(identifier, hit.timeDifference);
				}
			}
			//Threshold on aligned offsets. Ignores identifiers with less than 3 aligned offsets
			if(maxAlignedOffsets > 4){
				if(!scorePerIdentifier.containsKey(maxAlignedOffsets)){
					scorePerIdentifier.put(maxAlignedOffsets, new ArrayList<Integer>());
				}
				scorePerIdentifier.get(maxAlignedOffsets).add(identifier);						
			}
		}
		
		
		//Holds the maximum number of aligned and ordered offsets per identifier
			//The key is the number of aligned offsets. The list contains a list of identifiers. 
			//The list will most of the time only contain one entry.
			//The most common offset will be at the top of the list (reversed integer order).
			TreeMap<Integer,List<Integer>> scoreOderedPerIdentifier = new TreeMap<Integer,List<Integer>>(reverseIntegerOrder);
				
		//check if the order in the query is the same as the order in the reference audio
		for(Integer alignedOffsets : scorePerIdentifier.keySet()){
			List<Integer> identifiers = scorePerIdentifier.get(alignedOffsets);
			for(Integer identifier : identifiers){
				//by making it a set only unique times are left
				HashMap<Integer,NFFTFingerprintHit> hitsWithBestOffset = new HashMap<Integer,NFFTFingerprintHit>();
				for(NFFTFingerprintHit hit : hitsPerIdentifer.get(identifier)){
					if(hit.timeDifference == offsetPerIdentifier.get(identifier)){
						hitsWithBestOffset.put(hit.queryTime,hit);
					}
				}
				List<NFFTFingerprintHit> hitsToSortyByQueryTime = new ArrayList<NFFTFingerprintHit>(hitsWithBestOffset.values());
				List<NFFTFingerprintHit> hitsToSortyByReferenceTime = new ArrayList<NFFTFingerprintHit>(hitsWithBestOffset.values());
				Collections.sort(hitsToSortyByQueryTime,new Comparator<NFFTFingerprintHit>() {
					@Override
					public int compare(NFFTFingerprintHit o1,
							NFFTFingerprintHit o2) {
						return Integer.valueOf(o1.queryTime).compareTo(o2.queryTime);
					}
				});
				Collections.sort(hitsToSortyByReferenceTime,new Comparator<NFFTFingerprintHit>() {
					@Override
					public int compare(NFFTFingerprintHit o1,
							NFFTFingerprintHit o2) {
						return Integer.valueOf(o1.matchTime).compareTo(o2.matchTime);
					}
				});
				
				int countInOrderAlignedHits = 0;
				for(int i = 0 ; i < hitsToSortyByQueryTime.size() ; i++){
					if(hitsToSortyByQueryTime.get(i).equals(hitsToSortyByReferenceTime.get(i))){
						countInOrderAlignedHits++;
					}
				}
				if(countInOrderAlignedHits>4){
					if(!scoreOderedPerIdentifier.containsKey(countInOrderAlignedHits)){
						scoreOderedPerIdentifier.put(countInOrderAlignedHits, new ArrayList<Integer>());
					}
					scoreOderedPerIdentifier.get(countInOrderAlignedHits).add(identifier);	
				}
			}
		}
		
		
		List<NFFTFingerprintQueryMatch> matches = new ArrayList<NFFTFingerprintQueryMatch>();
		for(Integer alignedOffsets : scoreOderedPerIdentifier.keySet()){
			List<Integer> identifiers = scoreOderedPerIdentifier.get(alignedOffsets);
			for(Integer identifier : identifiers){
				NFFTFingerprintQueryMatch match = new NFFTFingerprintQueryMatch();
				match.identifier = identifier;
				match.score = alignedOffsets;
				match.mostPopularOffset = offsetPerIdentifier.get(identifier);
				if(matches.size() < size){
					matches.add(match);
				}
			}
			if(matches.size() >= size){
				break;
			}
		}
		return matches;
	}
	
	private Comparator<Integer> reverseIntegerOrder = new Comparator<Integer>(){
		@Override
		public int compare(Integer o1, Integer o2) {
			return o2.compareTo(o1);
		}
	};

}