/**
 * Copyright 2011 Caleb Richardson
 * 
 * Licensed 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 com.outerspacecat.icalendar;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import java.io.Serializable;
import java.time.Duration;
import java.time.Period;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;

/**
 * A representation of an iCalendar duration type defined by <a
 * href="http://tools.ietf.org/html/rfc5545">RFC 5545</a>.
 * <p>
 * <strong>Note:</strong> An iCalendar duration is not the same as a
 * {@link Duration}. It is similar to a {@link Period}, however {@link Period}
 * cannot ensure exclusivity between week based durations and other durations,
 * which is required by iCalendar. Furthermore, {@link Period} does not support
 * hours, minutes, or seconds.
 * 
 * @author Caleb Richardson
 */
@Immutable
@ThreadSafe
public final class DurationType
  implements HasICalendar, Serializable
{
  private final static long serialVersionUID = 1L;

  private final static Pattern DURATION_PATTERN =
      Pattern.compile("P((\\d+)W)?((\\d)+D)?(?:T((\\d)+H)?((\\d)+M)?((\\d)+S)?)?");

  private final boolean negative;
  private final int weeks;
  private final int days;
  private final int hours;
  private final int minutes;
  private final int seconds;

  /**
   * Creates a new duration.
   * 
   * @param negative whether or not the duration is negative
   * @param weeks the number of weeks in the duration. Must be &gt;= 0.
   */
  public DurationType ( final boolean negative, final int weeks )
  {
    Preconditions.checkArgument(weeks >= 0, "weeks must be non negative");

    this.negative = negative;
    this.weeks = weeks;
    this.days = 0;
    this.hours = 0;
    this.minutes = 0;
    this.seconds = 0;
  }

  /**
   * Creates a new duration.
   * 
   * @param negative whether or not the duration is negative
   * @param days the number of days in the duration. Must be &gt;= 0.
   * @param hours the number of hours in the duration. Must be &gt;= 0.
   * @param minutes the number of minutes in the duration. Must be &gt;= 0.
   * @param seconds the number of seconds in the duration. Must be &gt;= 0.
   */
  public DurationType ( final boolean negative, final int days,
      final int hours, final int minutes, final int seconds )
  {
    Preconditions.checkArgument(days >= 0, "days must be non negative");
    Preconditions.checkArgument(hours >= 0, "hours must be non negative");
    Preconditions.checkArgument(minutes >= 0, "minutes must be non negative");
    Preconditions.checkArgument(seconds >= 0, "seconds must be non negative");

    this.negative = negative;
    this.weeks = 0;
    this.days = days;
    this.hours = hours;
    this.minutes = minutes;
    this.seconds = seconds;
  }

  /**
   * Parses a duration.
   * 
   * @param seq the value to parse. Must be non {@code null}.
   * @return a duration. Never {@code null}.
   * @throws CalendarParseException if {@code seq} does not represent a valid
   *         duration
   */
  public static DurationType parse ( final CharSequence seq )
    throws CalendarParseException
  {
    Preconditions.checkNotNull(seq, "seq required");

    String unsigned = seq.toString().toUpperCase();
    boolean negative = false;
    if ( unsigned.startsWith("-") ) {
      unsigned = unsigned.substring(1);
      negative = true;
    } else if ( unsigned.startsWith("+") ) {
      unsigned = unsigned.substring(1);
    }

    Matcher m = DURATION_PATTERN.matcher(unsigned);
    if ( !m.matches() )
      throw new CalendarParseException("invalid duration: " + seq);

    int weeks = 0;
    int days = 0;
    int hours = 0;
    int minutes = 0;
    int seconds = 0;

    if ( m.group(2) != null ) {
      weeks = Integer.parseInt(m.group(2));
    }

    if ( m.group(4) != null ) {
      weeks = Integer.parseInt(m.group(4));
    }

    if ( m.group(6) != null ) {
      weeks = Integer.parseInt(m.group(6));
    }

    if ( m.group(8) != null ) {
      weeks = Integer.parseInt(m.group(8));
    }

    if ( m.group(10) != null ) {
      weeks = Integer.parseInt(m.group(10));
    }

    if ( weeks > 0 ) {
      if ( days > 0 || hours > 0 || minutes > 0 || seconds > 0 )
        throw new CalendarParseException("invalid duration: " + seq);
      return new DurationType(negative, weeks);
    } else {
      return new DurationType(negative, days, hours, minutes, seconds);
    }
  }

  /**
   * Returns whether or not this duration is negative.
   * 
   * @return whether or not this duration is negative.
   */
  public boolean isNegative ()
  {
    return negative;
  }

  /**
   * Returns the number of weeks in this duration.
   * 
   * @return the number of weeks in the duration. &gt;= 0.
   */
  public int getWeeks ()
  {
    return weeks;
  }

  /**
   * Returns the number of days in this duration.
   * 
   * @return the number of days in this duration. &gt;= 0.
   */
  public int getDays ()
  {
    return days;
  }

  /**
   * Returns the number of hours in this duration.
   * 
   * @return the number of hours in this duration. &gt;= 0.
   */
  public int getHours ()
  {
    return hours;
  }

  /**
   * Returns the number of minutes in this duration.
   * 
   * @return the number of minutes in this duration. &gt;= 0.
   */
  public int getMinutes ()
  {
    return minutes;
  }

  /**
   * Returns the number of seconds in this duration.
   * 
   * @return the number of seconds in this duration. &gt;= 0.
   */
  public int getSeconds ()
  {
    return seconds;
  }

  /**
   * Returns whether or not this duration has only weeks or only days specified.
   * 
   * @return whether or not this duration has only weeks or only days specified
   */
  public boolean isDayOrWeekOnly ()
  {
    return getWeeks() > 0 ||
        (getHours() == 0 && getMinutes() == 0 && getSeconds() == 0);
  }

  @Override
  public int hashCode ()
  {
    return Objects.hashCode(isNegative(), getWeeks(), getDays(), getHours(),
        getMinutes(), getSeconds());
  }

  @Override
  public boolean equals ( final Object obj )
  {
    return obj instanceof DurationType &&
        isNegative() == ((DurationType) obj).isNegative() &&
        getWeeks() == ((DurationType) obj).getWeeks() &&
        getDays() == ((DurationType) obj).getDays() &&
        getHours() == ((DurationType) obj).getHours() &&
        getMinutes() == ((DurationType) obj).getMinutes() &&
        getSeconds() == ((DurationType) obj).getSeconds();
  }

  @Override
  public String toString ()
  {
    return toICalendar();
  }

  @Override
  public String toICalendar ()
  {
    StringBuilder ret = new StringBuilder();
    if ( isNegative() )
      ret.append("-");
    if ( getWeeks() > 0 )
      ret.append(getWeeks()).append("W");
    if ( getDays() > 0 )
      ret.append(getDays()).append("D");
    if ( getHours() > 0 )
      ret.append(getHours()).append("H");
    if ( getMinutes() > 0 )
      ret.append(getMinutes()).append("M");
    if ( getSeconds() > 0 )
      ret.append(getSeconds()).append("S");
    return ret.toString();
  }
}
