require_relative 'dsl'
require_relative 'schema'

module JSON
  module SchemaBuilder
    class Entity
      include DSL
      class_attribute :registered_type
      attr_accessor :name, :parent, :children

      def self.attribute(name, as: nil, array: false)
        as_name = as || name.to_s.underscore.gsub(/_(\w)/){ $1.upcase }
        define_method name do |*values|
          value = array ? values.flatten : values.first
          if (array && value.empty?) || value.nil?
            self.schema[as_name]
          else
            self.schema[as_name] = value
          end
        end
        alias_method "#{ name }=", name
      end

      attribute :title
      attribute :description

      attribute :type
      attribute :enum, array: true
      attribute :all_of, array: true
      attribute :any_of, array: true
      attribute :one_of, array: true
      attribute :not_a, as: :not
      attribute :definitions

      def initialize(name, opts = { }, &block)
        @name = name
        @children = []
        self.type = self.class.registered_type
        initialize_parent_with opts
        initialize_with opts
        eval_block &block
      end

      def schema
        @schema ||= Schema.new
      end

      def required=(*values)
        @parent.required ||= []
        @parent.required << @name
      end

      def merge_children!
        children.each do |child|
          schema.merge! child.schema
        end
      end

      def as_json
        schema.to_h.as_json
      end

      def respond_to?(method_name, include_all = false)
        if @parent_context
          @parent_context.respond_to? method_name, include_all
        else
          super
        end
      end

      def method_missing(method_name, *args, &block)
        if @parent_context && respond_to?(method_name, true)
          @parent_context.send method_name, *args, &block
        else
          super
        end
      end

      protected

      def initialize_parent_with(opts)
        @parent = opts.delete :parent
        @parent.children << self if @parent
      end

      def initialize_with(opts)
        opts.each_pair do |key, value|
          next if value.nil?
          send :"#{ key }=", value
        end
      end

      def eval_block(&block)
        if block_given?
          @parent_context = block.binding.eval 'self'
          instance_exec self, &block
        end
      end
    end
  end
end