/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *     http://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.apache.sis.xml;

import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.Namespace;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.xml.LegacyNamespaces;

import static javax.xml.stream.XMLStreamConstants.*;


/**
 * A writer replacing the namespaces used by JAXB by other namespaces to be used in the XML document
 * at marshalling time. This class forwards every method calls to the wrapped {@link XMLEventWriter},
 * with all {@code namespaceURI} arguments transformed before to be delegated.
 *
 * See {@link Transformer} for more information.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @author  Cullen Rombach (Image Matters)
 * @version 1.0
 * @since   1.0
 * @module
 */
final class TransformingWriter extends Transformer implements XMLEventWriter {
    /**
     * Location of the file listing types and their properties contained in legacy namespaces.
     * This is used for mapping new ISO 19115-3:2016 namespaces to legacy ISO 19139:2007 ones,
     * where the same {@code "http://standards.iso.org/iso/19115/-3/…"} URI is used in places
     * where legacy schema had two distinct URIs:  {@code "http://www.isotc211.org/2005/gmd"}
     * and {@code "http://standards.iso.org/iso/19115/-2/gmi/1.0"}.
     */
    private static final String FILENAME = "RenameOnExport.lst";

    /**
     * The mapping from (<var>type</var>, <var>attribute</var>) pairs to legacy namespaces.
     *
     * <ul>
     *   <li>Keys are XML names of types, ignoring {@code "_TYPE"} suffix (e.g. {@code "MI_Georectified"})</li>
     *   <li>Values are maps where:<ul>
     *     <li>Keys are XML names of properties (e.g. {@code "checkPoint"})</li>
     *     <li>Values are either:<ul>
     *       <li>Namespace URI if {@link #isNamespace(String)} returns {@code true} for that value.</li>
     *       <li>New name of the element otherwise. In such case, the map must be queried again with
     *           that new name for obtaining the namespace.</li>
     *     </ul></li>
     *   </ul></li>
     * </ul>
     *
     * This map is initialized only once and should not be modified after that point.
     */
    private static final Map<String, Map<String,String>> NAMESPACES = load(true, FILENAME, Collections.emptySet(), 60);

    /**
     * Elements that appear in different order in ISO 19139:2007 (or other legacy standards) compared
     * to ISO 19115-3:2016 (or other newer standards). Key are names of elements to reorder. Values
     * are the elements to skip before to write the element to reorder.
     *
     * <div class="note"><b>Example:</b>
     * In {@code SV_ServiceIdentification}, {@code <srv:couplingType>} appears before {@code <srv:coupledResource>}
     * according ISO 19115-3:2016. But in ISO 19139:2007, it was the reverse order. Since Apache SIS writes elements
     * in the order defined by the newer standard, {@code <couplingType>} is encountered first. The set associated
     * to that key tells us that, when writing legacy ISO 19139 document, we should skip {@code <coupledResource>}
     * before to write {@code <srv:couplingType>}.</div>
     *
     * While this map is used for reordering elements according legacy standards, the {@link QName} keys and values
     * use the namespaces of newer standards. This is because newer standards like ISO 19115-3 uses many namespaces
     * where legacy ISO 19139:2007 used only one namespace, so using the newer names reduce the risk of confusion.
     *
     * @see #toSkip
     * @see #deferred
     *
     * @todo Hard-coded for now. Should move to a resource file in a future version.
     */
    private static final Map<QName, Set<QName>> ELEMENTS_TO_REORDER;
    static {
        final Map<QName, Set<QName>> m = new HashMap<>(4);
        m.put(new QName(Namespaces.SRV, "couplingType",  "srv"), Collections.singleton(new QName(Namespaces.SRV, "coupledResource", "srv")));
        m.put(new QName(Namespaces.SRV, "connectPoint",  "srv"), Collections.singleton(new QName(Namespaces.SRV, "parameter",       "srv")));
        /*
         * ISO 19139:2997 declared 'topicCategory' and 'extent' in MD_DataIdentification subclass, while ISO 19115-3
         * moves them to the MD_Identification parent class. In order to write topicCategory at location expected by
         * legacy metadata, we need to skip all properties declared after 'topicCategory' in that parent class.
         */
        QName first;
        HashSet<QName> toSkip = new HashSet<>(Arrays.asList(
            first = new QName(      Namespaces.MRI, "extent",                  "mri"),
                    new QName(      Namespaces.MRI, "additionalDocumentation", "mri"),
                    new QName(      Namespaces.MRI, "processingLevel",         "mri"),
                    new QName(      Namespaces.MRI, "resourceMaintenance",     "mri"),
                    new QName(      Namespaces.MRI, "graphicOverview",         "mri"),
                    new QName(      Namespaces.MRI, "resourceFormat",          "mri"),
                    new QName(      Namespaces.MRI, "descriptiveKeywords",     "mri"),
                    new QName(      Namespaces.MRI, "resourceSpecificUsage",   "mri"),
                    new QName(      Namespaces.MRI, "resourceConstraints",     "mri"),
                    new QName(      Namespaces.MRI, "associatedResource",      "mri"),
                    new QName(LegacyNamespaces.GMD, "aggregationInfo",         "gmd"),
                    new QName(LegacyNamespaces.GMD, "language",                "gmd"),
                    new QName(LegacyNamespaces.GMD, "characterSet",            "gmd"),
                    new QName(      Namespaces.MRI, "defaultLocale",           "mri"),
                    new QName(      Namespaces.MRI, "otherLocale",             "mri")));
        /*
         * The 'extent' element is right after 'topicCategory' in ISO 19115-3:2016, but there was an
         * 'environmentDescription' between them in legacy ISO 19139:2007. So we add the later in the
         * list of elements to skip for 'extent'.
         */
        m.put(new QName(Namespaces.MRI, "topicCategory", "mri"), CollectionsExt.clone(toSkip));
        toSkip.remove(first);
        toSkip.add(new QName(Namespaces.MRI, "environmentDescription", "mri"));
        m.put(first, toSkip);                                                     // For <mri:extent>
        ELEMENTS_TO_REORDER = m;
    }

    /**
     * Where events are sent.
     */
    private final XMLEventWriter out;

    /**
     * Keep track of namespace URIs that have already been declared so they don't get duplicated.
     * This map is recycled in two different contexts:
     *
     * <ul>
     *   <li>In a sequence of {@code NAMESPACE} events.</li>
     *   <li>In the namespaces of a start element.</li>
     * </ul>
     */
    private final Map<String,Namespace> uniqueNamespaces;

    /**
     * {@code true} if events should be sent to {@link #deferred} instead than to {@link #out}.
     * This is set to {@code true} when we see a {@link StartElement} having one of the names
     * contained in the {@link #ELEMENTS_TO_REORDER} keys set.
     */
    private boolean isDeferring;

    /**
     * Events for which writing is deferred as long as there is elements {@linkplain #toSkip to skip}.
     * The intent is to reorder elements that appear in a different order in legacy standards compared
     * to newer standards. This is a FIFO (First-In-First-Out) queue. Namespaces are the exported ones
     * (the ones after conversions from JAXB to the XML document to write).
     *
     * <p>Elements are instance of {@link XMLEvent} or {