package ru.yandex.qe.dispenser.domain.dao.person;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import org.jetbrains.annotations.NotNull;

import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.YaGroup;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.index.NormalizedPrimaryKeyBase;

public final class PersonProjectRelations {
    private final Table<NormalizedPrimaryKeyBase<String>, String, Set<Project>> projectsByEntityAndRole = HashBasedTable.create();

    private final Map<EntityType<?>, Table<Project, String, Set<?>>> entityByProjectAndRole = new HashMap<>();

    private <T extends NormalizedPrimaryKeyBase<String>> Table<Project, String, Set<T>> getEntityByProjectAndRole(final EntityType<T> type) {
        @SuppressWarnings("unchecked") final Map<EntityType<T>, Table<Project, String, Set<T>>> castedCache = (Map<EntityType<T>, Table<Project, String, Set<T>>>) ((Object) this.entityByProjectAndRole);

        return castedCache.computeIfAbsent(type, t -> HashBasedTable.create());
    }

    @NotNull
    @Deprecated
    public Set<Project> getLinkedProjects(final @NotNull Person user, @NotNull final Role role) {
        return getLinkedProjects(user, role.getKey());
    }

    @NotNull
    public Set<Project> getLinkedProjects(final @NotNull Person user, @NotNull final String roleKey) {
        final Set<Project> projects = projectsByEntityAndRole.get(user, roleKey);
        if (projects == null) {
            return Collections.emptySet();
        }
        return projects;
    }

    @NotNull
    public Map<String, Set<Project>> getLinkedProjects(final @NotNull Person user) {
        return projectsByEntityAndRole.row(user);
    }

    @NotNull
    @Deprecated
    public <T extends NormalizedPrimaryKeyBase<String>> SetMultimap<Project, T> getLinkedPersonGroups(@NotNull final EntityType<T> type,
                                                                                                      @NotNull final Collection<Project> projects,
                                                                                                      @NotNull final Role role) {
        return getLinkedEntities(type, projects, role.getKey());
    }

    @NotNull
    public <T extends NormalizedPrimaryKeyBase<String>> SetMultimap<Project, T> getLinkedEntities(@NotNull final EntityType<T> type,
                                                                                                  @NotNull final Collection<Project> projects,
                                                                                                  @NotNull final String roleKey) {
        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        final SetMultimap<Project, T> ret = HashMultimap.create();

        projects.forEach(project -> {
            final Set<T> values = table.get(project, roleKey);
            if (values != null) {
                ret.putAll(project, values);
            }
        });

        return ret;
    }

    @Deprecated
    public <T extends NormalizedPrimaryKeyBase<String>> Set<T> getLinkedPersonGroups(@NotNull final EntityType<T> type,
                                                                                     @NotNull final Project project,
                                                                                     @NotNull final Role role) {
        return getLinkedEntities(type, project, role.getKey());
    }

    public <T extends NormalizedPrimaryKeyBase<String>> Set<T> getLinkedEntities(@NotNull final EntityType<T> type,
                                                                                 @NotNull final Project project,
                                                                                 final String roleKey) {
        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);
        final Set<T> result = table.get(project, roleKey);
        if (result == null) {
            return Collections.emptySet();
        }
        return result;
    }

    @NotNull
    @Deprecated
    public <T extends NormalizedPrimaryKeyBase<String>> Multimap<Project, T> getLinkedPersonGroups(
            @NotNull final EntityType<T> type,
            @NotNull final Role role) {

        return getLinkedEntities(type, role.getKey());
    }

    @NotNull
    public <T extends NormalizedPrimaryKeyBase<String>> Multimap<Project, T> getLinkedEntities(
            @NotNull final EntityType<T> type,
            final String roleKey) {

        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        final Map<Project, Set<T>> projectEntities = table.column(roleKey);

        final HashMultimap<Project, T> result = HashMultimap.create();
        for (final Project project : projectEntities.keySet()) {
            result.putAll(project, projectEntities.get(project));
        }

        return result;
    }

    public <T extends NormalizedPrimaryKeyBase<String>> void attachAll(final EntityType<T> type, final Table<Project, String, Set<T>> linkedEntities) {

        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        table.putAll(linkedEntities);

        for (final Table.Cell<Project, String, Set<T>> cell : linkedEntities.cellSet()) {
            for (final T entity : cell.getValue()) {
                Set<Project> projects = projectsByEntityAndRole.get(entity, cell.getColumnKey());
                if (projects == null) {
                    projects = new HashSet<>();
                    projectsByEntityAndRole.put(entity, cell.getColumnKey(), projects);
                }
                projects.add(cell.getRowKey());
            }
        }

    }

    @Deprecated
    public void attach(@NotNull final Person person, final @NotNull Project project, @NotNull final Role role) {
        attach(EntityType.PERSON, person, project, role.getKey());
    }

    public <T extends NormalizedPrimaryKeyBase<String>> void attach(@NotNull final EntityType<T> type,
                                                                    @NotNull final T entity,
                                                                    final @NotNull Project project,
                                                                    final String roleKey) {

        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        Set<T> entities = table.get(project, roleKey);
        if (entities == null) {
            entities = new HashSet<>();
            table.put(project, roleKey, entities);
        }
        entities.add(entity);

        Set<Project> projects = projectsByEntityAndRole.get(entity, roleKey);
        if (projects == null) {
            projects = new HashSet<>();
            projectsByEntityAndRole.put(entity, roleKey, projects);
        }
        projects.add(project);
    }

    @Deprecated
    public void attach(@NotNull final YaGroup yaGroup, final @NotNull Project project, @NotNull final Role role) {
        attach(EntityType.GROUP, yaGroup, project, role.getKey());
    }

    public boolean detach(@NotNull final Person person) {
        return detach(EntityType.PERSON, person);

    }

    public <T extends NormalizedPrimaryKeyBase<String>> boolean detach(@NotNull final EntityType<T> type,
                                                                       @NotNull final T entity) {
        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        boolean result = false;

        for (final Set<?> entities : table.values()) {
            result |= entities.remove(entity);
        }


        final Set<String> roles = Sets.newHashSet(projectsByEntityAndRole.row(entity).keySet());
        for (final String role : roles) {
            projectsByEntityAndRole.remove(entity, role);
        }

        return result;
    }

    public boolean detach(@NotNull final YaGroup group) {
        return detach(EntityType.GROUP, group);
    }

    public void detach(final @NotNull Project project) {
        for (final EntityType<? extends NormalizedPrimaryKeyBase<String>> type : EntityType.VALUES) {
            @SuppressWarnings("unchecked") final Table<Project, String, Set<NormalizedPrimaryKeyBase<String>>> table = getEntityByProjectAndRole((EntityType<NormalizedPrimaryKeyBase<String>>) type);

            final Set<String> roleKeys = Sets.newHashSet(table.row(project).keySet());

            for (final String roleKey : roleKeys) {
                table.remove(project, roleKey);
            }
        }

        projectsByEntityAndRole.values().forEach(projects -> projects.remove(project));
    }

    public void detach(@NotNull final Person entity, @NotNull final Project project, @NotNull final Role role) {
        detach(entity, EntityType.PERSON, project, role);
    }

    public void detach(@NotNull final YaGroup entity, @NotNull final Project project, @NotNull final Role role) {
        detach(entity, EntityType.GROUP, project, role);
    }

    @Deprecated
    public <T extends NormalizedPrimaryKeyBase<String>> void detach(@NotNull final T entity,
                                                                    @NotNull final EntityType<T> type,
                                                                    @NotNull final Project project,
                                                                    @NotNull final Role role) {
        detach(entity, type, project, role.getKey());
    }

    public <T extends NormalizedPrimaryKeyBase<String>> void detach(@NotNull final T entity,
                                                                    @NotNull final EntityType<T> type,
                                                                    @NotNull final Project project,
                                                                    final String roleKey) {

        final Table<Project, String, Set<T>> table = getEntityByProjectAndRole(type);

        final Set<T> entities = table.get(project, roleKey);
        if (entities != null && !entities.isEmpty()) {
            entities.remove(entity);
        }
    }

    public interface EntityType<T> {
        EntityType<Person> PERSON = new EntityType<Person>() {
        };
        EntityType<YaGroup> GROUP = new EntityType<YaGroup>() {
        };

        List<EntityType<? extends NormalizedPrimaryKeyBase<String>>> VALUES = ImmutableList.of(PERSON, GROUP);
    }


}
