/* Copyright (c) 2012,
 * Mathias Kuhring, KuhringM@rki.de, Robert Koch-Institut, Germany, 
 * All rights reserved. For details, please note the license.txt.
 */

package de.rki.ng4.ipig.mapping;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Vector;

/**
 * <p>Implementation of the Wu-Manber algorithm for multi-pattern searching.</p>
 * 
 * <p>A short description: <a href="https://www.mi.fu-berlin.de/wiki/pub/ABI/AdvancedAlgorithms11_Searching/script-01_02-Intro_HorspoolWuManber.pdf">
 * https://www.mi.fu-berlin.de/wiki/pub/ABI/AdvancedAlgorithms11_Searching/script-01_02-Intro_HorspoolWuManber.pdf</a> (Page 7 and 8).</p>
 * 
 * @author Mathias Kuhring
 */
public class WuManber {

	private HashMap<String,Integer> shift;
	private HashMap<String,Vector<Integer>> hash;

	private HashMap<Integer,Vector<Integer>> results;

	private Vector<String> patterns;
	private String text;

	private StringBuffer output;

	private int alphabetSize;
	private int blockSize;
	private int lmin, lmax, defaultValue;

	/**
	 * The constructor takes a list of patterns and prepares the lookup tables (preprocessing).
	 * 
	 * @param patterns the list (Vector) of patterns to be searched
	 * @param alphabetSize the alphabet size, to determine an optimal block size
	 */
	public WuManber(Vector<String> patterns, int alphabetSize){
		shift = new HashMap<String,Integer>();
		hash = new HashMap<String,Vector<Integer>>();
		results = new HashMap<Integer,Vector<Integer>>();
		this.patterns = patterns;
		this.alphabetSize = alphabetSize;
		preprocessing();
	}

	/**
	 * The searchIn method takes a text and returns matches of the patterns in this text.
	 * The results are given as a HashMap with indices to the pattern input Vector as keys and Vectors containing the match positions as values.
	 * 
	 * @param text the text in which it is searched
	 * @return HashMap with pattern indices as keys and match position lists as values
	 */
	public HashMap<Integer,Vector<Integer>> searchIn(String text){
		this.text = text;
		results.clear();

		int pos = lmin-1;
		String key;
		Vector<Integer> idx;

		while (pos < text.length()){
			key = text.substring(pos-blockSize+1, pos+1);
			if (shift.containsKey(key)){
				if (shift.get(key) == 0){
					idx = hash.get(key);
					verification(pos, idx);
					pos++;
				}
				else
					pos = pos + shift.get(key);
			}
			else{
				pos = pos + defaultValue;
			}	
		}

		return results;
	}

	// the wu-manber verification step
	private void verification(int pos, Vector<Integer> indices){
		String part = text.substring(Math.max(0,(pos-lmax+1)), pos+1);
		String pattern;
		for (int i : indices){
			pattern = patterns.get(i);
			if (part.endsWith(pattern)){
				if (!results.containsKey(i))
					results.put(i, new Vector<Integer>());
				results.get(i).add(pos-pattern.length()+1);
			}
		}
	}

	// calculation of lmin, lmax, the block size and a default value for the shift table.
	private void preprocessing(){
		lmin = Integer.MAX_VALUE;
		lmax = Integer.MIN_VALUE;
		for (String p : this.patterns){
			lmin = Math.min(lmin, p.length());
			lmax = Math.max(lmax, p.length());
		}
		blockSize = (int) (Math.log(2.0 * lmin * patterns.size()) / Math.log(alphabetSize));
		defaultValue = lmin - blockSize + 1;
		buildShift();
		buildHash();
	}

	// building the shift table
	private void buildShift(){
		String block;
		for (String pattern : patterns){
			for (int i=0; i<pattern.length()-blockSize+1; i++){
				block = pattern.substring(i, i+blockSize);
				if (shift.get(block) == null){
					shift.put(block, Math.min(defaultValue, pattern.length()-(i+blockSize)));
				}
				else{
					shift.put(block, Math.min(shift.get(block), pattern.length()-(i+blockSize)));
				}
			}
		}
	}

	// building the hash table
	private void buildHash(){
		String block;
		for (int i=0; i<patterns.size(); i++){
			block = patterns.get(i).substring(patterns.get(i).length()-blockSize, patterns.get(i).length());
			if (!hash.containsKey(block))
				hash.put(block, new Vector<Integer>());
			hash.get(block).add(i);
		}
	}

	/**
	 * <p>A simple file output of the results. The text is printed in one row and the patterns are printed in the next row
	 * according to all their matching positions. Overlaps of patterns are not marked.</p>
	 * 
	 * <p>This method only builds a file if the results list is not empty, so usually after a search pass.</p>
	 * 
	 * @param filename name for the file
	 */
	public void printToFile(String filename){
		if (!results.isEmpty()){
			try {
				BufferedWriter resBuffer = new BufferedWriter (new FileWriter(new File(filename)));
				output = new StringBuffer();
				for (int i=0; i<text.length(); i++){
					output.append('_');
				}
				int idx;
				Vector<Integer> pos;
				for (Entry<Integer, Vector<Integer>> entry : results.entrySet()){
					idx = entry.getKey();
					pos = entry.getValue();
					for (int p : pos)
						output.replace(p, p+patterns.get(idx).length(), patterns.get(idx));
				}
				resBuffer.write(text);
				resBuffer.newLine();
				resBuffer.write(output.toString());
				resBuffer.close();
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}
		}
	}
}
