/*
 * Copyright 2014 Google Inc. All rights reserved.
 *
 * 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.google.samples.apps.iosched.server.schedule.server.cloudstorage;

import com.google.appengine.api.appidentity.AppIdentityService;
import com.google.appengine.api.appidentity.AppIdentityServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.tools.cloudstorage.GcsFileMetadata;
import com.google.appengine.tools.cloudstorage.GcsFileOptions;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsInputChannel;
import com.google.appengine.tools.cloudstorage.GcsOutputChannel;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.appengine.tools.cloudstorage.RetryParams;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.samples.apps.iosched.server.schedule.Config;
import com.google.samples.apps.iosched.server.schedule.input.fetcher.RemoteJsonHelper;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.channels.Channels;
import java.nio.charset.Charset;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Logger;

/**
 * Handle all interaction with GoogleCloudStorage.
 *
 */
public class CloudFileManager {

  private static final String DEFAULT_CHARSET_NAME = "UTF-8";

  private final GcsService gcsService = GcsServiceFactory.createGcsService(
      RetryParams.getDefaultInstance());

  private final String defaultBucket;
  private final GcsFilename productionManifestFile;
  private final GcsFilename stagingManifestFile;

  public CloudFileManager() {
    AppIdentityService appIdentityService = AppIdentityServiceFactory.getAppIdentityService();
    defaultBucket = appIdentityService.getDefaultGcsBucketName();
    productionManifestFile = new GcsFilename(defaultBucket, Config.MANIFEST_NAME);
    stagingManifestFile = new GcsFilename(defaultBucket, Config.MANIFEST_NAME_STAGING);
  }

  static public byte[] calulateHash(JsonElement contents) {
    MessageDigest md;
    try {
      md = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException e) {
      throw new InternalError("MD5 MessageDigest is not available");
    }
    OutputStream byteSink = new OutputStream() {
      @Override
      public void write(int b) throws IOException {
        // ignore, since this is only used to calculate MD5
      }
    };
    DigestOutputStream dis = new DigestOutputStream(byteSink, md);
    new Gson().toJson(contents, new OutputStreamWriter(dis, Charset.forName(DEFAULT_CHARSET_NAME)));
    return dis.getMessageDigest().digest();
  }

  /**
   * Create or update a file in a GCC bucket, using the default ACL for the bucket.
   *
   * @param filename Name of file to create
   * @param contents File contents
   * @param shortCache If true, sets cache expiry to 0 sec. Otherwise, cache expiry is set to 6,000 sec.
   * @throws IOException
   */
  public void createOrUpdate(String filename, JsonElement contents, boolean shortCache)
      throws IOException {
    GcsFilename file = new GcsFilename(defaultBucket, filename);
    GcsFileOptions options = new GcsFileOptions.Builder()
      .mimeType("application/json")
      .cacheControl("public, max-age="+(shortCache?0:6000))
      .build();
    GcsOutputChannel writeChannel = null;
    try {
      writeChannel = gcsService.createOrReplace(file, options);
      Writer writer = Channels.newWriter(writeChannel, DEFAULT_CHARSET_NAME);
      new Gson().toJson(contents, writer);
      writer.flush();
    } finally {
      if (writeChannel != null) {
        writeChannel.close();
      }
    }
  }

  public String getBucketName() {
    return defaultBucket;
  }

  public String getProductionManifestURL() {
    return productionManifestFile.getBucketName() + "/" + productionManifestFile.getObjectName();
  }

  public String getStagingManifestURL() {
    return stagingManifestFile.getBucketName() + "/" + stagingManifestFile.getObjectName();
  }

  public JsonObject readProductionManifest() throws IOException {
    return readFileAsJsonObject(productionManifestFile);
  }

  public JsonObject readStagingManifest() throws IOException {
    return readFileAsJsonObject(stagingManifestFile);
  }

  public void createOrUpdateProductionManifest(JsonObject contents) throws IOException {
    createOrUpdate(Config.MANIFEST_NAME, contents, true);
  }

  public void createOrUpdateStagingManifest(JsonObject contents) throws IOException {
    createOrUpdate(Config.MANIFEST_NAME_STAGING, contents, true);
  }

  public JsonObject readFileAsJsonObject(String filename) throws IOException {
    return readFileAsJsonObject(new GcsFilename(defaultBucket, filename));
  }

  public JsonObject readFileAsJsonObject(GcsFilename file) throws IOException {
    GcsFileMetadata metadata = gcsService.getMetadata(file);
    if (metadata == null) {
      if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Development) {
        // In the development server, try to fetch files on cloud storage via HTTP
        Logger.getAnonymousLogger().info("fetching "+file.getObjectName()+" at "+Config.CLOUD_STORAGE_BASE_URL+file.getObjectName());
        return RemoteJsonHelper.fetchJsonFromPublicURL(Config.CLOUD_STORAGE_BASE_URL+file.getObjectName());
      }
      return null;
    }
    GcsInputChannel readChannel = null;
    try {
      readChannel = gcsService.openReadChannel(file, 0);
      JsonElement element = new JsonParser().parse(Channels.newReader(readChannel,
          DEFAULT_CHARSET_NAME));
      return element.getAsJsonObject();
    } finally {
      if (readChannel != null) {
        readChannel.close();
      }
    }
  }
}