/*
 * 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
 *
 *   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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.sparql.service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import org.apache.jena.query.ARQ;
import org.apache.jena.sparql.ARQConstants;
import org.apache.jena.sparql.service.bulk.ChainingServiceExecutorBulk;
import org.apache.jena.sparql.service.single.ChainingServiceExecutor;
import org.apache.jena.sparql.service.single.ChainingServiceExecutorWrapper;
import org.apache.jena.sparql.service.single.ServiceExecutor;
import org.apache.jena.sparql.service.single.ServiceExecutorHttp;
import org.apache.jena.sparql.util.Context;

/**
 * Registry for service executors that can be extended with custom ones.
 * Bulk and single (=non-bulk) executors are maintained in two separate lists.
 *
 * Default execution will always start with the bulk list first.
 * Once that list is exhausted by means of all bulk executors having delegated the request,
 * then the non-bulk ones will be considered.
 * There is no need to explicitly register a bulk-to-non-bulk bridge.
 */
public class ServiceExecutorRegistry
{
    // A list of bulk service executors which are tried in the given order
    List<ChainingServiceExecutorBulk> bulkChain = new ArrayList<>();

    // A list of single (non-bulk) service executors which are tried in the given order
    // This list is only considered after after the bulk registry
    List<ChainingServiceExecutor> singleChain = new ArrayList<>();

    public static ServiceExecutorRegistry standardRegistry()
    {
        ServiceExecutorRegistry reg = get(ARQ.getContext()) ;
        return reg ;
    }

    /** A "call with SPARQL query" service executor. */
    public static ServiceExecutor httpService = new ServiceExecutorHttp();

    /** Blindly adds the default executor(s); concretely adds the http executor */
    public static void initWithDefaults(ServiceExecutorRegistry registry) {
        registry.add(httpService);
    }

    public static void init() {
        // Initialize if there is no registry already set
        ServiceExecutorRegistry reg = new ServiceExecutorRegistry();
        initWithDefaults(reg);
        set(ARQ.getContext(), reg) ;
    }

    /**
     * Return the global instance from the ARQ context; create that instance if needed.
     * Never returns null.
     */
    public static ServiceExecutorRegistry get()
    {
        // Initialize if there is no registry already set
        ServiceExecutorRegistry reg = get(ARQ.getContext()) ;
        if ( reg == null )
        {
            init() ;
            reg = get(ARQ.getContext()) ;
        }

        return reg ;
    }

    /** Return the registry from the given context if present; otherwise return the global one */
    public static ServiceExecutorRegistry chooseRegistry(Context context) {
        ServiceExecutorRegistry result = ServiceExecutorRegistry.get(context);
        if (result == null) {
            result = get();
        }
        return result;
    }

    /** Return the registry from the given context only; null if there is none */
    public static ServiceExecutorRegistry get(Context context) {
        if ( context == null )
            return null ;
        return (ServiceExecutorRegistry)context.get(ARQConstants.registryServiceExecutors) ;
    }

    public static void set(Context context, ServiceExecutorRegistry reg)
    {
        context.set(ARQConstants.registryServiceExecutors, reg) ;
    }

    /**
     * Copies the origin registry into a new one, or makes a fresh instance if the specified registry is {@code null}.
     * @param from {@link ServiceExecutorRegistry} or {@code null}
     * @return {@link ServiceExecutorRegistry} a new instance
     */
    public static ServiceExecutorRegistry createFrom(ServiceExecutorRegistry from) {
        ServiceExecutorRegistry res = new ServiceExecutorRegistry();
        if (from != null) {
            res.bulkChain.addAll(from.bulkChain);
            res.singleChain.addAll(from.singleChain);
        }
        return res;
    }

    public ServiceExecutorRegistry()
    {}

    /*
     * Non-bulk API
     */

    /** Prepend the given service executor as a link to the per-binding chain */
    public ServiceExecutorRegistry addSingleLink(ChainingServiceExecutor f) {
        Objects.requireNonNull(f) ;
        singleChain.add(0, f) ;
        return this;
    }

    /** Remove the given service executor from the per-binding chain */
    public ServiceExecutorRegistry removeSingleLink(ChainingServiceExecutor f) {
        singleChain.remove(f) ;
        return this;
    }

    /** Wraps the given service executor as a chaining one and prepends it
     *  to the non-bulk chain via {@link #addSingleLink(ChainingServiceExecutor)} */
    public ServiceExecutorRegistry add(ServiceExecutor f) {
        Objects.requireNonNull(f) ;
        return addSingleLink(new ChainingServiceExecutorWrapper(f));
    }

    /** Remove a given service executor - internally attempts to unwrap every chaining service executor */
    public ServiceExecutorRegistry remove(ServiceExecutor f) {
        Iterator<ChainingServiceExecutor> it = singleChain.iterator();
        while (it.hasNext()) {
            ChainingServiceExecutor cse = it.next();
            if (cse instanceof ChainingServiceExecutorWrapper) {
                ChainingServiceExecutorWrapper wrapper = (ChainingServiceExecutorWrapper)cse;
                ServiceExecutor delegate = wrapper.getDelegate();
                if (Objects.equals(delegate, f)) {
                    it.remove();
                }
            }
        }
        return this;
    }

    /** Retrieve the actual list of per-binding executors; allows for re-ordering */
    public List<ChainingServiceExecutor> getSingleChain() {
        return singleChain;
    }

    /*
     * Bulk API
     */

    /** Add a chaining bulk executor as a link to the executor chain */
    public ServiceExecutorRegistry addBulkLink(ChainingServiceExecutorBulk f) {
        Objects.requireNonNull(f) ;
        bulkChain.add(0, f) ;
        return this;
    }

    /** Remove the given service executor */
    public ServiceExecutorRegistry removeBulkLink(ChainingServiceExecutorBulk f) {
        bulkChain.remove(f) ;
        return this;
    }

    /** Retrieve the actual list of bulk executors; allows for re-ordering */
    public List<ChainingServiceExecutorBulk> getBulkChain() {
        return bulkChain;
    }

    /*
     * Utility
     */

    /** Create an independent copy of the registry */
    public ServiceExecutorRegistry copy() {
        ServiceExecutorRegistry result = new ServiceExecutorRegistry();
        result.getSingleChain().addAll(getSingleChain());
        result.getBulkChain().addAll(getBulkChain());
        return result;
    }

    /** Return a copy of the registry in the context (if present) or a fresh instance */
    public ServiceExecutorRegistry copyFrom(Context cxt) {
        ServiceExecutorRegistry tmp = ServiceExecutorRegistry.get(cxt);
        ServiceExecutorRegistry result = tmp == null ? new ServiceExecutorRegistry() : tmp.copy();
        return result;
    }
}
