/*
 * Copyright 2021-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.opentest4j.reporting.tooling.core.util;

import org.apiguardian.api.API;
import org.opentest4j.reporting.schema.Namespace;
import org.opentest4j.reporting.schema.QualifiedName;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.Arrays;
import java.util.Optional;
import java.util.function.IntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.apiguardian.api.API.Status.INTERNAL;

/**
 * Internal utils to work with DOM types
 *
 * @since 0.2.0
 */
@API(status = INTERNAL, since = "0.2.0")
public class DomUtils {

	private static final Pattern NAMESPACE_VERSION_PATTERN = Pattern.compile("^(.+)/([0-9.]+)$");

	private DomUtils() {
	}

	/**
	 * Stream the nodes of the supplied node list.
	 *
	 * @param nodeList the node list to stream
	 * @return a stream of the nodes in the node list
	 */
	public static Stream<Node> stream(NodeList nodeList) {
		return stream(nodeList.getLength(), nodeList::item);
	}

	/**
	 * Stream the nodes of the supplied named node map.
	 *
	 * @param namedNodeMap the named node map to stream
	 * @return a stream of the nodes in the named node map
	 */
	public static Stream<Node> stream(NamedNodeMap namedNodeMap) {
		return stream(namedNodeMap.getLength(), namedNodeMap::item);
	}

	private static Stream<Node> stream(int length, IntFunction<Node> item) {
		return IntStream.range(0, length).mapToObj(item);
	}

	/**
	 * Check whether the simple name of the supplied element matches the
	 * supplied qualified name and the namespace of the element is compatible
	 * with the namespace of the qualified name (same base URI and version less
	 * than or equal to the version of the qualified name).
	 *
	 * @param qualifiedName the qualified name to match
	 * @param element the element to check
	 * @return {@code true} if the element matches the qualified name
	 */
	public static boolean matches(QualifiedName qualifiedName, Element element) {
		return isNamespaceCompatible(qualifiedName, element)
				&& qualifiedName.getSimpleName().equals(element.getLocalName());
	}

	private static boolean isNamespaceCompatible(QualifiedName qualifiedName, Element element) {
		return qualifiedName.getNamespace().getUri().equals(element.getNamespaceURI()) //
				|| parseNamespace(qualifiedName.getNamespace().getUri()) //
						.flatMap(expected -> parseNamespace(element.getNamespaceURI()).filter(expected::isCompatible)) //
						.isPresent();
	}

	/**
	 * Parse the supplied namespace URI into a base URI and a version.
	 *
	 * @param namespaceUri the namespace URI to parse
	 * @return the versioned namespace
	 */
	public static Optional<VersionedNamespace> parseNamespace(String namespaceUri) {
		return Optional.ofNullable(namespaceUri) //
				.map(NAMESPACE_VERSION_PATTERN::matcher) //
				.filter(Matcher::matches) //
				.map(it -> new VersionedNamespace(it.group(1), NamespaceVersion.parse(it.group(2))));
	}

	/**
	 * A namespace base URI and version.
	 */
	public static class VersionedNamespace {

		private final String baseUri;
		private final NamespaceVersion version;

		VersionedNamespace(String baseUri, NamespaceVersion version) {
			this.baseUri = baseUri;
			this.version = version;
		}

		/**
		 * {@return the base URI}
		 */
		public String getBaseUri() {
			return baseUri;
		}

		/**
		 * {@return the version}
		 */
		public NamespaceVersion getVersion() {
			return version;
		}

		boolean isCompatible(VersionedNamespace that) {
			return this.getBaseUri().equals(that.getBaseUri()) //
					&& that.getVersion().compareTo(this.getVersion()) <= 0;
		}

		/**
		 * Create a new versioned namespace with the supplied version.
		 *
		 * @param version the version
		 * @return the new versioned namespace
		 */
		public VersionedNamespace withVersion(NamespaceVersion version) {
			return new VersionedNamespace(getBaseUri(), version);
		}

		/**
		 * Convert this versioned namespace to a namespace.
		 *
		 * @return the namespace
		 */
		public Namespace toNamespace() {
			return Namespace.of(getBaseUri() + "/" + getVersion());
		}
	}

	/**
	 * Represents a namespace version.
	 */
	public static class NamespaceVersion implements Comparable<NamespaceVersion> {

		/**
		 * Parse the supplied version string.
		 *
		 * @param version the version string to parse
		 * @return the parsed version
		 */
		public static NamespaceVersion parse(String version) {
			var parts = Arrays.stream(version.split("\\.")) //
					.mapToInt(Integer::parseInt) //
					.toArray();
			return new NamespaceVersion(version, parts);
		}

		private final String stringValue;
		private final int[] parts;

		NamespaceVersion(String stringValue, int[] parts) {
			this.stringValue = stringValue;
			this.parts = parts;
		}

		@Override
		public int compareTo(NamespaceVersion that) {
			for (int i = 0; i < Math.min(this.parts.length, that.parts.length); i++) {
				int comparison = Integer.compare(this.parts[i], that.parts[i]);
				if (comparison != 0) {
					return comparison;
				}
			}
			return Integer.compare(this.parts.length, that.parts.length);
		}

		@Override
		public String toString() {
			return stringValue;
		}
	}
}
