/*
 * 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.jena.shared.impl;

import java.util.ArrayList ;
import java.util.List ;
import java.util.Map ;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.jena.shared.PrefixMapping ;
import org.apache.jena.util.CollectionFactory ;
import org.apache.jena.util.SplitIRI;
import org.apache.jena.util.XML11Char;

/**
    An implementation of PrefixMapping. The mappings are stored in a pair
    of hash tables, one per direction. The test for a legal prefix is left to
    xerces's XMLChar.isValidNCName() predicate.
*/
public class PrefixMappingImpl implements PrefixMapping
    // See also PrefixMappingBase, PrefixMappingMem
    // in org.apache.jena.sparql.graph for a different implementation

    {
    private Map<String, String> prefixToURI;
    private Map<String, String> URItoPrefix;
    private boolean locked;

    public PrefixMappingImpl() {
        // ConcurrentHashMaps protects against breaking each datastructure
        // but does not protect against inconsistency.
        prefixToURI = new ConcurrentHashMap<>();
        URItoPrefix = new ConcurrentHashMap<>();
    }

    protected void set(String prefix, String uri) {
        prefixToURI.put(prefix, uri);
        URItoPrefix.put(uri, prefix);
    }

    protected String get(String prefix) {
        return prefixToURI.get(prefix);
    }

    protected void remove(String prefix) {
        prefixToURI.remove(prefix);
    }

    @Override
    public PrefixMapping lock() {
        locked = true;
        return this;
    }

    @Override
    public PrefixMapping setNsPrefix(String prefix, String uri) {
        checkUnlocked();
        checkLegalPrefix(prefix);
        if ( !prefix.equals("") )
            checkProperURI(uri);
        if ( uri == null )
            throw new NullPointerException("null URIs are prohibited as arguments to setNsPrefix");
        set(prefix, uri);
        return this;
    }

    @Override
    public PrefixMapping removeNsPrefix(String prefix) {
        checkUnlocked();
        remove(prefix);
        regenerateReverseMapping();
        return this;
    }

    @Override
    public PrefixMapping clearNsPrefixMap() {
        checkUnlocked();
        // Because of subclasses, do a clear by calling remove(prefix) for each prefix.
        // Copy prefixes to avoid possible concurrent modification exceptions
        List<String> prefixes = new ArrayList<>(prefixToURI.keySet());
        prefixes.forEach(p -> remove(p));
        regenerateReverseMapping();
        return this;
    }

    protected void regenerateReverseMapping() {
        URItoPrefix.clear();
        for ( Map.Entry<String, String> e : prefixToURI.entrySet() )
            URItoPrefix.put(e.getValue(), e.getKey());
    }

    protected void checkUnlocked() {
        if ( locked )
            throw new JenaLockedException(this);
    }

    // Is this suitable for use as a prefix.
    // Turtle does not have the XML QName restrictions.
    private void checkProperURI(String uri) {
//        if ( !isNiceURI(uri) )
//            throw new NamespaceEndsWithNameCharException(uri);
    }

    /**
     * Checks that a prefix is "legal" - it must be a valid XML NCName.
     * Jena 5.1.0 loosen the test from XML 1.0 to XML 1.1 NCName
     * which aligns with Turtle.
     */
    private void checkLegalPrefix(String prefix) {
        if ( prefix.length() > 0 && !XML11Char.isXML11ValidNCName(prefix) )
            throw new PrefixMapping.IllegalPrefixException(prefix);
    }

    /**
     * Test whether a URI is "nice" for RDF/XML (ends in a non-NCName character according to XML 1.0).
     * @deprecated To be removed.
     */
    @Deprecated
    public static boolean isNiceURI(String uri) {
        // Not used in Jena anymore.
        if ( uri.equals("") )
            return false;
        char last = uri.charAt(uri.length() - 1);
        return !org.apache.jena.util.XMLChar.isNCName(last);
    }

    /**
     * Add the bindings of other to our own. We defer to the general case because we
     * have to ensure the URIs are checked.
     *
     * @param other the PrefixMapping whose bindings we are to add to this.
     */
    @Override
    public PrefixMapping setNsPrefixes(PrefixMapping other) {
        return setNsPrefixes(other.getNsPrefixMap());
    }

    /**
     * Answer this PrefixMapping after updating it with the <code>(p, u)</code>
     * mappings in <code>other</code> where neither <code>p</code> nor <code>u</code>
     * appear in this mapping.
     */
    @Override
    public PrefixMapping withDefaultMappings(PrefixMapping other) {
        checkUnlocked();
        for ( Entry<String, String> e : other.getNsPrefixMap().entrySet() ) {
            String prefix = e.getKey(), uri = e.getValue();
            if ( getNsPrefixURI(prefix) == null && getNsURIPrefix(uri) == null )
                setNsPrefix(prefix, uri);
        }
        return this;
    }

    /**
     * Add the bindings in the prefixToURI to our own.
     * <p>
     * It will fail with an IllegalPrefixException if any prefix is illegal; we make
     * no guarantees about order or completeness if this happens.
     *
     * @param other the Map whose bindings we are to add to this.
     */
    @Override
    public PrefixMapping setNsPrefixes(Map<String, String> other) {
        checkUnlocked();
        for ( Entry<String, String> e : other.entrySet() )
            setNsPrefix(e.getKey(), e.getValue());
        return this;
    }

    @Override
    public String getNsPrefixURI(String prefix) {
        return get(prefix);
    }

    @Override
    public Map<String, String> getNsPrefixMap() {
        return CollectionFactory.createHashedMap(prefixToURI);
    }

    @Override
    public String getNsURIPrefix(String uri) {
        return URItoPrefix.get(uri);
    }

    /**
     * Expand a prefixed URI. There's an assumption that any URI of the form
     * Head:Tail is subject to mapping if Head is in the prefix mapping. So, if
     * someone takes it into their heads to define eg "http" or "ftp" we have
     * problems.
     */
    @Override
    public String expandPrefix(String prefixed) {
        int colon = prefixed.indexOf(':');
        if ( colon < 0 )
            return prefixed;
        else {
            String uri = get(prefixed.substring(0, colon));
            return uri == null ? prefixed : uri + prefixed.substring(colon + 1);
        }
    }

    /**
     * Answer a readable (we hope) representation of this prefix mapping.
     */
    @Override
    public String toString() {
        return "pm:" + prefixToURI;
    }

    /**
     * Answer the qname for <code>uri</code> which uses a prefix from this mapping,
     * or null if there isn't one.
     * <p>
     * Relies on <code>splitNamespace</code> to carve uri into namespace and
     * localname components; this ensures that the localname is legal and we just
     * have to (reverse-)lookup the namespace in the prefix table.
     */
    @Override
    public String qnameFor(String uri) {
        int split = SplitIRI.splitXML(uri);
        if ( split == uri.length() )
            return null;
        String ns = uri.substring(0, split);
        String local = uri.substring(split);
        String prefix = URItoPrefix.get(ns);
        return prefix == null ? null : prefix + ":" + local;
    }

    /**
     * Compress the URI using the prefix mapping.
     */
    @Override
    public String shortForm(String uri) {
        // This version of the code looks through all the maplets and checks each
        // candidate prefix URI for being a leading substring of the argument URI.
        Entry<String, String> e = findMapping(uri, true);
        return e == null ? uri : e.getKey() + ":" + uri.substring((e.getValue()).length());
    }

    @Override
    public boolean samePrefixMappingAs(PrefixMapping other) {
        return other instanceof PrefixMappingImpl ? equals((PrefixMappingImpl)other) : equalsByMap(other);
    }

    @Override
    public boolean hasNoMappings() {
        return prefixToURI.isEmpty();
    }

    @Override
    public int numPrefixes() {
        return prefixToURI.size();
    }

    protected boolean equals(PrefixMappingImpl other) {
        return other.sameAs(this);
    }

    protected boolean sameAs(PrefixMappingImpl other) {
        return prefixToURI.equals(other.prefixToURI);
    }

    protected final boolean equalsByMap(PrefixMapping other) {
        return getNsPrefixMap().equals(other.getNsPrefixMap());
    }

    /**
     * Answer a prefixToURI entry in which the value is an initial substring of
     * <code>uri</code>. If <code>partial</code> is false, then the value must equal
     * <code>uri</code>. Does a linear search of the entire prefixToURI, so not
     * terribly efficient for large maps.
     *
     * @param uri the value to search for
     * @param partial true if the match can be any leading substring, false for exact
     *     match
     * @return some entry (k, v) such that uri starts with v [equal for
     *     partial=false]
     */
    private Entry<String, String> findMapping(String uri, boolean partial) {
        for ( Entry<String, String> e : prefixToURI.entrySet() ) {
            String ss = e.getValue();
            if ( uri.startsWith(ss) && (partial || ss.length() == uri.length()) )
                return e;
        }
        return null;
    }
}
