/*******************************************************************************
 * Copyright (c) 2015, 2016 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:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.nd.java;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.org.eclipse.jdt.internal.core.nd.Nd;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.Database;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.IndexException;

public abstract class TagTreeReader {
	public static final int[] UNUSED_RESULT = new int[1];

	public static abstract class TagHandler<T> {
		abstract public T read(Nd nd, long address, TagTreeReader reader, int[] bytesRead);
		abstract public void write(Nd nd, long address, TagTreeReader reader, T toWrite, int[] bytesWritten);
		abstract public int getSize(Nd nd, T object, TagTreeReader reader);
		public void destruct(Nd nd, long address, TagTreeReader reader) {
			// Nothing to do by default
		}
	}

	public static abstract class FixedSizeTagHandler<T> extends TagHandler<T> {
		protected abstract T read(Nd nd, long address);
		protected abstract void write(Nd nd, long address, T value);
		protected abstract int getSize();
		protected void destruct(Nd nd, long address) {
			// Nothing to do by default
		}

		@Override
		public final T read(Nd nd, long address, TagTreeReader reader, int[] bytesRead) {
			bytesRead[0] = getSize();
			return read(nd, address);
		}

		@Override
		public final void write(Nd nd, long address, TagTreeReader reader, T value, int[] bytesWritten) {
			bytesWritten[0] = getSize();
			write(nd, address, value);
		}

		@Override
		public final int getSize(Nd nd, T object, TagTreeReader reader) {
			return getSize();
		}

		@Override
		public final void destruct(Nd nd, long address, TagTreeReader reader) {
			destruct(nd, address);
		}
	}

	private TagHandler<?> readers[] = new TagHandler[256];
	private Map<TagHandler<?>, Integer> values = new HashMap<>();

	public final void add(byte key, TagHandler<?> reader) {
		this.readers[key] = reader;
		this.values.put(reader, (int) key);
	}

	public final Object read(Nd nd, long address) {
		return read(nd, address, UNUSED_RESULT);
	}

	public final Object read(Nd nd, long address, int[] bytesRead) {
		long readAddress = address;
		Database db = nd.getDB();
		byte nextByte = db.getByte(address);
		readAddress += Database.BYTE_SIZE;
		TagHandler<?> reader = this.readers[nextByte];
		if (reader == null) {
			throw nd.describeProblem()
				.addProblemAddress("tag", address, 1) //$NON-NLS-1$
				.build("Found unknown tag with value " + nextByte + " at address " + address); //$NON-NLS-1$//$NON-NLS-2$
		}

		return reader.read(nd, readAddress, this, bytesRead);
	}

	protected abstract byte getKeyFor(Object toWrite);

	public final void write(Nd nd, long address, Object toWrite) {
		write(nd, address, toWrite, UNUSED_RESULT);
	}

	@SuppressWarnings("unchecked")
	public final void write(Nd nd, long address, Object toWrite, int[] bytesWritten) {
		byte key = getKeyFor(toWrite);

		@SuppressWarnings("rawtypes")
		TagHandler handler = this.readers[key];

		if (handler == null) {
			throw nd.describeProblem().build("Invalid key " + key + " returned from getKeyFor(...)"); //$NON-NLS-1$//$NON-NLS-2$
		}

		handler.write(nd, address, this, toWrite, bytesWritten);
	}

	public final void destruct(Nd nd, long address) {
		Database db = nd.getDB();
		long readAddress = address;
		byte nextByte = db.getByte(readAddress);
		readAddress += Database.BYTE_SIZE;

		TagHandler<?> handler = this.readers[nextByte];
		if (handler == null) {
			throw nd.describeProblem()
				.addProblemAddress("tag", address, 1) //$NON-NLS-1$
				.build("Found unknown tag with value " + nextByte + " at address " + address); //$NON-NLS-1$//$NON-NLS-2$
		}

		handler.destruct(nd, readAddress, this);
	}

	@SuppressWarnings("unchecked")
	public final int getSize(Nd nd, Object toMeasure) {
		byte key = getKeyFor(toMeasure);

		@SuppressWarnings("rawtypes")
		TagHandler handler = this.readers[key];
		if (handler == null) {
			throw new IndexException("Attempted to get size of object " + toMeasure.toString() + " with unknown key " //$NON-NLS-1$//$NON-NLS-2$
					+ key);
		}

		return handler.getSize(nd, toMeasure, this);
	}
}
