/*
 * Copyright 2010 Lari Natri <lari.natri@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package compid.report.protgroup;

import compid.data.tuples.DatasetInfo;
import compid.data.tuples.ProteinHit.GroupMemberStatus;
import net.aavalla.utils.jobpool.parameters.SyncEnumSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Protein Group. Thread-safe.
 * @author Lari Natri
 */
public final class ProtGroup {

    private static final Object groupMemberIdCounterLock = new Object();
    private static long groupMemberIdCounter = Long.MIN_VALUE;

    private Map<ProtGroupMember,Long> members = new HashMap<ProtGroupMember,Long>(8); // member->origGroup, not final to allow unmodifiable wrapper
    private final Set<Integer> memberDatasetIds;
    private final long defaultGroupMemberId;
    private boolean isComplete = false;

    /**
     * Constructs an empty protein group. After constructing the group, add
     * the member proteins by calling addMember(). After all proteins are
     * added, set the group complete with setComplete().
     */
    public ProtGroup() {
        synchronized(groupMemberIdCounterLock) {
            if (groupMemberIdCounter==Long.MAX_VALUE) {
                groupMemberIdCounter=Long.MIN_VALUE;
            }
            this.defaultGroupMemberId = ++groupMemberIdCounter;
        }
        this.memberDatasetIds = new HashSet<Integer>(1,1);
    }

    /**
     * Constructs a new protein group from two old ones. All the proteins
     * in the parameter groups are added to the new group. The constructed
     * group is set complete. Source groups must be completed before using
     * this.
     * @param group1 old group 1
     * @param group2 old group 2
     * @throws IllegalStateException Thrown if either of the given groups
     *         are not complete.
     */
    public ProtGroup(ProtGroup group1, ProtGroup group2) 
            throws IllegalStateException {
        this();
        try {
            for (ProtGroupMember member : group1.getMembers()) {
                this.addMember(member, group1.members.get(member));
            }
            for (ProtGroupMember member : group2.getMembers()) {
                this.addMember(member, group2.members.get(member));
            }
        } catch (IllegalStateException ex) {
            throw new IllegalStateException("Can not combine protein groups " +
                    "which are not marked complete.");
        }

        this.setComplete();
    }

    /**
     * Adds a member protein to this group. Group must not be set complete
     * before.
     * @param member protein to add
     * @return true if member was added, false if it already existed (no
     *         addition)
     * @throws IllegalStateException Thrown if group has been set complete
     *         before trying to add this (no addition).
     */
    public synchronized final boolean addMember(ProtGroupMember member) 
            throws IllegalStateException
    {
        return this.addMember(member, this.defaultGroupMemberId);
    }

    /**
     * Adds a member protein to this group with the specified group id.
     * Group must not be set complete before.
     * @param member protein to add
     * @param groupId original group id of the member to add
     * @return true if member was added, false if it already existed (no
     *         addition)
     * @throws IllegalStateException Thrown if group has been set complete
     *         before trying to add this (no addition).
     */
    
    private synchronized final boolean addMember(ProtGroupMember member, long groupId) 
            throws IllegalStateException
    {
        if (this.isComplete) {
            throw new IllegalStateException("Can not add members to a " +
                    "protein group which is already marked complete.");
        }
        this.memberDatasetIds.add(member.getDatasetId());
        return (this.members.put(member,groupId)==null);
    }


    /**
     * Returns all the proteins in this protein group as an unmodifiable set.
     * Group must be set complete before calling this function.
     * @return group member proteins
     * @throws IllegalStateException Thrown if group is not set complete
     */
    public synchronized final Set<ProtGroupMember> getMembers() 
            throws IllegalStateException
    {
        if (!this.isComplete) {
            throw new IllegalStateException("Protein group must be marked " +
                    "complete before retrieving it's members.");
        }
        return this.members.keySet();
    }

    /**
     * Returns a new 'sub'protein group containing only those members of this
     * protein group, which meet the criteria given as arguments.
     * @param statuses statuses to accept into the new group
     * @param datasetIds dataset ids to accept into the new group
     * @throws IllegalStateException Thrown if group is not set complete
     * @return new protein group containing a subset of this protein group's
     *         members
     */
    public synchronized final ProtGroup getSubGroup(SyncEnumSet<GroupMemberStatus> statuses, Set<Integer> datasetIds)
            throws IllegalStateException
    {
        ProtGroup pg = new ProtGroup();
        for (ProtGroupMember prot : this.getMembers()) { // throws
            if (datasetIds.contains(prot.getDatasetId()) /*&& statuses.contains(prot.getType())*/) {
                pg.addMember(prot,this.members.get(prot));
            }
        }
        pg.setComplete();
        return pg;
    }


    public final int getOriginalGroupCount() {
        HashSet<Long> uniqueGroupIds = new HashSet<Long>(this.members.values());
        return uniqueGroupIds.size();
    }

    /**
     * Marks this group as complete. Members can not be added to complete
     * groups, but only complete groups can be combined to create a new group.
     * @throws IllegalStateException Thrown, if this group doesn't yet contain
     *         any member proteins. Group is not set complete.
     */
    public synchronized final void setComplete() throws IllegalStateException
    {
        if (this.members.isEmpty()) {
            throw new IllegalStateException("Protein group must contain " +
                    "at least one protein before it can be set complete.");
        }
        this.members = Collections.unmodifiableMap(this.members);
        this.isComplete = true;
    }

    /**
     * Returns true, if this protein group is marked as complete. Members can
     * not be added to complete groups, but only complete groups can be
     * combined to create a new group.
     * @return true if group is complete, false otherwise
     */
    public synchronized final boolean isComplete() {
        return this.isComplete;
    }

    /**
     * Returns true if, and only if, this protein group has at least one
     * member protein originating in the dataset given as parameter.
     * @param dataset
     * @return
     * @throws IllegalArgumentException Thrown, if primary key is not set
     *         for the parameter dataset.
     */
    public synchronized final boolean hasMembersFrom(DatasetInfo dataset)
            throws IllegalArgumentException
    {
        if (dataset.getPrimaryKey()<1) {
            throw new IllegalArgumentException("Dataset must have it's " +
                    "primary key set");
        }
        return memberDatasetIds.contains(dataset.getPrimaryKey());
    }

    /**
     * Returns true if, and only if, this protein group has at least one
     * member protein originating in at least one dataset given as parameter.
     * @param datasets
     * @return
     * @throws IllegalArgumentException Thrown, if primary key is not set for
     *         some of the parameter datasets
     */
    public synchronized final boolean hasMembersFromAny(Collection<DatasetInfo> datasets)
            throws IllegalArgumentException
    {
        for (DatasetInfo dataset : datasets) {
            if (this.hasMembersFrom(dataset)) {
                return true;
            }
        }
        return false;
    }


}
