# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2023, by Samuel Williams.
# Copyright, 2019, by William T. Nelson.
# Copyright, 2021, by Aurora Nockert.

require_relative 'framer'
require 'securerandom'

module Protocol
	module WebSocket
		# Wraps a framer and implements for implementing connection specific interactions like reading and writing text.
		class Connection
			# @parameter mask [String] 4-byte mask to be used for frames generated by this connection.
			def initialize(framer, mask: nil, **options)
				@framer = framer
				@mask = mask
				
				@state = :open
				@frames = []
				
				@reserved = Frame::RESERVED
				
				@reader = self
				@writer = self
			end
			
			# The framer which is used for reading and writing frames.
			attr :framer
			
			# The (optional) mask which is used when generating frames.
			attr :mask
			
			# The allowed reserved bits:
			attr :reserved
			
			# Buffered frames which form part of a complete message.
			attr_accessor :frames
			
			attr_accessor :reader
			attr_accessor :writer
			
			def reserve!(bit)
				if (@reserved & bit).zero?
					raise ArgumentError, "Unable to use #{bit}!"
				end
				
				@reserved &= ~bit
				
				return true
			end
			
			def flush
				@framer.flush
			end
			
			def open!
				@state = :open
				
				return self
			end
			
			def close!(...)
				@state = :closed
				
				return self
			end
			
			def closed?
				@state == :closed
			end
			
			def close(...)
				unless @state == :closed
					close!
					
					begin
						send_close(...)
					rescue
						# Ignore.
					end
				end
				
				@framer.close
			end
			
			def read_frame
				return nil if closed?
				
				frame = @framer.read_frame
				
				unless (frame.flags & @reserved).zero?
					raise ProtocolError, "Received frame with reserved flags set!"
				end
				
				yield frame if block_given?
				
				frame.apply(self)
				
				return frame
			rescue ProtocolError => error
				close(error.code, error.message)
				
				raise
			rescue
				close(Error::PROTOCOL_ERROR, $!.message)
				
				raise
			end
			
			def write_frame(frame)
				@framer.write_frame(frame)
				
				return frame
			end
			
			def receive_text(frame)
				if @frames.empty?
					@frames << frame
				else
					raise ProtocolError, "Received text, but expecting continuation!"
				end
			end
			
			def receive_binary(frame)
				if @frames.empty?
					@frames << frame
				else
					raise ProtocolError, "Received binary, but expecting continuation!"
				end
			end
			
			def receive_continuation(frame)
				if @frames.any?
					@frames << frame
				else
					raise ProtocolError, "Received unexpected continuation!"
				end
			end
					
			def receive_close(frame)
				code, reason = frame.unpack
				
				# If we're already closed, then we don't need to send a close frame. Otherwise, according to the RFC, we should echo the close frame. However, it's possible it will fail to send if the connection is already closed.
				unless @state == :closed
					close!
					send_close(code, reason)
				end
				
				if code and code != Error::NO_ERROR
					raise ClosedError.new reason, code
				end
			end
			
			def send_ping(data = "")
				if @state != :closed
					frame = PingFrame.new(mask: @mask)
					frame.pack(data)
					
					write_frame(frame)
				else
					raise ProtocolError, "Cannot send ping in state #{@state}"
				end
			end
			
			def receive_ping(frame)
				if @state != :closed
					write_frame(frame.reply(mask: @mask))
				else
					raise ProtocolError, "Cannot receive ping in state #{@state}"
				end
			end
			
			def receive_pong(frame)
				# Ignore.
			end
			
			def receive_frame(frame)
				raise ProtocolError, "Unhandled frame: #{frame}"
			end
			
			def pack_text_frame(buffer, **options)
				frame = TextFrame.new(mask: @mask)
				frame.pack(buffer)
				
				return frame
			end
			
			def send_text(buffer, **options)
				write_frame(@writer.pack_text_frame(buffer, **options))
			end
			
			def pack_binary_frame(buffer, **options)
				frame = BinaryFrame.new(mask: @mask)
				frame.pack(buffer)
				
				return frame
			end
			
			def send_binary(buffer, **options)
				write_frame(@writer.pack_binary_frame(buffer, **options))
			end
			
			def send_close(code = Error::NO_ERROR, reason = "")
				frame = CloseFrame.new(mask: @mask)
				frame.pack(code, reason)
				
				self.write_frame(frame)
				self.flush
			end
			
			# Write a message to the connection.
			# @parameter message [Message] The message to send.
			def write(message, **options)
				# This is a compatibility shim for the previous implementation. We may want to eventually deprecate this use case... or maybe it's convenient enough to leave it around.
				if message.is_a?(String)
					if message.encoding == Encoding::UTF_8
						return send_text(message, **options)
					else
						return send_binary(message, **options)
					end
				end
				
				message.send(self, **options)
			end
			
			# The default implementation for reading a message buffer.
			def unpack_frames(frames)
				frames.map(&:unpack).join("")
			end
			
			# Read a message from the connection.
			# @returns message [Message] The received message.
			def read(**options)
				@framer.flush
				
				while read_frame
					if @frames.last&.finished?
						frames = @frames
						@frames = []
						
						buffer = @reader.unpack_frames(frames, **options)
						return frames.first.read_message(buffer)
					end
				end
			rescue ProtocolError => error
				close(error.code, error.message)
				
				raise
			end
		end
	end
end
