/*
 * Copyright 2021-2025 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.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() {
	}

	/**
	 * Find the first child of the supplied parent node with the supplied element name.
	 *
	 * @param parent the parent node
	 * @param elementName the element name to find
	 * @return the first child node with the supplied element name
	 */
	public static Optional<Node> findChild(Node parent, QualifiedName elementName) {
		return children(parent) //
				.filter(child -> DomUtils.matches(elementName, child)) //
				.findFirst();
	}

	/**
	 * Find the children of the supplied parent node with the supplied element name.
	 *
	 * @param parent the parent node
	 * @param elementName the element name to find
	 * @return the children nodes with the supplied element name
	 */
	public static Stream<Node> findChildren(Node parent, QualifiedName elementName) {
		return children(parent) //
				.filter(child -> DomUtils.matches(elementName, child));
	}

	/**
	 * Stream the children of the supplied node.
	 *
	 * @param parent the parent node
	 * @return a stream of the children of the node
	 */
	public static Stream<Node> children(Node parent) {
		return stream(parent.getChildNodes());
	}

	/**
	 * Get the value of the attribute with the supplied element name of the supplied node.
	 *
	 * @param node the node
	 * @param elementName the element name of the attribute
	 * @return the value of the attribute
	 */
	public static Optional<String> getAttributeValue(Node node, QualifiedName elementName) {
		return getAttribute(node, elementName).map(Node::getNodeValue);
	}

	/**
	 * Get the attribute with the supplied element name of the supplied node.
	 *
	 * @param node the node
	 * @param elementName the element name of the attribute
	 * @return the attribute
	 */
	public static Optional<Node> getAttribute(Node node, QualifiedName elementName) {
		var attributes = node.getAttributes();
		var namespaceURI = elementName.getNamespace().getUri();
		var simpleName = elementName.getSimpleName();
		return Optional.ofNullable(attributes.getNamedItemNS(namespaceURI, simpleName)) //
				.or(() -> Optional.ofNullable(attributes.getNamedItem(simpleName)));
	}

	/**
	 * 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 node matches the
	 * supplied qualified name and the namespace of the node 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 node the node to check
	 * @return {@code true} if the node matches the qualified name
	 */
	public static boolean matches(QualifiedName qualifiedName, Node node) {
		return isNamespaceCompatible(qualifiedName, node) && qualifiedName.getSimpleName().equals(node.getLocalName());
	}

	private static boolean isNamespaceCompatible(QualifiedName qualifiedName, Node node) {
		return qualifiedName.getNamespace().getUri().equals(node.getNamespaceURI()) //
				|| parseNamespace(qualifiedName.getNamespace().getUri()) //
						.flatMap(expected -> parseNamespace(node.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;
		}
	}
}
