/*******************************************************************************
 * Copyright (c) 2013 Institute for Software, HSR Hochschule fuer Technik
 * Rapperswil, University of applied sciences.
 * 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:
 *     Thomas Corbat (IFS) - Initial API and implementation
 *     Sergey Prigogin (Google)
 *******************************************************************************/

package org.eclipse.cdt.internal.core.pdom.dom.cpp;

import org.eclipse.cdt.core.dom.IPDOMVisitor;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.internal.core.pdom.PDOM;
import org.eclipse.cdt.internal.core.pdom.db.Database;
import org.eclipse.cdt.internal.core.pdom.dom.PDOMLinkage;
import org.eclipse.cdt.internal.core.pdom.dom.PDOMNode;
import org.eclipse.core.runtime.CoreException;

/**
 * PDOMCPPMemberBlock stores the members of a composite type and maps every member to
 * the corresponding visibility.
 */
public class PDOMCPPMemberBlock {
	/*
	 * The MAX_MEMBER_COUNT was chosen empirically by comparing PDOM file sizes of a real-life
	 * project. Six members per block resulted in the most compact PDOM.  
	 */
	private static final int MAX_MEMBER_COUNT = 6;
	private static final int VISIBILITY_BITS = 2;
	private static final int VISIBILITY_MASK = (1 << VISIBILITY_BITS) - 1;
	private static final int VISIBILITY_VALUES_PER_BYTE = 8 / VISIBILITY_BITS;

	private static final int MEMBER_POINTERS = 0;
	private static final int MEMBER_VISIBILITIES =
			MEMBER_POINTERS + Database.PTR_SIZE	* MAX_MEMBER_COUNT;
	private static final int NEXT_MEMBER_BLOCK =
			MEMBER_VISIBILITIES + (MAX_MEMBER_COUNT + VISIBILITY_VALUES_PER_BYTE - 1) / VISIBILITY_VALUES_PER_BYTE;

	protected static final int RECORD_SIZE = NEXT_MEMBER_BLOCK + Database.PTR_SIZE;

	static {
		assert (MAX_MEMBER_COUNT > 0);
	}

	private final PDOMLinkage linkage;
	private final long record;
	private int nextMemberPosition;

	public PDOMCPPMemberBlock(PDOMLinkage linkage, long record) throws CoreException {
		this.linkage = linkage;
		this.record = record;
		this.nextMemberPosition = -1;  // nextMemberPosition is unknown.
	}

	public PDOMCPPMemberBlock(PDOMLinkage linkage) throws CoreException {
		Database db = linkage.getDB();
		this.linkage = linkage;
		this.record = db.malloc(RECORD_SIZE);
	}

	private int getNextPosition() throws CoreException {
		if (nextMemberPosition < 0) {
			nextMemberPosition = 0;
			while (nextMemberPosition < MAX_MEMBER_COUNT && getMemberRecord(nextMemberPosition) != 0) {
				nextMemberPosition++;
			}
		}
		return nextMemberPosition;
	}

	private Database getDB() {
		return linkage.getDB();
	}

	private PDOM getPDOM() {
		return linkage.getPDOM();
	}

	public long getRecord() {
		return record;
	}

	public void setNextBlock(PDOMCPPMemberBlock nextBlock) throws CoreException {
		long rec = nextBlock != null ? nextBlock.getRecord() : 0;
		getDB().putRecPtr(record + NEXT_MEMBER_BLOCK, rec);
	}

	public PDOMCPPMemberBlock getNextBlock() throws CoreException {
		long rec = getDB().getRecPtr(record + NEXT_MEMBER_BLOCK);
		return rec != 0 ? new PDOMCPPMemberBlock(linkage, rec) : null;
	}

	public void addMember(PDOMNode member, int visibility) throws CoreException {
		addMember(this, member, visibility);
	}

	private static void addMember(PDOMCPPMemberBlock block, PDOMNode member, int visibility)
			throws CoreException {
		assert member.getPDOM() == block.getPDOM();
		while (true) {
			int pos = block.getNextPosition();
			if (pos < MAX_MEMBER_COUNT) {
				long memberLocationOffset = block.getMemberOffset(pos);
				block.getDB().putRecPtr(memberLocationOffset, member.getRecord());
				block.setVisibility(pos, visibility);
				block.nextMemberPosition++;
				break;
			}
			PDOMCPPMemberBlock previousBlock = block;
			block = block.getNextBlock();
			if (block == null) {
				block = new PDOMCPPMemberBlock(previousBlock.linkage);
				previousBlock.setNextBlock(block);
			}
		}
	}

	public void accept(IPDOMVisitor visitor) throws CoreException {
		PDOMCPPMemberBlock current = this;
		do {
			current.visitBlock(visitor);
		} while ((current = current.getNextBlock()) != null);
	}

	private void visitBlock(IPDOMVisitor visitor) throws CoreException {
		if (record == 0) {
			throw new NullPointerException();
		}

		int item = 0;
		long memberRecord;
		while (item < MAX_MEMBER_COUNT && (memberRecord = getMemberRecord(item++)) != 0) {
			PDOMNode node = linkage.getNode(memberRecord);
			if (node != null) {
				if (visitor.visit(node))
					node.accept(visitor);
				visitor.leave(node);
			}
		}
	}

	public void delete() throws CoreException {
		getDB().free(record);
	}

	private long getMemberRecord(int memberIndex) throws CoreException {
		return getDB().getRecPtr(getMemberOffset(memberIndex));
	}

	private long getMemberOffset(int memberIndex) {
		return record + MEMBER_POINTERS + Database.PTR_SIZE * memberIndex;
	}

	private void setVisibility(int memberIndex, int newVisibility) throws CoreException {
		newVisibility &= VISIBILITY_MASK;

		int visibilityBitOffset = memberIndex % VISIBILITY_VALUES_PER_BYTE;
		long visibilityOffset = record + MEMBER_VISIBILITIES + memberIndex / VISIBILITY_VALUES_PER_BYTE;
		int visibility = getDB().getByte(visibilityOffset);
		// Resetting the previous visibility bits of the target member.
		visibility &= ~(VISIBILITY_MASK << visibilityBitOffset * VISIBILITY_BITS);
		// Setting the new visibility bits of the target member.
		visibility |= newVisibility << visibilityBitOffset * VISIBILITY_BITS;

		getDB().putByte(visibilityOffset, (byte) visibility);
	}

	/**
	 * Returns visibility of the member, or -1 if the given binding is not a member.
	 */
	public int getVisibility(IBinding member) throws CoreException {
		return getVisibility(this, member);
	}

	private static int getVisibility(PDOMCPPMemberBlock block, IBinding member) throws CoreException {
		if (!(member instanceof PDOMNode))
			return -1;
		
		PDOMNode memberNode = (PDOMNode) member;
		if (memberNode.getPDOM() != block.getPDOM())
			return -1;
		long memberRecord = memberNode.getRecord();

		do {
			for (int memberIndex = 0; memberIndex < MAX_MEMBER_COUNT; memberIndex++) {
				long rec = block.getMemberRecord(memberIndex);
				if (rec == 0)
					return -1;
				if (rec == memberRecord)
					return block.getVisibility(memberIndex);
			}
		} while ((block = block.getNextBlock()) != null);

		return -1;
	}

	private int getVisibility(int memberIndex) throws CoreException {
		int visibilityBitOffset = memberIndex % VISIBILITY_VALUES_PER_BYTE;
		long visibilityOffset = record + MEMBER_VISIBILITIES + memberIndex / VISIBILITY_VALUES_PER_BYTE;
		int visibility = getDB().getByte(visibilityOffset);

		visibility >>>= visibilityBitOffset * VISIBILITY_BITS;
		// Filtering the visibility bits of the target member.
		visibility &= VISIBILITY_MASK;
		return visibility;
	}
}
