/*******************************************************************************
 * Copyright (c) 2011, 2014 Google, Inc and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * 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;
	}
}
