# frozen_string_literal: true

module Litany
  class UndefinedSetting < BasicObject;
  end

  class SettingsMash < ::Hashie::Mash
    include ::Hashie::Extensions::Mash::KeepOriginalKeys
  end

  class Settings < Object
    SETTING_REGEX = /^[a-z][a-z0-9_]*$/
    RESERVED = [:define_singleton_method, :disable, :enable, :fetch, :method_missing, :set, :singleton_class, :singleton_methods, :sub_scope]

    def disable(*flags)
      flags.each { |flag| set flag, false }
    end

    def enable(*flags)
      flags.each { |flag| set flag, true }
    end

    def fetch(setting, default: nil)
      set?(setting) ? __send__(setting) : default
    end

    def initialize(parent_scope = nil)
      @parent_scope = parent_scope
    end

    def method_missing(setting, *args)
      setting_name = (setting.to_s.end_with?('?', '=') ? setting[0..-2] : setting).to_sym
      return super unless setting_name =~ SETTING_REGEX

      set setting_name # calling without a value creates all the peices in place for the setting.
      __send__ setting, *args # lets us get proper values back for values like 'setting?' or 'setting = value'
    end

    def set(setting, value = UndefinedSetting)
      ivar = :"@#{setting}"
      setting = :"#{setting}"
      set_helper = :"#{setting}?"

      raise "Invalid setting name: `#{setting}` is reserved." if RESERVED.include?(setting)
      raise "Invalid setting name: `#{setting}` does not match #{SETTING_REGEX.inspect}." unless setting =~ SETTING_REGEX

      unless singleton_methods(false).include?(setting)
        singleton_class.send(:attr_writer, setting)

        define_singleton_method(setting) do
          stored_value = set?(setting) ? instance_variable_get(ivar) : nil
          return stored_value if set?(setting) || @parent_scope.nil?
          @parent_scope.send(setting)
        end

        define_singleton_method(set_helper) { set?(setting) || (@parent_scope.nil? ? false : @parent_scope.send(set_helper)) }
      end

      # Munge hashes to mashes and merge them if previously set to a Mash.
      value = SettingsMash.new(value) if value.is_a?(Hash)
      value = __send__(setting).clone.deep_merge(value) if __send__(set_helper) && __send__(setting).is_a?(SettingsMash)

      __send__(:"#{setting}=", value) unless value == UndefinedSetting
    end

    def sub_scope
      Settings.new(self)
    end

    def respond_to_missing?(setting, include_private = false)
      setting_name = (setting.to_s.end_with?('?', '=') ? setting[0..-2] : setting).to_sym
      setting_name =~ SETTING_REGEX || super
    end

    def set?(setting)
      instance_variable_defined? :"@#{setting}"
    end
  end
end