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

package org.apache.cayenne.unit;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.configuration.Constants;
import org.apache.cayenne.configuration.RuntimeProperties;
import org.apache.cayenne.dba.DbAdapter;
import org.apache.cayenne.dba.QuotingStrategy;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.exp.parser.ASTExtract;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.Procedure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

/**
 * Defines API and a common superclass for testing various database features.
 * Different databases support different feature sets that need to be tested
 * differently. Many things implemented in subclasses may become future
 * candidates for inclusion in the corresponding adapter code.
 */
public class UnitDbAdapter {

    private static final Logger logger = LoggerFactory.getLogger(UnitDbAdapter.class);

    @Inject
    protected RuntimeProperties runtimeProperties;

    protected DbAdapter adapter;

    public UnitDbAdapter(DbAdapter adapter) {
        if (adapter == null) {
            throw new CayenneRuntimeException("Null adapter.");
        }
        this.adapter = adapter;
    }

    public boolean supportsPKGeneratorConcurrency() {
        return true;
    }

    public String getIdentifiersStartQuote() {
        return "\"";
    }
    
    public String getIdentifiersEndQuote() {
        return "\"";
    }
    
    /**
     * Returns whether the target DB treats REAL values as DOUBLEs. Default is
     * false, i.e. REALs are treated as FLOATs.
     */
    public boolean realAsDouble() {
        return false;
    }

    /**
     * Drops all table constraints.
     */
    public void willDropTables(Connection conn, DataMap map, Collection<String> tablesToDrop) throws Exception {
        dropConstraints(conn, map, tablesToDrop);
    }

    protected void dropConstraints(Connection conn, DataMap map, Collection<String> tablesToDrop) throws Exception {
        Map<String, Collection<String>> constraintsMap = getConstraints(conn, map, tablesToDrop);

        for (Map.Entry<String, Collection<String>> entry : constraintsMap.entrySet()) {

            Collection<String> constraints = entry.getValue();
            if (constraints == null || constraints.isEmpty()) {
                continue;
            }

            Object tableName = entry.getKey();
            DbEntity entity = map.getDbEntity(tableName.toString());
            if (entity == null) {
                continue;
            }

            QuotingStrategy strategy = adapter.getQuotingStrategy();

            for (String constraint : constraints) {
                StringBuilder drop = new StringBuilder();

                drop.append("ALTER TABLE ").append(strategy.quotedFullyQualifiedName(entity))
                        .append(" DROP CONSTRAINT ").append(constraint);
                executeDDL(conn, drop.toString());
            }
        }
    }

    public void droppedTables(Connection con, DataMap map) throws Exception {

    }

    /**
     * Callback method that allows Delegate to customize test procedure.
     */
    public void tweakProcedure(Procedure proc) {
    }

    public void willCreateTables(Connection con, DataMap map) throws Exception {
    }

    public void createdTables(Connection con, DataMap map) throws Exception {

    }

    public boolean supportsStoredProcedures() {
        return false;
    }

    /**
     * Returns whether the target database supports expressions in the WHERE
     * clause in the form "VALUE = COLUMN".
     */
    public boolean supportsReverseComparison() {
        return true;
    }

    /**
     * Returns whether an aggregate query like "SELECT min(X) FROM" returns a
     * NULL row when no data matched the WHERE clause. Most DB's do.
     */
    public boolean supportNullRowForAggregateFunctions() {
        return true;
    }

    /**
     * Returns whether the database supports synatax like "X = NULL".
     */
    public boolean supportsEqualNullSyntax() {
        return true;
    }

    public boolean supportsAllAnySome() {
        return true;
    }

    /**
     * Returns whether the DB supports a TRIM function for an arbitrary
     * character.
     */
    public boolean supportsTrimChar() {
        return false;
    }

    /**
     * Returns false if stored procedures are not supported or if it is a victim
     * of CAY-148 (column name capitalization).
     */
    public boolean canMakeObjectsOutOfProcedures() {
        return supportsStoredProcedures();
    }

    /**
     * Returns whether LOBs can be inserted from test XML files.
     */
    public boolean supportsLobInsertsAsStrings() {
        return supportsLobs();
    }

    public boolean supportsFKConstraints(DbEntity entity) {
        if ("FK_OF_DIFFERENT_TYPE".equals(entity.getName())) {
            return false;
        }

        return true;
    }

    public boolean supportsFKConstraints() {
        return true;
    }

    public boolean supportsColumnTypeReengineering() {
        return true;
    }

    /**
     * Returns true if the target database has support for large objects (BLOB,
     * CLOB).
     */
    public boolean supportsLobs() {
        return false;
    }

    public boolean supportsLobComparisons() {
        return supportsLobs();
    }

    /**
     * Returns true if the target database has native json data type.
     */
    public boolean supportsJsonType() {
        return false;
    }

    public boolean supportsBinaryPK() {
        return true;
    }

    public boolean supportsHaving() {
        return true;
    }

    public boolean supportsCaseSensitiveLike() {
        return !runtimeProperties.getBoolean(Constants.CI_PROPERTY, false);
    }

    public boolean supportsCaseInsensitiveOrder() {
        return true;
    }

    public boolean supportsCatalogs() {
        return false;
    }

    public boolean supportsBatchPK() {
        return true;
    }

    public boolean supportsBitwiseOps() {
        return false;
    }

    protected void executeDDL(Connection con, String ddl) throws Exception {
        logger.info(ddl);
        Statement st = con.createStatement();

        try {
            st.execute(ddl);
        } finally {
            st.close();
        }
    }

    protected void executeDDL(Connection con, String database, String name) throws Exception {
        executeDDL(con, ddlString(database, name));
    }

    /**
     * Returns a file under test resources DDL directory for the specified
     * database.
     */
    private String ddlString(String database, String name) {
        StringBuffer location = new StringBuffer();
        location.append("ddl/").append(database).append("/").append(name);

        InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream(location.toString());

        if (resource == null) {
            throw new CayenneRuntimeException("Can't find DDL file: " + location);
        }

        BufferedReader in = new BufferedReader(new InputStreamReader(resource));
        StringBuffer buf = new StringBuffer();
        try {
            String line = null;
            while ((line = in.readLine()) != null) {
                buf.append(line).append('\n');
            }
        } catch (IOException e) {
            throw new CayenneRuntimeException("Error reading DDL file: " + location);
        } finally {

            try {
                in.close();
            } catch (IOException e) {

            }
        }
        return buf.toString();
    }

    public boolean handlesNullVsEmptyLOBs() {
        return supportsLobs();
    }

    /**
     * Returns a map of database constraints with DbEntity names used as keys,
     * and Collections of constraint names as values.
     */
    protected Map<String, Collection<String>> getConstraints(Connection conn, DataMap map,
            Collection<String> includeTables) throws SQLException {

        Map<String, Collection<String>> constraintMap = new HashMap<>();

        DatabaseMetaData metadata = conn.getMetaData();

        for (String name : includeTables) {
            DbEntity entity = map.getDbEntity(name);
            if (entity == null) {
                continue;
            }

            QuotingStrategy strategy = adapter.getQuotingStrategy();

            // Get all constraints for the table
            ResultSet rs = metadata.getExportedKeys(entity.getCatalog(), entity.getSchema(), entity.getName());
            try {
                while (rs.next()) {
                    String fk = rs.getString("FK_NAME");
                    String fkTable = rs.getString("FKTABLE_NAME");

                    if (fk != null && fkTable != null) {
                        Collection<String> constraints = constraintMap.get(fkTable);
                        if (constraints == null) {
                            // use a set to avoid duplicate constraints
                            constraints = new HashSet<String>();
                            constraintMap.put(fkTable, constraints);
                        }

                        constraints.add(strategy.quotedIdentifier(entity, fk));
                    }
                }
            } finally {
                rs.close();
            }
        }

        return constraintMap;
    }

    public boolean isLowerCaseNames() {
        return false;
    }

    public boolean onlyGenericNumberType() {
        return false;
    }

    public boolean supportsTimeSqlType() {
        return true;
    }

    public boolean onlyGenericDateType() {
        return false;
    }

    public boolean supportsNullBoolean() {
        return true;
    }

    public boolean supportsBoolean() {
        return true;
    }

    public boolean supportsGeneratedKeys() {
        return adapter.supportsGeneratedKeys();
    }

    public boolean supportsGeneratedKeysAdd() {
        return false;
    }

    public boolean supportsGeneratedKeysDrop() {
        return false;
    }

    public boolean supportsEscapeInLike() {
        return true;
    }

    public boolean supportsExpressionInHaving() {
        return true;
    }

    /**
     * Support for select like this:
     * SELECT (intColumn < 10) AS bool FROM table
     */
    public boolean supportsSelectBooleanExpression() {
        return true;
    }

    public boolean supportsExtractPart(ASTExtract.DateTimePart part) {
        return true;
    }

    public boolean supportsSerializableTransactionIsolation() {
        return false;
    }

    public boolean supportsLongIn() {
        return true;
    }

    public boolean supportsNullComparison() {
        return true;
    }

    /**
     * Support for select like this:
     * SELECT t0.ARTIST_NAME FROM ARTIST t0 WHERE 'abc'
     */
    public boolean supportScalarAsExpression(){
        return false;
    }
}
