package liquibase.integration.ant;

import liquibase.Liquibase;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.integration.ant.logging.AntTaskLogFactory;
import liquibase.integration.ant.type.ChangeLogParametersType;
import liquibase.integration.ant.type.DatabaseType;
import liquibase.logging.LogFactory;
import liquibase.logging.Logger;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.CompositeResourceAccessor;
import liquibase.resource.FileSystemResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.util.ui.UIFactory;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.resources.FileResource;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

/**
 * Base class for all Ant Liquibase tasks.  This class sets up Liquibase and defines parameters
 * that are common to all tasks.
 */
public abstract class BaseLiquibaseTask extends Task {
    private AntClassLoader classLoader;
    private Liquibase liquibase;

    private Path classpath;
    private DatabaseType databaseType;
    private ChangeLogParametersType changeLogParameters;
    private boolean promptOnNonLocalDatabase = false;

    public BaseLiquibaseTask() {
        super();
    }

    @Override
    public void init() throws BuildException {
        LogFactory.setInstance(new AntTaskLogFactory(this));
        classpath = new Path(getProject());
    }

    @Override
    public final void execute() throws BuildException {
        super.execute();
        log("Starting Liquibase.", Project.MSG_INFO);
        classLoader = getProject().createClassLoader(classpath);
        classLoader.setParent(this.getClass().getClassLoader());
        classLoader.setThreadContextLoader();
        validateParameters();
        Database database = null;
        try {
            ResourceAccessor resourceAccessor = createResourceAccessor(classLoader);
            database = createDatabaseFromType(databaseType);
            liquibase = new Liquibase(getChangeLogFile(), resourceAccessor, database);
            if(changeLogParameters != null) {
                changeLogParameters.applyParameters(liquibase);
            }
            if (isPromptOnNonLocalDatabase() && !liquibase.isSafeToRunUpdate() &&
                    UIFactory.getInstance().getFacade().promptForNonLocalDatabase(liquibase.getDatabase())) {
                log("User chose not to run task against a non-local database.", Project.MSG_INFO);
                return;
            }
            if(shouldRun()) {
                executeWithLiquibaseClassloader();
            }
        } catch (LiquibaseException e) {
            throw new BuildException("Unable to initialize Liquibase. " + e.toString(), e);
        } finally {
            closeDatabase(database);
            classLoader.resetThreadContextLoader();
            classLoader.cleanup();
            classLoader = null;
        }
    }

    protected abstract void executeWithLiquibaseClassloader() throws BuildException;

    protected Database createDatabaseFromConfiguredDatabaseType() {
        return createDatabaseFromType(databaseType);
    }

    protected Database createDatabaseFromType(DatabaseType databaseType) {
        return databaseType.createDatabase(classLoader);
    }

    protected Liquibase getLiquibase() {
        return liquibase;
    }

    /**
     * This method is designed to be overridden by subclasses when a change log is needed. By default it returns null.
     *
     * @return Returns null in this implementation. Subclasses that need a change log should implement.
     * @see AbstractChangeLogBasedTask#getChangeLogFile()
     */
    protected String getChangeLogFile() {
        return null;
    }

    protected boolean shouldRun() {
        LiquibaseConfiguration configuration = LiquibaseConfiguration.getInstance();
        GlobalConfiguration globalConfiguration = configuration.getConfiguration(GlobalConfiguration.class);
        if (!globalConfiguration.getShouldRun()) {
            log("Liquibase did not run because " + configuration.describeValueLookupLogic(globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN)) + " was set to false", Project.MSG_INFO);
            return false;
        }
        return true;
    }

    protected String getDefaultOutputEncoding() {
        LiquibaseConfiguration liquibaseConfiguration = LiquibaseConfiguration.getInstance();
        GlobalConfiguration globalConfiguration = liquibaseConfiguration.getConfiguration(GlobalConfiguration.class);
        return globalConfiguration.getOutputEncoding();
    }

    /**
     * Subclasses that override this method must always call <code>super.validateParameters()</code> method.
     */
    protected void validateParameters() {
        if(databaseType == null) {
            throw new BuildException("A database or databaseref is required.");
        }
    }

    /**
     * Creates a suitable ResourceAccessor for use in an Ant task..
     *
     * @param classLoader The ClassLoader to use in the ResourceAccessor. It is preferable that it is an AntClassLoader.
     * @return A ResourceAccessor.
     */
    private ResourceAccessor createResourceAccessor(ClassLoader classLoader) {
        FileSystemResourceAccessor fileSystemResourceAccessor = new FileSystemResourceAccessor();
        ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor(classLoader);
        return new CompositeResourceAccessor(fileSystemResourceAccessor, classLoaderResourceAccessor);
    }

    /**
     * Convenience method to safely close the database connection.
     *
     * @param database The database to close.
     */
    protected void closeDatabase(Database database) {
        try {
            if(database != null) {
                database.close();
            }
        } catch (DatabaseException e) {
            log("Error closing the database connection.", e, Project.MSG_WARN);
        }
    }

    /*
     * Ant parameters
     */

    public Path createClasspath() {
        if (this.classpath == null) {
            this.classpath = new Path(getProject());
        }
        return this.classpath.createPath();
    }

    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }

    public void addDatabase(DatabaseType databaseType) {
        if(this.databaseType != null) {
            throw new BuildException("Only one <database> element is allowed.");
        }
        this.databaseType = databaseType;
    }

    public void setDatabaseRef(Reference databaseRef) {
        databaseType = new DatabaseType(getProject());
        databaseType.setRefid(databaseRef);
    }

    public void addChangeLogParameters(ChangeLogParametersType changeLogParameters) {
        if(this.changeLogParameters != null) {
            throw new BuildException("Only one <changeLogParameters> element is allowed.");
        }
        this.changeLogParameters = changeLogParameters;
    }

    public void setChangeLogParametersRef(Reference changeLogParametersRef) {
        changeLogParameters = new ChangeLogParametersType(getProject());
        changeLogParameters.setRefid(changeLogParametersRef);
    }

    public boolean isPromptOnNonLocalDatabase() {
        return promptOnNonLocalDatabase;
    }

    public void setPromptOnNonLocalDatabase(boolean promptOnNonLocalDatabase) {
        this.promptOnNonLocalDatabase = promptOnNonLocalDatabase;
    }

    /*************************
     * Deprecated parameters *
     *************************/

    /**
     * Helper method for deprecated ant attributes. This method will be removed when the deprecated methods are removed.
     * Do not rely on this method.
     *
     * @return DatabaseType object. Created if doesn't exist.
     */
    private DatabaseType getDatabaseType() {
        if(databaseType == null) {
            databaseType = new DatabaseType(getProject());
        }
        return databaseType;
    }

    /**
     * Helper method for deprecated ant attributes. This method will be removed when the deprecated methods are removed.
     * Do not rely on this method.
     *
     * @return ChangeLogParametersType
     */
    private ChangeLogParametersType getChangeLogParametersType() {
        if(changeLogParameters == null) {
            changeLogParameters = new ChangeLogParametersType(getProject());
        }
        return changeLogParameters;
    }

    /**
     * @deprecated Use {@link DatabaseType#getDriver()} instead.
     */
    @Deprecated
    public String getDriver() {
        return getDatabaseType().getDriver();
    }

    /**
     * @deprecated Use {@link DatabaseType#setDriver(String)} instead.
     */
    @Deprecated
    public void setDriver(String driver) {
        log("The driver attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setDriver(driver);
    }

    /**
     * @deprecated Use {@link DatabaseType#getUrl()} instead.
     */
    @Deprecated
    public String getUrl() {
        return getDatabaseType().getUrl();
    }

    /**
     * @deprecated Use {@link DatabaseType#setUrl(String)} instead.
     */
    @Deprecated
    public void setUrl(String url) {
        log("The url attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setUrl(url);
    }

    /**
     * @deprecated Use {@link DatabaseType#getUser()} instead.
     */
    @Deprecated
    public String getUsername() {
        return getDatabaseType().getUser();
    }

    /**
     * @deprecated Use {@link DatabaseType#setUser(String)} instead.
     */
    @Deprecated
    public void setUsername(String username) {
        log("The username attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setUser(username);
    }

    /**
     * @deprecated Use {@link DatabaseType#getPassword()} instead.
     */
    @Deprecated
    public String getPassword() {
        return getDatabaseType().getPassword();
    }

    /**
     * @deprecated Use {@link DatabaseType#setPassword(String)} instead.
     */
    @Deprecated
    public void setPassword(String password) {
        log("The password attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setPassword(password);
    }

    public void setChangeLogFile(String changeLogFile) {
        // This method is deprecated. Use child implementation.
    }

    /**
     * @deprecated Use {@link DatabaseType#getCurrentDateTimeFunction()} instead.
     */
    @Deprecated
    public String getCurrentDateTimeFunction() {
        return getDatabaseType().getCurrentDateTimeFunction();
    }

    /**
     * @deprecated Use {@link DatabaseType#setCurrentDateTimeFunction(String)} instead.
     */
    @Deprecated
    public void setCurrentDateTimeFunction(String currentDateTimeFunction) {
        log("The currentDateTimeFunction attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setCurrentDateTimeFunction(currentDateTimeFunction);
    }

    /**
     * This method does nothing. Use child implementations.
     */
    public FileResource getOutputFile() {
        // This method is deprecated. Use child implementations.
        return null;
    }

    /**
     * This method does nothing. Use child implementations.
     */
    public void setOutputFile(FileResource outputFile) {
        // This method is deprecated. Use child implementation.
    }

    /**
     * @deprecated Subclasses of this class should either instantiate their own output writers or use
     * {@link liquibase.integration.ant.AbstractChangeLogBasedTask} if their task involves a change log.
     */
    @Deprecated
    public Writer createOutputWriter() throws IOException {
        if (getOutputFile() == null) {
            return null;
        }
        GlobalConfiguration globalConfiguration = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class);
        String encoding = globalConfiguration.getOutputEncoding();
        return new OutputStreamWriter(getOutputFile().getOutputStream(), encoding);
    }

    /**
     * @deprecated Subclasses of this class should either instantiate their own output writers or use
     * {@link liquibase.integration.ant.AbstractChangeLogBasedTask} if the task involves a change log.
     */
    @Deprecated
    public PrintStream createPrintStream() throws IOException {
        if (getOutputFile() == null) {
            return null;
        }
        GlobalConfiguration globalConfiguration = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class);
        String encoding = globalConfiguration.getOutputEncoding();
        return new PrintStream(getOutputFile().getOutputStream(), false, encoding);
    }

    public void setOutputEncoding(String outputEncoding) {
        // This method is deprecated. Use child implementation.
    }

    /**
     * @deprecated Use {@link DatabaseType#getDefaultCatalogName()} instead.
     */
    @Deprecated
    public String getDefaultCatalogName() {
        return getDatabaseType().getDefaultCatalogName();
    }

    /**
     * @deprecated Use {@link DatabaseType#setDefaultCatalogName(String)} instead.
     */
    @Deprecated
    public void setDefaultCatalogName(String defaultCatalogName) {
        log("The defaultCatalogName attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setDefaultCatalogName(defaultCatalogName);
    }

    /**
     * @deprecated Use {@link DatabaseType#getDefaultSchemaName()} instead.
     */
    @Deprecated
    public String getDefaultSchemaName() {
        return getDatabaseType().getDefaultSchemaName();
    }

    /**
     * @deprecated Use {@link DatabaseType#setDefaultSchemaName(String)} instead.
     */
    @Deprecated
    public void setDefaultSchemaName(String defaultSchemaName) {
        log("The driver attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setDefaultSchemaName(defaultSchemaName);
    }

    /**
     * @deprecated Use {@link ChangeLogParametersType#addConfiguredChangeLogParameter(Property)} instead.
     */
    @Deprecated
    public void addConfiguredChangeLogProperty(ChangeLogProperty changeLogProperty) {
        log("The <changeLogProperty> element is deprecated. Use a nested <changeLogParameters> element instead.", Project.MSG_WARN);
        Property property = new Property();
        property.setName(changeLogProperty.getName());
        property.setValue(changeLogProperty.getValue());
        getChangeLogParametersType().addConfiguredChangeLogParameter(property);
    }

    /**
     * @deprecated The Liquibase class is now created automatically when the Ant task is executed. Use
     * {@link #getLiquibase()} instead.
     */
    @Deprecated
    protected Liquibase createLiquibase() throws Exception {
        return this.liquibase;
    }

    /**
     * @deprecated Use {@link #createDatabaseFromType(DatabaseType)} instead.
     */
    @Deprecated
    protected Database createDatabaseObject(String driverClassName,
                                            String databaseUrl,
                                            String username,
                                            String password,
                                            String defaultCatalogName,
                                            String defaultSchemaName,
                                            String databaseClass) throws Exception {
        return createDatabaseFromType(databaseType);
    }

    /**
     * This method no longer does anything. Please extend from
     * {@link liquibase.integration.ant.AbstractChangeLogBasedTask} which has the equivalent method.
     */
    public String getContexts() {
        return null;
    }

    /**
     * This method no longer does anything. Please extend from
     * {@link liquibase.integration.ant.AbstractChangeLogBasedTask} which has the equivalent method.
     */
    public void setContexts(String cntx) {
        // Parent method is deprecated. Use child implementations.
    }


    /**
     * Redirector of logs from java.util.logging to ANT's logging
     */
    @Deprecated
    protected static class LogRedirector {

        private final Task task;

        /**
         * Constructor
         *
         * @param task Ant task
         */
        protected LogRedirector(Task task) {
            super();
            this.task = task;
        }

        protected void redirectLogger() {
            registerHandler(createHandler());
        }

        protected void registerHandler(Handler theHandler) {
            Logger logger = LogFactory.getInstance().getLog();
        }


        protected Handler createHandler() {
            return new Handler() {
                @Override
                public void publish(LogRecord logRecord) {
                    task.log(logRecord.getMessage(), mapLevelToAntLevel(logRecord.getLevel()));
                }

                @Override
                public void close() throws SecurityException {
                }

                @Override
                public void flush() {
                }

                protected int mapLevelToAntLevel(Level level) {
                    if (Level.ALL == level) {
                        return Project.MSG_INFO;
                    } else if (Level.SEVERE == level) {
                        return Project.MSG_ERR;
                    } else if (Level.WARNING == level) {
                        return Project.MSG_WARN;
                    } else if (Level.INFO == level) {
                        return Project.MSG_INFO;
                    } else {
                        return Project.MSG_VERBOSE;
                    }
                }
            };
        }

    }

    /**
     * @deprecated Use {@link #closeDatabase(liquibase.database.Database)} instead.
     */
    @Deprecated
    protected void closeDatabase(Liquibase liquibase) {
        if (liquibase != null && liquibase.getDatabase() != null && liquibase.getDatabase().getConnection() != null) {
            try {
                liquibase.getDatabase().close();
            } catch (DatabaseException e) {
                log("Error closing database: " + e.getMessage());
            }
        }
    }

    /**
     * @deprecated Use {@link DatabaseType#getDatabaseClass()} instead.
     */
    @Deprecated
    public String getDatabaseClass() {
        return getDatabaseType().getDatabaseClass();
    }

    /**
     * @deprecated Use {@link DatabaseType#setDatabaseClass(String)} instead.
     */
    @Deprecated
    public void setDatabaseClass(String databaseClass) {
        log("The databaseClass attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setDatabaseClass(databaseClass);
    }

    /**
     * @deprecated Use {@link DatabaseType#getDatabaseChangeLogTableName()} instead.
     */
    @Deprecated
    public String getDatabaseChangeLogTableName() {
        return getDatabaseType().getDatabaseChangeLogTableName();
    }

    /**
     * @deprecated Use {@link DatabaseType#setDatabaseChangeLogTableName(String)} instead.
     */
    @Deprecated
    public void setDatabaseChangeLogTableName(String tableName) {
        log("The databaseChangeLogTableName attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setDatabaseChangeLogTableName(tableName);
    }

    /**
     * @deprecated Use {@link DatabaseType#getDatabaseChangeLogLockTableName()} instead.
     */
    @Deprecated
    public String getDatabaseChangeLogLockTableName() {
        return getDatabaseType().getDatabaseChangeLogLockTableName();
    }

    @Deprecated
    public void setDatabaseChangeLogLockTableName(String tableName) {
        log("The databaseChangeLogLockTableName attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        databaseType.setDatabaseChangeLogLockTableName(tableName);
    }

    /**
     * @deprecated Use {@link DatabaseType#getLiquibaseTablespaceName()} instead.
     */
    @Deprecated
    public String getDatabaseChangeLogObjectsTablespace() {
        return databaseType.getLiquibaseTablespaceName();
    }

    /**
     * @deprecated Use {@link DatabaseType#setLiquibaseTablespaceName(String)} instead.
     */
    @Deprecated
    public void setDatabaseChangeLogObjectsTablespace(String tablespaceName) {
        log("The databaseChangeLogObjectsTablespace attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setLiquibaseTablespaceName(tablespaceName);
    }

    /**
     * @deprecated Use {@link DatabaseType#isOutputDefaultSchema()} instead.
     */
    @Deprecated
    public boolean isOutputDefaultSchema() {
        return getDatabaseType().isOutputDefaultSchema();
    }

    /**
     * If not set, defaults to true.
     *
     * @deprecated Use a nested {@link DatabaseType DatabaseType} instead.
     * @param outputDefaultSchema True to output the default schema.
     */
    @Deprecated
    public void setOutputDefaultSchema(boolean outputDefaultSchema) {
        log("The outputDefaultSchema attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setOutputDefaultSchema(outputDefaultSchema);
    }

    /**
     * @deprecated Use {@link DatabaseType#isOutputDefaultCatalog()} instead.
     */
    @Deprecated
    public boolean isOutputDefaultCatalog() {
        return getDatabaseType().isOutputDefaultCatalog();
    }

    /**
     * If not set, defaults to true
     *
     * @deprecated Use {@link DatabaseType#setOutputDefaultCatalog(boolean)} instead.
     * @param outputDefaultCatalog True to output the default catalog.
     */
    @Deprecated
    public void setOutputDefaultCatalog(boolean outputDefaultCatalog) {
        log("The outputDefaultCatalog attribute is deprecated. Use a nested <database> element or set the databaseRef attribute instead.", Project.MSG_WARN);
        getDatabaseType().setOutputDefaultCatalog(outputDefaultCatalog);
    }

    /**
     * @deprecated No longer needed. This method has no replacement.
     * @return Log level.
     */
    @Deprecated
    public String getLogLevel() {
        return LogFactory.getInstance().getLog().getLogLevel().name();
    }

    /**
     * @deprecated Use the ant logging flags (-debug, -verbose, -quiet) instead of this method to control logging
     * output. This will no longer change log levels.
     * @param level Log level to set.
     */
    @Deprecated
    public void setLogLevel(String level) {
        LogFactory.getInstance().getLog().setLogLevel(level);
    }

    /**
     * @deprecated Use {@link ChangeLogParametersType} instead.
     */
    @Deprecated
    public static class ChangeLogProperty {
        private String name;
        private String value;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }
}
