/*
 * 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.sparql.graph;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.BiConsumer;

import org.apache.jena.riot.system.Prefixes;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.util.SplitIRI;

/**
 * Framework for implementing {@link PrefixMapping}. It is stateless (unlike
 * {@code PrefixMappingImpl}) and implements the contract of {@link PrefixMapping},
 * providing the key algorithm and delegating storage to the subclasses.
 * <p>
 * Reverse mapping, looking up a URI to find a prefix is complex because there may be several
 * possibilities. Applications should not rely on every implementation being consistent
 * when there is a choice of which prefix to use to shorten a URI.
 */
public abstract class PrefixMappingBase implements PrefixMapping {
    /* Reverse mappings.
     *
     * The strict contract of PrefixMapping requires a separate reverse mapping to be
     * stored and manipulated, which in turn adds complexity to the storage
     * implementations. However, applications removing prefixes is unusual so we end
     * up with a lot of complexity with little value.
     *
     * Beware of the details of removing a mapping when there is another to the same URI.
     * If we had:
     *
     *     Add (pref1, U)
     *     Add (pref2, U)
     *
     * so that {@code U} reverse maps ({@link #getNsURIPrefix}) to {@code pref2} (it was
     * done second) then
     *
     *     Remove (pref2)
     *
     * causes {@code U} to reverse map to {@code pref1}.
     *
     * This feature is quite a burden on implementations and should be regarded as "legacy" -
     * an implementation may not support this complex effect.
     *
     * PrefixMappingMem does.
     * Database backed ones typically don't.
     */

    protected PrefixMappingBase() {}

    // The storage operations of an implementation.

    /** Add prefix */
    abstract protected void add(String prefix, String uri);

    /** Remove prefix. */
    abstract protected void remove(String prefix);

    /** Clear all mappings */
    abstract protected void clear();

    abstract protected boolean isEmpty();

    abstract protected int size();

    /** Return the URI that the prefix maps to. */
    abstract protected String prefixToUri(String prefix);

    /** Return a prefix that maps to the URI.
     * There may be several; the answer is any one of them.
     */
    abstract protected String uriToPrefix(String uri);

    /** Return as a map. This map is only used within this class.
     * It can be as efficient as possible.
     * It will not be modified.
     * It will not be returned to a caller of {@code PrefixMappingBase}.
     */
    abstract protected Map<String, String> asMap();

    /**
     * Return as a map. The map return is not connected to the prefix mapping implementation,
     * and does not reflect subsequent prefix mapping changes.
     */
    abstract protected Map<String, String> asMapCopy();

    /** Apply the {@link BiConsumer} to each (prefix, uri) pair. */
    abstract protected void apply(BiConsumer<String, String> action);

    /**
     * This part of the subclass API and may be overridden if an implementation can do
     * better This general implementation is based on asMap() which may be a copy or may
     * be a view of the storage directly.
     */
    protected Optional<Entry<String, String>> findMapping( String uri, boolean partial ) {
        return asMap().entrySet().stream().sequential().filter(e->{
            String ss = e.getValue();
            if (uri.startsWith( ss ) && (partial || ss.length() == uri.length()))
                return true;
            return false;
        }).findFirst();
    }

    @Override
    public PrefixMapping setNsPrefix(String prefix, String uri) {
        checkLegalPrefix(prefix);
        add(prefix, uri);
        return this;
    }

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

    @Override
    public PrefixMapping clearNsPrefixMap() {
        clear();
        return this;
    }

    /**
     * Check for legal prefix. If not, throw
     * {@link org.apache.jena.shared.PrefixMapping.IllegalPrefixException}
     */
    private static void checkLegalPrefix(String prefix) {
        if ( prefix == null )
            throw new PrefixMapping.IllegalPrefixException("null for prefix");
        if ( ! Prefixes.isLegalPrefix(prefix) )
            throw new PrefixMapping.IllegalPrefixException(prefix);
    }

    @Override
    public PrefixMapping setNsPrefixes(PrefixMapping pmap) {
        if ( pmap instanceof PrefixMappingBase ) {
            PrefixMappingBase pmap2 = (PrefixMappingBase)pmap;
            pmap2.apply((p,u)->setNsPrefix(p, u));
            return this;
        }
        // Need to create as a map (a copy) and then add.
        setNsPrefixes(pmap.getNsPrefixMap());
        return this;
    }

    @Override
    public PrefixMapping setNsPrefixes(Map<String, String> map) {
        map.forEach(this::setNsPrefix);
        return this;
    }

    /* Not javadoc.
     * This is the unusual contract as defined by the interface: update
     * this PrefixMapping with the bindings in <code>map</code>, only adding those
     * (p, u) pairs for which neither p nor u appears in this mapping.
     */
    @Override
    public PrefixMapping withDefaultMappings(PrefixMapping pmap) {
        if ( pmap instanceof PrefixMappingBase ) {
            // Direct. No intermediate Map<> object created,
            PrefixMappingBase pmap2 = (PrefixMappingBase)pmap;
            pmap2.apply(this::addWith);
            return this;
        }
        // Creates a Map<>.
        Map<String, String> map = pmap.getNsPrefixMap();
        map.forEach(this::addWith);
        return this;
    }

    private void addWith(String p, String u) {
        if ( prefixToUri(p) == null && uriToPrefix(u) == null )
            add(p,u);
    }

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

    @Override
    public String getNsURIPrefix(String uri) {
        return uriToPrefix(uri);
    }

    @Override
    public Map<String, String> getNsPrefixMap() {
        return asMapCopy();
    }

    @Override
    public String expandPrefix(String prefixed) {
        int colon = prefixed.indexOf(':');
        if ( colon < 0 )
            return prefixed;
        else {
            String prefix = prefixed.substring(0, colon);
            String uri = getNsPrefixURI(prefix);
            return uri == null ? prefixed : uri + prefixed.substring(colon + 1);
        }
    }

    @Override
    public String qnameFor(String uri) {
        // This operation applies a fixed splitting rule and
        // does not search for other possibilities.
        // See findMapping.
        int split = SplitIRI.splitXML(uri); // Split by XML rules (no digit starting the local part.)
        String ns = uri.substring(0, split);
        String local = uri.substring(split);
        if ( local.equals("") )
            return null;
        String prefix = getNsURIPrefix(ns);
        return prefix == null ? null : prefix + ":" + local;
    }

    @Override
    public String shortForm(String uri) {
        Optional<Entry<String, String>> e = findMapping(uri, true);
        if ( ! e.isPresent() )
            return uri;
        return e.get().getKey() + ":" + uri.substring((e.get().getValue()).length());
    }

    @Override
    public boolean samePrefixMappingAs(PrefixMapping other) {
        if ( numPrefixes() != other.numPrefixes() )
            return false;
        return getNsPrefixMap().equals(other.getNsPrefixMap());
    }

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

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

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

    @Override
    public String toString() {
        StringJoiner sj = new StringJoiner(", ");
        apply((p,u)->sj.add(p+"->"+u));
        return "pm:["+numPrefixes()+"]{"+sj.toString()+"}";
    }
}
