/**
 * 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.jdbc.remote.connections;

import java.net.http.HttpClient;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.List;

import org.apache.jena.jdbc.JdbcCompatibility;
import org.apache.jena.jdbc.connections.JenaConnection;
import org.apache.jena.jdbc.remote.metadata.RemoteEndpointMetadata;
import org.apache.jena.jdbc.remote.statements.RemoteEndpointPreparedStatement;
import org.apache.jena.jdbc.remote.statements.RemoteEndpointStatement;
import org.apache.jena.jdbc.statements.JenaPreparedStatement;
import org.apache.jena.jdbc.statements.JenaStatement;

/**
 * Represents a connection to a remote endpoint
 *
 */
public class RemoteEndpointConnection extends JenaConnection {

    private String queryService, updateService;
    private boolean closed = false;
    private boolean readonly = false;
    private List<String> defaultGraphUris;
    private List<String> namedGraphUris;
    private List<String> usingGraphUris;
    private List<String> usingNamedGraphUris;
    private HttpClient client;
    private DatabaseMetaData metadata;
    private String selectResultsType, modelResultsType;

    /**
     * Creates a new remote connection
     *
     * @param queryEndpoint
     *            SPARQL Query Endpoint
     * @param updateEndpoint
     *            SPARQL Update Endpoint
     * @param holdability
     *            Result Set holdability
     * @param compatibilityLevel
     *            JDBC compatibility level, see {@link JdbcCompatibility}
     * @throws SQLException
     *             Thrown if there is a problem creating the connection
     */
    public RemoteEndpointConnection(String queryEndpoint, String updateEndpoint, int holdability, int compatibilityLevel)
            throws SQLException {
        this(queryEndpoint, updateEndpoint, null, null, null, null, null, holdability, compatibilityLevel, null, null);
    }

    /**
     * Creates a new remote connection
     *
     * @param queryEndpoint
     *            SPARQL Query Endpoint
     * @param updateEndpoint
     *            SPARQL Update Endpoint
     * @param defaultGraphUris
     *            Default Graph URIs for SPARQL queries
     * @param namedGraphUris
     *            Named Graph URIs for SPARQL queries
     * @param usingGraphUris
     *            Default Graph URIs for SPARQL updates
     * @param usingNamedGraphUris
     *            Named Graph URIs for SPARQL updates
     * @param client
     *            HTTP client
     * @param holdability
     *            Result Set holdability
     * @param compatibilityLevel
     *            JDBC compatibility level, see {@link JdbcCompatibility}
     * @param selectResultsType
     *            Results Type to request for {@code SELECT} queries against the
     *            remote endpoint
     * @param modelResultsType
     *            Results Type to request for {@code CONSTRUCT} and
     *            {@code DESCRIBE} queries against the remote endpoint
     * @throws SQLException
     *             Thrown if there is a problem creating the connection
     */
    public RemoteEndpointConnection(String queryEndpoint, String updateEndpoint, List<String> defaultGraphUris,
            List<String> namedGraphUris, List<String> usingGraphUris, List<String> usingNamedGraphUris,
            HttpClient client, int holdability, int compatibilityLevel, String selectResultsType,
            String modelResultsType) throws SQLException {
        super(holdability, true, Connection.TRANSACTION_NONE, compatibilityLevel);
        if (queryEndpoint == null && updateEndpoint == null)
            throw new SQLException("Must specify one/both of a query endpoint and update endpoint");
        this.queryService = queryEndpoint;
        this.updateService = updateEndpoint;
        this.readonly = this.updateService == null;
        this.defaultGraphUris = defaultGraphUris;
        this.namedGraphUris = namedGraphUris;
        this.usingGraphUris = usingGraphUris;
        this.usingNamedGraphUris = usingNamedGraphUris;
        this.client = client;
        this.metadata = new RemoteEndpointMetadata(this);
        this.selectResultsType = selectResultsType;
        this.modelResultsType = modelResultsType;
    }

    /**
     * Gets the SPARQL query endpoint that is in use
     *
     * @return Endpoint URI or null for write only connections
     */
    public String getQueryEndpoint() {
        return this.queryService;
    }

    /**
     * Gets the SPARQL update endpoint that is in use
     *
     * @return Endpoint URI or null for read-only connections
     */
    public String getUpdateEndpoint() {
        return this.updateService;
    }

    /**
     * Gets the default graphs for SPARQL queries (may be null if none were set)
     *
     * @return Default graphs
     */
    public List<String> getDefaultGraphURIs() {
        return this.defaultGraphUris;
    }

    /**
     * Gets the named graphs for SPARQL queries (may be null if none were set)
     *
     * @return Named graphs
     */
    public List<String> getNamedGraphURIs() {
        return this.namedGraphUris;
    }

    /**
     * Gets the default graphs for SPARQL updates (may be null if none were set)
     *
     * @return Default graphs
     */
    public List<String> getUsingGraphURIs() {
        return this.usingGraphUris;
    }

    /**
     * Gets the named graphs for SPARQL updates (may be null if none were set)
     *
     * @return Named graphs
     */
    public List<String> getUsingNamedGraphURIs() {
        return this.usingNamedGraphUris;
    }

    /**
     * Gets the results type that will be requested from the remote endpoint for
     * {@code SELECT} queries
     *
     * @return Select results type if set, otherwise null which indicates that
     *         the ARQ default will be used
     */
    public String getSelectResultsType() {
        return this.selectResultsType;
    }

    /**
     * Gets the results type that will be requested from the remote endpoint for
     * {@code CONSTRUCT} and {@code DESCRIBE} queries.
     *
     * @return Model results type if set, otherwise null which indicates that
     *         the ARQ default will be used
     */
    public String getModelResultsType() {
        return this.modelResultsType;
    }

    @Override
    protected void closeInternal() {
        this.closed = true;
    }

    @Override
    protected JenaStatement createStatementInternal(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
            throws SQLException {
        if (this.isClosed())
            throw new SQLException("Cannot create a statement after the connection was closed");
        if (resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE)
            throw new SQLFeatureNotSupportedException(
                    "Remote endpoint backed connection do not support scroll sensitive result sets");
        if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY)
            throw new SQLFeatureNotSupportedException("Remote endpoint backed connections only support read-only result sets");
        return new RemoteEndpointStatement(this, this.client, resultSetType, ResultSet.FETCH_FORWARD, 0,
                resultSetHoldability);
    }

    @Override
    protected JenaPreparedStatement createPreparedStatementInternal(String sparql, int resultSetType, int resultSetConcurrency,
            int resultSetHoldability) throws SQLException {
        if (this.isClosed())
            throw new SQLException("Cannot create a statement after the connection was closed");
        if (resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE)
            throw new SQLFeatureNotSupportedException(
                    "Remote endpoint backed connection do not support scroll sensitive result sets");
        if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY)
            throw new SQLFeatureNotSupportedException("Remote endpoint backed connections only support read-only result sets");
        return new RemoteEndpointPreparedStatement(sparql, this, this.client, resultSetType, ResultSet.FETCH_FORWARD, 0,
                resultSetHoldability);
    }

    @Override
    public boolean isClosed() {
        return this.closed;
    }

    @Override
    public boolean isReadOnly() {
        return this.readonly;
    }

    @Override
    public boolean isValid(int timeout) {
        return !this.isClosed();
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {
        if (this.isClosed())
            throw new SQLException("Cannot set read-only mode on a closed connection");
        if (readOnly) {
            this.readonly = readOnly;
        } else {
            if (this.updateService == null)
                throw new SQLException("Cannot set connection to read/write as it was created without an update endpoint");
            this.readonly = readOnly;
        }
    }

    @Override
    public DatabaseMetaData getMetaData() {
        return this.metadata;
    }

    @Override
    protected void commitInternal() {
        // No-op
    }

    @Override
    protected void rollbackInternal() {
        // No-op
    }

    @Override
    protected void checkTransactionIsolation(int level) throws SQLException {
        switch (level) {
        case Connection.TRANSACTION_NONE:
            return;
        default:
            throw new SQLFeatureNotSupportedException("Transactions are not supported for remote endpoint backed connections");
        }
    }
}
