# Copyright 2016 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 "stackdriver/core/async_actor"


module Google
  module Cloud
    module ErrorReporting
      ##
      # # AsyncErrorReporter
      #
      # @private Used by {Google::Cloud::ErrorReporting} and
      # {Google::Cloud::ErrorReporting::Middleware} to asynchronously submit
      # error events to Stackdriver Error Reporting service when used in
      # Ruby applications.
      class AsyncErrorReporter
        include Stackdriver::Core::AsyncActor

        ##
        # @private Default maximum backlog size for the job queue
        DEFAULT_MAX_QUEUE_SIZE = 1000

        ##
        # The {Google::Cloud::ErrorReporting::Project} object to submit events
        # with.
        attr_accessor :error_reporting

        ##
        # The max number of items the queue holds
        attr_accessor :max_queue_size

        ##
        # @private Construct a new instance of AsyncErrorReporter
        def initialize error_reporting, max_queue_size = DEFAULT_MAX_QUEUE_SIZE
          super()
          @error_reporting = error_reporting
          @max_queue_size = max_queue_size
          @queue = []
          @queue_resource = new_cond
        end

        ##
        # Add the error event to the queue. Signal the child thread an item
        # has been added.
        def report error_event
          async_start

          synchronize do
            @queue.push error_event
            @queue_resource.broadcast

            retries = 0
            while @max_queue_size && @queue.size > @max_queue_size
              retries += 1
              @queue_resource.wait 1

              # Drop early queue entries when have waited long enough.
              @queue.pop while @queue.size > @max_queue_size && retries > 3
            end
          end
        end

        ##
        # @private Callback fucntion for AsyncActor module to run the async
        # job in a loop
        def run_backgrounder
          error_event = wait_next_item
          return if error_event.nil?
          begin
            error_reporting.report error_event
          rescue StandardError => e
            warn error_event.message if error_event.message
            warn ["#{e.class}: #{e.message}", e.backtrace].join("\n\t")
            @last_exception = e
          end
        end

        ##
        # @private Callback function when the async actor thread state changes
        def on_async_state_change
          synchronize do
            @queue_resource.broadcast
          end
        end

        private

        ##
        # @private The the next item from the reporter queue. If there are
        # no more item, it blocks the reporter thread until an item is
        # enqueued
        def wait_next_item
          synchronize do
            @queue_resource.wait_while do
              async_suspended? || (async_running? && @queue.empty?)
            end
            @queue.pop
          end
        end

        ##
        # @private Override the #backgrounder_stoppable? method from AsyncActor
        # module. The actor can be gracefully stopped when queue is
        # empty.
        def backgrounder_stoppable?
          synchronize do
            @queue.empty?
          end
        end
      end
    end
  end
end
