# Copyright 2015 Google LLC
#
# 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
#
#     https://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 "English"

module Google
  module Cloud
    ##
    # Base google-cloud exception class.
    class Error < StandardError
      ##
      # Construct a new Google::Cloud::Error object, optionally passing in a
      # message.
      def initialize msg = nil
        super
        @cause = $ERROR_INFO
      end

      # Add Error#cause (introduced in 2.1) to Ruby 2.0.
      unless respond_to?(:cause)
        ##
        # The previous exception at the time this exception was raised. This is
        # useful for wrapping exceptions and retaining the original exception
        # information.
        define_method(:cause) do
          @cause
        end
      end

      ##
      # Returns the value of `status_code` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @returns [Object, nil]
      def status_code
        return nil unless cause && cause.respond_to?(:status_code)
        cause.status_code
      end

      ##
      # Returns the value of `body` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @returns [Object, nil]
      def body
        return nil unless cause && cause.respond_to?(:body)
        cause.body
      end

      ##
      # Returns the value of `header` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over HTTP/REST.
      #
      # @returns [Object, nil]
      def header
        return nil unless cause && cause.respond_to?(:header)
        cause.header
      end

      ##
      # Returns the value of `code` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @returns [Object, nil]
      def code
        return nil unless cause && cause.respond_to?(:code)
        cause.code
      end

      ##
      # Returns the value of `details` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @returns [Object, nil]
      def details
        return nil unless cause && cause.respond_to?(:details)
        cause.details
      end

      ##
      # Returns the value of `metadata` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @returns [Object, nil]
      def metadata
        return nil unless cause && cause.respond_to?(:metadata)
        cause.metadata
      end

      ##
      # Returns the value of `status_details` from the underlying cause error
      # object, if both are present. Otherwise returns `nil`.
      #
      # This is typically present on errors originating from calls to an API
      # over gRPC.
      #
      # @returns [Object, nil]
      def status_details
        return nil unless cause && cause.respond_to?(:status_details)
        cause.status_details
      end

      # @private Create a new error object from a client error
      def self.from_error error
        klass = if error.respond_to? :code
                  grpc_error_class_for error.code
                elsif error.respond_to? :status_code
                  gapi_error_class_for error.status_code
                else
                  self
                end
        klass.new error.message
      end

      # @private Identify the subclass for a gRPC error
      def self.grpc_error_class_for grpc_error_code
        # The gRPC status code 0 is for a successful response.
        # So there is no error subclass for a 0 status code, use current class.
        [
          self, CanceledError, UnknownError, InvalidArgumentError,
          DeadlineExceededError, NotFoundError, AlreadyExistsError,
          PermissionDeniedError, ResourceExhaustedError,
          FailedPreconditionError, AbortedError, OutOfRangeError,
          UnimplementedError, InternalError, UnavailableError, DataLossError,
          UnauthenticatedError
        ][grpc_error_code] || self
      end

      # @private Identify the subclass for a Google API Client error
      def self.gapi_error_class_for http_status_code
        # The http status codes mapped to their error classes.
        {
          400 => InvalidArgumentError, # FailedPreconditionError/OutOfRangeError
          401 => UnauthenticatedError,
          403 => PermissionDeniedError,
          404 => NotFoundError,
          409 => AlreadyExistsError, # AbortedError
          412 => FailedPreconditionError,
          429 => ResourceExhaustedError,
          499 => CanceledError,
          500 => InternalError, # UnknownError/DataLossError
          501 => UnimplementedError,
          503 => UnavailableError,
          504 => DeadlineExceededError
        }[http_status_code] || self
      end
    end

    ##
    # Canceled indicates the operation was cancelled (typically by the caller).
    class CanceledError < Error
    end

    ##
    # Unknown error.  An example of where this error may be returned is
    # if a Status value received from another address space belongs to
    # an error-space that is not known in this address space.  Also
    # errors raised by APIs that do not return enough error information
    # may be converted to this error.
    class UnknownError < Error
    end

    ##
    # InvalidArgument indicates client specified an invalid argument.
    # Note that this differs from FailedPrecondition. It indicates arguments
    # that are problematic regardless of the state of the system
    # (e.g., a malformed file name).
    class InvalidArgumentError < Error
    end

    ##
    # DeadlineExceeded means operation expired before completion.
    # For operations that change the state of the system, this error may be
    # returned even if the operation has completed successfully. For
    # example, a successful response from a server could have been delayed
    # long enough for the deadline to expire.
    class DeadlineExceededError < Error
    end

    ##
    # NotFound means some requested entity (e.g., file or directory) was
    # not found.
    class NotFoundError < Error
    end

    ##
    # AlreadyExists means an attempt to create an entity failed because one
    # already exists.
    class AlreadyExistsError < Error
    end

    ##
    # PermissionDenied indicates the caller does not have permission to
    # execute the specified operation. It must not be used for rejections
    # caused by exhausting some resource (use ResourceExhausted
    # instead for those errors).  It must not be
    # used if the caller cannot be identified (use Unauthenticated
    # instead for those errors).
    class PermissionDeniedError < Error
    end

    ##
    # Unauthenticated indicates the request does not have valid
    # authentication credentials for the operation.
    class UnauthenticatedError < Error
    end

    ##
    # ResourceExhausted indicates some resource has been exhausted, perhaps
    # a per-user quota, or perhaps the entire file system is out of space.
    class ResourceExhaustedError < Error
    end

    ##
    # FailedPrecondition indicates operation was rejected because the
    # system is not in a state required for the operation's execution.
    # For example, directory to be deleted may be non-empty, an rmdir
    # operation is applied to a non-directory, etc.
    #
    # A litmus test that may help a service implementor in deciding
    # between FailedPrecondition, Aborted, and Unavailable:
    #  (a) Use Unavailable if the client can retry just the failing call.
    #  (b) Use Aborted if the client should retry at a higher-level
    #      (e.g., restarting a read-modify-write sequence).
    #  (c) Use FailedPrecondition if the client should not retry until
    #      the system state has been explicitly fixed.  E.g., if an "rmdir"
    #      fails because the directory is non-empty, FailedPrecondition
    #      should be returned since the client should not retry unless
    #      they have first fixed up the directory by deleting files from it.
    #  (d) Use FailedPrecondition if the client performs conditional
    #      REST Get/Update/Delete on a resource and the resource on the
    #      server does not match the condition. E.g., conflicting
    #      read-modify-write on the same resource.
    class FailedPreconditionError < Error
    end

    ##
    # Aborted indicates the operation was aborted, typically due to a
    # concurrency issue like sequencer check failures, transaction aborts,
    # etc.
    #
    # See litmus test above for deciding between FailedPrecondition,
    # Aborted, and Unavailable.
    class AbortedError < Error
    end

    ##
    # OutOfRange means operation was attempted past the valid range.
    # E.g., seeking or reading past end of file.
    #
    # Unlike InvalidArgument, this error indicates a problem that may
    # be fixed if the system state changes. For example, a 32-bit file
    # system will generate InvalidArgument if asked to read at an
    # offset that is not in the range [0,2^32-1], but it will generate
    # OutOfRange if asked to read from an offset past the current
    # file size.
    #
    # There is a fair bit of overlap between FailedPrecondition and
    # OutOfRange.  We recommend using OutOfRange (the more specific
    # error) when it applies so that callers who are iterating through
    # a space can easily look for an OutOfRange error to detect when
    # they are done.
    class OutOfRangeError < Error
    end

    ##
    # Unimplemented indicates operation is not implemented or not
    # supported/enabled in this service.
    class UnimplementedError < Error
    end

    ##
    # Internal errors.  Means some invariants expected by underlying
    # system has been broken.  If you see one of these errors,
    # something is very broken.
    class InternalError < Error
    end

    ##
    # Unavailable indicates the service is currently unavailable.
    # This is a most likely a transient condition and may be corrected
    # by retrying with a backoff.
    #
    # See litmus test above for deciding between FailedPrecondition,
    # Aborted, and Unavailable.
    class UnavailableError < Error
    end

    ##
    # DataLoss indicates unrecoverable data loss or corruption.
    class DataLossError < Error
    end
  end
end
