/*
 * 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.jclouds.blobstore.options;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Date;
import java.util.List;

import com.google.common.collect.Lists;

/**
 * Contains options supported for HTTP GET operations. <h2>
 * Usage</h2> The recommended way to instantiate a {@link GetOptions} object is to statically import
 * GetOptions.Builder.* and invoke a static creation method followed by an instance mutator (if
 * needed):
 * <p/>
 * <code>
 * import static org.jclouds.blobstore.options.GetOptions.Builder.*
 * 
 * 
 * // this will get the first megabyte of an blob, provided it wasn't modified since yesterday
 * blob = blobStore.getBlob("container, "blobName",range(0,1024).ifUnmodifiedSince(new Date().minusDays(1)));
 * <code>
 */
public class GetOptions {

   public static final GetOptions NONE = new GetOptions();

   private final List<String> ranges = Lists.newArrayList();
   private Date ifModifiedSince;
   private Date ifUnmodifiedSince;
   private String ifMatch;
   private String ifNoneMatch;

   /**
    * download the specified range of the object.
    * @param start first offset included in the response
    * @param end last offset included in the response (inclusive).
    * @return itself to enable daisy-chaining of expressions
    */
   public GetOptions range(long start, long end) {
      checkArgument(start >= 0, "start must be >= 0");
      checkArgument(end >= 0, "end must be >= 0");
      getRanges().add(String.format("%d-%d", start, end));
      return this;
   }

   /**
    * download the specified range of the object.
    */
   public GetOptions startAt(long start) {
      checkArgument(start >= 0, "start must be >= 0");
      getRanges().add(String.format("%d-", start));
      return this;
   }


   /**
    * download the specified range of the object starting from the end of the object.
    */
   public GetOptions tail(long length) {
      checkArgument(length >= 0, "length must be >= 0");
      getRanges().add(String.format("-%d", length));
      return this;
   }

   /**
    * Only return the object if it has changed since this time.
    * <p />
    * Not compatible with {@link #ifETagMatches(String)} or {@link #ifUnmodifiedSince(Date)}
    */
   public GetOptions ifModifiedSince(Date ifModifiedSince) {
      checkArgument(getIfMatch() == null, "ifETagMatches() is not compatible with ifModifiedSince()");
      checkArgument(getIfUnmodifiedSince() == null, "ifUnmodifiedSince() is not compatible with ifModifiedSince()");
      this.ifModifiedSince = checkNotNull(ifModifiedSince, "ifModifiedSince");
      return this;
   }

   /**
    * For use in the header If-Modified-Since
    * <p />
    * Return the object only if it has been modified since the specified time, otherwise return a
    * 304 (not modified).
    * 
    * @see GetOptions#ifModifiedSince(Date)
    */
   public Date getIfModifiedSince() {
      return this.ifModifiedSince;
   }

   /**
    * Only return the object if it hasn't changed since this time.
    * <p />
    * Not compatible with {@link #ifETagDoesntMatch(String)} or {@link #ifModifiedSince(Date)}
    */
   public GetOptions ifUnmodifiedSince(Date ifUnmodifiedSince) {
      checkArgument(getIfNoneMatch() == null, "ifETagDoesntMatch() is not compatible with ifUnmodifiedSince()");
      checkArgument(getIfModifiedSince() == null, "ifModifiedSince() is not compatible with ifUnmodifiedSince()");
      this.ifUnmodifiedSince = checkNotNull(ifUnmodifiedSince, "ifUnmodifiedSince");
      return this;
   }

   /**
    * For use in the header If-Unmodified-Since
    * <p />
    * Return the object only if it has not been modified since the specified time, otherwise return
    * a 412 (precondition failed).
    * 
    * @see GetOptions#ifUnmodifiedSince(Date)
    */
   public Date getIfUnmodifiedSince() {
      return this.ifUnmodifiedSince;
   }

   /**
    * The object's eTag hash should match the parameter <code>eTag</code>.
    * 
    * <p />
    * Not compatible with {@link #ifETagDoesntMatch(String)} or {@link #ifModifiedSince(Date)}
    * 
    * @param eTag
    *           hash representing the payload
    */
   public GetOptions ifETagMatches(String eTag) {
      checkArgument(getIfNoneMatch() == null, "ifETagDoesntMatch() is not compatible with ifETagMatches()");
      checkArgument(getIfModifiedSince() == null, "ifModifiedSince() is not compatible with ifETagMatches()");
      this.ifMatch = checkNotNull(eTag, "eTag");
      return this;
   }

   /**
    * For use in the request header: If-Match
    * <p />
    * Return the object only if its payload tag (ETag) is the same as the eTag specified, otherwise
    * return a 412 (precondition failed).
    * 
    * @see GetOptions#ifETagMatches(String)
    */
   public String getIfMatch() {
      return this.ifMatch;
   }

   /**
    * The object should not have a eTag hash corresponding with the parameter <code>eTag</code>.
    * <p />
    * Not compatible with {@link #ifETagMatches(String)} or {@link #ifUnmodifiedSince(Date)}
    * 
    * @param eTag
    *           hash representing the payload
    */
   public GetOptions ifETagDoesntMatch(String eTag) {
      checkArgument(getIfMatch() == null, "ifETagMatches() is not compatible with ifETagDoesntMatch()");
      checkArgument(getIfUnmodifiedSince() == null, "ifUnmodifiedSince() is not compatible with ifETagDoesntMatch()");
      this.ifNoneMatch = checkNotNull(eTag, "eTag");
      return this;
   }

   /**
    * For use in the request header: If-None-Match
    * <p />
    * Return the object only if its payload tag (ETag) is different from the one specified,
    * otherwise return a 304 (not modified).
    * 
    * @see GetOptions#ifETagDoesntMatch(String)
    */
   public String getIfNoneMatch() {
      return this.ifNoneMatch;
   }

   public List<String> getRanges() {
      return ranges;
   }

   public static class Builder {

      /**
       * @see GetOptions#range(long, long)
       */
      public static GetOptions range(long start, long end) {
         GetOptions options = new GetOptions();
         return options.range(start, end);
      }

      /**
       * @see GetOptions#getIfModifiedSince()
       */
      public static GetOptions ifModifiedSince(Date ifModifiedSince) {
         GetOptions options = new GetOptions();
         return options.ifModifiedSince(ifModifiedSince);
      }

      /**
       * @see GetOptions#ifUnmodifiedSince(Date)
       */
      public static GetOptions ifUnmodifiedSince(Date ifUnmodifiedSince) {
         GetOptions options = new GetOptions();
         return options.ifUnmodifiedSince(ifUnmodifiedSince);
      }

      /**
       * @see GetOptions#ifETagMatches(String)
       */
      public static GetOptions ifETagMatches(String eTag) {
         GetOptions options = new GetOptions();
         return options.ifETagMatches(eTag);
      }

      /**
       * @see GetOptions#ifETagDoesntMatch(String)
       */
      public static GetOptions ifETagDoesntMatch(String eTag) {
         GetOptions options = new GetOptions();
         return options.ifETagDoesntMatch(eTag);
      }

   }

   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((ifMatch == null) ? 0 : ifMatch.hashCode());
      result = prime * result + ((ifModifiedSince == null) ? 0 : ifModifiedSince.hashCode());
      result = prime * result + ((ifNoneMatch == null) ? 0 : ifNoneMatch.hashCode());
      result = prime * result + ((ifUnmodifiedSince == null) ? 0 : ifUnmodifiedSince.hashCode());
      result = prime * result + ((ranges == null) ? 0 : ranges.hashCode());
      return result;
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      GetOptions other = (GetOptions) obj;
      if (ifMatch == null) {
         if (other.ifMatch != null)
            return false;
      } else if (!ifMatch.equals(other.ifMatch))
         return false;
      if (ifModifiedSince == null) {
         if (other.ifModifiedSince != null)
            return false;
      } else if (!ifModifiedSince.equals(other.ifModifiedSince))
         return false;
      if (ifNoneMatch == null) {
         if (other.ifNoneMatch != null)
            return false;
      } else if (!ifNoneMatch.equals(other.ifNoneMatch))
         return false;
      if (ifUnmodifiedSince == null) {
         if (other.ifUnmodifiedSince != null)
            return false;
      } else if (!ifUnmodifiedSince.equals(other.ifUnmodifiedSince))
         return false;
      if (ranges == null) {
         if (other.ranges != null)
            return false;
      } else if (!ranges.equals(other.ranges))
         return false;
      return true;
   }

   @Override
   public String toString() {
      return "[ranges=" + ranges + ", ifModifiedSince=" + ifModifiedSince + ", ifUnmodifiedSince=" + ifUnmodifiedSince
               + ", ifMatch=" + ifMatch + ", ifNoneMatch=" + ifNoneMatch + "]";
   }

}
