/*
 * Licensed 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.
 *
 * Other licenses:
 * -----------------------------------------------------------------------------
 * Commercial licenses for this work are available. These replace the above
 * Apache-2.0 license and offer limited warranties, support, maintenance, and
 * commercial database integrations.
 *
 * For more information, please visit: https://www.jooq.org/legal/licensing
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package org.jooq.meta.extensions.liquibase;

import static org.jooq.impl.DSL.unquotedName;
import static org.jooq.tools.StringUtils.isBlank;

import java.io.File;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.jooq.impl.DSL;
import org.jooq.meta.TableDefinition;
import org.jooq.meta.extensions.AbstractInterpretingDatabase;
import org.jooq.tools.Convert;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.StringUtils;

import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.CompositeResourceAccessor;
import liquibase.resource.DirectoryResourceAccessor;
import liquibase.resource.ResourceAccessor;

/**
 * The Liquibase database.
 * <p>
 * This meta data source parses a set of XML files that follow the Liquibase
 * migration DSL, runs them on an in-memory H2 database before reverse
 * engineering the outcome.
 * <p>
 * The XML scripts are located in the <code>scripts</code> scripts property
 * available from {@link #getProperties()}.
 *
 * @author Lukas Eder
 */
public class LiquibaseDatabase extends AbstractInterpretingDatabase {

    private static final JooqLogger          log = JooqLogger.getLogger(LiquibaseDatabase.class);
    private static final Map<String, Method> SETTERS;
    private boolean                          includeLiquibaseTables;
    private String                           databaseChangeLogTableName;
    private String                           databaseChangeLogLockTableName;
    private String                           databaseLiquibaseSchemaName;

    static {
        SETTERS = new HashMap<>();

        try {
            for (Method method : Database.class.getMethods()) {
                String name = method.getName();

                if (name.startsWith("set") && method.getParameterTypes().length == 1)
                    SETTERS.put(name, method);
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void export() throws Exception {
        String rootPath = getProperties().getProperty("rootPath");
        String scripts = getProperties().getProperty("scripts");
        includeLiquibaseTables = Boolean.parseBoolean(getProperties().getProperty("includeLiquibaseTables", "false"));

        if (isBlank(scripts)) {
            scripts = "";
            log.warn("No scripts defined", "It is recommended that you provide an explicit script directory to scan");
        }

        Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection()));
        String contexts = "";
        Map<String, Object> changeLogParameters = new LinkedHashMap<>();

        // [#9514] Forward all database.xyz properties to matching Liquibase
        //         Database.setXyz() configuration setter calls
        for (Entry<Object, Object> entry : getProperties().entrySet()) {
            String key = "" + entry.getKey();

            if (key.startsWith("database.")) {
                String property = key.substring("database.".length());
                Method setter = SETTERS.get("set" + Character.toUpperCase(property.charAt(0)) + property.substring(1));

                try {
                    if (setter != null)
                        setter.invoke(database, Convert.convert(entry.getValue(), setter.getParameterTypes()[0]));
                }
                catch (Exception e) {
                    log.warn("Configuration error", e.getMessage(), e);
                }
            }

            // [#9872] Some changeLogParameters can also be passed along
            else if (key.startsWith("changeLogParameters.")) {
                String property = key.substring("changeLogParameters.".length());

                if ("contexts".equals(property))
                    contexts = "" + entry.getValue();
                else
                    changeLogParameters.put(property, entry.getValue());
            }
        }

        // Retrieve changeLog table names as they might be overridden by configuration setters
        databaseChangeLogTableName = database.getDatabaseChangeLogTableName();
        databaseChangeLogLockTableName = database.getDatabaseChangeLogLockTableName();
        databaseLiquibaseSchemaName = database.getLiquibaseSchemaName();

        if (!StringUtils.isBlank(databaseLiquibaseSchemaName))
            create().createSchemaIfNotExists(unquotedName(databaseLiquibaseSchemaName)).execute();

        // [#9866] Allow for loading included files from the classpath or using absolute paths.
        // [#12872] [#13021] The decision is made based on the presence of the rootPath property
        ResourceAccessor ra = StringUtils.isBlank(rootPath)
            ? new CompositeResourceAccessor(
                new ClassLoaderResourceAccessor(),
                new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader())
            )
            : new DirectoryResourceAccessor(new File(rootPath));

        Liquibase liquibase = new Liquibase(scripts, ra, database);
        changeLogParameters.forEach(liquibase::setChangeLogParameter);
        liquibase.update(contexts);
    }

    @Override
    protected List<TableDefinition> getTables0() throws SQLException {
        List<TableDefinition> result = new ArrayList<>(super.getTables0());

        if (!includeLiquibaseTables) {
            List<String> liquibaseTables = Arrays.asList(databaseChangeLogTableName, databaseChangeLogLockTableName);
            result.removeIf(t -> liquibaseTables.contains(t.getName()));
        }

        return result;
    }
}
