#  Union Station - https://www.unionstationapp.com/
#  Copyright (c) 2010-2015 Phusion Holding B.V.
#
#  "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.

UnionStationHooks.require_lib 'log'
UnionStationHooks.require_lib 'context'
UnionStationHooks.require_lib 'time_point'
UnionStationHooks.require_lib 'utils'

module UnionStationHooks
  # @private
  class Transaction
    attr_reader :txn_id

    def initialize(connection, txn_id)
      @connection = connection
      @txn_id = txn_id
      if connection
        raise ArgumentError, 'Transaction ID required' if txn_id.nil?
        connection.ref
      end
    end

    def null?
      !@connection || !@connection.connected?
    end

    def message(text)
      if !@connection
        log_message_to_null(text)
        return
      end

      @connection.synchronize do
        if !@connection.connected?
          log_message_to_null(text)
          return
        end

        UnionStationHooks::Log.debug('[Union Station log] ' \
          "#{@txn_id} #{Utils.encoded_timestamp} #{text}")

        io_operation do
          @connection.channel.write('log', @txn_id, Utils.encoded_timestamp)
          @connection.channel.write_scalar(text)
        end
      end
    end

    def log_activity_block(name, extra_info = nil)
      has_error = false
      log_activity_begin(name, UnionStationHooks.now, extra_info)
      begin
        yield
      rescue Exception
        has_error = true
        is_closed = closed?
        raise
      ensure
        if !is_closed
          log_activity_end(name, UnionStationHooks.now, has_error)
        end
      end
    end

    def log_activity_begin(name, time = UnionStationHooks.now, extra_info = nil)
      if extra_info
        extra_info_base64 = Utils.base64(extra_info)
      else
        extra_info_base64 = nil
      end
      if time.is_a?(TimePoint)
        message "BEGIN: #{name} (#{Utils.encoded_timestamp(time)}," \
          "#{time.utime.to_s(36)},#{time.stime.to_s(36)}) " \
          "#{extra_info_base64}"
      else
        message "BEGIN: #{name} (#{Utils.encoded_timestamp(time)})" \
          " #{extra_info_base64}"
      end
    end

    def log_activity_end(name, time = UnionStationHooks.now, has_error = false)
      if time.is_a?(TimePoint)
        if has_error
          message "FAIL: #{name} (#{Utils.encoded_timestamp(time)}," \
            "#{time.utime.to_s(36)},#{time.stime.to_s(36)})"
        else
          message "END: #{name} (#{Utils.encoded_timestamp(time)}," \
            "#{time.utime.to_s(36)},#{time.stime.to_s(36)})"
        end
      else
        if has_error
          message "FAIL: #{name} (#{Utils.encoded_timestamp(time)})"
        else
          message "END: #{name} (#{Utils.encoded_timestamp(time)})"
        end
      end
    end

    def log_activity(name, begin_time, end_time, extra_info = nil,
                     has_error = false)
      log_activity_begin(name, begin_time, extra_info)
      log_activity_end(name, end_time, has_error)
    end

    def close(should_flush_to_disk = false)
      return if !@connection

      @connection.synchronize do
        return if !@connection.connected?

        begin
          io_operation do
            # We need an ACK here so that we the UstRouter doesn't end up
            # processing the Core's openTransaction and closeTransaction pair
            # before it has received this process's openTransaction command.
            @connection.channel.write('closeTransaction', @txn_id,
              Utils.encoded_timestamp, true)
            Utils.process_ust_router_reply(@connection.channel,
              "Error handling reply for 'closeTransaction' message")
            if should_flush_to_disk
              flush_to_disk
            end
          end
        ensure
          @connection.unref
          @connection = nil
        end
      end
    end

    def closed?
      return nil if !@connection
      @connection.synchronize do
        !@connection.connected?
      end
    end

  private

    def log_message_to_null(text)
      UnionStationHooks::Log.debug('[Union Station log to null] ' \
        "#{@txn_id} #{Utils.encoded_timestamp} #{text}")
    end

    def io_operation
      yield
    rescue SystemCallError, IOError => e
      @connection.disconnect
      UnionStationHooks::Log.warn(
        "Error communicating with the UstRouter: #{e.message}")
    rescue Exception => e
      @connection.disconnect
      raise e
    end

    def flush_to_disk
      @connection.channel.write('flush')
      Utils.process_ust_router_reply(@connection.channel,
        "Error handling reply for 'flush' message")
    end
  end
end
