/*
 * jaudiotagger library
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jaudiotagger.audio.generic;

import org.jaudiotagger.tag.*;
import org.jaudiotagger.tag.datatype.Artwork;

import java.util.*;

/**
 * This class is the default implementation for
 * {@link org.jaudiotagger.tag.Tag} and introduces some more useful
 * functionality to be implemented.<br>
 *
 * @author Raphaël Slinckx
 */
public abstract class AbstractTag implements Tag {
    /**
     * Stores the amount of {@link TagField} with {@link TagField#isCommon()}
     * <code>true</code>.
     */
    protected int commonNumber = 0;

    /**
     * This map stores the {@linkplain TagField#getId() ids} of the stored
     * fields to the {@linkplain TagField fields} themselves. Because a linked hashMap is used the order
     * that they are added in is preserved, the only exception to this rule is when two fields of the same id
     * exist, both will be returned according to when the first item was added to the file. <br>
     */
    protected Map<String, List<TagField>> fields = new LinkedHashMap<String, List<TagField>>();

    /**
     * Add field
     *
     * @see org.jaudiotagger.tag.Tag#addField(org.jaudiotagger.tag.TagField)
     *      <p/>
     *      Changed so add empty fields
     */
    public void addField(TagField field) {
        if (field == null) {
            return;
        }

        List<TagField> list = fields.get(field.getId());

        // There was no previous item
        if (list == null) {
            list = new ArrayList<TagField>();
            list.add(field);
            fields.put(field.getId(), list);
            if (field.isCommon()) {
                commonNumber++;
            }
        } else {
            // We append to existing list
            list.add(field);
        }
    }


    /**
     * Get list of fields within this tag with the specified id
     *
     * @see org.jaudiotagger.tag.Tag#getFields(java.lang.String)
     */
    public List<TagField> getFields(String id) {
        List<TagField> list = fields.get(id);

        if (list == null) {
            return new ArrayList<TagField>();
        }

        return list;
    }

    /**
     * @param id
     * @return
     */

    //Needs to be overridden
    //TODO remove
    public List<TagField> getFields(FieldKey id) throws KeyNotFoundException {
        List<TagField> list = fields.get(id.name());
        if (list == null) {
            return new ArrayList<TagField>();
        }
        return list;
    }


    /**
     * @param id
     * @param index
     * @return
     */
    public String getItem(String id, int index) {
        List<TagField> l = getFields(id);
        return (l.size() > index) ? l.get(index).toString() : "";
    }

    /**
     * Retrieve the first value that exists for this generic key
     *
     * @param genericKey
     * @return
     */
    public String getFirst(FieldKey genericKey) throws KeyNotFoundException {
        return getValue(genericKey, 0);
    }

    /**
     * @param id
     * @return
     */
    public String getFirst(String id) {
        List<TagField> l = getFields(id);
        return (l.size() != 0) ? l.get(0).toString() : "";
    }

    /**
     * @param id audio specific key
     * @return
     */
    public TagField getFirstField(String id) {
        List<TagField> l = getFields(id);
        return (l.size() != 0) ? l.get(0) : null;
    }


    /**
     * @see org.jaudiotagger.tag.Tag#getFields()
     */
    public Iterator<TagField> getFields() {
        final Iterator<Map.Entry<String, List<TagField>>> it = this.fields.entrySet().iterator();
        return new Iterator<TagField>() {
            private Iterator<TagField> fieldsIt;

            private void changeIt() {
                if (!it.hasNext()) {
                    return;
                }

                Map.Entry<String, List<TagField>> e = it.next();
                List<TagField> l = e.getValue();
                fieldsIt = l.iterator();
            }

            public boolean hasNext() {
                if (fieldsIt == null) {
                    changeIt();
                }
                return it.hasNext() || (fieldsIt != null && fieldsIt.hasNext());
            }

            public TagField next() {
                if (!fieldsIt.hasNext()) {
                    changeIt();
                }

                return fieldsIt.next();
            }

            public void remove() {
                fieldsIt.remove();
            }
        };
    }

    /**
     * Return field count
     * <p/>
     * TODO:There must be a more efficient way to do this.
     *
     * @return field count
     */
    public int getFieldCount() {
        Iterator it = getFields();
        int count = 0;
        while (it.hasNext()) {
            count++;
            it.next();
        }
        return count;
    }

    public int getFieldCountIncludingSubValues() {
        return getFieldCount();
    }

    /**
     * Does this tag contain any comon fields
     *
     * @see org.jaudiotagger.tag.Tag#hasCommonFields()
     */
    public boolean hasCommonFields() {
        return commonNumber != 0;
    }

    /**
     * Does this tag contain a field with the specified id
     *
     * @see org.jaudiotagger.tag.Tag#hasField(java.lang.String)
     */
    public boolean hasField(String id) {
        return getFields(id).size() != 0;
    }

    /**
     * Determines whether the given charset encoding may be used for the
     * represented tagging system.
     *
     * @param enc charset encoding.
     * @return <code>true</code> if the given encoding can be used.
     */
    protected abstract boolean isAllowedEncoding(String enc);

    /**
     * Is this tag empty
     *
     * @see org.jaudiotagger.tag.Tag#isEmpty()
     */
    public boolean isEmpty() {
        return fields.size() == 0;
    }

    /**
     * Create new field and set it in the tag
     *
     * @param genericKey
     * @param value
     * @throws KeyNotFoundException
     * @throws FieldDataInvalidException
     */
    public void setField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException {
        TagField tagfield = createField(genericKey, value);
        setField(tagfield);
    }


    /**
     * Create new field and add it to the tag
     *
     * @param genericKey
     * @param value
     * @throws KeyNotFoundException
     * @throws FieldDataInvalidException
     */
    public void addField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException {
        TagField tagfield = createField(genericKey, value);
        addField(tagfield);
    }

    /**
     * Set field
     * <p/>
     * Changed:Just because field is empty it doesn't mean it should be deleted. That should be the choice
     * of the developer. (Or does this break things)
     *
     * @see org.jaudiotagger.tag.Tag#setField(org.jaudiotagger.tag.TagField)
     */
    public void setField(TagField field) {
        if (field == null) {
            return;
        }

        // If there is already an existing field with same id
        // and both are TextFields, we replace the first element
        List<TagField> list = fields.get(field.getId());
        if (list != null) {
            list.set(0, field);
            return;
        }

        // Else we put the new field in the fields.
        list = new ArrayList<TagField>();
        list.add(field);
        fields.put(field.getId(), list);
        if (field.isCommon()) {
            commonNumber++;
        }
    }

    /**
     * The m parameter is effectively ignored
     *
     * @param id
     * @param n
     * @param m
     * @return
     */
    public String getSubValue(FieldKey id, int n, int m) {
        return getValue(id, n);
    }

    /**
     * Set or add encoding
     *
     * @see org.jaudiotagger.tag.Tag#setEncoding(java.lang.String)
     */
    public boolean setEncoding(String enc) {
        if (!isAllowedEncoding(enc)) {
            return false;
        }

        Iterator it = getFields();
        while (it.hasNext()) {
            TagField field = (TagField) it.next();
            if (field instanceof TagTextField) {
                ((TagTextField) field).setEncoding(enc);
            }
        }

        return true;
    }

    /**
     * (overridden)
     *
     * @see java.lang.Object#toString()
     */
    public String toString() {
        StringBuffer out = new StringBuffer();
        out.append("Tag content:\n");
        Iterator it = getFields();
        while (it.hasNext()) {
            TagField field = (TagField) it.next();
            out.append("\t");
            out.append(field.getId());
            out.append(":");
            out.append(field.toString());
            out.append("\n");
        }
        return out.toString().substring(0, out.length() - 1);
    }

    /**
     * @param genericKey
     * @param value
     * @return
     * @throws KeyNotFoundException
     * @throws FieldDataInvalidException
     */
    public abstract TagField createField(FieldKey genericKey, String value) throws KeyNotFoundException, FieldDataInvalidException;

    /**
     * @param genericKey
     * @return
     * @throws KeyNotFoundException
     */
    public abstract TagField getFirstField(FieldKey genericKey) throws KeyNotFoundException;

    /**
     * @param fieldKey
     * @throws KeyNotFoundException
     */
    public abstract void deleteField(FieldKey fieldKey) throws KeyNotFoundException;


    /**
     * Delete all occurrences of field with this id.
     *
     * @param key
     */
    public void deleteField(String key) {
        fields.remove(key);
    }

    public Artwork getFirstArtwork() {
        List<Artwork> artwork = getArtworkList();
        if (artwork.size() > 0) {
            return artwork.get(0);
        }
        return null;
    }

    /**
     * Create field and then set within tag itself
     *
     * @param artwork
     * @throws FieldDataInvalidException
     */
    public void setField(Artwork artwork) throws FieldDataInvalidException {
        this.setField(createField(artwork));
    }

    /**
     * Create field and then add within tag itself
     *
     * @param artwork
     * @throws FieldDataInvalidException
     */
    public void addField(Artwork artwork) throws FieldDataInvalidException {
        this.addField(createField(artwork));
    }


    /**
     * Delete all instance of artwork Field
     *
     * @throws KeyNotFoundException
     */
    public void deleteArtworkField() throws KeyNotFoundException {
        this.deleteField(FieldKey.COVER_ART);
    }
}
