module ShEx::Algebra
  ##
  class ShapeRef < Operator::Unary
    include Satisfiable
    NAME = :shapeRef

    def initialize(arg, **options)
      structure_error("Shape reference must be an IRI or BNode: #{arg}", exception: ArgumentError) unless arg.is_a?(RDF::Resource)
      super
    end

    ##
    # Creates an operator instance from a parsed ShExJ representation
    # @param (see Operator#from_shexj)
    # @return [Operator]
    def self.from_shexj(operator, options = {})
      raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "ShapeRef"
      raise ArgumentError, "missing reference in #{operator.inspect}" unless operator.has_key?('reference')
      super
    end

    ##
    # Satisfies referenced shape.
    # @param  (see Satisfiable#satisfies?)
    # @return (see Satisfiable#satisfies?)
    # @raise  (see Satisfiable#satisfies?)
    # @see [https://shexspec.github.io/spec/#shape-expression-semantics]
    def satisfies?(focus, depth: 0)
      status "ref #{operands.first.to_s}", depth: depth
      schema.enter_shape(operands.first, focus) do |shape|
        if shape
          matched_shape = shape.satisfies?(focus, depth: depth + 1)
          satisfy focus: focus, satisfied: matched_shape, depth: depth
        else
          status "Satisfy as #{operands.first} was re-entered for #{focus}", depth: depth
          satisfy focus: focus, satisfied: referenced_shape, depth: depth
        end
      end
    rescue ShEx::NotSatisfied => e
      not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth
    end

    ##
    # Returns the referenced shape
    #
    # @return [Shape]
    def referenced_shape
      @referenced_shape ||= schema.shapes.detect {|s| s.label == operands.first}
    end

    ##
    # A ShapeRef is valid if it's ancestor schema has any shape with a label
    # the same as it's reference.
    # A ref cannot reference itself (via whatever path) without going through a TripleConstraint.
    # Even when going through TripleConstraints, there can't be a negative reference.
    def validate!
      structure_error("Missing referenced shape: #{operands.first}") if referenced_shape.nil?
      raise ShEx::StructureError, "Self referencing shape: #{operands.first}" if referenced_shape == first_ancestor(Satisfiable)
      super
    end

    ##
    # Returns the binary S-Expression (SXP) representation of this operator.
    #
    # @return [Array]
    # @see    https://en.wikipedia.org/wiki/S-expression
    def to_sxp_bin
      ([:shapeRef, ([:label, @label] if @label)].compact + operands).to_sxp_bin
    end
  end
end