package io.dropwizard.migrations;

import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.Configuration;
import io.dropwizard.db.DatabaseConfiguration;
import liquibase.CatalogAndSchema;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.diff.DiffGeneratorFactory;
import liquibase.diff.DiffResult;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.output.DiffOutputControl;
import liquibase.diff.output.changelog.DiffToChangeLog;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotControl;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Column;
import liquibase.structure.core.Data;
import liquibase.structure.core.ForeignKey;
import liquibase.structure.core.Index;
import liquibase.structure.core.PrimaryKey;
import liquibase.structure.core.Sequence;
import liquibase.structure.core.Table;
import liquibase.structure.core.UniqueConstraint;
import liquibase.structure.core.View;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.ArgumentGroup;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;

public class DbDumpCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {

    private PrintStream outputStream = System.out;

    @VisibleForTesting
    void setOutputStream(PrintStream outputStream) {
        this.outputStream = outputStream;
    }

    public DbDumpCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass, String migrationsFileName) {
        super("dump",
              "Generate a dump of the existing database state.",
              strategy,
              configurationClass,
              migrationsFileName);
    }

    @Override
    public void configure(Subparser subparser) {
        super.configure(subparser);

        subparser.addArgument("-o", "--output")
                 .dest("output")
                 .help("Write output to <file> instead of stdout");

        final ArgumentGroup tables = subparser.addArgumentGroup("Tables");
        tables.addArgument("--tables")
              .action(Arguments.storeTrue())
              .dest("tables")
              .help("Check for added or removed tables (default)");
        tables.addArgument("--ignore-tables")
              .action(Arguments.storeFalse())
              .dest("tables")
              .help("Ignore tables");

        final ArgumentGroup columns = subparser.addArgumentGroup("Columns");
        columns.addArgument("--columns")
               .action(Arguments.storeTrue())
               .dest("columns")
               .help("Check for added, removed, or modified columns (default)");
        columns.addArgument("--ignore-columns")
               .action(Arguments.storeFalse())
               .dest("columns")
               .help("Ignore columns");

        final ArgumentGroup views = subparser.addArgumentGroup("Views");
        views.addArgument("--views")
             .action(Arguments.storeTrue())
             .dest("views")
             .help("Check for added, removed, or modified views (default)");
        views.addArgument("--ignore-views")
             .action(Arguments.storeFalse())
             .dest("views")
             .help("Ignore views");

        final ArgumentGroup primaryKeys = subparser.addArgumentGroup("Primary Keys");
        primaryKeys.addArgument("--primary-keys")
                   .action(Arguments.storeTrue())
                   .dest("primary-keys")
                   .help("Check for changed primary keys (default)");
        primaryKeys.addArgument("--ignore-primary-keys")
                   .action(Arguments.storeFalse())
                   .dest("primary-keys")
                   .help("Ignore primary keys");

        final ArgumentGroup uniqueConstraints = subparser.addArgumentGroup("Unique Constraints");
        uniqueConstraints.addArgument("--unique-constraints")
                         .action(Arguments.storeTrue())
                         .dest("unique-constraints")
                         .help("Check for changed unique constraints (default)");
        uniqueConstraints.addArgument("--ignore-unique-constraints")
                         .action(Arguments.storeFalse())
                         .dest("unique-constraints")
                         .help("Ignore unique constraints");

        final ArgumentGroup indexes = subparser.addArgumentGroup("Indexes");
        indexes.addArgument("--indexes")
               .action(Arguments.storeTrue())
               .dest("indexes")
               .help("Check for changed indexes (default)");
        indexes.addArgument("--ignore-indexes")
               .action(Arguments.storeFalse())
               .dest("indexes")
               .help("Ignore indexes");

        final ArgumentGroup foreignKeys = subparser.addArgumentGroup("Foreign Keys");
        foreignKeys.addArgument("--foreign-keys")
                   .action(Arguments.storeTrue())
                   .dest("foreign-keys")
                   .help("Check for changed foreign keys (default)");
        foreignKeys.addArgument("--ignore-foreign-keys")
                   .action(Arguments.storeFalse())
                   .dest("foreign-keys")
                   .help("Ignore foreign keys");

        final ArgumentGroup sequences = subparser.addArgumentGroup("Sequences");
        sequences.addArgument("--sequences")
                 .action(Arguments.storeTrue())
                 .dest("sequences")
                 .help("Check for changed sequences (default)");
        sequences.addArgument("--ignore-sequences")
                 .action(Arguments.storeFalse())
                 .dest("sequences")
                 .help("Ignore sequences");

        final ArgumentGroup data = subparser.addArgumentGroup("Data");
        data.addArgument("--data")
            .action(Arguments.storeTrue())
            .dest("data")
            .help("Check for changed data")
            .setDefault(Boolean.FALSE);
        data.addArgument("--ignore-data")
            .action(Arguments.storeFalse())
            .dest("data")
            .help("Ignore data (default)")
            .setDefault(Boolean.FALSE);
    }

    @Override
    @SuppressWarnings("UseOfSystemOutOrSystemErr")
    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
        final Set<Class<? extends DatabaseObject>> compareTypes = new HashSet<>();

        if (isTrue(namespace.getBoolean("columns"))) {
            compareTypes.add(Column.class);
        }
        if (isTrue(namespace.getBoolean("data"))) {
            compareTypes.add(Data.class);
        }
        if (isTrue(namespace.getBoolean("foreign-keys"))) {
            compareTypes.add(ForeignKey.class);
        }
        if (isTrue(namespace.getBoolean("indexes"))) {
            compareTypes.add(Index.class);
        }
        if (isTrue(namespace.getBoolean("primary-keys"))) {
            compareTypes.add(PrimaryKey.class);
        }
        if (isTrue(namespace.getBoolean("sequences"))) {
            compareTypes.add(Sequence.class);
        }
        if (isTrue(namespace.getBoolean("tables"))) {
            compareTypes.add(Table.class);
        }
        if (isTrue(namespace.getBoolean("unique-constraints"))) {
            compareTypes.add(UniqueConstraint.class);
        }
        if (isTrue(namespace.getBoolean("views"))) {
            compareTypes.add(View.class);
        }

        final DiffToChangeLog diffToChangeLog = new DiffToChangeLog(new DiffOutputControl());
        final Database database = liquibase.getDatabase();

        final String filename = namespace.getString("output");
        if (filename != null) {
            try (PrintStream file = new PrintStream(filename, StandardCharsets.UTF_8.name())) {
                generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, file, compareTypes);
            }
        } else {
            generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, outputStream, compareTypes);
        }
    }

    private void generateChangeLog(final Database database, final CatalogAndSchema catalogAndSchema,
                                   final DiffToChangeLog changeLogWriter, PrintStream outputStream,
                                   final Set<Class<? extends DatabaseObject>> compareTypes)
            throws DatabaseException, IOException, ParserConfigurationException {
        @SuppressWarnings({"unchecked", "rawtypes"})
        final SnapshotControl snapshotControl = new SnapshotControl(database,
                compareTypes.toArray(new Class[compareTypes.size()]));
        final CompareControl compareControl = new CompareControl(new CompareControl.SchemaComparison[]{
            new CompareControl.SchemaComparison(catalogAndSchema, catalogAndSchema)}, compareTypes);
        final CatalogAndSchema[] compareControlSchemas = compareControl
                .getSchemas(CompareControl.DatabaseRole.REFERENCE);

        try {
            final DatabaseSnapshot referenceSnapshot = SnapshotGeneratorFactory.getInstance()
                    .createSnapshot(compareControlSchemas, database, snapshotControl);
            final DatabaseSnapshot comparisonSnapshot = SnapshotGeneratorFactory.getInstance()
                    .createSnapshot(compareControlSchemas, null, snapshotControl);
            final DiffResult diffResult = DiffGeneratorFactory.getInstance()
                    .compare(referenceSnapshot, comparisonSnapshot, compareControl);

            changeLogWriter.setDiffResult(diffResult);
            changeLogWriter.print(outputStream);
        } catch (InvalidExampleException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    private static boolean isTrue(Boolean nullableCondition) {
        return nullableCondition != null && nullableCondition;
    }
}