module OM::XML::TermXpathGenerator
  
  # Generate relative xpath for a term
  # @param [OM::XML::Term] term that you want to generate relative xpath for
  #
  # In most cases, the resulting xpath will be the Term's path with the appropriate namespace appended to it.
  # If the Term specifies any attributes, 
  # Special Case: attribute Terms
  # If the Term's path is set to {:attribute=>attr_name}, the resulting xpath will points to a node attribute named attr_name 
  # ie. a path fo {:attribute=>"lang"} will result in a relative xpath of "@lang"
  # Special Case: xpath functions
  # If the Term's path variable is text(), it will be treated as an xpath function (no namespace) and turned into "text()[normalize-space(.)]"
  def self.generate_relative_xpath(term)
    template = ""
    predicates = []
    
    if term.namespace_prefix.nil?
      complete_prefix = ""
    else
      complete_prefix = term.namespace_prefix + ":"
    end
    
    if term.path.kind_of?(Hash)
      if term.path.has_key?(:attribute)
        base_path = "@"+term.path[:attribute]
      else
        raise "#{term.path} is an invalid path for an OM::XML::Term.  You should provide either a string or {:attributes=>XXX}"
      end
    else
      if term.path == "text()"
        base_path = "#{term.path}[normalize-space(.)]"
      else
        unless term.namespace_prefix.nil?
          template << complete_prefix
        end
        base_path = term.path
      end
    end
    template << base_path
    
    unless term.attributes.nil?
      term.attributes.each_pair do |attr_name, attr_value|
        if attr_value == :none
          predicates << "not(@#{attr_name})"
        else
          predicates << "@#{attr_name}=\"#{attr_value}\""
        end
      end
    end
    
    unless predicates.empty? 
      template << "["+ delimited_list(predicates, " and ")+"]"
    end
    
    return template
  end
  
  # Generate absolute xpath for a Term
  # @param [OM::XML::Term] term that you want to generate absolute xpath for
  #
  # Absolute xpaths always begin with "//".  They are generated by relying on the Term's relative xpath and the absolute xpath of its parent node.
  def self.generate_absolute_xpath(term)
    relative = generate_relative_xpath(term)
    if term.parent.nil?
      return "//#{relative}"
    else
      return term.parent.xpath_absolute + "/" + relative
    end
  end
  
  def self.generate_constrained_xpath(term)
    if term.namespace_prefix.nil?
      complete_prefix = ""
    else
      complete_prefix = term.namespace_prefix + ":"
    end
    
    absolute = generate_absolute_xpath(term)
    constraint_predicates = []
    
    arguments_for_contains_function = []

    if !term.default_content_path.nil?
      arguments_for_contains_function << "#{complete_prefix}#{term.default_content_path}"
    end
      
    # If no subelements have been specified to search within, set contains function to search within the current node
    if arguments_for_contains_function.empty?
      arguments_for_contains_function << "."
    end
    
    arguments_for_contains_function << "\":::constraint_value:::\""
  
    contains_function = "contains(#{delimited_list(arguments_for_contains_function)})"

    template = add_predicate(absolute, contains_function)
    return template.gsub( /:::(.*?):::/ ) { '#{'+$1+'}' }.gsub('"', '\"')
  end
  
  # Generate an xpath of the chosen +type+ for the given Term.
  # @param [OM::XML::Term] term that you want to generate relative xpath for
  # @param [Symbol] the type of xpath to generate, :relative, :abolute, or :constrained
  def self.generate_xpath(term, type)
    case type
    when :relative
      self.generate_relative_xpath(term)
    when :absolute
      self.generate_absolute_xpath(term)
    when :constrained
      self.generate_constrained_xpath(term)
    end
  end
  
  # Use the given +terminology+ to generate an xpath with (optional) node indexes for each of the term pointers.
  # Ex.  OM::XML::TermXpathGenerator.xpath_with_indexes(my_terminology, {:conference=>0}, {:role=>1}, :text ) 
  #      will yield an xpath similar to this: '//oxns:name[@type="conference"][1]/oxns:role[2]/oxns:roleTerm[@type="text"]'
  # @param [OM::XML::Terminology] terminology to generate xpath based on
  # @param [String -- OM term pointer] pointers identifying the node to generate xpath for
  def self.generate_xpath_with_indexes(terminology, *pointers)
    if pointers.first.nil?
      root_term = terminology.root_terms.first
      if root_term.nil?
        return "/"
      else
        return root_term.xpath
      end
    end
    
    query_constraints = nil
    
    if pointers.length > 1 && pointers.last.kind_of?(Hash)
      constraints = pointers.pop
      unless constraints.empty?
        query_constraints = constraints
      end 
    end

    if pointers.length == 1 && pointers.first.instance_of?(String)
      return xpath_query = pointers.first
    end
      
    # if pointers.first.kind_of?(String)
    #   return pointers.first
    # end
    
    keys = []
    xpath = "//"

    pointers = OM.destringify(pointers)
    pointers.each_with_index do |pointer, pointer_index|
      
      if pointer.kind_of?(Hash)
        k = pointer.keys.first
        index = pointer[k]
      else
        k = pointer
        index = nil
      end
      
      keys << k
      
      term = terminology.retrieve_term(*keys)  
      # Return nil if there is no term to work with
      if term.nil? then return nil end
      
      # If we've encountered a NamedTermProxy, insert path sections corresponding to 
      # terms corresponding to each entry in its proxy_pointer rather than just the final term that it points to.
      if term.kind_of? OM::XML::NamedTermProxy
        current_location = term.parent.nil? ? term.terminology : term.parent
        relative_path = ""
        term.proxy_pointer.each_with_index do |proxy_pointer, proxy_pointer_index|
          proxy_term = current_location.retrieve_term(proxy_pointer)
          proxy_relative_path = proxy_term.xpath_relative
          if proxy_pointer_index > 0
            proxy_relative_path = "/"+proxy_relative_path
          end
          relative_path << proxy_relative_path
          current_location = proxy_term
        end
      else  
        relative_path = term.xpath_relative
      
        unless index.nil?
          relative_path = add_node_index_predicate(relative_path, index)
        end
      end
      
      if pointer_index > 0
        relative_path = "/"+relative_path
      end
      xpath << relative_path 
    end
      
    final_term = terminology.retrieve_term(*keys) 
    
    if query_constraints.kind_of?(Hash)
      contains_functions = []
      query_constraints.each_pair do |k,v|
        if k.instance_of?(Symbol)
          constraint_path = final_term.children[k].xpath_relative
        else
          constraint_path = k
        end
        contains_functions << "contains(#{constraint_path}, \"#{v}\")"
      end
      
      xpath = add_predicate(xpath, delimited_list(contains_functions, " and ") )
    end
    
    return xpath
  end
  
  # Turns an Array into a String containing values separated by a delimiter.  Defaults to comma as a delimiter.
  # @param [Array] values_array to convert
  # @param [String] delimiter.  Default: ", "
  def self.delimited_list( values_array, delimiter=", ")
    result = values_array.collect{|a| a + delimiter}.to_s.chomp(delimiter)
  end
  
  # Adds xpath xpath node index predicate to the end of your xpath query
  # Example: 
  # add_node_index_predicate("//oxns:titleInfo",0)
  #   => "//oxns:titleInfo[1]"
  #
  # add_node_index_predicate("//oxns:titleInfo[@lang=\"finnish\"]",0)
  #   => "//oxns:titleInfo[@lang=\"finnish\"][1]"
  def self.add_node_index_predicate(xpath_query, array_index_value)
    modified_query = xpath_query.dup
    modified_query << "[#{array_index_value + 1}]"
  end
  
  # Adds xpath:position() method call to the end of your xpath query
  # Examples: 
  #
  # add_position_predicate("//oxns:titleInfo",0)
  # => "//oxns:titleInfo[position()=1]"
  #
  # add_position_predicate("//oxns:titleInfo[@lang=\"finnish\"]",0)
  # => "//oxns:titleInfo[@lang=\"finnish\" and position()=1]"
  def self.add_position_predicate(xpath_query, array_index_value)
    position_function = "position()=#{array_index_value + 1}"
    self.add_predicate(xpath_query, position_function)
  end
  
  def self.add_predicate(xpath_query, predicate)
    modified_query = xpath_query.dup
    # if xpath_query.include?("]")
    if xpath_query[xpath_query.length-1..xpath_query.length] == "]"
      modified_query.insert(xpath_query.rindex("]"), " and #{predicate}")
    else
      modified_query << "[#{predicate}]"
    end
    return modified_query
  end

end