/**
 * Copyright (C) 2015 Zalando SE (http://tech.zalando.com)
 *
 * 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
 *
 *         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.zalando.stups.junit.postgres;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.sql.DataSource;

import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.init.ScriptUtils;

import ru.yandex.qatools.embed.postgresql.EmbeddedPostgres;
import ru.yandex.qatools.embed.postgresql.distribution.Version;

/**
 * @author jbellmann
 *
 */
public class PostgreSqlRule extends ExternalResource {

    private final Logger log = LoggerFactory.getLogger(PostgreSqlRule.class);

    public static final String SKIP_POSTGRE_SQL_RULE = "skipPostgreSqlRule";

    private EmbeddedPostgres pg;
    private Builder builder;

    private PostgreSqlRule(Builder builder) {
        this.builder = builder;
    }

    @Override
    protected void before() throws Throwable {
        if (System.getProperty(builder.skipProperty) != null) {
            log.info("Skip PostgreSqlRule because of existing property '" + builder.skipProperty + "'");
            return;
        }

        pg = new EmbeddedPostgres(builder.version);
        final String url = pg.start(EmbeddedPostgres.cachedRuntimeConfig(getCachedPath(builder.version)), "localhost", builder.port, builder.dbName, builder.username, builder.password,Collections.emptyList());

        log.info("PostgreSQL started");
        try {
            applyScripts(url);
        } catch (IOException e) {
            e.printStackTrace();
            stopPostgres();
            throw e;
        } catch (SQLException e) {
            e.printStackTrace();
            stopPostgres();
            // rethrow exception to fail the test
            throw e;
        }
    }

    private Path getCachedPath(Version version) {
        return Paths.get(System.getProperty("user.home"), ".embeddedPostgres", version.asInDownloadPath());
    }

    private void stopPostgres() {
        log.info("Stopping PostgreSQL ...");
        pg.stop();
        log.info("PostgreSQL-Process stopped");
        pg = null;
    }

    private void applyScripts(String url) throws SQLException, IOException {
        log.info("Apply Scripts ...");
        Connection connection = getConnection(url);
        DataSource ds = new SingleConnectionDataSource(connection, false);
        FileSystemScanner scanner = new FileSystemScanner();
        for (String location : builder.locations) {
            File directory = new File(location);
            if (directory.exists() && directory.isDirectory()) {
                Resource[] resources = scanner.scanForResources(location, "", ".sql");
                ResourceDatabasePopulator populator = new ResourceDatabasePopulator(resources);
                populator.setSeparator(builder.separator);
                populator.execute(ds);
            } else {
                // log not existing directory
            }
        }
        log.info("Scripts applied!");
    }

    protected Connection getConnection(String url) throws SQLException {
        return DriverManager.getConnection(url);
    }

    @Override
    protected void after() {
        if(pg != null) {
            pg.stop();
        }
    }

    public static class Builder {

        private int port = 5432;
        private String username = "postgres";
        private String password = "postgres";
        private String dbName = "test";
        private Version version = Version.V10_3;
        private List<String> locations = new LinkedList<String>();
//        private boolean fullExtractOutput = false;
        private String separator = ScriptUtils.EOF_STATEMENT_SEPARATOR;
        private String skipProperty = SKIP_POSTGRE_SQL_RULE;

        public Builder withPort(int port) {
            this.port = port;
            return this;
        }

        public Builder withUsername(String username) {
            this.username = username;
            return this;
        }

        public Builder withPassword(String password) {
            this.password = password;
            return this;
        }

        public Builder withDbName(String dbName) {
            this.dbName = dbName;
            return this;
        }

        public Builder addScriptLocation(String location) {
            this.locations.add(location);
            return this;
        }

        /**
         * Define a separator to use while processing the script.
         * 
         * @param separator
         *            ScriptUtils#EOF_STATEMENT_SEPARATOR
         * @return
         * @see ScriptUtils#EOF_STATEMENT_SEPARATOR
         */
        public Builder withSeparator(String separator) {
            this.separator = separator;
            return this;
        }

        public Builder skipOnProperty(String skipProperty) {
            this.skipProperty = skipProperty;
            return this;
        }

        public Builder withVersion(Version version) {
            this.version = version;
            return this;
        }

        public PostgreSqlRule build() {
            return new PostgreSqlRule(this);
        }

    }

    /**
     * taken from flyway.
     * 
     * FileSystem scanner.
     */
    private static class FileSystemScanner {
        private static final Logger LOG = LoggerFactory.getLogger(FileSystemScanner.class);

        /**
         * Scans the FileSystem for resources under the specified location,
         * starting with the specified prefix and ending with the specified
         * suffix.
         *
         * @param path
         *            The path in the filesystem to start searching.
         *            Subdirectories are also searched.
         * @param prefix
         *            The prefix of the resource names to match.
         * @param suffix
         *            The suffix of the resource names to match.
         * @return The resources that were found.
         * @throws java.io.IOException
         *             when the location could not be scanned.
         */
        Resource[] scanForResources(String path, String prefix, String suffix) throws IOException {
            LOG.debug("Scanning for filesystem resources at '" + path + "' (Prefix: '" + prefix + "', Suffix: '"
                    + suffix + "')");

            if (!new File(path).isDirectory()) {
                throw new IOException("Invalid filesystem path: " + path);
            }

            Set<Resource> resources = new TreeSet<Resource>();

            Set<String> resourceNames = findResourceNames(path, prefix, suffix);
            for (String resourceName : resourceNames) {
                resources.add(new ExtFileSystemResource(resourceName));
                LOG.debug("Found filesystem resource: " + resourceName);
            }

            return resources.toArray(new Resource[resources.size()]);
        }

        /**
         * Finds the resources names present at this location and below on the
         * classpath starting with this prefix and ending with this suffix.
         *
         * @param path
         *            The path on the classpath to scan.
         * @param prefix
         *            The filename prefix to match.
         * @param suffix
         *            The filename suffix to match.
         * @return The resource names.
         * @throws java.io.IOException
         *             when scanning this location failed.
         */
        private Set<String> findResourceNames(String path, String prefix, String suffix) throws IOException {
            Set<String> resourceNames = findResourceNamesFromFileSystem(path, new File(path));
            return filterResourceNames(resourceNames, prefix, suffix);
        }

        /**
         * Finds all the resource names contained in this file system folder.
         *
         * @param scanRootLocation
         *            The root location of the scan on disk.
         * @param folder
         *            The folder to look for resources under on disk.
         * @return The resource names;
         * @throws IOException
         *             when the folder could not be read.
         */
        private Set<String> findResourceNamesFromFileSystem(String scanRootLocation, File folder) throws IOException {
            LOG.debug("Scanning for resources in path: " + folder.getPath() + " (" + scanRootLocation + ")");

            Set<String> resourceNames = new TreeSet<String>();

            File[] files = folder.listFiles();
            for (File file : files) {
                if (file.canRead()) {
                    if (file.isDirectory()) {
                        resourceNames.addAll(findResourceNamesFromFileSystem(scanRootLocation, file));
                    } else {
                        resourceNames.add(file.getPath());
                    }
                }
            }

            return resourceNames;
        }

        /**
         * Filters this list of resource names to only include the ones whose
         * filename matches this prefix and this suffix.
         *
         * @param resourceNames
         *            The names to filter.
         * @param prefix
         *            The prefix to match.
         * @param suffix
         *            The suffix to match.
         * @return The filtered names set.
         */
        private Set<String> filterResourceNames(Set<String> resourceNames, String prefix, String suffix) {
            Set<String> filteredResourceNames = new TreeSet<String>();
            for (String resourceName : resourceNames) {
                String fileName = resourceName.substring(resourceName.lastIndexOf(File.separator) + 1);
                if (fileName.startsWith(prefix) && fileName.endsWith(suffix)
                        && (fileName.length() > (prefix + suffix).length())) {
                    filteredResourceNames.add(resourceName);
                } else {
                    LOG.debug("Filtering out resource: " + resourceName + " (filename: " + fileName + ")");
                }
            }
            return filteredResourceNames;
        }
    }

    static class ExtFileSystemResource extends FileSystemResource implements Comparable<ExtFileSystemResource> {

        ExtFileSystemResource(File file) {
            super(file);
        }

        ExtFileSystemResource(String path) {
            super(path);
        }

        // for now compare by path
        @Override
        public int compareTo(ExtFileSystemResource o) {
            return getPath().compareTo(o.getPath());
        }
    }
}