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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.openstreetmap.osmosis.core.domain.common.SimpleTimestampContainer;
import org.openstreetmap.osmosis.core.domain.common.TimestampContainer;
import org.openstreetmap.osmosis.core.store.StoreClassRegister;
import org.openstreetmap.osmosis.core.store.StoreReader;
import org.openstreetmap.osmosis.core.store.StoreWriter;


/**
 * A data class representing a single OSM way.
 * 
 * @author Brett Henderson
 */
public class Way extends Entity implements Comparable<Way> {

	private List<WayNode> wayNodes;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param id
	 *            The unique identifier.
	 * @param version
	 *            The version of the entity.
	 * @param timestamp
	 *            The last updated timestamp.
	 * @param user
	 *            The user that last modified this entity.
	 * @param changesetId
	 *            The id of the changeset that this version of the entity was created by.
	 * @deprecated As of 0.40, replaced by Way(entityData).
	 */
	@Deprecated
	public Way(long id, int version, Date timestamp, OsmUser user, long changesetId) {
		// Chain to the more specific constructor
		this(id, version, new SimpleTimestampContainer(timestamp), user, changesetId);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param id
	 *            The unique identifier.
	 * @param version
	 *            The version of the entity.
	 * @param timestampContainer
	 *            The container holding the timestamp in an alternative
	 *            timestamp representation.
	 * @param user
	 *            The name of the user that last modified this entity.
	 * @param changesetId
	 *            The id of the changeset that this version of the entity was created by.
	 * @deprecated As of 0.40, replaced by Way(entityData).
	 */
	@Deprecated
	public Way(long id, int version, TimestampContainer timestampContainer, OsmUser user, long changesetId) {
		super(id, version, timestampContainer, user, changesetId);
		
		this.wayNodes = new ArrayList<WayNode>();
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param entityData
	 *            The common entity data.
	 */
	public Way(CommonEntityData entityData) {
		super(entityData);
		
		this.wayNodes = new ArrayList<WayNode>();
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param id
	 *            The unique identifier.
	 * @param version
	 *            The version of the entity.
	 * @param timestamp
	 *            The last updated timestamp.
	 * @param user
	 *            The user that last modified this entity.
	 * @param changesetId
	 *            The id of the changeset that this version of the entity was created by.
	 * @param tags
	 *            The tags to apply to the object.
	 * @param wayNodes
	 *            The way nodes to apply to the object
	 * @deprecated As of 0.40, replaced by Way(entityData, wayNodes).
	 */
	@Deprecated
	public Way(long id, int version, Date timestamp, OsmUser user, long changesetId, Collection<Tag> tags,
			List<WayNode> wayNodes) {
		// Chain to the more specific constructor
		this(id, version, new SimpleTimestampContainer(timestamp), user, changesetId, tags, wayNodes);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param id
	 *            The unique identifier.
	 * @param version
	 *            The version of the entity.
	 * @param timestampContainer
	 *            The container holding the timestamp in an alternative
	 *            timestamp representation.
	 * @param user
	 *            The name of the user that last modified this entity.
	 * @param changesetId
	 *            The id of the changeset that this version of the entity was created by.
	 * @param tags
	 *            The tags to apply to the object.
	 * @param wayNodes
	 *            The way nodes to apply to the object
	 * @deprecated As of 0.40, replaced by Way(entityData, wayNodes).
	 */
	@Deprecated
	public Way(
			long id, int version, TimestampContainer timestampContainer, OsmUser user, long changesetId,
			Collection<Tag> tags, List<WayNode> wayNodes) {
		super(id, version, timestampContainer, user, changesetId, tags);
		
		this.wayNodes = new ArrayList<WayNode>(wayNodes);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param entityData
	 *            The common entity data.
	 * @param wayNodes
	 *            The way nodes to apply to the object
	 */
	public Way(
			CommonEntityData entityData, List<WayNode> wayNodes) {
		super(entityData);
		
		this.wayNodes = new ArrayList<WayNode>(wayNodes);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param originalWay
	 *            The way to clone from.
	 */
	private Way(Way originalWay) {
		super(originalWay);
		
		this.wayNodes = new ArrayList<WayNode>(originalWay.wayNodes);
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param sr
	 *            The store to read state from.
	 * @param scr
	 *            Maintains the mapping between classes and their identifiers
	 *            within the store.
	 */
	public Way(StoreReader sr, StoreClassRegister scr) {
		super(sr, scr);
		
		int featureCount;
		
		featureCount = sr.readInteger();
		
		wayNodes = new ArrayList<WayNode>();
		for (int i = 0; i < featureCount; i++) {
			wayNodes.add(new WayNode(sr, scr));
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void store(StoreWriter sw, StoreClassRegister scr) {
		super.store(sw, scr);
		
		sw.writeInteger(wayNodes.size());
		for (WayNode wayNode : wayNodes) {
			wayNode.store(sw, scr);
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public EntityType getType() {
		return EntityType.Way;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean equals(Object o) {
		if (o instanceof Way) {
			return compareTo((Way) o) == 0;
		} else {
			return false;
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public int hashCode() {
		/*
		 * As per the hashCode definition, this doesn't have to be unique it
		 * just has to return the same value for any two objects that compare
		 * equal. Using both id and version will provide a good distribution of
		 * values but is simple to calculate.
		 */
		return (int) getId() + getVersion();
	}


	/**
	 * Compares this node list to the specified node list. The comparison is
	 * based on a direct comparison of the node ids.
	 * 
	 * @param comparisonWayNodes
	 *            The node list to compare to.
	 * @return 0 if equal, &lt; 0 if considered "smaller", and &gt; 0 if
	 *         considered "bigger".
	 */
	protected int compareWayNodes(List<WayNode> comparisonWayNodes) {
		Iterator<WayNode> i;
		Iterator<WayNode> j;
		
		// The list with the most entities is considered bigger.
		if (wayNodes.size() != comparisonWayNodes.size()) {
			return wayNodes.size() - comparisonWayNodes.size();
		}
		
		// Check the individual way nodes.
		i = wayNodes.iterator();
		j = comparisonWayNodes.iterator();
		while (i.hasNext()) {
			int result = i.next().compareTo(j.next());
			
			if (result != 0) {
				return result;
			}
		}
		
		// There are no differences.
		return 0;
	}


	/**
	 * Compares this way to the specified way. The way comparison is based on a
	 * comparison of id, version, timestamp, wayNodeList and tags in that order.
	 * 
	 * @param comparisonWay
	 *            The way to compare to.
	 * @return 0 if equal, &lt; 0 if considered "smaller", and &gt; 0 if
	 *         considered "bigger".
	 */
	public int compareTo(Way comparisonWay) {
		int wayNodeListResult;
		
		if (this.getId() < comparisonWay.getId()) {
			return -1;
		}
		if (this.getId() > comparisonWay.getId()) {
			return 1;
		}
		
		if (this.getVersion() < comparisonWay.getVersion()) {
			return -1;
		}
		if (this.getVersion() > comparisonWay.getVersion()) {
			return 1;
		}
		
		if (this.getTimestamp() == null && comparisonWay.getTimestamp() != null) {
			return -1;
		}
		if (this.getTimestamp() != null && comparisonWay.getTimestamp() == null) {
			return 1;
		}
		if (this.getTimestamp() != null && comparisonWay.getTimestamp() != null) {
			int result;
			
			result = this.getTimestamp().compareTo(comparisonWay.getTimestamp());
			
			if (result != 0) {
				return result;
			}
		}
		
		wayNodeListResult = compareWayNodes(
			comparisonWay.getWayNodes()
		);
		
		if (wayNodeListResult != 0) {
			return wayNodeListResult;
		}
		
		return compareTags(comparisonWay.getTags());
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void makeReadOnly() {
		if (!isReadOnly()) {
			wayNodes = Collections.unmodifiableList(wayNodes);
		}
		
		super.makeReadOnly();
	}


	/**
	 * Returns the attached list of way nodes. The returned list is read-only.
	 * 
	 * @return The wayNodeList.
	 */
	public List<WayNode> getWayNodes() {
		return wayNodes;
	}


	/**
	 * {@inheritDoc}
	 */
	@Override
	public Way getWriteableInstance() {
		if (isReadOnly()) {
			return new Way(this);
		} else {
			return this;
		}
	}


    /**
     * Is this way closed? (A way is closed if the first node id equals the last node id.)
     *
     * @return True or false
     */
    public boolean isClosed() {
        return wayNodes.get(0).getNodeId() == wayNodes.get(wayNodes.size() - 1).getNodeId();
    }

    /** 
     * ${@inheritDoc}.
     */
    @Override
    public String toString() {
        String name = null;
        Collection<Tag> tags = getTags();
        for (Tag tag : tags) {
            if (tag.getKey() != null && tag.getKey().equalsIgnoreCase("name")) {
                name = tag.getValue();
                break;
            }
        }
        if (name != null) {
            return "Way(id=" + getId() + ", #tags=" +  getTags().size() + ", name='" + name + "')";
        }
        return "Way(id=" + getId() + ", #tags=" +  getTags().size() + ")";
    }
}
