package liquibase.structure;

import liquibase.database.Database;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.parser.NamespaceDetails;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.LiquibaseSerializable;

import java.util.*;

public class DatabaseObjectCollection implements LiquibaseSerializable {

    private Map<Class<? extends DatabaseObject>, Map<String, Set<DatabaseObject>>> cache = new HashMap<Class<? extends DatabaseObject>, Map<String, Set<DatabaseObject>>>();
    private Database database;

    public DatabaseObjectCollection(Database database) {
        this.database = database;
    }

    @Override
    public String getSerializedObjectName() {
        return "objects";
    }

    @Override
    public String getSerializedObjectNamespace() {
        return STANDARD_SNAPSHOT_NAMESPACE;
    }

    @Override
    public String getSerializableFieldNamespace(String field) {
        return getSerializedObjectNamespace();
    }


    @Override
    public Set<String> getSerializableFields() {
        SortedSet<String> types = new TreeSet<String>();
        for (Class type : cache.keySet()) {
            types.add(type.getName());
        }
        return types;

    }

    @Override
    public Object getSerializableFieldValue(String field) {
        SortedSet<DatabaseObject> objects = new TreeSet<DatabaseObject>(new DatabaseObjectComparator());
        try {
            Map<String, Set<DatabaseObject>> map = cache.get(Class.forName(field));
            if (map == null) {
                return null;
            }
            for (Set<DatabaseObject> set : map.values()) {
                objects.addAll(set);
            }
            return objects;
        } catch (ClassNotFoundException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    @Override
    public SerializationType getSerializableFieldType(String field) {
        return SerializationType.NAMED_FIELD;
    }

    public void add(DatabaseObject databaseObject) {
        if (databaseObject == null) {
            return;
        }
        Map<String, Set<DatabaseObject>> collectionMap = cache.get(databaseObject.getClass());
        if (collectionMap == null) {
            collectionMap = new HashMap<String, Set<DatabaseObject>>();
            cache.put(databaseObject.getClass(), collectionMap);
        }

        String[] hashes = DatabaseObjectComparatorFactory.getInstance().hash(databaseObject, null, database);

        for (String hash : hashes) {
            Set<DatabaseObject> collection = collectionMap.get(hash);
            if (collection == null) {
                collection = new HashSet<DatabaseObject>();
                collectionMap.put(hash, collection);
            }
            collection.add(databaseObject);
        }
    }

    /**
     * Returns the object described by the passed example if it is already included in this snapshot.
     */
    public <DatabaseObjectType extends DatabaseObject> DatabaseObjectType get(DatabaseObjectType example, CompareControl.SchemaComparison[] schemaComparisons) {
        Map<String, Set<DatabaseObject>> databaseObjectsByHash = cache.get(example.getClass());

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

        String[] hashes = DatabaseObjectComparatorFactory.getInstance().hash(example, null, database);

        SortedSet<Set<DatabaseObject>> objectSets = new TreeSet<Set<DatabaseObject>>(new Comparator<Set<DatabaseObject>>() {
            @Override
            public int compare(Set<DatabaseObject> o1, Set<DatabaseObject> o2) {
                int sizeComparison = Integer.valueOf(o1.size()).compareTo(o2.size());
                if (sizeComparison == 0) {
                    return o1.toString().compareTo(o2.toString());
                }
                return sizeComparison;
            }
        });

        for (String hash : hashes) {
            Set<DatabaseObject> databaseObjects = databaseObjectsByHash.get(hash);
            if (databaseObjects != null) {
                objectSets.add(databaseObjects);
            }
        }

        for (Set<DatabaseObject> databaseObjects : objectSets) {
            for (DatabaseObject obj : databaseObjects) {
                if (DatabaseObjectComparatorFactory.getInstance().isSameObject(obj, example, schemaComparisons, database)) {
                    //noinspection unchecked
                    return (DatabaseObjectType) obj;
                }
            }
        }

        return null;
    }

    /**
     * Returns all objects of the given type that are already included in this snapshot.
     */
    public <DatabaseObjectType extends DatabaseObject> Set<DatabaseObjectType> get(Class<DatabaseObjectType> type) {

        Set<DatabaseObject> returnSet = new HashSet<DatabaseObject>();

        Map<String, Set<DatabaseObject>> allFound = cache.get(type);
        if (allFound != null) {
            for (Set<DatabaseObject> objects : allFound.values()) {
                returnSet.addAll(objects);
            }
        }

        return (Set<DatabaseObjectType>) Collections.unmodifiableSet(returnSet);
    }


    public boolean contains(DatabaseObject wantedObject, CompareControl.SchemaComparison[] schemaComparisons) {
        return get(wantedObject, schemaComparisons) != null;
    }

    @Override
    public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        throw new RuntimeException("TODO");
    }

    @Override
    public ParsedNode serialize() {
        throw new RuntimeException("TODO");
    }

    public Map<Class<? extends DatabaseObject>, Set<? extends DatabaseObject>> toMap() {
        Map<Class<? extends DatabaseObject>, Set<? extends DatabaseObject>> returnMap = new HashMap<Class<? extends DatabaseObject>, Set<? extends DatabaseObject>>();
        for (Class<? extends DatabaseObject> type : this.cache.keySet()) {
            returnMap.put(type, get(type));
        }

        return returnMap;
    }

}
