module Neo4j
  module Wrapper
    module NodeMixin
      module ClassMethods

        # Creates a new node or loads an already existing Neo4j node.
        #
        # You can use two callback method to initialize the node
        # init_on_load - this method is called when the node is loaded from the database
        # init_on_create - called when the node is created, will be provided with the same argument as the new method
        #
        # == Does
        # * sets the neo4j property '_classname' to self.class.to_s
        # * creates a neo4j node java object (in @_java_node)
        #
        # If you want to provide your own initialize method you should instead implement the
        # method init_on_create method.
        #
        # @example Create your own Ruby wrapper around a Neo4j::Node java object
        #   class MyNode
        #     include Neo4j::NodeMixin
        #   end
        #
        #   node = MyNode.new(:name => 'jimmy', :age => 23)
        #
        # @example Using your own initialize method
        #   class MyNode
        #     include Neo4j::NodeMixin
        #
        #     def init_on_create(name, age)
        #        self[:name] = name
        #        self[:age] = age
        #     end
        #   end
        #
        #   node = MyNode.new('jimmy', 23)
        #
        # @param args typically a hash of properties, but could be anything which will be handled in the super method
        # @return the object return from the super method
        def new(*args)
          node = Neo4j::Node.create
          wrapped_node = super()
          Neo4j::IdentityMap.add(node, wrapped_node)
          wrapped_node.init_on_load(node)
          wrapped_node.init_on_create(*args)
          wrapped_node
        end

        alias_method :create, :new


        # Get the indexed entity, creating it (exactly once) if no indexed entity exist.
        #
        # @example Creating a Unique node
        #
        #   class MyNode
        #     include Neo4j::NodeMixin
        #     property :email, :index => :exact, :unique => true
        #   end
        #
        #   node = MyNode.get_or_create(:email =>'jimmy@gmail.com', :name => 'jimmy')
        #
        # @note It must not be called in a transaction. It will create and finish a new transaction.
        # @see #put_if_absent
        def get_or_create(*args)
          props = args.first
          raise "Can't get or create entity since #{props.inspect} does not included unique key #{props[unique_factory_key]}'" unless props[unique_factory_key]
          index = index_for_type(_decl_props[unique_factory_key][:index])
          Neo4j::Core::Index::UniqueFactory.new(unique_factory_key, index) { |*| new(*args) }.get_or_create(unique_factory_key, props[unique_factory_key]).wrapper
        end

        # @throws Exception if there are more then one property having unique index
        # @return [Symbol,nil] the property which has an unique index or nil
        def unique_factory_key
          @unique_factory_key ||= begin
            unique = []
            _decl_props.each_pair { |k, v| unique << k if v[:unique] }
            return nil if unique.empty?
            raise "Only one property can be unique, got #{unique.join(', ')}" if unique.size > 1
            unique.first
          end
        end

        # Loads a wrapped node from the database given a neo id.
        # @param [#to_i, nil] neo_id
        # @return [Object, nil] If the node does not exist it will return nil otherwise the loaded node or wrapped node.
        # @note it will return nil if the node returned is not kind of this class
        def load_entity(neo_id)
          node = Neo4j::Node.load(neo_id)
          return nil if node.nil?
          return node if node.class == Neo4j::Node
          node.kind_of?(self) ? node : nil
        end

      end
    end
  end
end