module Binda
	class FieldSetting < ApplicationRecord
		cattr_accessor :field_settings_array
		cattr_accessor :get_field_classes

    # An array of all classes which represent fields associated to Binda::FieldSetting
    # This definition must stay on the top of the file
		def self.get_field_classes
			%w( String Text Date Image Video Audio Repeater Radio Selection Checkbox Relation Svg )
		end

		# ASSOCIATIONS
		# ------------

		belongs_to :field_group
		has_ancestry orphan_strategy: :destroy

		# FIELDS ASSOCIATIONS
		# 
		# If you add a new field remember to update:
		#   - get_field_classes (see here below)
		#   - component_params (app/controllers/binda/components_controller.rb)
		has_many :texts,         as: :fieldable
		has_many :strings,       as: :fieldable
		has_many :dates,         as: :fieldable
		has_many :galleries,     as: :fieldable
		has_many :assets,        as: :fieldable
		has_many :images,        as: :fieldable
		has_many :videos,        as: :fieldable
		has_many :audios,        as: :fieldable
		has_many :repeaters,     as: :fieldable
		has_many :radios,        as: :fieldable
		has_many :selections,    as: :fieldable
		has_many :checkboxes,    as: :fieldable
		has_many :relations,     as: :fieldable
		has_many :svgs,          as: :fieldable

		# The following direct associations are used to securely delete associated fields
		# Infact via `fieldable` the associated fields might not be deleted 
		# as the fieldable_id is related to the `component`, `board` or `repeater` rather than `field_setting`
		has_many :texts,          dependent: :destroy
		has_many :strings,        dependent: :destroy
		has_many :dates,          dependent: :destroy
		has_many :galleries,      dependent: :destroy
		has_many :assets,         dependent: :destroy
		has_many :images,         dependent: :destroy
		has_many :videos,         dependent: :destroy
		has_many :audios,         dependent: :destroy
		has_many :repeaters,      dependent: :destroy
		has_many :radios,         dependent: :destroy
		has_many :selections,     dependent: :destroy
		has_many :checkboxes,     dependent: :destroy
		has_many :relations,      dependent: :destroy
		has_many :svgs,           dependent: :destroy

		# We don't want to run callbacks for choices!
		# If you run a callback the last choice will throw a error
		# @see `Binda::Choice` `before_destroy :check_last_choice`
		has_many :choices,        dependent: :delete_all 

		has_one  :default_choice, -> (field_setting) { where(id: field_setting.default_choice_id) }, class_name: 'Binda::Choice'
		
		has_and_belongs_to_many :accepted_structures, class_name: 'Binda::Structure'

		accepts_nested_attributes_for :accepted_structures, :texts, :strings, :dates, :galleries,
		                              :assets, :images, :videos, :audios, :repeaters, :radios, :selections,
		                              :checkboxes, :relations, :svgs, :choices, allow_destroy: true, reject_if: :is_rejected

		# Sets the validation rules to accept and save an attribute
		def is_rejected( attributes )
			attributes['label'].blank? || attributes['content'].blank?
		end



		# CALLBACKS
		# ---------

		before_save do
			check_allow_null_for_radio
		end

		after_save do
    	self.class.reset_field_settings_array
			add_choice_if_allow_null_is_false
			create_field_instances
		end

    after_create do 
    	convert_allow_null__nil_to_false
    	set_default_position
    end

    after_update do
			self.class.remove_orphan_fields
			if %w(radio selection checkbox).include?(self.field_type) && self.choices.empty?
				check_allow_null_option
			end
    end

    after_destroy do 
    	self.class.reset_field_settings_array 
    end



    # VALIDATIONS
    # -----------
    # @see http://guides.rubyonrails.org/active_record_validations.html#message

		validates :name, presence: { 
			message: I18n.t("binda.field_setting.validation_message.name") 
		}
		validates :field_type, inclusion: { 
			in: [ *FieldSetting.get_field_classes.map{ |fc| fc.to_s.underscore } ], 
			allow_nil: false,
			message: -> (field_setting, _) { 
				I18n.t(
					"binda.field_setting.validation_message.field_type", 
					{ arg1: field_setting.name, arg2: FieldSetting.get_field_classes.join(", ") }
				)
			}
		}
		validates :field_group_id, presence: {
			message: -> (field_setting, data) {
				I18n.t(
					"binda.field_setting.validation_messag e.field_group_id", 
					{ arg1: field_setting.name, arg2: data[:value] }
				)
			}
		}
		validate :slug_uniqueness


    # FRIENDLY ID
    # -----------

		extend FriendlyId
		friendly_id :default_slug, use: [:slugged, :finders]

		# Friendly id preference on slug generation
		#
		# Method inherited from friendly id 
		# @see https://github.com/norman/friendly_id/issues/436
	  def should_generate_new_friendly_id?
	    slug.blank?
	  end

		# Set slug name
		#
		# It generates 4 possible slugs before falling back to FriendlyId default behaviour
		def default_slug
			return self.name if self.field_group_id.nil?
			slug = ''
			slug << self.field_group.structure.name
			slug << '-'
			slug << self.field_group.name
			unless self.parent.nil?
				slug << '-' 
				slug << self.parent.name 
			end
			return [ 
				"#{ slug }--#{ self.name }",
				"#{ slug }--#{ self.name }-1",
				"#{ slug }--#{ self.name }-2",
				"#{ slug }--#{ self.name }-3"
			]
		end

		# Check slug uniqueness
		def slug_uniqueness
			record_with_same_slug = self.class.where(slug: slug)
			if record_with_same_slug.any? && !record_with_same_slug.ids.include?(id)
				errors.add(:slug, I18n.t("binda.field_setting.validation_message.slug", { arg1: slug }))
				return false
			else
				return true 
			end
		end

		# It makes sure radio buttons have allow_null set to false.
		def check_allow_null_for_radio
			if field_type == 'radio' && allow_null?
				self.allow_null = false
				warn "WARNING: it's not possible that a field setting with type `radio` has allow_null=true."
			end
		end


		# MISCELLANEOUS
		# -------------

		# Retrieve the ID if a slug is provided and update the field_settings_array 
		#   in order to avoid calling the database (or the cached response) every time.
		#   This should speed up requests and make Rails logs are cleaner.
		# 
		# @return [integer] The ID of the field setting
		def self.get_id(field_slug)
			# Get field setting id from slug, without multiple calls to database 
			# (the query runs once and caches the result, then any further call uses the cached result)
			@@field_settings_array = self.pluck(:slug, :id) if @@field_settings_array.nil?
			selected_field_setting = @@field_settings_array.select{ |fs| fs[0] == field_slug }[0]
			raise ArgumentError, "There isn't any field setting with the current slug \"#{field_slug}\".", caller if selected_field_setting.nil?
			id = selected_field_setting[1]
			return id
		end

		# Reset the field_settings_array. It's called every time 
		#   the user creates or destroyes a Binda::FieldSetting
		# 
		# @return [null]
		def self.reset_field_settings_array
			# Reset the result of the query taken with the above method,
			# this is needed when a user creates a new field_setting but 
			# `get_field_setting_id` has already run once
			@@field_settings_array = nil
		end

		# Make sure that allow_null is set to false instead of nil.
		#   (This isn't done with a database constraint in order to gain flexibility)
		# 
		# REVIEW: not sure what flexibility is needed. Maybe should be done differently
		def convert_allow_null__nil_to_false
			self.allow_null = false if self.allow_null.nil?
		end

		# Get structure on which the current field setting is attached. It should be one, but in order to 
		#   be able to add other methods to the query this methods returns a `ActiveRecord::Relation` object, not
		#   a `ActiveRecord`
		# 
		# @return [ActiveRecord::Relation] An array of `Binda::Structure` instances
		def structures
			Structure.left_outer_joins(
				field_groups: [:field_settings]
			).where(
				binda_field_settings: { id: self.id }
			)
		end

		# Get the structure of the field group to which the field setting belongs.
		# 
		# @return [ActiveRecord] The `Binda::Structure` instance
		def structure
			self.structures.first
		end

		# Generates a default field instances for each existing component or board
		#   which is associated to that field setting. This avoid having issues 
		#   with Binda::FieldSetting.get_id method which would throw an ambiguous error 
		#   saying that there isn't any field setting associated when infact it's 
		#   the actual field missing, not the field setting itself.
		#   
		# A similar script runs after saving components and boards which makes sure
		#   a field instance is always present no matter if the component has been created
		#   before the field setting or the other way around.
		#   
		# TODO this MUST be optimized
		def create_field_instances
      CreateFieldInstancesJob.perform_later self
		end

		def create_field_instance_for(instance)
			field_class = "Binda::#{self.field_type.classify}"
			if self.is_root?
				create_field_instance_for_instance(instance, field_class, self.id)
			else
				instance.repeaters.select{|r| r.field_setting_id == self.parent_id}.each do |repeater|
					create_field_instance_for_instance(repeater, field_class, self.id)
				end
			end
		end

		# Helper for create_field_instances method
		def create_field_instance_for_instance(instance, field_class, field_setting_id)
			field_class.constantize.find_or_create_by!(
				field_setting_id: field_setting_id,
				fieldable_id: instance.id,
				fieldable_type: instance.class.name
			)
		end

		# Check `allow_null` option
		# 
		# Creating a selection with `allow_null` set to `false` will automatically generate a critical error.
		#   This is due to the fact that 1) there is no choice to select, but 2) the selection field must have at least one.
		#   The error can be easily removed by assigning a choice to the current field setting.
		# 
		# This method is preferred to a validation because it allows to remove all choices before adding new ones.
		#   
		def check_allow_null_option
    	return if self.allow_null?
  		Selection.check_all_selections_depending_on(self)
		end

		# Validation method that check if the current `Binda::Selection` instance has at least a choice before 
		#   updating allow null to false
		def add_choice_if_allow_null_is_false
			if %(selection radio checkbox).include?(self.field_type) && 
				!self.allow_null?
				if self.choices.empty?
					# Add a choice if there is none, it will be automatically assign as default choice
					self.choices.create!(label: I18n.t("binda.choice.default_label"), value: I18n.t("binda.choice.default_value"))
				elsif self.default_choice_id.nil?
					# Assign a choice as default one if there is any
					# REVIEW there is some deprecation going on, but I'm not sure i directly involves the `update` method
					self.update!(default_choice_id: self.choices.first.id)
				end
			end
		end

		# Remove all orphan fields
		# 
		# Specifically:
		# - all fields where their field setting doesn't exist anymore
		# - all fields where their field setting has change type
		# 
		# Used by task `rails binda:remove_orphan_fields`
		def self.remove_orphan_fields
			FieldSetting.get_field_classes.each do |field_class|
				FieldSetting.remove_orphan_fields_with_no_settings(field_class)
				FieldSetting.remove_orphan_fields_with_wrong_field_type(field_class)
			end
		end

		# Remove orphan fields that isn't associated to any field setting
		def self.remove_orphan_fields_with_no_settings(field_class)
			"Binda::#{field_class}"
				.constantize
				.includes(:field_setting)
				.where(binda_field_settings: {id: nil})
				.each do |s| 
					s.destroy!
					puts "Binda::#{field_class} with id ##{s.id} successfully destroyed"
				end
		end

		# Remove orphan fields with wrong field type
		def self.remove_orphan_fields_with_wrong_field_type(field_class)
			field_types = []
			case field_class
			when 'Selection'
				field_types = %w(selection checkbox radio)
			when 'Text'
				field_types = %w(string text)
			else
				field_types = [ field_class.underscore ]
			end
			"Binda::#{field_class}"
				.constantize
				.includes(:field_setting)
				.where.not(binda_field_settings: {field_type: field_types})
				.each do |s| 
					s.destroy!
					puts "Binda::#{field_class} with id ##{s.id} but wrong type successfully destroyed"
				end
		end

		# Set a default position if isn't set and updates all related field settings
		# Update all field settings related to the one created
		def set_default_position
			FieldSetting
				.where(
					field_group_id: self.field_group_id,
					ancestry: self.ancestry
				)
        .update_all('position = position + 1')
		end

	end
end
