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

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.io.BaseEncoding.base64;
import static org.jclouds.googlecloudstorage.domain.DomainResourceReferences.ObjectRole.READER;

import java.util.List;
import java.util.Set;
import java.util.UUID;

import javax.inject.Inject;

import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.domain.BlobAccess;
import org.jclouds.blobstore.domain.BlobMetadata;
import org.jclouds.blobstore.domain.ContainerAccess;
import org.jclouds.blobstore.domain.MultipartPart;
import org.jclouds.blobstore.domain.MultipartUpload;
import org.jclouds.blobstore.domain.MutableBlobMetadata;
import org.jclouds.blobstore.domain.PageSet;
import org.jclouds.blobstore.domain.StorageMetadata;
import org.jclouds.blobstore.domain.internal.BlobImpl;
import org.jclouds.blobstore.domain.internal.PageSetImpl;
import org.jclouds.blobstore.functions.BlobToHttpGetOptions;
import org.jclouds.blobstore.internal.BaseBlobStore;
import org.jclouds.blobstore.options.CopyOptions;
import org.jclouds.blobstore.options.CreateContainerOptions;
import org.jclouds.blobstore.options.GetOptions;
import org.jclouds.blobstore.options.ListContainerOptions;
import org.jclouds.blobstore.options.PutOptions;
import org.jclouds.blobstore.util.BlobUtils;
import org.jclouds.collect.Memoized;
import org.jclouds.domain.Location;
import org.jclouds.googlecloud.config.CurrentProject;
import org.jclouds.googlecloud.domain.ListPage;
import org.jclouds.googlecloudstorage.GoogleCloudStorageApi;
import org.jclouds.googlecloudstorage.blobstore.functions.BlobMetadataToObjectTemplate;
import org.jclouds.googlecloudstorage.blobstore.functions.BlobStoreListContainerOptionsToListObjectOptions;
import org.jclouds.googlecloudstorage.blobstore.functions.BucketToStorageMetadata;
import org.jclouds.googlecloudstorage.blobstore.functions.ObjectListToStorageMetadata;
import org.jclouds.googlecloudstorage.blobstore.functions.ObjectToBlobMetadata;
import org.jclouds.googlecloudstorage.domain.Bucket;
import org.jclouds.googlecloudstorage.domain.DomainResourceReferences;
import org.jclouds.googlecloudstorage.domain.GoogleCloudStorageObject;
import org.jclouds.googlecloudstorage.domain.ListPageWithPrefixes;
import org.jclouds.googlecloudstorage.domain.ObjectAccessControls;
import org.jclouds.googlecloudstorage.domain.templates.BucketTemplate;
import org.jclouds.googlecloudstorage.domain.templates.ComposeObjectTemplate;
import org.jclouds.googlecloudstorage.domain.templates.ObjectAccessControlsTemplate;
import org.jclouds.googlecloudstorage.domain.templates.ObjectTemplate;
import org.jclouds.googlecloudstorage.options.InsertObjectOptions;
import org.jclouds.googlecloudstorage.options.ListObjectOptions;
import org.jclouds.http.HttpResponseException;
import org.jclouds.io.ContentMetadata;
import org.jclouds.io.Payload;
import org.jclouds.io.PayloadSlicer;
import org.jclouds.util.Strings2;

import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;

public final class GoogleCloudStorageBlobStore extends BaseBlobStore {

   private final GoogleCloudStorageApi api;
   private final BucketToStorageMetadata bucketToStorageMetadata;
   private final ObjectToBlobMetadata objectToBlobMetadata;
   private final ObjectListToStorageMetadata objectListToStorageMetadata;
   private final BlobMetadataToObjectTemplate blobMetadataToObjectTemplate;
   private final BlobStoreListContainerOptionsToListObjectOptions listContainerOptionsToListObjectOptions;
   private final Supplier<String> projectId;
   private final BlobToHttpGetOptions blob2ObjectGetOptions;

   @Inject GoogleCloudStorageBlobStore(BlobStoreContext context, BlobUtils blobUtils, Supplier<Location> defaultLocation,
            @Memoized Supplier<Set<? extends Location>> locations, PayloadSlicer slicer, GoogleCloudStorageApi api,
            BucketToStorageMetadata bucketToStorageMetadata, ObjectToBlobMetadata objectToBlobMetadata,
            ObjectListToStorageMetadata objectListToStorageMetadata,
            BlobMetadataToObjectTemplate blobMetadataToObjectTemplate,
            BlobStoreListContainerOptionsToListObjectOptions listContainerOptionsToListObjectOptions,
            @CurrentProject Supplier<String> projectId,
            BlobToHttpGetOptions blob2ObjectGetOptions) {
      super(context, blobUtils, defaultLocation, locations, slicer);
      this.api = api;
      this.bucketToStorageMetadata = bucketToStorageMetadata;
      this.objectToBlobMetadata = objectToBlobMetadata;
      this.objectListToStorageMetadata = objectListToStorageMetadata;
      this.blobMetadataToObjectTemplate = blobMetadataToObjectTemplate;
      this.listContainerOptionsToListObjectOptions = listContainerOptionsToListObjectOptions;
      this.projectId = projectId;
      this.blob2ObjectGetOptions = checkNotNull(blob2ObjectGetOptions, "blob2ObjectGetOptions");
   }

   @Override
   public PageSet<? extends StorageMetadata> list() {
      return new Function<ListPage<Bucket>, PageSet<? extends StorageMetadata>>() {
         public PageSet<? extends StorageMetadata> apply(ListPage<Bucket> from) {
            return new PageSetImpl<StorageMetadata>(Iterables.transform(from, bucketToStorageMetadata),
                  from.nextPageToken());
         }
      }.apply(api.getBucketApi().listBucket(projectId.get()));
   }

   @Override
   public boolean containerExists(String container) {
      return api.getBucketApi().bucketExist(container);
   }

   @Override
   public boolean createContainerInLocation(Location location, String container) {
      BucketTemplate template = new BucketTemplate().name(container);
      if (location != null) {
         DomainResourceReferences.Location gcsLocation = DomainResourceReferences.Location.fromValue(location.getId());
         template = template.location(gcsLocation);
      }
      return api.getBucketApi().createBucket(projectId.get(), template) != null;
   }

   @Override
   public boolean createContainerInLocation(Location location, String container, CreateContainerOptions options) {
      BucketTemplate template = new BucketTemplate().name(container);
      if (location != null) {
         DomainResourceReferences.Location gcsLocation = DomainResourceReferences.Location.fromValue(location.getId());
         template = template.location(gcsLocation);
      }
      Bucket bucket = api.getBucketApi().createBucket(projectId.get(), template);
      if (options.isPublicRead()) {
         try {
            ObjectAccessControlsTemplate doAclTemplate = ObjectAccessControlsTemplate.create("allUsers", READER);
            api.getDefaultObjectAccessControlsApi().createDefaultObjectAccessControls(container, doAclTemplate);
         } catch (HttpResponseException e) {
            // If DefaultObjectAccessControls operation fail, Reverse create operation the operation.
            api.getBucketApi().deleteBucket(container);
            return false;
         }
      }

      return bucket != null;
   }

   @Override
   public ContainerAccess getContainerAccess(String container) {
      ObjectAccessControls controls = api.getDefaultObjectAccessControlsApi().getDefaultObjectAccessControls(container, "allUsers");
      if (controls == null || controls.role() == DomainResourceReferences.ObjectRole.OWNER) {
         return ContainerAccess.PRIVATE;
      } else {
         return ContainerAccess.PUBLIC_READ;
      }
   }

   @Override
   public void setContainerAccess(String container, ContainerAccess access) {
      ObjectAccessControlsTemplate doAclTemplate;
      if (access == ContainerAccess.PUBLIC_READ) {
         doAclTemplate = ObjectAccessControlsTemplate.create("allUsers", READER);
         api.getDefaultObjectAccessControlsApi().createDefaultObjectAccessControls(container, doAclTemplate);
      } else {
         api.getDefaultObjectAccessControlsApi().deleteDefaultObjectAccessControls(container, "allUsers");
      }
   }

   /** Returns list of of all the objects */
   @Override
   public PageSet<? extends StorageMetadata> list(String container) {
      return list(container, ListContainerOptions.NONE);
   }

   @Override
   public PageSet<? extends StorageMetadata> list(String container, ListContainerOptions options) {
      ListObjectOptions listOptions = listContainerOptionsToListObjectOptions.apply(options);
      ListPageWithPrefixes<GoogleCloudStorageObject> gcsList = api.getObjectApi().listObjects(container, listOptions);
      return objectListToStorageMetadata.apply(gcsList);
   }

   /**
    * Checks whether an accessible object is available. Google cloud storage does not support directly support
    * BucketExist or ObjectExist operations
    */
   @Override
   public boolean blobExists(String container, String name) {
      return api.getObjectApi().objectExists(container, Strings2.urlEncode(name));
   }

   /**
    * This supports multipart/related upload which has exactly 2 parts, media-part and metadata-part
    */
   @Override
   public String putBlob(String container, Blob blob) {
      return putBlob(container, blob, PutOptions.NONE);
   }

   @Override
   public String putBlob(String container, Blob blob, PutOptions options) {
      long length = checkNotNull(blob.getPayload().getContentMetadata().getContentLength());

      if (length != 0 && options.isMultipart()) {
         // JCLOUDS-912 prevents using single-part uploads with InputStream payloads.
         // Work around this with multi-part upload which buffers parts in-memory.
         return putMultipartBlob(container, blob, options);
      } else {
         ObjectTemplate template = blobMetadataToObjectTemplate.apply(blob.getMetadata());

         HashCode md5 = blob.getMetadata().getContentMetadata().getContentMD5AsHashCode();
         if (md5 != null) {
            template.md5Hash(base64().encode(md5.asBytes()));
         }

         if (options.getBlobAccess() == BlobAccess.PUBLIC_READ) {
            ObjectAccessControls controls = ObjectAccessControls.builder()
                  .entity("allUsers")
                  .bucket(container)
                  .role(READER)
                  .build();
            template.addAcl(controls);
         }

         return api.getObjectApi().multipartUpload(container, template, blob.getPayload()).etag();
      }
   }

   @Override
   public BlobMetadata blobMetadata(String container, String name) {
      return objectToBlobMetadata.apply(api.getObjectApi().getObject(container, Strings2.urlEncode(name)));
   }

   @Override
   public Blob getBlob(String container, String name, GetOptions options) {
      GoogleCloudStorageObject gcsObject = api.getObjectApi().getObject(container, Strings2.urlEncode(name));
      if (gcsObject == null) {
         return null;
      }
      org.jclouds.http.options.GetOptions httpOptions = blob2ObjectGetOptions.apply(options);
      MutableBlobMetadata metadata = objectToBlobMetadata.apply(gcsObject);
      Blob blob = new BlobImpl(metadata);
      // TODO: Does getObject not get the payload?!
      Payload payload = api.getObjectApi().download(container, Strings2.urlEncode(name), httpOptions).getPayload();
      payload.setContentMetadata(metadata.getContentMetadata()); // Doing this first retains it on setPayload.
      blob.setPayload(payload);
      return blob;
   }

   @Override
   public void removeBlob(String container, String name) {
      api.getObjectApi().deleteObject(container, Strings2.urlEncode(name));
   }

   @Override
   public BlobAccess getBlobAccess(String container, String name) {
      ObjectAccessControls controls = api.getObjectAccessControlsApi().getObjectAccessControls(container,
            Strings2.urlEncode(name), "allUsers");
      if (controls != null && controls.role() == DomainResourceReferences.ObjectRole.READER) {
         return BlobAccess.PUBLIC_READ;
      } else {
         return BlobAccess.PRIVATE;
      }
   }

   @Override
   public void setBlobAccess(String container, String name, BlobAccess access) {
      if (access == BlobAccess.PUBLIC_READ) {
         ObjectAccessControls controls = ObjectAccessControls.builder()
               .entity("allUsers")
               .bucket(container)
               .role(READER)
               .build();
         api.getObjectApi().patchObject(container, Strings2.urlEncode(name), new ObjectTemplate().addAcl(controls));
      } else {
         api.getObjectAccessControlsApi().deleteObjectAccessControls(container, Strings2.urlEncode(name), "allUsers");
      }
   }

   @Override
   protected boolean deleteAndVerifyContainerGone(String container) {
      ListPageWithPrefixes<GoogleCloudStorageObject> list = api.getObjectApi().listObjects(container);

      if (list == null || (!list.iterator().hasNext() && list.prefixes().isEmpty())) {
         if (!api.getBucketApi().deleteBucket(container)) {
            return true;
         } else {
            return !api.getBucketApi().bucketExist(container);
         }
      }

      return false;
   }

   @Override
   public String copyBlob(String fromContainer, String fromName, String toContainer, String toName,
         CopyOptions options) {
      if (options.ifMatch() != null) {
         throw new UnsupportedOperationException("GCS does not support ifMatch");
      }
      if (options.ifNoneMatch() != null) {
         throw new UnsupportedOperationException("GCS does not support ifNoneMatch");
      }
      if (options.ifModifiedSince() != null) {
         throw new UnsupportedOperationException("GCS does not support ifModifiedSince");
      }
      if (options.ifUnmodifiedSince() != null) {
         throw new UnsupportedOperationException("GCS does not support ifUnmodifiedSince");
      }

      if (options.contentMetadata() == null && options.userMetadata() == null) {
         return api.getObjectApi().copyObject(toContainer, Strings2.urlEncode(toName), fromContainer,
               Strings2.urlEncode(fromName)).etag();
      }

      ObjectTemplate template = new ObjectTemplate();

      if (options.contentMetadata() != null) {
         ContentMetadata contentMetadata = options.contentMetadata();

         String contentDisposition = contentMetadata.getContentDisposition();
         if (contentDisposition != null) {
            template.contentDisposition(contentDisposition);
         }

         // TODO: causes failures with subsequent GET operations:
         // HTTP/1.1 failed with response: HTTP/1.1 503 Service Unavailable; content: [Service Unavailable]
/*
         String contentEncoding = contentMetadata.getContentEncoding();
         if (contentEncoding != null) {
            template.contentEncoding(contentEncoding);
         }
*/

         String contentLanguage = contentMetadata.getContentLanguage();
         if (contentLanguage != null) {
            template.contentLanguage(contentLanguage);
         }

         String contentType = contentMetadata.getContentType();
         if (contentType != null) {
            template.contentType(contentType);
         }
      }

      if (options.userMetadata() != null) {
         template.customMetadata(options.userMetadata());
      }

      return api.getObjectApi().copyObject(toContainer, Strings2.urlEncode(toName), fromContainer,
            Strings2.urlEncode(fromName), template).etag();
   }

   @Override
   public MultipartUpload initiateMultipartUpload(String container, BlobMetadata blobMetadata, PutOptions options) {
      String uploadId = UUID.randomUUID().toString();
      return MultipartUpload.create(container, blobMetadata.getName(), uploadId, blobMetadata, options);
   }

   @Override
   public void abortMultipartUpload(MultipartUpload mpu) {
      ImmutableList.Builder<String> builder = ImmutableList.builder();
      List<MultipartPart> parts = listMultipartUpload(mpu);
      for (MultipartPart part : parts) {
         builder.add(getMPUPartName(mpu, part.partNumber()));
      }
      removeBlobs(mpu.containerName(), builder.build());
   }

   @Override
   public String completeMultipartUpload(MultipartUpload mpu, List<MultipartPart> parts) {
      ImmutableList.Builder<GoogleCloudStorageObject> objectsBuilder = ImmutableList.builder();
      for (MultipartPart part : parts) {
         objectsBuilder.add(api.getObjectApi().getObject(mpu.containerName(),
               Strings2.urlEncode(getMPUPartName(mpu, part.partNumber()))));
      }

      ObjectTemplate destination = blobMetadataToObjectTemplate.apply(mpu.blobMetadata());
      final ImmutableList<GoogleCloudStorageObject> objects = objectsBuilder.build();
      if (!objects.isEmpty()) {
         destination.storageClass(objects.get(0).storageClass());
      }
      if (mpu.putOptions().getBlobAccess() == BlobAccess.PUBLIC_READ) {
         ObjectAccessControls controls = ObjectAccessControls.builder()
               .entity("allUsers")
               .bucket(mpu.containerName())
               .role(READER)
               .build();
         destination.addAcl(controls);
      }

      ComposeObjectTemplate template = ComposeObjectTemplate.builder()
            .fromGoogleCloudStorageObject(objects)
            .destination(destination).build();
      String eTag = api.getObjectApi().composeObjects(mpu.containerName(), Strings2.urlEncode(mpu.blobName()), template)
            .etag();

      // remove parts, composite object keeps a reference to them
      ImmutableList.Builder<String> builder = ImmutableList.builder();
      for (MultipartPart part : parts) {
         builder.add(getMPUPartName(mpu, part.partNumber()));
      }
      removeBlobs(mpu.containerName(), builder.build());

      return eTag;
   }

   @Override
   public MultipartPart uploadMultipartPart(MultipartUpload mpu, int partNumber, Payload payload) {
      String partName = getMPUPartName(mpu, partNumber);
      long partSize = payload.getContentMetadata().getContentLength();
      GoogleCloudStorageObject object = api.getObjectApi().simpleUpload(
            mpu.containerName(), "application/unknown", partSize, payload, new InsertObjectOptions().name(partName));
      return MultipartPart.create(partNumber, partSize, object.etag(), object.updated());
   }

   @Override
   public List<MultipartPart> listMultipartUpload(MultipartUpload mpu) {
      ImmutableList.Builder<MultipartPart> parts = ImmutableList.builder();
      PageSet<? extends StorageMetadata> pageSet = list(mpu.containerName(),
            new ListContainerOptions().prefix(mpu.id() + "_"));
      // TODO: pagination
      for (StorageMetadata sm : pageSet) {
         int lastUnderscore = sm.getName().lastIndexOf('_');
         int partNumber = Integer.parseInt(sm.getName().substring(lastUnderscore + 1));
         parts.add(MultipartPart.create(partNumber, sm.getSize(), sm.getETag(), sm.getLastModified()));
      }
      return parts.build();
   }

   @Override
   public List<MultipartUpload> listMultipartUploads(String container) {
      throw new UnsupportedOperationException("not supported");
   }

   @Override
   public long getMinimumMultipartPartSize() {
      return 5L * 1024L * 1024L;
   }

   @Override
   public long getMaximumMultipartPartSize() {
      return 5L * 1024L * 1024L * 1024L;
   }

   @Override
   public int getMaximumNumberOfParts() {
      // can raise limit via composite objects of composites
      return 32;
   }

   private static String getMPUPartName(MultipartUpload mpu, int partNumber) {
      return String.format("%s_%08d", mpu.id(), partNumber);
   }
}
