/*
 * 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.events.api;

import org.apiguardian.api.API;
import org.opentest4j.reporting.schema.Namespace;
import org.opentest4j.reporting.schema.QualifiedName;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;

import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Pattern;

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

/**
 * Base class for elements.
 *
 * @param <T> element type
 * @since 0.1.0
 */
@API(status = EXPERIMENTAL, since = "0.1.0")
public class Element<T extends Element<T>> implements Appendable<T> {

	// Using zero-width assertions in the split pattern simplifies the splitting process: All split parts
	// (including the first and last one) can be used directly, without having to re-add separator characters.
	private static final Pattern CDATA_SPLIT_PATTERN = Pattern.compile("(?<=]])(?=>)");
	static final String CDATA_WORKAROUND_COMMENT = "CDATA-WORKAROUND-" + UUID.randomUUID();

	private final Context context;
	private final Namespace namespace;
	final org.w3c.dom.Element domElement;

	/**
	 * Create an instance with the supplied {@linkplain Context context} and {@linkplain QualifiedName qualified name}.
	 *
	 * @param context       the context of this instance
	 * @param qualifiedName the qualified name of this instance
	 */
	public Element(Context context, QualifiedName qualifiedName) {
		this.context = context;
		this.namespace = qualifiedName.getNamespace();
		this.domElement = context.createElement(qualifiedName);
	}

	org.w3c.dom.Element getDomElement() {
		return domElement;
	}

	/**
	 * Add text content to this element.
	 *
	 * @param value the text content
	 * @return this element
	 */
	public Element<T> withContent(String value) {
		domElement.setTextContent(value);
		return this;
	}

	/**
	 * Add a CDATA section to this element.
	 *
	 * @param value the content of the CDATA section
	 * @return this element
	 */
	@SuppressWarnings("UnusedReturnValue")
	public Element<T> withCDataSection(String value) {
		String[] parts = CDATA_SPLIT_PATTERN.split(value);
		domElement.appendChild(document().createCDATASection(parts[0]));
		for (int i = 1; i < parts.length; i++) {
			// Force XML serializer to create a new CDATA section by inserting a comment node in between
			// that is filtered out in DefaultDocumentWriter.SingleElementStreamWriter#writeComment.
			domElement.appendChild(document().createComment(CDATA_WORKAROUND_COMMENT));
			domElement.appendChild(document().createCDATASection(parts[i]));
		}
		return this;
	}

	/**
	 * Add an attribute to this element.
	 *
	 * @param qualifiedName the qualified name of the attribute
	 * @param value         the attribute value
	 * @return this element
	 */
	public Element<T> withAttribute(QualifiedName qualifiedName, String value) {
		Attr attr = createAttr(qualifiedName);
		attr.setValue(value);
		domElement.setAttributeNode(attr);
		return this;
	}

	private Attr createAttr(QualifiedName qualifiedName) {
		return namespace.equals(qualifiedName.getNamespace()) //
				? document().createAttribute(qualifiedName.getSimpleName()) //
				: document().createAttributeNS(qualifiedName.getNamespace().getUri(), context.prefixed(qualifiedName));
	}

	private Document document() {
		return context.getDocument();
	}

	@Override
	public <C extends ChildElement<T, ? super C>> Appendable<T> append(Factory<C> creator,
			Consumer<? super C> configurer) {
		C child = creator.createAndConfigure(context, configurer);
		domElement.appendChild(child.domElement);
		return this;
	}

	@Override
	public String toString() {
		return "Element{domElement=" + domElement + '}';
	}

}
