# Copyright 2015 Google Inc.
#
# 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/apis/core/multipart'
require 'google/apis/core/http_command'
require 'google/apis/core/api_command'
require 'google/apis/errors'
require 'addressable/uri'
require 'mime-types'

module Google
  module Apis
    module Core
      # Extension of Hurley's UploadIO to add length accessor
      class UploadIO < Hurley::UploadIO
        OCTET_STREAM_CONTENT_TYPE = 'application/octet-stream'

        # Get the length of the stream
        # @return [Fixnum]
        def length
          io.respond_to?(:length) ? io.length : File.size(local_path)
        end

        # Create a new instance given a file path
        # @param [String, File] file_name
        #  Path to file
        # @param [String] content_type
        #  Optional content type. If nil, will attempt to auto-detect
        # @return [Google::Apis::Core::UploadIO]
        def self.from_file(file_name, content_type: nil)
          if content_type.nil?
            type = MIME::Types.of(file_name)
            content_type = type.first.content_type unless type.nil? || type.empty?
          end
          new(file_name, content_type || OCTET_STREAM_CONTENT_TYPE)
        end

        # Wraps an IO stream in UploadIO
        # @param [#read] io
        #  IO to wrap
        # @param [String] content_type
        #  Optional content type.
        # @return [Google::Apis::Core::UploadIO]
        def self.from_io(io, content_type: OCTET_STREAM_CONTENT_TYPE)
          new(io, content_type)
        end
      end

      # Base upload command. Not intended to be used directly
      # @private
      class BaseUploadCommand < ApiCommand
        UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
        UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
        UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'

        # File name or IO containing the content to upload
        # @return [String, File, #read]
        attr_accessor :upload_source

        # Content type of the upload material
        # @return [String]
        attr_accessor :upload_content_type

        # Content, as UploadIO
        # @return [Google::Apis::Core::UploadIO]
        attr_accessor :upload_io

        # Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
        #
        # @return [void]
        # @raise [Google::Apis::ClientError] if upload source is invalid
        def prepare!
          super
          if upload_source.is_a?(IO) || upload_source.is_a?(StringIO)
            self.upload_io = UploadIO.from_io(upload_source, content_type: upload_content_type)
            @close_io_on_finish = false
          elsif upload_source.is_a?(String)
            self.upload_io = UploadIO.from_file(upload_source, content_type: upload_content_type)
            @close_io_on_finish = true
          else
            fail Google::Apis::ClientError, 'Invalid upload source'
          end
        end

        # Close IO stream when command done. Only closes the stream if it was opened by the command.
        def release!
          upload_io.close if @close_io_on_finish
        end
      end

      # Implementation of the raw upload protocol
      class RawUploadCommand < BaseUploadCommand
        RAW_PROTOCOL = 'raw'

        # Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
        #
        # @return [void]
        # @raise [Google::Apis::ClientError] if upload source is invalid
        def prepare!
          super
          self.body = upload_io
          header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
          header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type
        end
      end

      # Implementation of the multipart upload protocol
      class MultipartUploadCommand < BaseUploadCommand
        UPLOAD_BOUNDARY = 'RubyApiClientUpload'
        MULTIPART_PROTOCOL = 'multipart'
        MULTIPART_RELATED = 'multipart/related'

        # Encode the multipart request
        #
        # @return [void]
        # @raise [Google::Apis::ClientError] if upload source is invalid
        def prepare!
          super
          @multipart = Multipart.new(boundary: UPLOAD_BOUNDARY, content_type: MULTIPART_RELATED)
          @multipart.add_json(body)
          @multipart.add_upload(upload_io)
          self.body = @multipart.assemble
          header[:content_type] = @multipart.content_type
          header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
        end
      end

      # Implementation of the resumable upload protocol
      class ResumableUploadCommand < BaseUploadCommand
        UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command'
        UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset'
        BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received'
        UPLOAD_URL_HEADER = 'X-Goog-Upload-URL'
        UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status'
        STATUS_ACTIVE = 'active'
        STATUS_FINAL = 'final'
        STATUS_CANCELLED = 'cancelled'
        RESUMABLE = 'resumable'
        START_COMMAND = 'start'
        QUERY_COMMAND = 'query'
        UPLOAD_COMMAND = 'upload, finalize'

        # Reset upload to initial state.
        #
        # @return [void]
        # @raise [Google::Apis::ClientError] if upload source is invalid
        def prepare!
          @state = :start
          @upload_url = nil
          @offset = 0
          super
        end

        # Check the to see if the upload is complete or needs to be resumed.
        #
        # @param [Fixnum] status
        #   HTTP status code of response
        # @param [Hurley::Header] header
        #   Response headers
        # @param [String, #read] body
        #  Response body
        # @return [Object]
        #   Response object
        # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
        # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
        # @raise [Google::Apis::AuthorizationError] Authorization is required
        def process_response(status, header, body)
          @offset = Integer(header[BYTES_RECEIVED_HEADER]) if header.key?(BYTES_RECEIVED_HEADER)
          @upload_url = header[UPLOAD_URL_HEADER] if header.key?(UPLOAD_URL_HEADER)

          upload_status = header[UPLOAD_STATUS_HEADER]
          logger.debug { sprintf('Upload status %s', upload_status) }
          if upload_status == STATUS_ACTIVE
            @state = :active
          elsif upload_status == STATUS_FINAL
            @state = :final
          elsif upload_status == STATUS_CANCELLED
            @state = :cancelled
            fail Google::Apis::ClientError, body
          end
          super(status, header, body)
        end

        # Send the start command to initiate the upload
        #
        # @param [Hurley::Client] client
        #   HTTP client
        # @return [Hurley::Response]
        # @raise [Google::Apis::ServerError] Unable to send the request
        def send_start_command(client)
          logger.debug { sprintf('Sending upload start command to %s', url) }
          client.send(method, url, body) do |req|
            apply_request_options(req)
            req.header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
            req.header[UPLOAD_COMMAND_HEADER] = START_COMMAND
            req.header[UPLOAD_CONTENT_LENGTH] = upload_io.length.to_s
            req.header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type
          end
        rescue => e
          raise Google::Apis::ServerError, e.message
        end

        # Query for the status of an incomplete upload
        #
        # @param [Hurley::Client] client
        #   HTTP client
        # @return [Hurley::Response]
        # @raise [Google::Apis::ServerError] Unable to send the request
        def send_query_command(client)
          logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
          client.post(@upload_url, nil) do |req|
            apply_request_options(req)
            req.header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
          end
        end

        # Send the actual content
        #
        # @param [Hurley::Client] client
        #   HTTP client
        # @return [Hurley::Response]
        # @raise [Google::Apis::ServerError] Unable to send the request
        def send_upload_command(client)
          logger.debug { sprintf('Sending upload command to %s', @upload_url) }
          content = upload_io
          content.pos = @offset
          client.post(@upload_url, content) do |req|
            apply_request_options(req)
            req.header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
            req.header[UPLOAD_OFFSET_HEADER] = @offset.to_s
          end
        end

        # Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
        # for the status of the upload, the second to send the (remaining) content.
        #
        # @private
        # @param [Hurley::Client] client
        #   HTTP client
        # @yield [result, err] Result or error if block supplied
        # @return [Object]
        # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
        # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
        # @raise [Google::Apis::AuthorizationError] Authorization is required
        def execute_once(client, &block)
          if @state == :start
            response = send_start_command(client)
          else
            response = send_query_command(client)
          end
          result = process_response(response.status_code, response.header, response.body)
          if @state == :active
            response = send_upload_command(client)
            result = process_response(response.status_code, response.header, response.body)
          end

          success(result, &block) if @state == :final
        rescue => e
          error(e, rethrow: true, &block)
        end
      end
    end
  end
end
