/*******************************************************************************
 * Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors
 * This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Evgeny Mandrikov - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.core.internal.analysis.filter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.BitSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodNode;

/**
 * Filters out instructions that were inlined by Kotlin compiler.
 */
public final class KotlinInlineFilter implements IFilter {

	private int firstGeneratedLineNumber = -1;

	public void filter(final MethodNode methodNode,
			final IFilterContext context, final IFilterOutput output) {
		if (context.getSourceDebugExtension() == null) {
			return;
		}

		if (!KotlinGeneratedFilter.isKotlinClass(context)) {
			return;
		}

		if (firstGeneratedLineNumber == -1) {
			firstGeneratedLineNumber = getFirstGeneratedLineNumber(
					context.getSourceFileName(),
					context.getSourceDebugExtension());
		}

		int line = 0;
		for (final AbstractInsnNode i : methodNode.instructions) {
			if (AbstractInsnNode.LINE == i.getType()) {
				line = ((LineNumberNode) i).line;
			}
			if (line >= firstGeneratedLineNumber) {
				output.ignore(i, i);
			}
		}
	}

	private static int getFirstGeneratedLineNumber(final String sourceFileName,
			final String smap) {
		try {
			final BufferedReader br = new BufferedReader(
					new StringReader(smap));
			expectLine(br, "SMAP");
			// OutputFileName
			expectLine(br, sourceFileName);
			// DefaultStratumId
			expectLine(br, "Kotlin");
			// StratumSection
			expectLine(br, "*S Kotlin");
			// FileSection
			expectLine(br, "*F");
			final BitSet sourceFileIds = new BitSet();
			String line;
			while (!"*L".equals(line = br.readLine())) {
				// AbsoluteFileName
				br.readLine();

				final Matcher m = FILE_INFO_PATTERN.matcher(line);
				if (!m.matches()) {
					throw new IllegalStateException(
							"Unexpected SMAP line: " + line);
				}
				final String fileName = m.group(2);
				if (fileName.equals(sourceFileName)) {
					sourceFileIds.set(Integer.parseInt(m.group(1)));
				}
			}
			if (sourceFileIds.isEmpty()) {
				throw new IllegalStateException("Unexpected SMAP FileSection");
			}
			// LineSection
			int min = Integer.MAX_VALUE;
			while (true) {
				line = br.readLine();
				if (line.equals("*E") || line.equals("*S KotlinDebug")) {
					break;
				}
				final Matcher m = LINE_INFO_PATTERN.matcher(line);
				if (!m.matches()) {
					throw new IllegalStateException(
							"Unexpected SMAP line: " + line);
				}
				final int inputStartLine = Integer.parseInt(m.group(1));
				final int lineFileID = Integer
						.parseInt(m.group(2).substring(1));
				final int outputStartLine = Integer.parseInt(m.group(4));
				if (sourceFileIds.get(lineFileID)
						&& inputStartLine == outputStartLine) {
					continue;
				}
				min = Math.min(outputStartLine, min);
			}
			return min;
		} catch (final IOException e) {
			// Must not happen with StringReader
			throw new AssertionError(e);
		}
	}

	private static void expectLine(final BufferedReader br,
			final String expected) throws IOException {
		final String line = br.readLine();
		if (!expected.equals(line)) {
			throw new IllegalStateException("Unexpected SMAP line: " + line);
		}
	}

	private static final Pattern LINE_INFO_PATTERN = Pattern.compile("" //
			+ "([0-9]++)" // InputStartLine
			+ "(#[0-9]++)?+" // LineFileID
			+ "(,[0-9]++)?+" // RepeatCount
			+ ":([0-9]++)" // OutputStartLine
			+ "(,[0-9]++)?+" // OutputLineIncrement
	);

	private static final Pattern FILE_INFO_PATTERN = Pattern.compile("" //
			+ "\\+ ([0-9]++)" // FileID
			+ " (.++)" // FileName
	);

}
