/*  Copyright (C) 2003-2011 JabRef contributors.
    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 2 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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package net.sf.jabref.groups;

import java.util.*;

import javax.swing.undo.AbstractUndoableEdit;

import net.sf.jabref.*;
import net.sf.jabref.util.QuotedStringTokenizer;

/**
 * @author jzieren
 *
 */
public class ExplicitGroup extends AbstractGroup implements SearchRule {
    public static final String ID = "ExplicitGroup:";

    private final Set<BibtexEntry> m_entries;

    public ExplicitGroup(String name, int context) {
        super(name, context);
        m_entries = new HashSet<BibtexEntry>();
    }

    public static AbstractGroup fromString(String s, BibtexDatabase db,
            int version) throws Exception {
        if (!s.startsWith(ID))
            throw new Exception(
                    "Internal error: ExplicitGroup cannot be created from \""
                            + s
                            + "\". "
                            + "Please report this on www.sf.net/projects/jabref");
        QuotedStringTokenizer tok = new QuotedStringTokenizer(s.substring(ID
                .length()), SEPARATOR, QUOTE_CHAR);
        switch (version) {
        case 0:
        case 1:
        case 2: {
            ExplicitGroup newGroup = new ExplicitGroup(tok.nextToken(),
                    AbstractGroup.INDEPENDENT);
            newGroup.addEntries(tok, db);
            return newGroup;
        }
        case 3: {
            String name = tok.nextToken();
            int context = Integer.parseInt(tok.nextToken());
            ExplicitGroup newGroup = new ExplicitGroup(name, context);
            newGroup.addEntries(tok, db);
            return newGroup;
        }
        default:
            throw new UnsupportedVersionException("ExplicitGroup", version);
        }
    }

    /** Called only when created fromString */
    protected void addEntries(QuotedStringTokenizer tok, BibtexDatabase db) {
        BibtexEntry[] entries;
        while (tok.hasMoreTokens()) {
            entries = db.getEntriesByKey(Util.unquote(tok.nextToken(),
                    QUOTE_CHAR));
            for (int i = 0; i < entries.length; ++i)
                m_entries.add(entries[i]);
        }
    }

    public SearchRule getSearchRule() {
        return this;
    }

    public boolean supportsAdd() {
        return true;
    }

    public boolean supportsRemove() {
        return true;
    }

    public AbstractUndoableEdit add(BibtexEntry[] entries) {
        if (entries.length == 0)
            return null; // nothing to do

        HashSet<BibtexEntry> entriesBeforeEdit = new HashSet<BibtexEntry>(m_entries);
        for (int i = 0; i < entries.length; ++i)
            m_entries.add(entries[i]);

        return new UndoableChangeAssignment(entriesBeforeEdit, m_entries);
    }

    public boolean addEntry(BibtexEntry entry) {
        return m_entries.add(entry);
    }

    public AbstractUndoableEdit remove(BibtexEntry[] entries) {
        if (entries.length == 0)
            return null; // nothing to do

        HashSet<BibtexEntry> entriesBeforeEdit = new HashSet<BibtexEntry>(m_entries);
        for (int i = 0; i < entries.length; ++i)
            m_entries.remove(entries[i]);

        return new UndoableChangeAssignment(entriesBeforeEdit, m_entries);
    }

    public boolean removeEntry(BibtexEntry entry) {
        return m_entries.remove(entry);
    }

    public boolean contains(BibtexEntry entry) {
        return m_entries.contains(entry);
    }

    public boolean contains(Map<String, String> searchOptions, BibtexEntry entry) {
        return contains(entry);
    }

    public int applyRule(Map<String, String> searchStrings, BibtexEntry bibtexEntry) {
        return contains(searchStrings, bibtexEntry) ? 1 : 0;
    }

    public boolean validateSearchStrings(Map<String, String> searchStrings) {
        return true;
    }

    public AbstractGroup deepCopy() {
        ExplicitGroup copy = new ExplicitGroup(m_name, m_context);
        copy.m_entries.addAll(m_entries);
        return copy;
    }

    public boolean equals(Object o) {
        if (!(o instanceof ExplicitGroup))
            return false;
        ExplicitGroup other = (ExplicitGroup) o;
        // compare entries assigned to both groups
        if (m_entries.size() != other.m_entries.size())
            return false; // add/remove
        HashSet<String> keys = new HashSet<String>();
        BibtexEntry entry;
        String key;
        // compare bibtex keys for all entries that have one
        for (Iterator<BibtexEntry> it = m_entries.iterator(); it.hasNext(); ) {
            entry = it.next();
            key = entry.getCiteKey();
            if (key != null)
                keys.add(key);
        }
        for (Iterator<BibtexEntry> it = other.m_entries.iterator(); it.hasNext(); ) {
            entry = it.next();
            key = entry.getCiteKey();
            if (key != null)
                if (!keys.remove(key))
                    return false;
        }
        if (!keys.isEmpty())
            return false;
        return other.m_name.equals(m_name)
                && other.getHierarchicalContext() == getHierarchicalContext();
    }

    /**
     * Returns a String representation of this group and its entries. Entries
     * are referenced by their Bibtexkey. Entries that do not have a Bibtexkey
     * are not included in the representation and will thus not be available
     * upon recreation.
     */
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append(ID).append(Util.quote(m_name, SEPARATOR, QUOTE_CHAR)).append(SEPARATOR).append(m_context).append(SEPARATOR);
        String s;
        // write entries in well-defined order for CVS compatibility
        Set<String> sortedKeys = new TreeSet<String>();
        for (Iterator<BibtexEntry> it = m_entries.iterator(); it.hasNext();) {
            s = it.next().getCiteKey();
            if (s != null && !s.equals("")) // entries without a key are lost
                sortedKeys.add(s);
        }
        for (Iterator<String> it = sortedKeys.iterator(); it.hasNext();) {
            sb.append(Util.quote(it.next(), SEPARATOR, QUOTE_CHAR)).append(SEPARATOR);
        }
        return sb.toString();
    }

    /** Remove all assignments, resulting in an empty group. */
    public void clearAssignments() {
        m_entries.clear();
    }

    public boolean isDynamic() {
        return false;
    }

    public String getDescription() {
        return getDescriptionForPreview();
    }

    public static String getDescriptionForPreview() {
        return Globals.lang("This group contains entries based on manual assignment. "
                        + "Entries can be assigned to this group by selecting them "
                        + "then using either drag and drop or the context menu. "
                        + "Entries can be removed from this group by selecting them "
                        + "then using the context menu. Every entry assigned to this group "
                        + "must have a unique key. The key may be changed at any time "
                        + "as long as it remains unique.");
    }

    public String getShortDescription() {
        StringBuffer sb = new StringBuffer();
        sb.append("<b>").append(getName()).append("</b> -").append(Globals.lang("static group"));
        switch (getHierarchicalContext()) {
        case AbstractGroup.INCLUDING:
            sb.append(", ").append(Globals.lang("includes subgroups"));
            break;
        case AbstractGroup.REFINING:
        	sb.append(", ").append(Globals.lang("refines supergroup"));
            break;
        default:
            break;
        }
        return sb.toString();
    }

    /**
     * Update the group to handle the situation where the group
     * is applied to a different BibtexDatabase than it was created for.
     * This group type contains a Set of BibtexEntry objects, and these will not
     * be the same objects as in the new database. We must reset the entire Set with
     * matching entries from the new database.
     *
     * @param db The database to refresh for.
     */
        public void refreshForNewDatabase(BibtexDatabase db) {
            Set<BibtexEntry> newSet = new HashSet<BibtexEntry>();
            for (Iterator<BibtexEntry> i=m_entries.iterator(); i.hasNext();) {
                BibtexEntry entry = i.next();
                BibtexEntry sameEntry = db.getEntryByKey(entry.getCiteKey());
                /*if (sameEntry == null) {
                    System.out.println("Error: could not find entry '"+entry.getCiteKey()+"'");
                } else {
                    System.out.println("'"+entry.getCiteKey()+"' ok");
                }*/
                newSet.add(sameEntry);
            }
            m_entries.clear();
            m_entries.addAll(newSet);
        }

		public Set<BibtexEntry> getEntries(){
			return m_entries;
		}

    public String getTypeId() {
        return ID;
    }

	public int getNumEntries() {
		return m_entries.size();
	}

}
