# frozen_string_literal: true

require "blather"

class FormTemplate
	def initialize(template, filename="template", **kwargs)
		@args = kwargs
		@template = template
		@filename = filename
		freeze
	end

	def self.for(path, **kwargs)
		if path.is_a?(FormTemplate)
			raise "Sent args and a FormTemplate" unless kwargs.empty?

			return path
		end

		full_path = File.dirname(__dir__) + "/forms/#{path}.rb"
		new(File.read(full_path), full_path, **kwargs)
	end

	def self.render(path, context=OneRender.new, **kwargs)
		self.for(path).render(context, **kwargs)
	end

	def render(context=OneRender.new, **kwargs)
		one = context.merge(**@args).merge(**kwargs)
		one.instance_eval(@template, @filename)
		one.form
	end

	class OneRender
		def initialize(**kwargs)
			kwargs.each do |k, v|
				instance_variable_set("@#{k}", v)
			end
			@__form = Blather::Stanza::X.new
			@__builder = Nokogiri::XML::Builder.with(@__form)
		end

		def merge(**kwargs)
			OneRender.new(**to_h.merge(kwargs))
		end

		def form!
			@__type_set = true
			@__form.type = :form
		end

		def result!
			@__type_set = true
			@__form.type = :result
		end

		def title(s)
			@__form.title = s
		end

		def instructions(s)
			@__form.instructions = s
		end

		def validate(field, datatype: nil, **kwargs)
			Nokogiri::XML::Builder.with(field) do |xml|
				xml.validate(
					xmlns: "http://jabber.org/protocol/xdata-validate",
					datatype: datatype || "xs:string"
				) do
					xml.basic unless validation_type(xml, **kwargs)
				end
			end
		end

		def validation_type(xml, open: false, regex: nil, range: nil)
			xml.open if open
			xml.range({ min: range.first, max: range.last }.compact) if range
			xml.regex(regex.source) if regex
			open || regex || range
		end

		def add_media(field, media)
			Nokogiri::XML::Builder.with(field) do |xml|
				xml.media(xmlns: "urn:xmpp:media-element") do
					media.each do |item|
						xml.uri(item[:uri], type: item[:type])
					end
				end
			end
		end

		# Given a map of fields to labels, and a list of objects this will
		# produce a table from calling each field's method on every object in the
		# list. So, this list is value_semantics / OpenStruct style
		def table(list, **fields)
			keys = fields.keys
			FormTable.new(
				list.map { |x| keys.map { |k| x.public_send(k) } },
				**fields
			).add_to_form(@__form)
		end

		def simple_child(field, name, xmlns, content)
			return unless content

			Nokogiri::XML::Builder.with(field) do |xml|
				xml.public_send(name, content, xmlns: xmlns)
			end
		end

		def field(
			datatype: nil, open: false, regex: nil, range: nil,
			suffix: nil, prefix: nil, media: [],
			**kwargs
		)
			f = Blather::Stanza::X::Field.new(kwargs)
			add_media(f, media) unless media.empty?
			if datatype || open || regex || range
				validate(f, datatype: datatype, open: open, regex: regex, range: range)
			end
			simple_child(f, :x, "https://ns.cheogram.com/suffix-label", suffix)
			simple_child(f, :x, "https://ns.cheogram.com/prefix-label", prefix)
			@__form.fields += [f]
		end

		def context
			PartialRender.new(@__form, @__builder, **to_h)
		end

		def render(path, **kwargs)
			FormTemplate.render(path, context, **kwargs)
		end

		def to_h
			instance_variables
				.reject { |sym| sym.to_s.start_with?("@__") }
				.each_with_object({}) { |var, acc|
					name = var.to_s[1..-1]
					acc[name.to_sym] = instance_variable_get(var)
				}
		end

		def xml
			@__builder
		end

		def form
			raise "Type never set" unless @__type_set

			@__form
		end

		class PartialRender < OneRender
			def initialize(form, builder, **kwargs)
				kwargs.each do |k, v|
					instance_variable_set("@#{k}", v)
				end
				@__form = form
				@__builder = builder
			end

			def merge(**kwargs)
				PartialRender.new(@__form, @__builder, **to_h.merge(kwargs))
			end

			# As a partial, we are not a complete form
			def form; end

			def form!
				raise "Invalid 'form!' in Partial"
			end

			def result!
				raise "Invalid 'result!' in Partial"
			end

			def title(_)
				raise "Invalid 'title' in Partial"
			end

			def instructions(_)
				raise "Invalid 'instructions' in Partial"
			end
		end
	end
end
