package com.atlassian.stash.internal.backup.liquibase;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;

import java.sql.Time;
import java.sql.Timestamp;
import java.util.*;

/**
 * Enumeration of all data types used in database columns in Stash. Used by
 * {@link DefaultLiquibaseDaoTest#testLiquibaseInsertedDataMatchesOutput()} to check that liquibase doesn't adversely
 * affect data when inserting into the database.
 */
public enum ColumnType {

    BLOB("BLOB", ImmutableSet.of("oracle"), testBinary(100000)),
    BOOLEAN("BOOLEAN", true, false),
    BYTEA("BYTEA", ImmutableSet.of("postgresql"), testBinary(100000)),
    DATE("DATE", testDate(Date.class)),
    DATETIME("DATETIME", testDate(Timestamp.class)),
    TIMESTAMP("TIMESTAMP", testDate(Timestamp.class)),
    BIGINT("BIGINT", Long.MAX_VALUE), // 2^63 is max in HSQL
    INT("INT", Integer.MAX_VALUE),
    INTEGER("INTEGER", Integer.MAX_VALUE),
    LONGBLOB("LONGBLOB", ImmutableSet.of("mysql"), testBinary(100000)),
    NUMERIC("NUMERIC", 1234567890L),
    SMALLINT("SMALLINT", Short.MAX_VALUE),
    VARBINARY_2G("VARBINARY(2147483647)", ImmutableSet.of("h2", "hsqldb"), testBinary(100000)),
    VARBINARY_MAX("VARBINARY(MAX)", ImmutableSet.of("mssql"), testBinary(100000)),
    VARCHAR_20("VARCHAR(20)", testString(20)),
    VARCHAR_32("VARCHAR(32)", testString(32)),
    VARCHAR_40("VARCHAR(40)", testString(40)),
    VARCHAR_50("VARCHAR(50)", testString(50)),
    VARCHAR_64("VARCHAR(64)", testString(64)),
    VARCHAR_127("VARCHAR(127)", testString(127)),
    VARCHAR_128("VARCHAR(128)", testString(128)),
    VARCHAR_255("VARCHAR(255)", testString(255)),
    VARCHAR_1024("VARCHAR(1024)", testString(1024)),
    VARCHAR_2000("VARCHAR(2000)", testString(2000)),
    //VARCHAR_4000("VARCHAR(4000)", testString(4000)),
    VARCHAR_4000_W_2000CH("VARCHAR(4000)", testString(2000)),
    CHAR_1("CHAR(1)", testString(1)),
    CLOB("CLOB", testString(10000));

    public static boolean isKnownDataTypeInDatabase(String dataType, String database) {
        ColumnType columnType = columnTypesByName.get(dataType.toUpperCase());
        return columnType != null && columnType.isUsedInDatabase(database);
    }

    public static final Set<String> SUPPORTED_DATABASES = ImmutableSet.of("h2", "hsqldb", "mssql", "mysql", "oracle",
            "postgresql");

    public static byte[] testBinary(int length) {
        byte[] result = new byte[length];
        for (int i = 0; i < length; i++) {
            result[i] = (byte) (i & 0xff);
        }
        return result;
    }

    /**
     * Create a java.sql.Time, Timestamp or Date depending on the supplied class. The following precision is used:
     * <table>
     *     <tr><td>Time</td><td>Time only (date set to epoch)</td></tr>
     *     <tr><td>Date</td><td>Date only (time zeroed out)</td></tr>
     *     <tr><td>Timestamp</td><td>Date + Time to second precision</td></tr>
     *     </tr>
     * </table>
     */
    private static <T extends java.util.Date> T testDate(Class<T> clazz) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(1997, Calendar.AUGUST, 29, 2, 14, 13);
        calendar.set(Calendar.MILLISECOND, 0);

        if (Time.class.equals(clazz)) {
            // create a java.sql.Time object (with no date set - or more accurately set to epoch)
            calendar.set(Calendar.YEAR, 1970);
            calendar.set(Calendar.MONTH, Calendar.JANUARY);
            calendar.set(Calendar.DATE, 1);
            return (T) new Time(calendar.getTimeInMillis());
        } else if (Timestamp.class.equals(clazz)) {
            // create a java.sql.Timestamp object
            return (T) new Timestamp(calendar.getTimeInMillis());
        } else if (Date.class.equals(clazz)) {
            // create a java.sql.Date object for testing (with no h/m/s set)
            calendar.set(Calendar.HOUR_OF_DAY, 0);
            calendar.set(Calendar.MINUTE, 0);
            calendar.set(Calendar.SECOND, 0);
            return (T) new java.sql.Date(calendar.getTimeInMillis());
        } else {
            throw new IllegalArgumentException("Unsupported type " + clazz.getName());
        }
    }

    /**
     * Generate a test string of the desired length
     */
    private static String testString(int length) {
        // create a test String with some CJK characters
        String str = "\u5718Testing testing 1-\u96c6";
        return StringUtils.repeat(str, (length / str.length()) + 1).substring(0, length);
    }

    private static final Map<String, ColumnType> columnTypesByName;

    static {
        ImmutableMap.Builder<String, ColumnType> builder = ImmutableMap.builder();
        for (ColumnType columnType : values()) {
            builder.put(columnType.dataType, columnType);
        }
        columnTypesByName = builder.build();
    }

    private final String dataType;
    private final List<?> testValues;
    private final Set<String> databases;

    ColumnType(String dataType, Object... testValues) {
        this(dataType, null, testValues);
    }

    ColumnType(String dataType, Set<String> databases, Object... testValues) {
        this.dataType = dataType;
        this.databases = databases;
        this.testValues = ImmutableList.copyOf(testValues);
    }

    public String getDataType() {
        return dataType;
    }

    public List<?> getTestValues() {
        return testValues;
    }

    public boolean isUsedInDatabase(String database) {
        return databases == null || databases.contains(database);
    }

    public static long numericToLongBits(Number number) {
        return Double.doubleToLongBits((number).doubleValue());
    }
}
