/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * This file is part of Beaver Parser Generator.                       *
 * Copyright (C) 2003,2004 Alexander Demenchuk <alder@softanvil.com>.  *
 * All rights reserved.                                                *
 * See the file "LICENSE" for the terms and conditions for copying,    *
 * distribution and modification of Beaver.                            *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package beaver;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.InflaterInputStream;

/**
 * Parsing Tables
 */
public final class ParsingTables
{
	/** A table with all actions */
	private final short[] actions;

	/**
	 * A table containing the lookahead for each entry in "actions" table.
	 * Used to detect "collisions".
	 */
	final short[] lookaheads;

	/**
	 * For each state, the offset into "actions" table that is used to find action for a terminal
	 * that has been fetched from the scanner.
	 */
	final int[] actn_offsets;

	/**
	 * For each state, the offset into "actions" table that is used to find a next parser's state
	 * using a nonterminal that has been created by a reduced production.
	 */
	private final int[] goto_offsets;

	/** Default action for each state */
	private final short[] default_actions;

	/**
	 * A table with encoded production information.
	 * <p/>
	 * Each slot in this table is a "structure":
	 * <pre>
	 *   short lhs_symbol_id ; // Symbol on the left-hand side of the production
	 *   short rhs_length    ; // Number of right-hand side symbols in the production
	 * </pre>
	 * where lhs_symbol_id uses high 16 bit of this structure, and rhs_length - lower 16 bits
	 */
	final int[] rule_infos;

	/** ID of the "error" nonterminal */
	final short error_symbol_id;

	/** Indicates whether action tables were compressed. */
	final boolean compressed;
	
	/** Number of terminal symbols. */
	final int n_term;

	/**
	 * Ensures that parser tables are loaded.
	 *
	 * @param impl_class class of the instance of the Parser
	 */
	public ParsingTables(Class impl_class)
	{
		this(getSpecAsResourceStream(impl_class));
	}
	
	public ParsingTables(String spec)
	{
		this(new ByteArrayInputStream(decode(spec)));
	}
	
	private ParsingTables(InputStream in)
	{
		try
		{
			DataInputStream data = new DataInputStream(new InflaterInputStream(in));
			try
			{
				int len = data.readInt();
				actions = new short[len];
				for (int i = 0; i < len; i++)
				{
					actions[i] = data.readShort();
				}
				lookaheads = new short[len];
				for (int i = 0; i < len; i++)
				{
					lookaheads[i] = data.readShort();
				}
				
				len = data.readInt();
				actn_offsets = new int[len];
				for (int i = 0; i < len; i++)
				{
					actn_offsets[i] = data.readInt();
				}
				goto_offsets = new int[len];
				for (int i = 0; i < len; i++)
				{
					goto_offsets[i] = data.readInt();
				}
				
				len = data.readInt();
				compressed = len != 0;
				if (compressed)
				{
					default_actions = new short[len];
					for (int i = 0; i < len; i++)
					{
						default_actions[i] = data.readShort();
					}
				}
				else
				{
					default_actions = null;
				}
				
				int min_nt_id = Integer.MAX_VALUE;
				len = data.readInt();
				rule_infos = new int[len];
				for (int i = 0; i < len; i++)
				{
					rule_infos[i] = data.readInt();
					min_nt_id = Math.min(min_nt_id, rule_infos[i] >>> 16);
				}
				n_term = min_nt_id;
				
				error_symbol_id = data.readShort();
			}
			finally
			{
				data.close();
			}
		}
		catch (IOException e)
		{
			throw new IllegalStateException("cannot initialize parser tables: " + e.getMessage());
		}
	}
	
	/**
	 * Scans lookaheads expected in a given state for a terminal symbol.
	 * Used in error recovery when an unexpected terminal is replaced with one that is expected.
	 * 
	 * @param state in which error occured
	 * @return ID of the expected terminal symbol or -1 if there is none
	 */
	final short findFirstTerminal(int state)
	{
		int offset = actn_offsets[state];
		for (short term_id = offset < 0 ? (short) -offset : 0; term_id < n_term; term_id++)
		{
			int index = offset + term_id;
			if (index >= lookaheads.length)
				break;
			if (lookaheads[index] == term_id)
				return term_id;
		}
		return -1;
	}

	/**
	 * Find the appropriate action for a parser in a given state with a specified terminal look-ahead.
	 *
	 * @param state     of a parser
	 * @param lookahead
	 * @return parser action
	 */
	final short findParserAction(int state, short lookahead)
	{
		int index = actn_offsets[state];
		if (index != UNUSED_OFFSET)
		{
			index += lookahead;
			if (0 <= index && index < actions.length && lookaheads[index] == lookahead)
			{
				return actions[index];
			}
		}
		return compressed ? default_actions[state] : 0;
	}

	/**
	 * Find the appropriate action for a parser in a given state with a specified nonterminal look-ahead.
	 * In this case the only possible outcomes are either a state to shift to or an accept action.
	 *
	 * @param state     of a parser
	 * @param lookahead
	 * @return parser action
	 */
	final short findNextState(int state, short lookahead)
	{
		int index = goto_offsets[state];
		if (index != UNUSED_OFFSET)
		{
			index += lookahead;
			if (0 <= index && index < actions.length && lookaheads[index] == lookahead)
			{
				return actions[index];
			}
		}
		return compressed ? default_actions[state] : 0;
	}

	static final int UNUSED_OFFSET = Integer.MIN_VALUE;
	
	static byte[] decode(String spec)
	{
		char[] chars = spec.toCharArray();
		if (chars.length % 4 != 0)
			throw new IllegalArgumentException("corrupted encoding");
		int len = chars.length / 4 * 3;
		byte[] bytes = new byte[chars[chars.length - 1] == '=' ? chars[chars.length - 2] == '=' ? len - 2 : len - 1 : len];
		
		len -= 3;
		int ci = 0, bi = 0;
		while (bi < len)
		{
			int acc = decode(chars[ci++]) << 18 | decode(chars[ci++]) << 12 | decode(chars[ci++]) << 6 | decode(chars[ci++]);
			bytes[bi++] = (byte) (acc >> 16);
			bytes[bi++] = (byte) (acc >> 8 & 0xFF);
			bytes[bi++] = (byte) (acc & 0xFF);
		}
		int acc = decode(chars[ci++]) << 18 | decode(chars[ci++]) << 12 | decode(chars[ci++]) << 6 | decode(chars[ci++]);
		bytes[bi++] = (byte) (acc >> 16);
		if (bi < bytes.length)
		{
			bytes[bi++] = (byte) (acc >> 8 & 0xFF);
			if (bi < bytes.length)
			{
				bytes[bi++] = (byte) (acc & 0xFF);
			}
		}
		return bytes;
	}
	
	static int decode(char c)
	{
		if (c <= '9')
		{
			if (c >= '0')
				return c - '0';
			if (c == '#')
				return 62;
			if (c == '$')
				return 63;
		}
		else if (c <= 'Z')
		{
			if (c >= 'A')
				return c - 'A' + 10;
			if (c == '=')
				return 0;
		}
		else if ('a' <= c && c <= 'z')
			return c - 'a' + 36;
		throw new IllegalStateException("illegal encoding character '" + c + "'");
	}
	
	static InputStream getSpecAsResourceStream(Class impl_class)
	{
		String name = impl_class.getName();
		name = name.substring(name.lastIndexOf('.') + 1) + ".spec";
		InputStream spec_stream = impl_class.getResourceAsStream(name);
		if (spec_stream == null)
			throw new IllegalStateException("parser specification not found");
		return spec_stream;
	}
}

