/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.examples.readmetool;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Hashtable;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;

/**
 * This class is a simple parser implementing the IReadmeFileParser interface.
 * It parses a Readme file into sections based on the existence of numbered
 * section tags in the input. A line beginning with a number followed by a dot
 * will be taken as a section indicator (for example, 1., 2., or 12.). As well,
 * a line beginning with a subsection-style series of numbers will also be taken
 * as a section indicator, and can be used to indicate subsections (for example,
 * 1.1, or 1.1.12).
 */
public class DefaultSectionsParser implements IReadmeFileParser {
	/**
	 * Returns the mark element that is the logical parent of the given mark number.
	 * Each dot in a mark number represents a parent-child separation. For example,
	 * the parent of 1.2 is 1, the parent of 1.4.1 is 1.4. Returns null if there is
	 * no appropriate parent.
	 */
	protected IAdaptable getParent(Hashtable<String, MarkElement> toc, String number) {
		int lastDot = number.lastIndexOf('.');
		if (lastDot < 0)
			return null;
		String parentNumber = number.substring(0, lastDot);
		return toc.get(parentNumber);
	}

	/**
	 * Returns a string containing the contents of the given file. Returns an empty
	 * string if there were any errors reading the file.
	 */
	protected String getText(IFile file) {
		try {
			InputStream in = file.getContents();
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			byte[] buf = new byte[1024];
			int read = in.read(buf);
			while (read > 0) {
				out.write(buf, 0, read);
				read = in.read(buf);
			}
			return out.toString();
		} catch (CoreException | IOException e) {
			// do nothing
		}
		return ""; //$NON-NLS-1$
	}

	/**
	 * Parses the input given by the argument.
	 *
	 * @param file the element containing the input text
	 * @return an element collection representing the parsed input
	 */
	@Override
	public MarkElement[] parse(IFile file) {
		Hashtable<String, MarkElement> markTable = new Hashtable<>(40);
		ArrayList<MarkElement> topLevel = new ArrayList<>();
		String s = getText(file);
		int start = 0;
		int end = -1;
		int lineno = 0;
		int lastlineno = 0;
		MarkElement lastme = null;
		int ix;

		// parse content for headings
		ix = s.indexOf('\n', start);
		while (ix != -1) {
			start = end + 1;
			end = ix = s.indexOf('\n', start);
			lineno++;
			if (ix != -1) {
				// skip blanks
				while (s.charAt(start) == ' ' || s.charAt(start) == '\t') {
					start++;
				}
				if (Character.isDigit(s.charAt(start))) {
					if (lastme != null) {
						lastme.setNumberOfLines(lineno - lastlineno - 1);
					}
					lastlineno = lineno;
					String markName = parseHeading(s, start, end);

					// get the parent mark, if any.
					String markNumber = parseNumber(markName);
					IAdaptable parent = getParent(markTable, markNumber);
					if (parent == null)
						parent = file;

					MarkElement me = new MarkElement(parent, markName, start, end - start);
					lastme = me;

					markTable.put(markNumber, me);
					if (parent == file) {
						topLevel.add(me);
					}
				}
			}
		}
		if (lastme != null) {
			// set the number of lines for the last section
			lastme.setNumberOfLines(lineno - lastlineno - 1);
		}
		MarkElement[] results = new MarkElement[topLevel.size()];
		return topLevel.toArray(results);
	}

	/**
	 * Creates a section name from the buffer and trims trailing space characters.
	 *
	 * @param buffer the string from which to create the section name
	 * @param start  the start index
	 * @param end    the end index
	 * @return a section name
	 */
	private String parseHeading(String buffer, int start, int end) {
		while (Character.isWhitespace(buffer.charAt(end - 1)) && end > start) {
			end--;
		}
		return buffer.substring(start, end);
	}

	/**
	 * Returns the number for this heading. A heading consists of a number (an
	 * arbitrary string of numbers and dots), followed by arbitrary text.
	 */
	protected String parseNumber(String heading) {
		int start = 0;
		int end = heading.length();
		char c;
		do {
			c = heading.charAt(start++);
		} while ((c == '.' || Character.isDigit(c)) && start < end);

		// disregard trailing dots
		while (heading.charAt(start - 1) == '.' && start > 0) {
			start--;
		}
		return heading.substring(0, start);
	}
}
