# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2018-2023, by Samuel Williams.

require_relative 'header/split'
require_relative 'header/multiple'
require_relative 'header/cookie'
require_relative 'header/connection'
require_relative 'header/cache_control'
require_relative 'header/etag'
require_relative 'header/etags'
require_relative 'header/vary'
require_relative 'header/authorization'
require_relative 'header/date'

module Protocol
	module HTTP
		# Headers are an array of key-value pairs. Some header keys represent multiple values.
		class Headers
			Split = Header::Split
			Multiple = Header::Multiple
			
			TRAILER = 'trailer'
			
			# Construct an instance from a headers Array or Hash. No-op if already an instance of `Headers`. If the underlying array is frozen, it will be duped.
			# @return [Headers] an instance of headers.
			def self.[] headers
				if headers.nil?
					return self.new
				end
				
				if headers.is_a?(self)
					if headers.frozen?
						return headers.dup
					else
						return headers
					end
				end
				
				fields = headers.to_a
				
				if fields.frozen?
					fields = fields.dup
				end
				
				return self.new(fields)
			end
			
			def initialize(fields = [], indexed = nil)
				@fields = fields
				@indexed = indexed
				
				# Marks where trailer start in the @fields array.
				@tail = nil
			end
			
			def initialize_dup(other)
				super
				
				@fields = @fields.dup
				@indexed = @indexed.dup
			end
			
			def clear
				@fields.clear
				@indexed = nil
				@tail = nil
			end
			
			# Flatten trailer into the headers.
			def flatten!
				if @tail
					self.delete(TRAILER)
					@tail = nil
				end
				
				return self
			end
			
			def flatten
				self.dup.flatten!
			end
			
			# An array of `[key, value]` pairs.
			attr :fields
			
			# @returns Whether there are any trailers.
			def trailer?
				@tail != nil
			end
			
			# Record the current headers, and prepare to add trailers.
			#
			# This method is typically used after headers are sent to capture any
			# additional headers which should then be sent as trailers.
			#
			# A sender that intends to generate one or more trailer fields in a
			# message should generate a trailer header field in the header section of
			# that message to indicate which fields might be present in the trailers.
			#
			# @parameter names [Array] The trailer header names which will be added later.
			# @yields block {|name, value| ...} The trailer headers if any.
			# @returns An enumerator which is suitable for iterating over trailers.
			def trailer!(&block)
				@tail ||= @fields.size
				
				return trailer(&block)
			end
			
			# Enumerate all headers in the trailer, if there are any.
			def trailer(&block)
				return to_enum(:trailer) unless block_given?
				
				if @tail
					@fields.drop(@tail).each(&block)
				end
			end
			
			def freeze
				return if frozen?
				
				# Ensure @indexed is generated:
				self.to_h
				
				@fields.freeze
				@indexed.freeze
				
				super
			end
			
			def empty?
				@fields.empty?
			end
			
			def each(&block)
				@fields.each(&block)
			end
			
			def include? key
				self[key] != nil
			end
			
			alias key? include?
			
			def keys
				self.to_h.keys
			end
			
			def extract(keys)
				deleted, @fields = @fields.partition do |field|
					keys.include?(field.first.downcase)
				end
				
				if @indexed
					keys.each do |key|
						@indexed.delete(key)
					end
				end
				
				return deleted
			end
			
			# Add the specified header key value pair.
			#
			# @param key [String] the header key.
			# @param value [String] the header value to assign.
			def add(key, value)
				self[key] = value
			end
			
			# Set the specified header key to the specified value, replacing any existing header keys with the same name.
			# @param key [String] the header key to replace.
			# @param value [String] the header value to assign.
			def set(key, value)
				# TODO This could be a bit more efficient:
				self.delete(key)
				self.add(key, value)
			end
			
			def merge!(headers)
				headers.each do |key, value|
					self[key] = value
				end
				
				return self
			end
			
			def merge(headers)
				self.dup.merge!(headers)
			end
			
			# Append the value to the given key. Some values can be appended multiple times, others can only be set once.
			# @param key [String] The header key.
			# @param value The header value.
			def []= key, value
				if @indexed
					merge_into(@indexed, key.downcase, value)
				end
				
				@fields << [key, value]
			end
			
			POLICY = {
				# Headers which may only be specified once.
				'content-type' => false,
				'content-disposition' => false,
				'content-length' => false,
				'user-agent' => false,
				'referer' => false,
				'host' => false,
				'if-modified-since' => false,
				'if-unmodified-since' => false,
				'from' => false,
				'location' => false,
				'max-forwards' => false,
				
				# Custom headers:
				'connection' => Header::Connection,
				'cache-control' => Header::CacheControl,
				'vary' => Header::Vary,
				
				# Headers specifically for proxies:
				'via' => Split,
				'x-forwarded-for' => Split,
				
				# Authorization headers:
				'authorization' => Header::Authorization,
				'proxy-authorization' => Header::Authorization,
				
				# Cache validations:
				'etag' => Header::ETag,
				'if-match' => Header::ETags,
				'if-none-match' => Header::ETags,
				
				# Headers which may be specified multiple times, but which can't be concatenated:
				'www-authenticate' => Multiple,
				'proxy-authenticate' => Multiple,
				
				# Custom headers:
				'set-cookie' => Header::SetCookie,
				'cookie' => Header::Cookie,
				
				'date' => Header::Date,
				'expires' => Header::Date,
			}.tap{|hash| hash.default = Split}
			
			# Delete all headers with the given key, and return the merged value.
			def delete(key)
				deleted, @fields = @fields.partition do |field|
					field.first.downcase == key
				end
				
				if deleted.empty?
					return nil
				end
				
				if @indexed
					return @indexed.delete(key)
				elsif policy = POLICY[key]
					(key, value), *tail = deleted
					merged = policy.new(value)
					
					tail.each{|k,v| merged << v}
					
					return merged
				else
					key, value = deleted.last
					return value
				end
			end
			
			protected def merge_into(hash, key, value)
				if policy = POLICY[key]
					if current_value = hash[key]
						current_value << value
					else
						hash[key] = policy.new(value)
					end
				else
					# We can't merge these, we only expose the last one set.
					hash[key] = value
				end
			end
			
			def [] key
				to_h[key]
			end
			
			# A hash table of `{key, policy[key].map(values)}`
			def to_h
				@indexed ||= @fields.inject({}) do |hash, (key, value)|
					merge_into(hash, key.downcase, value)
					
					hash
				end
			end
			
			def inspect
				"#<#{self.class} #{@fields.inspect}>"
			end
			
			def == other
				case other
				when Hash
					to_h == other
				when Headers
					@fields == other.fields
				else
					@fields == other
				end
			end
			
			# Used for merging objects into a sequential list of headers. Normalizes header keys and values.
			class Merged
				include Enumerable
				
				def initialize(*all)
					@all = all
				end
				
				def fields
					each.to_a
				end
				
				def flatten
					Headers.new(fields)
				end
				
				def clear
					@all.clear
				end
				
				def << headers
					@all << headers
					
					return self
				end
				
				# @yields [String, String] header key (lower case string) and value (as string).
				def each(&block)
					return to_enum unless block_given?
					
					@all.each do |headers|
						headers.each do |key, value|
							yield key.to_s.downcase, value.to_s
						end
					end
				end
			end
		end
	end
end
