/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jackrabbit.vault.fs.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.Property;
import javax.jcr.RepositoryException;

import org.apache.jackrabbit.vault.fs.PropertyValueArtifact;
import org.apache.jackrabbit.vault.fs.SerializerArtifact;
import org.apache.jackrabbit.vault.fs.api.Artifact;
import org.apache.jackrabbit.vault.fs.api.ArtifactIterator;
import org.apache.jackrabbit.vault.fs.api.ArtifactSet;
import org.apache.jackrabbit.vault.fs.api.ArtifactType;
import org.apache.jackrabbit.vault.fs.api.DumpContext;
import org.apache.jackrabbit.vault.fs.api.Dumpable;
import org.apache.jackrabbit.vault.fs.api.ItemFilter;
import org.apache.jackrabbit.vault.fs.api.ItemFilterSet;
import org.apache.jackrabbit.vault.fs.io.Serializer;

/**
 * Implements a set of artifacts. The artifacts can be retrieved via an
 * {@link ArtifactIterator}. Special artifacts like {@link ArtifactType#PRIMARY}
 * and {@link ArtifactType#DIRECTORY} can be retrieved individually. This set
 * only allows one of those artifacts per type.
 *
 * All modifications to this set have no influence on the persisted data.
 *
 */
public class ArtifactSetImpl implements Dumpable, ArtifactSet {

    /**
     * the list of artifacts
     */
    private final List<Artifact> artifacts = new LinkedList<Artifact>();

    /**
     * the list of removed
     */
    private List<Artifact> removed;
    
    /**
     * Set of artifacts that are only allowed once
     */
    private final Map<ArtifactType, Artifact> single = new EnumMap<ArtifactType, Artifact>(ArtifactType.class);

    /**
     * list of hint artifacts
     */
    private Set<Artifact> hints;

    /**
     * The item filter set that defines the coverage of this artifact set
     */
    private ItemFilterSet coverageFilter = (ItemFilterSet) new ItemFilterSet().addInclude(ItemFilter.ALL);

    public ItemFilterSet getCoverage() {
        return coverageFilter;
    }

    /**
     * Sets the item filter set that defines the coverage of the items in
     * this artifact set.
     * @param filter the item filter set.
     */
    public void setCoverage(ItemFilterSet filter) {
        this.coverageFilter = filter;
    }

    public void addAll(Collection<? extends Artifact> artifacts) {
         for (Artifact artifact: artifacts) {
             add(artifact);
         }
     }

     public void addAll(ArtifactSet artifacts) {
        addAll(((ArtifactSetImpl) artifacts).artifacts);
    }

    public void add(Artifact artifact) {
        ArtifactType type = artifact.getType();
        if (type == ArtifactType.PRIMARY || type == ArtifactType.DIRECTORY) {
            if (single.containsKey(type)) {
                throw new IllegalArgumentException("Only 1 " + type + " artifact allowed.");
            }
            single.put(type, artifact);
            artifacts.add(artifact);
        } else if (type == ArtifactType.HINT) {
            if (hints == null) {
                hints = new HashSet<Artifact>();
            }
            hints.add(artifact);
        } else {
            artifacts.add(artifact);
        }
    }

    /**
     * Adds an {@link SerializerArtifact} with the given name, type and
     * serializer.
     *
     * @param parent the parent artifact
     * @param name the name of the artifact
     * @param ext the extension of the artifact
     * @param type the type of the artifact
     * @param ser the serializer of the artifact
     * @param lastModified the last modified date
     * @return the added artifact
     */
    public SerializerArtifact add(Artifact parent, String name, String ext,
                                  ArtifactType type, Serializer ser,
                                  long lastModified) {
        SerializerArtifact a = new SerializerArtifact(parent, name, ext, type, ser, lastModified);
        add(a);
        return a;
    }

    /**
     * Adds an {@link PropertyValueArtifact} with the given name, type and
     * serializer
     *
     * @param parent parent artifact
     * @param relPath the name of the artifact
     * @param ext the extension
     * @param type the type of the artifact
     * @param prop the property of the artifact
     * @param lastModified the last modified date. can be 0.
     *
     * @return a collection of artifacts
     * @throws RepositoryException if an error occurs.
     */
    public Collection<PropertyValueArtifact> add(Artifact parent, String relPath,
                 String ext, ArtifactType type, Property prop, long lastModified)
            throws RepositoryException {
        Collection<PropertyValueArtifact> a =
                PropertyValueArtifact.create(parent, relPath, ext, type, prop, lastModified);
        addAll(a);
        return a;
    }


    public Artifact getPrimaryData() {
        return single.get(ArtifactType.PRIMARY);
    }

    public Artifact getDirectory() {
        return single.get(ArtifactType.DIRECTORY);
    }

    public boolean isEmpty() {
        return artifacts.isEmpty();
    }

    public int size() {
        return artifacts.size();
    }

    /**
     * Removes the artifact from this set.
     *
     * @param a the artifact to remove
     * @return {@code true} if the artifact was removed.
     */
    public boolean remove(Artifact a) {
        if (artifacts.remove(a)) {
            single.remove(a.getType());
            if (removed == null) {
                removed = new LinkedList<Artifact>();
                removed.add(a);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Puts the given artifact to this set. In comparison to the add operations,
     * an already existing artifact is replaced. If the type of the newly
     * artifact is {@link ArtifactType#BINARY} it equivalent with the
     * same name is replaced.
     *
     * @param a the artifact to put
     * @return the previous artifact or {@code null}
     */
    public Artifact put(Artifact a) {
        Artifact prev = null;
        int idx = artifacts.indexOf(a);
        if (idx >=0) {
            prev = artifacts.remove(idx);
        }
        single.put(a.getType(), a);
        artifacts.add(a);
        return prev;
    }

    /**
     * Returns the number of artifacts in this set that have the given type.
     * @param type the artifact type
     * @return the number of artifacts in this set that have the given type.
     */
    public int size(ArtifactType type) {
        int num = 0;
        if (type == ArtifactType.HINT) {
            num = hints == null ? 0 : hints.size();
        } else {
            for (Artifact a: artifacts) {
                if (a.getType() == type) {
                    num++;
                }
            }
        }
        return num;
    }

    /**
     * Returns a collection of all artifacts that have the given type.
     * @param type the type of the artifacts to return
     * @return the artifacts
     */
    public Collection<Artifact> values(ArtifactType type) {
        List<Artifact> ret = new LinkedList<Artifact>();
        if (type == ArtifactType.HINT) {
            if (hints != null) {
                ret.addAll(hints);
            }
        } else {
            for (Artifact a: artifacts) {
                if (type == null || a.getType() == type) {
                    ret.add(a);
                }
            }
        }
        return ret;
    }

    /**
     * Returns a collection of all artifacts
     * @return the artifacts
     */
    public Collection<Artifact> values() {
        return Collections.unmodifiableList(artifacts);
    }

    /**
     * Returns the collection of removed artifacts
     * @return the removed artifacts
     */
    public Collection<Artifact> removed() {
        if (removed == null) {
            return Collections.emptyList();
        } else {
            return Collections.unmodifiableList(removed);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String toString() {
        StringBuilder buf = new StringBuilder("ArtifactSet={");
        Iterator<Artifact> iter = artifacts.iterator();
        while (iter.hasNext()) {
            buf.append(iter.next());
            if (iter.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append('}');
        return buf.toString();
    }

    /**
     * Returns the artifact with the given path or {@code null} if it does
     * not exist.
     * @param path the name of the artifact.
     * @return the desired artifact or {@code null}
     */
    public Artifact getArtifact(String path) {
        for (Artifact a: artifacts) {
            if (a.getRelativePath().equals(path)) {
                return a;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void dump(DumpContext ctx, boolean isLast) {
        ctx.println(isLast, "Artifacts");
        ctx.indent(isLast);
        for (Artifact a: artifacts) {
            a.dump(ctx, false);
        }
        ctx.println(true, "Coverage");
        ctx.indent(true);
        coverageFilter.dump(ctx, true);
        ctx.outdent();
        ctx.outdent();
    }

}