// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.tagfilter.v0_6;

import java.util.Set;
import java.util.Map;
import java.util.logging.Logger;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;
import org.openstreetmap.osmosis.core.container.v0_6.NodeContainer;
import org.openstreetmap.osmosis.core.container.v0_6.WayContainer;
import org.openstreetmap.osmosis.core.container.v0_6.RelationContainer;
import org.openstreetmap.osmosis.core.domain.v0_6.Tag;
import org.openstreetmap.osmosis.core.task.v0_6.Sink;
import org.openstreetmap.osmosis.core.task.v0_6.SinkSource;

/**
 * A simple class to filter node, way, and relation entities by their tag keys and/or values.
 * 
 * @author Andrew Byrd
 */
public class TagFilter implements SinkSource {
    private Sink sink;
    private Set<String> tagKeys;
    private Map<String, Set<String>> tagKeyValues;
    private Class<? extends EntityContainer> filterClass;
    private boolean reject;
    private boolean matchesEverything;
    private static final Logger LOG = Logger.getLogger(TagFilter.class.getName());
    

    /**
     * Creates a new instance. 
     *
     * @param filterMode
     *          A 2-field dash-separated string specifying:
     *          1. Whether the filter accepts or rejects entities
     *          2. The entity type upon which it operates
     *
     * @param tagKeys
     *          A Set of tag key Strings. The filter will match these tags irrespective of tag values.
     *
     * @param tagKeyValues
     *          A map of tag key Strings to Sets of tag key values. These key-value pairs are checked 
     *          against each entity's tags to determine whether or not it matches the filter.
     */
    public TagFilter(String filterMode, Set<String> tagKeys, Map<String, Set<String>> tagKeyValues) {        
        String[] filterModeSplit = filterMode.toLowerCase().split("-");
        if (filterModeSplit.length != 2) { 
            throw new OsmosisRuntimeException(
            "The TagFilter task's default parameter must consist of an action and an entity type separated by '-'."); 
        }

        String action = filterModeSplit[0];
        if (action.equals("accept")) { 
            reject = false; 
        } else if (action.equals("reject")) { 
            reject = true;  
        } else { 
            throw new OsmosisRuntimeException(
            "The TagFilter action must be either 'accept' or 'reject'. '" + action + "' is not a supported mode."); 
        }

        String entity = filterModeSplit[1];
        if  (entity.endsWith("s")) { 
            entity = entity.substring(0, entity.length() - 1); 
        }
        if  (entity.equals("node")) { 
            filterClass = NodeContainer.class; 
        } else if (entity.equals("way")) { 
            filterClass = WayContainer.class;      
        } else if (entity.equals("relation")) { 
            filterClass = RelationContainer.class; 
        } else { 
            throw new OsmosisRuntimeException(
            "The TagFilter entity type must be one of 'node', 'way', or 'relation'. '" + entity 
            + "' is not a supported entity type."); 
        }
        
        matchesEverything = (tagKeys.size() == 0 && tagKeyValues.size() == 0);
        this.tagKeys = tagKeys;
        this.tagKeyValues = tagKeyValues;
        
        String logString = "New TagFilter ";
        if (reject) {
            logString += "rejects ";
        } else {
            logString += "accepts ";
        }    
        logString += filterClass;
        if (matchesEverything) {
            logString += " (no tag-based filtering).";
        } else {
            logString += " having tag keys " + tagKeys + " or tag key-value pairs " + tagKeyValues + ".";
        }
        LOG.finer(logString);
    }


    /**
     * Checks whether the Entity in a container has tags that match this filter.
     *
     * @param container
     *      The container holding the entity whose tags shall be examined.
     */
    private boolean matches(EntityContainer container) {
        boolean matched = false;
        for (Tag tag : container.getEntity().getTags()) {
            String key = tag.getKey();
            if (tagKeys.contains(key)) {
                matched = true;
                break; 
            }
            Set<String> values = tagKeyValues.get(key);
            if ((values != null) && values.contains(tag.getValue())) {
                matched = true;
                break; 
            } 
        }
        return matched;    
    }
    
    
    /**
     * {@inheritDoc}
     */
    public void initialize(Map<String, Object> metaData) {
		sink.initialize(metaData);
	}


    /**
     * {@inheritDoc}
     */
    public void process(EntityContainer container) {
        if (filterClass.isInstance(container)) {
            if (reject ^ (matchesEverything || matches(container))) {
                sink.process(container);
            }
        } else {
            sink.process(container);
        }
    }
        

    /**
     * {@inheritDoc}
     */
    public void complete() {
        sink.complete();
    }


    /**
     * {@inheritDoc}
     */
    public void close() {
        sink.close();
    }


    /**
     * {@inheritDoc}
     */
    public void setSink(Sink sink) {
        this.sink = sink;
    }
}
