module BBLib::Attr

  private

  def attr_type method, opts, &block
    define_method("#{method}=", &block)
    define_method(method){ instance_variable_get("@#{method}")}
    if defined?(:before) && opts.include?(:default)
      define_method("__reset_#{method}".to_sym){ send("#{method}=", opts[:default]) }
    end
  end

  def attr_sender call, *methods, **opts
    methods.each do |m|
      attr_type(
        m,
        opts,
        &attr_set(
          m,
          opts.merge(sender: true)
        ){ |x| x.nil? && opts[:allow_nil] ? nil : x.send(call) }
      )
    end
  end

  def attr_of klass, *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x|
        if x.is_a?(klass)
          instance_variable_set("@#{m}", x)
        else
          raise ArgumentError, "#{method} must be set to a #{klass}!"
        end
      }
    )
  }
  end

  def attr_boolean *methods, **opts
    methods.each{ |m|
      attr_type(m, opts) { |x| instance_variable_set("@#{m}", !!x && x.to_s != 'false') }
      alias_method "#{m}?", m unless opts[:no_q]
    }
  end

  alias_method :attr_bool, :attr_boolean

  def attr_string *methods, **opts
    attr_sender :to_s, *methods, opts
  end

  alias_method :attr_str, :attr_string
  alias_method :attr_s, :attr_string

  def attr_integer *methods, **opts
    attr_sender :to_i, *methods, opts
  end

  alias_method :attr_int, :attr_integer
  alias_method :attr_i, :attr_integer

  def attr_float *methods, **opts
    attr_sender :to_f, *methods, opts
  end

  alias_method :attr_f, :attr_float

  def attr_integer_between min, max, *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x| BBLib::keep_between(x, min, max) })}
  end

  alias_method :attr_int_between, :attr_integer_between
  alias_method :attr_i_between, :attr_integer_between
  alias_method :attr_float_between, :attr_integer_between
  alias_method :attr_f_between, :attr_float_between

  def attr_symbol *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x| x.to_s.to_sym } )}
  end

  alias_method :attr_sym, :attr_symbol

  def attr_clean_symbol *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x| x.to_s.to_clean_sym } )}
  end

  alias_method :attr_clean_sym, :attr_clean_symbol

  def attr_array *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |*x| instance_variable_set("@#{m}", x) } )}
  end

  alias_method :attr_ary, :attr_array

  def attr_element_of list, *methods, **opts
    methods.each do |m|
      attr_type(m, opts, &attr_set(m, opts) do |x|
          if !list.include?(x)
            raise ArgumentError, "#{m} only accepts the following (first 10 shown) #{list[0...10]}"
          else
            instance_variable_set("@#{m}", x)
          end
        end
      )
    end
  end

  def attr_array_of klass, *methods, raise: false, **opts
    methods.each do |m|
      attr_type(m, opts, &attr_set(m, opts) do |*x|
          if raise && x.any?{ |i| klass.is_a?(Array) ? !klass.any?{ |k| i.is_a?(k) } : !i.is_a?(klass) }
            raise ArgumentError, "#{m} only accepts items of class #{klass}."
          end
          instance_variable_set("@#{m}", x.reject{|i| klass.is_a?(Array) ? !klass.any?{ |k| i.is_a?(k) } : !i.is_a?(klass) })
        end
      )
    end
  end

  alias_method :attr_ary_of, :attr_array_of

  def attr_hash *methods, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts) do |*a|
          begin
            hash = a.find_all{ |i| i.is_a?(Hash) }.inject({}){ |m, h| m.merge(h) } || Hash.new
            instance_variable_set("@#{m}", hash)
          rescue ArgumentError => e
            raise ArgumentError, "#{m} only accepts a hash for its parameters"
          end
        end
      )
    }
  end

  def attr_valid_file *methods, raise: true, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x| File.exists?(x.to_s) ? x.to_s : (raise ? raise(ArgumentError, "File '#{x}' does not exist. @#{m} must be set to a valid file location!") : nil)} )}
  end

  def attr_valid_dir *methods, raise: true, **opts
    methods.each{ |m| attr_type(m, opts, &attr_set(m, opts){ |x| Dir.exists?(x.to_s) ? x.to_s : (raise ? raise(ArgumentError, "Dir '#{x}' does not exist. @#{m} must be set to a valid directory location!") : nil)} )}
  end

  def attr_time *methods, **opts
    methods.each do |m|
      attr_type(
        m,
        opts,
        &attr_set(m, opts){ |x|
          if x.is_a?(Time) || x.nil? && opt[:allow_nil]
            x
          elsif x.is_a?(Numeric)
            Time.at(x)
          elsif x.is_a?(String)
            Time.parse(x)
          else
            raise "#{x} is an invalid Time object and could not be converted into a Time object."
          end
        }
      )
    end
  end

  def attr_set method, allow_nil: false, fallback: :_nil, sender: false, default: nil, &block
    proc{ |x|
      if x.nil? && !allow_nil && fallback == :_nil && !sender
        raise ArgumentError, "#{method} cannot be set to nil!"
      elsif x.nil? && !allow_nil && fallback != :_nil && !sender
        instance_variable_set("@#{method}", fallback)
      else
        begin
          instance_variable_set("@#{method}", x.nil? && !sender ? x : yield(x) )
        rescue Exception => e
          if fallback != :_nil
            instance_variable_set("@#{method}", fallback)
          else
            raise e
          end
        end
      end
    }
  end

end