/**
 * Copyright (c) 2012, Ben Fortuna
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  o Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 *  o Neither the name of Ben Fortuna nor the names of any other contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package net.fortuna.ical4j.model;

import java.io.Serializable;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * $Id$ [23-Apr-2004]
 *
 * Defines a list of iCalendar periods. NOTE: By implementing the
 * <code>java.util.SortedSet</code> interface period lists will always be
 * sorted according to natural ordering.
 * 
 * @author Ben Fortuna
 */
public class PeriodList implements Set, Serializable {

	private static final long serialVersionUID = -2317587285790834492L;

	private final Set periods;
    
    private TimeZone timezone;
    
    private boolean utc;

    private final boolean unmodifiable;

    /**
     * Default constructor.
     */
    public PeriodList() {
        this(true);
    }

    /**
     * @param utc indicates whether the period list is in UTC time
     */
    public PeriodList(boolean utc) {
    	this(utc, false);
    }

    /**
     * @param utc indicates whether the period list is in UTC time
     */
    public PeriodList(boolean utc, final boolean unmodifiable) {
        this.utc = utc;
        this.unmodifiable = unmodifiable;
        if (unmodifiable) {
        	periods = Collections.EMPTY_SET;
        }
        else {
        	periods = new TreeSet();
        }
    }
    
    /**
     * Parses the specified string representation to create a list of periods.
     * 
     * @param aValue
     *            a string representation of a list of periods
     * @throws ParseException
     *             thrown when an invalid string representation of a period list
     *             is specified
     */
    public PeriodList(final String aValue) throws ParseException {
        this();
        final StringTokenizer t = new StringTokenizer(aValue, ",");
        while (t.hasMoreTokens()) {
            add((Object) new Period(t.nextToken()));
        }
    }

    /**
     * {@inheritDoc}
     */
    public final String toString() {
        final StringBuffer b = new StringBuffer();
        for (final Iterator i = iterator(); i.hasNext();) {
            b.append(i.next().toString());
            if (i.hasNext()) {
                b.append(',');
            }
        }
        return b.toString();
    }

    /**
     * Add a period to the list.
     * 
     * @param period
     *            the period to add
     * @return true
     * @see java.util.List#add(java.lang.Object)
     */
    public final boolean add(final Period period) {
        if (isUtc()) {
            period.setUtc(true);
        }
        else {
            period.setTimeZone(timezone);
        }
        return add((Object) period);
    }

    /**
     * Overrides superclass to throw an <code>IllegalArgumentException</code>
     * where argument is not a <code>net.fortuna.ical4j.model.Period</code>.
     * @param period a period to add to the list
     * @return true if the period was added, otherwise false
     * @see java.util.List#add(Object)
     */
    public final boolean add(final Object period) {
        if (!(period instanceof Period)) {
            throw new IllegalArgumentException("Argument not a "
                    + Period.class.getName());
        }
        return periods.add(period);
    }

    /**
     * Remove a period from the list.
     * 
     * @param period
     *            the period to remove
     * @return true if the list contained the specified period
     * @see java.util.List#remove(java.lang.Object)
     */
    public final boolean remove(final Period period) {
        return remove((Object) period);
    }

    /**
     * Returns a normalised version of this period list. Normalisation includes
     * combining overlapping periods, removing periods contained by other
     * periods, combining adjacent periods, and removing periods that consume
     * no time. NOTE: If the period list is
     * already normalised then this period list is returned.
     * 
     * @return a period list
     */
    public final PeriodList normalise() {
        Period prevPeriod = null;
        Period period = null;
        final PeriodList newList = new PeriodList(isUtc());
        if (timezone != null) {
            newList.setTimeZone(timezone);
        }
        boolean normalised = false;
        for (final Iterator i = iterator(); i.hasNext();) {
            period = (Period) i.next();
            if (period.isEmpty()) {
                period = prevPeriod;
                normalised = true;
            }
            else if (prevPeriod != null) {
                // ignore periods contained by other periods..
                if (prevPeriod.contains(period)) {
                    period = prevPeriod;
                    normalised = true;
                }
                // combine intersecting periods..
                else if (prevPeriod.intersects(period)) {
                    period = prevPeriod.add(period);
                    normalised = true;
                }
                // combine adjacent periods..
                else if (prevPeriod.adjacent(period)) {
                    period = prevPeriod.add(period);
                    normalised = true;
                }
                else {
                    // if current period is recognised as distinct
                    // from previous period, add the previous period
                    // to the list..
                    newList.add(prevPeriod);
                }
            }
            prevPeriod = period;
        }
        // remember to add the last period to the list..
        if (prevPeriod != null) {
            newList.add(prevPeriod);
        }
        // only return new list if normalisation
        // has ocurred..
        if (normalised) {
            return newList;
        }
        else {
            return this;
	}
    }

    /**
     * A convenience method that combines all the periods in the specified list to
     * this list. The result returned is a new PeriodList instance, except where
     * no periods are specified in the arguments. In such cases this instance is returned.
     * 
     * Normalisation is also performed automatically after all periods have been added.
     * 
     * @param periods a list of periods to add
     * @return a period list instance
     */
    public final PeriodList add(final PeriodList periods) {
        if (periods != null) {
            final PeriodList newList = new PeriodList();
            newList.addAll(this);
            for (final Iterator i = periods.iterator(); i.hasNext();) {
                newList.add((Period) i.next());
            }
            return newList.normalise();
        }
        return this;
    }

    /**
     * Subtracts the intersection of this list with the specified list of
     * periods from this list and returns the results as a new period list. If
     * no intersection is identified this list is returned.
     * 
     * @param subtractions
     *            a list of periods to subtract from this list
     * @return a period list
     */
    public final PeriodList subtract(final PeriodList subtractions) {
        if (subtractions == null || subtractions.isEmpty()) {
            return this;
        }
        
        PeriodList result = this;
        PeriodList tmpResult = new PeriodList();

        for (final Iterator i = subtractions.iterator(); i.hasNext();) {
            final Period subtraction = (Period) i.next();
            for (final Iterator j = result.iterator(); j.hasNext();) {
                final Period period = (Period) j.next();
                tmpResult.addAll(period.subtract(subtraction));
            }
            result = tmpResult;
            tmpResult = new PeriodList();
        }

        return result;
    }

    public final boolean isUnmodifiable() {
        return unmodifiable;
    }

    /**
     * Indicates whether this list is in local or UTC format.
     * @return Returns true if in UTC format, otherwise false.
     */
    public final boolean isUtc() {
        return utc;
    }

    /**
     * Sets whether this list is in UTC or local time format.
     * @param utc The utc to set.
     */
    public final void setUtc(final boolean utc) {
        for (final Iterator i = iterator(); i.hasNext();) {
            final Period period = (Period) i.next();
            period.setUtc(utc);
        }
        this.timezone = null;
        this.utc = utc;
    }
    
    /**
     * Applies the specified timezone to all dates in the list.
     * All dates added to this list will also have this timezone
     * applied.
     * @param timeZone the timezone for the period list
     */
    public final void setTimeZone(final TimeZone timeZone) {
        for (final Iterator i = iterator(); i.hasNext();) {
            final Period period = (Period) i.next();
            period.setTimeZone(timeZone);
        }
        this.timezone = timeZone;
        this.utc = false;
    }

    /**
     * @return Returns the timeZone.
     */
    public final TimeZone getTimeZone() {
        return timezone;
    }

	/**
	 * {@inheritDoc}
	 */
	public final boolean addAll(Collection arg0) {
		for (Iterator i = arg0.iterator(); i.hasNext();) {
			add(i.next());
		}
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public final void clear() {
		periods.clear();
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean contains(Object o) {
		return periods.contains(o);
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean containsAll(Collection arg0) {
		return periods.containsAll(arg0);
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean isEmpty() {
		return periods.isEmpty();
	}

	/**
	 * {@inheritDoc}
	 */
	public final Iterator iterator() {
		return periods.iterator();
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean remove(Object o) {
		return periods.remove(o);
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean removeAll(Collection arg0) {
		return periods.removeAll(arg0);
	}

	/**
	 * {@inheritDoc}
	 */
	public final boolean retainAll(Collection arg0) {
		return periods.retainAll(arg0);
	}

	/**
	 * {@inheritDoc}
	 */
	public final int size() {
		return periods.size();
	}

	/**
	 * {@inheritDoc}
	 */
	public final Object[] toArray() {
		return periods.toArray();
	}

	/**
	 * {@inheritDoc}
	 */
	public final Object[] toArray(Object[] arg0) {
		return periods.toArray(arg0);
	}
	
	public final boolean equals(Object obj) {
		if (!getClass().isAssignableFrom(obj.getClass())) {
			return false;
		}
		final PeriodList rhs = (PeriodList) obj;
		return new EqualsBuilder().append(periods, rhs.periods)
			.append(timezone, rhs.timezone)
			.append(utc, utc)
			.isEquals();
	}
	
	public final int hashCode() {
		return new HashCodeBuilder().append(periods)
			.append(timezone)
			.append(utc)
			.toHashCode();
	}
}
