package liquibase.change;

import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogFactory;
import liquibase.logging.Logger;
import liquibase.servicelocator.ServiceLocator;

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

/**
 * Factory class for constructing the correct liquibase.change.Change implementation based on a command name.
 * For XML-based changelogs, the tag name is the command name.
 * Change implementations are looked up via the {@link ServiceLocator}.
 *
 * @see liquibase.change.Change
 */
public class ChangeFactory {

    private static ChangeFactory instance;

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

    private Logger log;

    private ChangeFactory() {
      log = LogFactory.getInstance().getLog();
    }

    protected Logger getLogger() {
      return log;
    }
    
    private void init() {
        Class<? extends Change>[] classes;
        classes = ServiceLocator.getInstance().findClasses(Change.class);

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

    /**
     * Return the singleton ChangeFactory instance.
     */
    public static synchronized ChangeFactory getInstance() {
        if (instance == null) {
            instance = new ChangeFactory();
            instance.init();
        }
        return instance;
    }

    /**
     * Reset the ChangeFactory so it reloads the registry on the next call to @{link #getInstance()}. Mainly used in testing
     */
    public static synchronized void reset() {
        instance = null;
    }


    /**
     * Register a new Change class.
     * Normally called automatically by ChangeFactory on all Change implementations found by the ServiceLocator, but it can be called manually if needed.
     */
    public void register(Class<? extends Change> changeClass) {
        try {
            Change instance = changeClass.newInstance();
            ChangeMetaData metaData = getChangeMetaData(instance);
            String name = metaData.getName();
            if (registry.get(name) == null) {
                registry.put(name, new TreeSet<Class<? extends Change>>(new Comparator<Class<? extends Change>>() {
                    @Override
                    public int compare(Class<? extends Change> o1, Class<? extends Change> o2) {
                        try {
                            return -1 * Integer.valueOf(getChangeMetaData(o1.newInstance()).getPriority()).compareTo(getChangeMetaData(o2.newInstance()).getPriority());
                        } catch (Exception e) {
                            throw new UnexpectedLiquibaseException(e);
                        }
                    }
                }));
            }
            registry.get(name).add(changeClass);
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
		}
    }

    public ChangeMetaData getChangeMetaData(String change) {
        Change changeObj = create(change);
        if (changeObj == null) {
            return null;
        }
        return getChangeMetaData(changeObj);
    }

    public ChangeMetaData getChangeMetaData(Change change) {
        if (!metaDataByClass.containsKey(change.getClass())) {
            metaDataByClass.put(change.getClass(), change.createChangeMetaData());
        }
        return metaDataByClass.get(change.getClass());
    }

    /**
     * Unregister all instances of a given Change name. Normally used for testing, but can be called manually if needed.
     */
    public void unregister(String name) {
        registry.remove(name);
    }

    /**
     * Return the registry of all Changes found. Key is the change name and the values are a sorted set of implementations, ordered by Priority descending.
     * Normally used only for information/debugging purposes. The returned map is read only.
     */
    public Map<String, SortedSet<Class<? extends Change>>> getRegistry() {
        return Collections.unmodifiableMap(registry);
    }


    /**
     * Returns all defined changes in the registry. Returned set is not modifiable.
     */
    public Set<String> getDefinedChanges() {
        return Collections.unmodifiableSet(registry.keySet());
    }

    /**
     * Clear the registry of all Changes found. Normally used for testing.
     */
    public void clear() {
        registry.clear();
        metaDataByClass.clear();
    }

    /**
     * Create a new Change implementation for the given change name. The class of the constructed object will be the Change implementation with the highest priority.
     * Each call to create will return a new instance of the Change.
     */
    public Change create(String name) {
        SortedSet<Class<? extends Change>> classes = registry.get(name);

        if (classes == null) {
            return null;
        }

        try {
            return classes.iterator().next().newInstance();
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    public String[] getAllChangeNamespaces() {
        Set<String> namespaces = new HashSet<String>();
        for (String changeName : getDefinedChanges()) {
            Change change = create(changeName);
            namespaces.add(change.getSerializedObjectNamespace());
        }

        return namespaces.toArray(new String[namespaces.size()]);
    }

    public Map<String, Object> getParameters(Change change) {
        Map<String, Object> returnMap = new HashMap<String, Object>();
        ChangeMetaData changeMetaData = getChangeMetaData(change);
        for (ChangeParameterMetaData param : changeMetaData.getParameters().values()) {
            Object currentValue = param.getCurrentValue(change);
            if (currentValue != null) {
                returnMap.put(param.getParameterName(), currentValue);
            }
        }

        return returnMap;
    }

    // exposed for test only
    protected void setLogger(Logger mockLogger) {
      log = mockLogger;
    }
}
