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

import java.util.Objects;
import java.util.Optional;

import org.apache.jena.atlas.lib.Cache;
import org.apache.jena.atlas.lib.CacheFactory;

/**
 * A resolver is a base IRI and a policy for resolution. The policy choices are
 * whether to resolve against the base, or only consider the IRI being processed, and
 * whether to allow relative IRIs or to enforce that "base resolve IRI" is an
 * acceptable IRI for use in RDF (the test is {@link IRIx#isReference()}).
 * <p>
 * Normal use is to resolve and not allow relative IRIs.
 * <p>
 * The base may be null to support passing around a resolver that accepts/reject
 * IRIs. For application to check IRIs, use {@link IRIs#check}/{@link IRIs#checkEx}
 * directly.
 */
public class IRIxResolver {

    private final IRIx base;
    private final boolean resolve;
    private final boolean allowRelative;

    private IRIxResolver(IRIx base, boolean resolve, boolean allowRelative) {
        this.base = base;
        this.resolve = resolve;
        this.allowRelative = allowRelative;
    }

    /** Return the base of this resolver */
    public IRIx getBase() { return base; }

    /** Return the base of this resolver as a string */
    public String getBaseURI() { return base == null ? null : base.str(); }

    /*
     * Some providers are expensive compared to the cost of parsing.
     * During parsing a lot of IRIs are being created, many the same. For example,
     * e.g. properties or blocks of triples with the same subject.
     */
    private static int DftCacheSize = 500;
    private Cache<String, IRIx> cache = CacheFactory.createCache(DftCacheSize);

    /** Resolve the argument URI string according to resolver policy */
    public IRIx resolve(String other) {
        Objects.requireNonNull(other);
        if ( cache == null )
            return resolve0(other);

        // To allow for exceptions, we try the cache, and if no hit,
        // make the result then try again to fill the cache.
        // Because for a given key there is one right answer, it does not matter
        // if any thread gets in and changes the cache (cache operations are
        // thread safe).

        IRIx iri = cache.getIfPresent(other);
        if ( iri != null )
            return iri;
        // May throw an exception
        IRIx iriValue = resolve0(other);
        return cache.get(other, k->iriValue);
    }

    private IRIx resolve0(String str) {
        IRIx x = (base != null && resolve)
                ? base.resolve(str)
                : IRIx.create(str);
        if ( ! allowRelative && x.isRelative() )
            throw new RelativeIRIException("Relative IRI: <"+str+">");
        return x;
    }

    /** Create a new resolver with the same policies as the old one. */
    public IRIxResolver resetBase(IRIx newBase) {
        return new IRIxResolver(newBase, resolve, allowRelative);
    }

    @Override
    public String toString() {
        return "IRIxResolver[base=" + base + ", resolve=" + resolve + ", relative=" + allowRelative+"]";
    }

   public static Builder create() { return new Builder(); }

   /**
    * Create a {@link IRIxResolver} with the base URI which is resolved against the
    * current system default base.
    */
   public static Builder create(IRIxResolver original) {
       Builder builder = new Builder();
       builder.base = Optional.ofNullable(original.base);
       builder.resolve = original.resolve;
       builder.allowRelative = original.allowRelative;
       return builder;
   }

   /**
    * Create a builder for a {@link IRIxResolver} with the base URI which is resolved against the
    * current system default base.
    */
   public static Builder create(IRIx baseIRI) {
       return new Builder().base(baseIRI);
   }

   /**
    * Create a builder for a {@link IRIxResolver} with the base URI
    * which is resolved against the current system default base.
    */
   public static Builder create(String baseStr) {
       IRIx base = (baseStr == null) ? null : IRIs.resolveIRI(baseStr);
       return new Builder().base(base);
   }

   public static class Builder {
       // null is "unset".
       private Optional<IRIx> base   = null;
       private boolean resolve       = true;
       private boolean allowRelative = true;

       private Builder() {}

       public Builder base(IRIx baseURI) {
           this.base = Optional.ofNullable(baseURI);
           return this;
       }

       public Builder base(String baseStr) {
           IRIx baseIRI = (baseStr == null) ? null : IRIs.resolveIRI(baseStr);
           this.base = Optional.ofNullable(baseIRI);
           return this;
       }

       public Builder noBase() {
           this.base = Optional.empty();
           return this;
       }

       public Builder resolve(boolean resolveURIs) {
           this.resolve = resolveURIs;
           return this;
       }

       public Builder allowRelative(boolean allowRelative) {
           this.allowRelative = allowRelative;
           return this;
       }

       public IRIxResolver build() {
           if ( base == null )
               throw new IRIException("Base has not been set");
           IRIx baseIRI = base.orElse(null);
           return new IRIxResolver(baseIRI, resolve, allowRelative);
       }
   }
}

