#    rubycon.rb
#    Modified: 8-10-04
#
#    Copyright (c) 2004, Ben Bongalon (ben@enablix.com)
#
#    This file is part of RubyCon, a software toolkit for building concept processing and 
#     other intelligent reasoning systems.
#
#    RubyCon is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    RubyCon is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with RubyCon; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

require 'concept'
require 'link'


# Global lexical resources
#   Note: This has to be in this order because ConceptMap needs CSemanticConcept.linkstore
#             to be defined first. 
Public_LStore = LinkStore.new  if !defined?(Public_LStore)
CSemanticConcept.linkstore = Public_LStore
Public_CMap = ConceptMapPersistent.new  if !defined?(Public_CMap)
CSemanticLink.conceptmap = Public_CMap


class String
  def match(other)
    # This is called by the RubyCon.match() pattern-matching operator
    if other.kind_of?(String)
      self == other
    elsif other.kind_of?(Regexp)
      other.match(self) != nil
    else
      raise "Don't know how to handle this object type."
    end
  end
end


class RubyCon
  attr_reader :conceptmap, :linkstore, :config
  
  def inspect
    "<RubyCon: #{id}>"
  end
  
  def initialize(predFile=nil)
    @conceptmap = Public_CMap
    @linkstore = Public_LStore
    @config = CFindPathConfig.new
    return true
  end

  def save
    @linkstore.save
  end
  
  # Concept methods
  
  def newConcept(cname)
    @conceptmap.newConcept(nil, cname)
  end
  alias create newConcept
  
  def fetch(cname)
    @conceptmap.fetch(cname)
  end

  def delete(cname)
    @conceptmap.delete(cname)
  end
  
  # Link methods
  
  def assert(pred)
    raise "Bad predicate format."  if pred.class != Array || pred.size < 3
    s_rel, s_src, s_dest, rest = pred
    if s_rel.class != String || s_src.class != String || s_dest.class != String
      raise "Terms of the predicate must be String objects."
    end
    rel = fetch(s_rel) || create(s_rel)
    src = fetch(s_src) || create(s_src)
    dest = fetch(s_dest) || create(s_dest)
    @linkstore.newLink(nil, rel.semanticID, src.semanticID, dest.semanticID)
  end

  def retract(pred)
    raise "Bad predicate format."  if pred.class != Array || pred.size < 3
    s_rel, s_src, s_dest, rest = pred
    if s_rel.class != String || s_src.class != String || s_dest.class != String
      raise "Arguments must be String objects."
    end
    rslts = match(pred)
    if rslts.size > 1
      raise "Duplicate assertion detected."
    elsif rslts.size == 1
      link = rslts.first
      @linkstore.delete(link.semanticID)
    end
  end
  
  def match(pred)
    # Find the assertions that match the given patterns. Each argument can be a String or Regexp
    raise "Bad predicate format."  if pred.class != Array || pred.size < 3
    s_rel, s_src, s_dest, rest = pred
    if s_rel.class == String
      rel = fetch(s_rel)  || raise("Concept '#{s_rel}' is not defined.")
    end
    if s_src.class == String
     src = fetch(s_src)   || raise("Concept '#{s_src}' is not defined.")
    end
    if s_dest.class == String
      dest = fetch(s_dest)   || raise("Concept '#{s_dest}' is not defined.")
    end
    rslts = if src && s_dest && s_rel
      src.forwardLinks.select {|link| link.destination.name.match(s_dest) &&
					   link.relation.name.match(s_rel)}
    elsif src && s_rel
      src.forwardLinks.select {|link| link.relation.name.match(s_rel)}
    elsif src && s_dest
      src.forwardLinks.select {|link| link.destination.name.match(s_dest)}
    elsif src
      src.forwardLinks
    elsif dest && s_src && s_rel
      dest.backwardLinks.select {|link| link.source.name.match(s_src) &&
					   link.relation.name.match(s_rel)}
    elsif dest && s_rel
      dest.backwardLinks.select {|link| link.relation.name.match(s_rel)}
    elsif dest && s_src
      dest.backwardLinks.select {|link| link.source.name.match(s_src)}
    elsif dest
      dest.backwardLinks
    elsif rel
      raise "Not implemented."
    else
      raise "Bad match() arguments."
    end
    if rslts
      rslts.each {|link| puts link.to_s(true)}
    end
    rslts  
  end

  # Utility methods

  def import(predFile)
    # Reads a textfile containing assertions and saves them into RubyCon's internal databases
    #   NOTE: Before importing, make sure you are in the "rubycon" folder.
    nAsserts = 0
    puts "Reading assertions file '#{predFile}'"
    File.foreach(predFile) do |predStr|
      m = /\((\w+) "(.+)" "(.+)"\)/.match(predStr)
      if m
	print "."  if nAsserts % 1000 == 1
	pred = m[1..3]
	assert(pred)
	nAsserts += 1
      end #if (m)
    end
    puts "\nSaving assertions to RubyCon databases..."
    save
    puts "Done. #{nAsserts} assertions were imported."
  end
  
  def findPath(s_src, s_dest, maxDepth=2)
    src = fetch(s_src)   || raise("Source concept '#{s_src}' not found.")
    dest = fetch(s_dest)   || raise("Destination concept '#{s_dest}' not found.")
    rslts = depthSearch(src, dest, maxDepth)
    # Show results
    if rslts
      puts "\nRESULTS"
      rslts.each_with_index {|path, i|
	puts "Path #{i+1}:"
	path.each {|link| puts "  #{link.to_s(true)}"}
      }
    end
    return rslts
  end

  def depthSearch(init, goal, maxDepth)
    @config.reset
    recurseDepthSearch(init, goal, maxDepth)
    puts "Num nodes visited: #{@config.numNodeVisits}"
    puts "Num results: #{@config.results.size}\n\n"
    @config.results
  end

  def recurseDepthSearch(curr, goal, maxDepth)
    @config.numNodeVisits += 1
    if curr.semanticID == goal.semanticID
      # Save a copy of the current Path in reults, because subsequent steps mutate the current Path.
      @config.results.push( @config.currentPath.clone )
    elsif @config.currentPath.length >= maxDepth || curr.forwardLinks.nil?
      nil
    else
      curr.forwardLinks.each {|link|
	if @config.inCurrentPath?(link)
	  puts "DBG: Loop detected in currentPath: #{@config.currentPath}"
	  next
	else
	  @config.visitLink(link)
	  dest = link.destination
	  rslt = recurseDepthSearch(dest, goal, maxDepth)  if dest
	  @config.leaveLink
	end
      }
    end   # if curr == goal
  end

end


class CFindContextConfig
  attr_reader :resultsList
  
  @@discountFactor = 0.5
  @@maxNodeVisits = 1000	# Max number of concepts to be visited during searching
  @@maxNodeResults = 200	# Max number of search results to return

  def initialize
    @curNodeVisits = 0
    @resultsList = []			# List of search results
    @d_scoreQueue = []		# Processing queue of scored semantic concepts
  end
  
  def allowVisit?
    @curNodeVisits < @@maxNodeVisits
  end

  #  for GetNumResults' use @resultList.size
  #  for GetResult(indx, *score) use @resultList[indx] w/c returns [concept, score] pair

  def addNodeToResults(concept, score)
    # Appends a concept to the search results list
    if @resultList.size < @@maxNodeResults
      @resultsList << [concept, score]
    else
	false
    end
  end
  
  def queueScore(concept, score)
    @d_queueScore << [concept, score]
  end
  
  def getScoreQueue
    # Gets the first available scored concept from the processing queue, removing it from the queue
    @d_queueScore.each_with_index {|i, csPair|
      concept, score = csPair
      if score
	@d_queueScore.delete_at(i)
	return csPair
      end
    }
  end
  
end  # RubyCon class


class CFindPathConfig
  attr_reader :currentPath, :pathQueue, :results
  attr_accessor :numNodeVisits
  
  @@maxNodeVisits  = 50000
  @@maxNodeResults = 200
  @@maxDepth = 5
  
  def initialize
    reset
  end
  
  def reset
    @numNodeVisits = 0
    @curNodeDepth = 0
    @currentPath = []		# list of CSemanticLinks
    @pathQueue = []
    @results = []
  end
  
  def allowVisit?
    # Gets whether an additional semantic concept may be visited during searching
    (@numNodeVisits <= @@maxNodeVisits) && (@results.size <= @@maxNodeResults)
  end

  def visitLink(link)
    raise "Argument must be a CSemanticLink"  if link.class != CSemanticLink
    @currentPath.push(link)
  end
  
  def leaveLink
    @currentPath.pop
  end
  
  def inCurrentPath?(link)
    @currentPath.include?(link)
  end
  
end


