/*******************************************************************************
 * Copyright (c) 2011, 2014 Google, Inc and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.core.parser.scanner;

import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.cdt.core.parser.ISignificantMacros;
import org.eclipse.cdt.core.parser.util.CharArrayObjectMap;
import org.eclipse.cdt.core.parser.util.CharArrayUtils;

/**
 * A set of static methods to encode Map<String, String> as an array of characters and to decode
 * it back.
 *
 * The map is encoded as:
 * {@code <number_of_entries>,<key1><value1>...<keyN><valueN>}.
 * <p>
 * Each string is encoded as: {@code <number_of_characters>,<characters>}.
 * A {@code null} string is encoded as a single comma.
 */
public class SignificantMacros implements ISignificantMacros {
	public static final char[] DEFINED = {0};
	public static final char[] UNDEFINED = {1}; 
	private static final Comparator<char[]> SORTER = new Comparator<char[]>() {
		@Override
		public int compare(char[] s1, char[] s2) {
			return CharArrayUtils.compare(s1, s2);
		}
	}; 
			
	private final char[] fEncoded;
	private int fHash;

	public SignificantMacros(char[] encoded) {
		assert encoded != null;
		fEncoded= encoded;
	}

	public SignificantMacros(CharArrayObjectMap<char[]> sigMacros) {
		fEncoded= encode(sigMacros);
	}

	private char[] encode(CharArrayObjectMap<char[]> sigMacros) {
		StringBuilder buffer= new StringBuilder();
		char[][] keys= sigMacros.keys();
		Arrays.sort(keys, SORTER);
		for (char[] name : keys) {
			char[] value= sigMacros.get(name);
			buffer.append((char) name.length).append(name);
			buffer.append((char) value.length).append(value);
		}
		int len= buffer.length();
		char[] result= new char[len];
		buffer.getChars(0, len, result, 0);
		return result;
	}

	@Override
	public int hashCode() {
		int h = fHash;
		if (h == 0) {
		    char val[] = fEncoded;
		    int len = fEncoded.length;
		    for (int i = 0; i < len; i++) {
		    	h = 31*h + val[i];
		    }
		    fHash = h;
		}
		return h;
	}

	@Override
	public boolean equals(Object obj) {
		return obj instanceof SignificantMacros 
				&& hashCode() == obj.hashCode() 
				&& CharArrayUtils.equals(fEncoded, ((SignificantMacros) obj).fEncoded);
	}

	@Override
	public boolean accept(IVisitor visitor) {
		final char[] encoded = fEncoded;
		final int len = encoded.length;
		int i= 0;
		while (i < len) {
			final int len1 = encoded[i++];
			int v= i + len1;
			if (v >= len) 
				break;
			final int len2 = encoded[v++];
			if (v+len2 > len) 
				break;
			
			char[] macro= extract(encoded, i, len1);
			i= v+len2;
			if (len2 == 1) {
				if (encoded[v] == UNDEFINED[0]) {
					if (!visitor.visitUndefined(macro))
						return false;
					continue;
				} 
				if (encoded[v] == DEFINED[0]) {
					if (!visitor.visitDefined(macro))
						return false;
					continue;
				} 
			}				
			final char[] value = extract(encoded, v, len2);
			if (!visitor.visitValue(macro, value))
				return false;
		}
		return true;
	}

	public char[] extract(final char[] source, int from, final int length) {
		char[] value= new char[length];
		System.arraycopy(source, from, value, 0, length);
		return value;
	}

	@Override
	public char[] encode() {
		return fEncoded;
	}
	
	/**
	 * For debugging purposes.
	 */
	@SuppressWarnings("nls")
	@Override
	public String toString() {
		final StringBuilder buf= new StringBuilder();
		buf.append('{');
		accept(new IVisitor() {
			@Override
			public boolean visitValue(char[] macro, char[] value) {
				buf.append(macro).append('=').append(value).append(',');
				return true;
			}
			@Override
			public boolean visitUndefined(char[] macro) {
				buf.append(macro).append('=').append("null,");
				return true;
			}
			@Override
			public boolean visitDefined(char[] macro) {
				buf.append(macro).append('=').append("*,");
				return true;
			}
		});
		int buflen = buf.length();
		if (buflen > 1)
			buf.setLength(buflen-1);
		buf.append('}');
		return buf.toString();
	}

	public static char[] shortenValue(char[] expansion) {
		if (expansion.length <= 16)
			return expansion;
		char[] result= new char[16];
		System.arraycopy(expansion, 0, result, 0, 8);
		StreamHasher hasher= new StreamHasher();
		hasher.addChunk(expansion);
		long hash= hasher.computeHash();
		for(int i= 0; i<8; i++) {
			result[8+i]= (char) (hash & 0xff);
			hash= hash >> 1;
		}
		return result;
	}
}
