# 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.


require "google/cloud/storage/version"
require "google/apis/storage_v1"
require "digest"
require "mime/types"
require "pathname"

module Google
  module Cloud
    module Storage
      ##
      # @private Represents the connection to Storage,
      # as well as expose the API calls.
      class Service
        ##
        # Alias to the Google Client API module
        API = Google::Apis::StorageV1

        # @private
        attr_accessor :project

        # @private
        attr_accessor :credentials

        ##
        # Creates a new Service instance.
        def initialize project, credentials, retries: nil, timeout: nil
          @project = project
          @credentials = credentials
          @credentials = credentials
          @service = API::StorageService.new
          @service.client_options.application_name    = "gcloud-ruby"
          @service.client_options.application_version = \
            Google::Cloud::Storage::VERSION
          @service.request_options.retries = retries || 3
          @service.request_options.timeout_sec      = timeout
          @service.request_options.open_timeout_sec = timeout
          @service.authorization = @credentials.client
        end

        def service
          return mocked_service if mocked_service
          @service
        end
        attr_accessor :mocked_service

        ##
        # Retrieves a list of buckets for the given project.
        def list_buckets prefix: nil, token: nil, max: nil
          execute do
            service.list_buckets @project, prefix: prefix, page_token: token,
                                           max_results: max
          end
        end

        ##
        # Retrieves bucket by name.
        # Returns Google::Apis::StorageV1::Bucket.
        def get_bucket bucket_name
          execute { service.get_bucket bucket_name }
        end

        ##
        # Creates a new bucket.
        # Returns Google::Apis::StorageV1::Bucket.
        def insert_bucket bucket_gapi, options = {}
          execute do
            service.insert_bucket \
              @project, bucket_gapi,
              predefined_acl: options[:acl],
              predefined_default_object_acl: options[:default_acl]
          end
        end

        ##
        # Updates a bucket, including its ACL metadata.
        def patch_bucket bucket_name, bucket_gapi = nil, predefined_acl: nil,
                         predefined_default_acl: nil
          bucket_gapi ||= Google::Apis::StorageV1::Bucket.new
          bucket_gapi.acl = nil if predefined_acl
          bucket_gapi.default_object_acl = nil if predefined_default_acl

          execute do
            service.patch_bucket \
              bucket_name, bucket_gapi,
              predefined_acl: predefined_acl,
              predefined_default_object_acl: predefined_default_acl
          end
        end

        ##
        # Permanently deletes an empty bucket.
        def delete_bucket bucket_name
          execute { service.delete_bucket bucket_name }
        end

        ##
        # Retrieves a list of ACLs for the given bucket.
        def list_bucket_acls bucket_name
          execute { service.list_bucket_access_controls bucket_name }
        end

        ##
        # Creates a new bucket ACL.
        def insert_bucket_acl bucket_name, entity, role
          new_acl = Google::Apis::StorageV1::BucketAccessControl.new \
            entity: entity, role: role
          execute { service.insert_bucket_access_control bucket_name, new_acl }
        end

        ##
        # Permanently deletes a bucket ACL.
        def delete_bucket_acl bucket_name, entity
          execute { service.delete_bucket_access_control bucket_name, entity }
        end

        ##
        # Retrieves a list of default ACLs for the given bucket.
        def list_default_acls bucket_name
          execute { service.list_default_object_access_controls bucket_name }
        end

        ##
        # Creates a new default ACL.
        def insert_default_acl bucket_name, entity, role
          new_acl = Google::Apis::StorageV1::ObjectAccessControl.new \
            entity: entity, role: role
          execute do
            service.insert_default_object_access_control bucket_name, new_acl
          end
        end

        ##
        # Permanently deletes a default ACL.
        def delete_default_acl bucket_name, entity
          execute do
            service.delete_default_object_access_control bucket_name, entity
          end
        end

        ##
        # Retrieves a list of files matching the criteria.
        def list_files bucket_name, options = {}
          execute do
            service.list_objects \
              bucket_name, delimiter: options[:delimiter],
                           max_results: options[:max],
                           page_token: options[:token],
                           prefix: options[:prefix],
                           versions: options[:versions]
          end
        end

        ##
        # Inserts a new file for the given bucket
        def insert_file bucket_name, source, path = nil, acl: nil,
                        cache_control: nil, content_disposition: nil,
                        content_encoding: nil, content_language: nil,
                        content_type: nil, crc32c: nil, md5: nil, metadata: nil,
                        storage_class: nil, key: nil
          file_obj = Google::Apis::StorageV1::Object.new \
            cache_control: cache_control, content_type: content_type,
            content_disposition: content_disposition, md5_hash: md5,
            content_encoding: content_encoding, crc32c: crc32c,
            content_language: content_language, metadata: metadata,
            storage_class: storage_class
          content_type ||= mime_type_for(Pathname(source).to_path)
          execute do
            service.insert_object \
              bucket_name, file_obj,
              name: path, predefined_acl: acl, upload_source: source,
              content_encoding: content_encoding, content_type: content_type,
              options: key_options(key)
          end
        end

        ##
        # Retrieves an object or its metadata.
        def get_file bucket_name, file_path, generation: nil, key: nil
          execute do
            service.get_object \
              bucket_name, file_path,
              generation: generation,
              options: key_options(key)
          end
        end

        ## Copy a file from source bucket/object to a
        # destination bucket/object.
        def copy_file source_bucket_name, source_file_path,
                      destination_bucket_name, destination_file_path,
                      options = {}
          execute do
            service.copy_object \
              source_bucket_name, source_file_path,
              destination_bucket_name, destination_file_path,
              destination_predefined_acl: options[:acl],
              source_generation: options[:generation],
              options: key_options(options[:key])
          end
        end

        ## Rewrite a file from source bucket/object to a
        # destination bucket/object.
        def rewrite_file source_bucket_name, source_file_path,
                         destination_bucket_name, destination_file_path,
                         options = {}
          options = rewrite_key_options options[:source_key],
                                        options[:destination_key]
          execute do
            service.rewrite_object \
              source_bucket_name, source_file_path,
              destination_bucket_name, destination_file_path,
              destination_predefined_acl: options[:acl],
              source_generation: options[:generation],
              rewrite_token: options[:token],
              options: options
          end
        end

        ## Rewrite a file from source bucket/object to a
        # destination bucket/object.
        def update_file_storage_class bucket_name, file_path, storage_class
          execute do
            service.rewrite_object \
              bucket_name, file_path,
              bucket_name, file_path,
              Google::Apis::StorageV1::Object.new(storage_class: storage_class)
          end
        end

        ##
        # Download contents of a file.
        def download_file bucket_name, file_path, target_path, generation: nil,
                          key: nil
          execute do
            service.get_object \
              bucket_name, file_path,
              download_dest: target_path, generation: generation,
              options: key_options(key)
          end
        end

        ##
        # Updates a file's metadata.
        def patch_file bucket_name, file_path, file_gapi = nil,
                       predefined_acl: nil
          file_gapi ||= Google::Apis::StorageV1::Object.new
          execute do
            service.patch_object \
              bucket_name, file_path, file_gapi,
              predefined_acl: predefined_acl
          end
        end

        ##
        # Permanently deletes a file.
        def delete_file bucket_name, file_path
          execute { service.delete_object bucket_name, file_path }
        end

        ##
        # Retrieves a list of ACLs for the given file.
        def list_file_acls bucket_name, file_name
          execute { service.list_object_access_controls bucket_name, file_name }
        end

        ##
        # Creates a new file ACL.
        def insert_file_acl bucket_name, file_name, entity, role, options = {}
          new_acl = Google::Apis::StorageV1::ObjectAccessControl.new \
            entity: entity, role: role
          execute do
            service.insert_object_access_control \
              bucket_name, file_name, new_acl, generation: options[:generation]
          end
        end

        ##
        # Permanently deletes a file ACL.
        def delete_file_acl bucket_name, file_name, entity, options = {}
          execute do
            service.delete_object_access_control \
              bucket_name, file_name, entity, generation: options[:generation]
          end
        end

        ##
        # Retrieves the mime-type for a file path.
        # An empty string is returned if no mime-type can be found.
        def mime_type_for path
          MIME::Types.of(path).first.to_s
        end

        # @private
        def inspect
          "#{self.class}(#{@project})"
        end

        protected

        def key_options key
          options = {}
          encryption_key_headers options, key if key
          options
        end

        def rewrite_key_options source_key, destination_key
          options = {}
          if source_key
            encryption_key_headers options, source_key, copy_source: true
          end
          encryption_key_headers options, destination_key if destination_key
          options
        end

        # @private
        # @param copy_source If true, header names are those for source object
        #   in rewrite request. If false, the header names are for use with any
        #   method supporting customer-supplied encryption keys.
        #   See https://cloud.google.com/storage/docs/encryption#request
        def encryption_key_headers options, key, copy_source: false
          source = copy_source ? "copy-source-" : ""
          key_sha256 = Digest::SHA256.digest key
          headers = (options[:header] ||= {})
          headers["x-goog-#{source}encryption-algorithm"] = "AES256"
          headers["x-goog-#{source}encryption-key"] = Base64.strict_encode64 key
          headers["x-goog-#{source}encryption-key-sha256"] = \
            Base64.strict_encode64 key_sha256
          options
        end

        def execute
          yield
        rescue Google::Apis::Error => e
          raise Google::Cloud::Error.from_error(e)
        end
      end
    end
  end
end