package liquibase.datatype;

import liquibase.database.Database;
import liquibase.datatype.core.BigIntType;
import liquibase.datatype.core.IntType;
import liquibase.datatype.core.UnknownType;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.servicelocator.ServiceLocator;
import liquibase.structure.core.DataType;
import liquibase.util.ObjectUtil;
import liquibase.util.StringUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class DataTypeFactory {

    private static DataTypeFactory instance;

    private Map<String, SortedSet<Class<? extends LiquibaseDataType>>> registry = new ConcurrentHashMap<String, SortedSet<Class<? extends LiquibaseDataType>>>();

    protected DataTypeFactory() {
        Class<? extends LiquibaseDataType>[] classes;
        try {
            classes = ServiceLocator.getInstance().findClasses(LiquibaseDataType.class);

            for (Class<? extends LiquibaseDataType> clazz : classes) {
                //noinspection unchecked
                register(clazz);
            }

        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }

    }

    public static synchronized DataTypeFactory getInstance() {
        if (instance == null) {
            instance = new DataTypeFactory();
        }
        return instance;
    }

    public static void reset() {
        instance = new DataTypeFactory();
    }


    public void register(Class<? extends LiquibaseDataType> dataTypeClass) {
        try {
            LiquibaseDataType example = dataTypeClass.newInstance();
            List<String> names = new ArrayList<String>();
            names.add(example.getName());
            names.addAll(Arrays.asList(example.getAliases()));

            for (String name : names) {
                name = name.toLowerCase();
                if (registry.get(name) == null) {
                    registry.put(name, new TreeSet<Class<? extends LiquibaseDataType>>(new Comparator<Class<? extends LiquibaseDataType>>() {
                        @Override
                        public int compare(Class<? extends LiquibaseDataType> o1, Class<? extends LiquibaseDataType> o2) {
                            try {
                                return -1 * new Integer(o1.newInstance().getPriority()).compareTo(o2.newInstance().getPriority());
                            } catch (Exception e) {
                                throw new UnexpectedLiquibaseException(e);
                            }
                        }
                    }));
                }
                registry.get(name).add(dataTypeClass);
            }
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    public void unregister(String name) {
        registry.remove(name.toLowerCase());
    }

    public Map<String, SortedSet<Class<? extends LiquibaseDataType>>> getRegistry() {
        return registry;
    }

    public LiquibaseDataType fromDescription(String dataTypeDefinition) {
        String dataTypeName = dataTypeDefinition;
        if (dataTypeName.matches(".+\\(.*\\).*")) {
            dataTypeName = dataTypeDefinition.replaceFirst("\\s*\\(.*\\)", "");
        }
        if (dataTypeName.matches(".+\\{.*")) {
            dataTypeName = dataTypeName.replaceFirst("\\s*\\{.*", "");
        }
        boolean primaryKey = false;
        if (dataTypeName.endsWith(" identity")) {
            dataTypeName = dataTypeName.replaceFirst(" identity$", "");
            primaryKey = true;
        }

        String additionalInfo = null;
        if (dataTypeName.toLowerCase().startsWith("bit varying")) {
            //not going to do anything. Special case for postgres in our tests, need to better support handling these types of differences
        } else {
            String[] splitTypeName = dataTypeName.split("\\s+", 2);
            dataTypeName = splitTypeName[0];
            if (splitTypeName.length > 1) {
                additionalInfo = splitTypeName[1];
            }
        }

        SortedSet<Class<? extends LiquibaseDataType>> classes = registry.get(dataTypeName.toLowerCase());

        LiquibaseDataType liquibaseDataType = null;
        if (classes == null) {
            if (dataTypeName.toUpperCase().startsWith("INTERVAL")) {
                liquibaseDataType = new UnknownType(dataTypeDefinition);
            } else {
                liquibaseDataType = new UnknownType(dataTypeName);
            }
        } else {

            try {
                liquibaseDataType = classes.iterator().next().newInstance();
            } catch (Exception e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }
        if (liquibaseDataType == null) {
            liquibaseDataType = new UnknownType(dataTypeName);

        }
        liquibaseDataType.setAdditionalInformation(additionalInfo);

        if (dataTypeDefinition.matches(".+\\s*\\(.*")) {
            String paramStrings = dataTypeDefinition.replaceFirst(".*?\\(", "").replaceFirst("\\).*", "");
            String[] params = paramStrings.split(",");
            for (String param : params) {
                param = StringUtils.trimToNull(param);
                if (param != null) {
                    liquibaseDataType.addParameter(param);
                }
            }
        }

        /*
        The block below seems logically incomplete.
        It will always end up putting the second word after the entire type name
        e.g. character varying will become CHARACTER VARYING varying

        //try to something like "int(11) unsigned" or int unsigned but not "varchar(11 bytes)"
        String lookingForAdditionalInfo = dataTypeDefinition;
        lookingForAdditionalInfo = lookingForAdditionalInfo.replaceFirst("\\(.*\\)", "");
        if (lookingForAdditionalInfo.contains(" ")) {
            liquibaseDataType.setAdditionalInformation(lookingForAdditionalInfo.split(" ", 2)[1]);
        }*/

        if (dataTypeDefinition.matches(".*\\{.*")) {
            String paramStrings = dataTypeDefinition.replaceFirst(".*?\\{", "").replaceFirst("\\}.*", "");
            String[] params = paramStrings.split(",");
            for (String param : params) {
                param = StringUtils.trimToNull(param);
                if (param != null) {
                    String[] paramAndValue = param.split(":", 2);
                    try {
                        ObjectUtil.setProperty(liquibaseDataType, paramAndValue[0], paramAndValue[1]);
                    } catch (Exception e) {
                        throw new RuntimeException("Unknown property "+paramAndValue[0]+" for "+liquibaseDataType.getClass().getName()+" "+liquibaseDataType.toString());
                    }
                }
            }
        }

        if (primaryKey && liquibaseDataType instanceof IntType) {
            ((IntType) liquibaseDataType).setAutoIncrement(true);
        }
        if (primaryKey && liquibaseDataType instanceof BigIntType) {
            ((BigIntType) liquibaseDataType).setAutoIncrement(true);
        }

        if (primaryKey && liquibaseDataType instanceof IntType) {
            ((IntType) liquibaseDataType).setAutoIncrement(true);
        }
        if (primaryKey && liquibaseDataType instanceof BigIntType) {
            ((BigIntType) liquibaseDataType).setAutoIncrement(true);
        }

        liquibaseDataType.finishInitialization(dataTypeDefinition);

        return liquibaseDataType;

    }


    public LiquibaseDataType fromObject(Object object, Database database) {
        return fromDescription(object.getClass().getName());
    }

    public LiquibaseDataType from(DataType type) {
        return fromDescription(type.toString());
    }

    public LiquibaseDataType from(DatabaseDataType type) {
        return fromDescription(type.toString());
    }

    public String getTrueBooleanValue(Database database) {
        return fromDescription("boolean").objectToSql(true, database);
    }

    public String getFalseBooleanValue(Database database) {
        return fromDescription("boolean").objectToSql(false, database);
    }
}

@@ -7105,299 +7105,8 @@
  %7D%0A%0A
-        if (primaryKey && liquibaseDataType instanceof IntType) %7B%0A            ((IntType) liquibaseDataType).setAutoIncrement(true);%0A        %7D%0A        if (primaryKey && liquibaseDataType instanceof BigIntType) %7B%0A            ((BigIntType) liquibaseDataType).setAutoIncrement(true);%0A        %7D%0A%0A
     

remove duplicate lines that have no effect