#    link.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
#


class CSemanticLink
  # This version gives access to the actual CSemanticConcepts
  
  attr_reader :semanticID, :rel, :src, :dest

  @@conceptmap = nil  if !defined?(@@conceptmap)
  
  def CSemanticLink.conceptmap=(cmap)
    @@conceptmap = cmap
  end
  
  def initialize(id, rel, src, dest)
    raise "Pointer to ConceptMap object must be assigned first."  if @@conceptmap.nil?
    @semanticID, @rel, @src, @dest  = id.to_s, rel.to_s, src.to_s, dest.to_s
  end
  
  def relation
    @@conceptmap.fetchByIndex(@rel)  if @rel
  end
  
  def source
    @@conceptmap.fetchByIndex(@src)  if @src
  end
  
  def destination
    @@conceptmap.fetchByIndex(@dest)  if @dest
  end
  
  def to_s(showNames=false)
    # NOTE: Do not change this output format without making corresponding changes to LinkStore.store() and fetch()
    if showNames
      cmap = @@conceptmap
      "(Link#{@semanticID} '#{cmap.name(@rel)}' '#{cmap.name(@src)}' '#{cmap.name(@dest)}')"
    else
      "(#{@semanticID} #{@rel} #{@src} #{@dest})"
    end
  end

  def inspect
    "Link#{@semanticID}"
  end
  
end


class LinkStore
  attr_reader :linksdb, :relationsdb, :forwardsdb, :backwardsdb, :relations, :forwards, :backwards
  require 'sdbm'
  require 'pstore'
  
  DBDIR = "./db"
  LINKS_DB = "#{DBDIR}/predicates"
  RELATIONS_DB = "#{DBDIR}/relationLinks"
  FORWARDS_DB = "#{DBDIR}/forwardLinks"
  BACKWARDS_DB = "#{DBDIR}/backwardLinks"
  
  def initialize(linksFile=LINKS_DB, relationsFile=RELATIONS_DB, forwardsFile=FORWARDS_DB, backwardsFile=BACKWARDS_DB)
    @linksdb = SDBM.open(linksFile)
    initialize_maxID
    
    puts "Loading relations into memory..."
    @relations = {}
    @relationsdb = PStore.new(relationsFile)
    @relationsdb.transaction do |db|
      db.roots.each {|key| @relations[key] = db[key]}
    end
    
    puts "Loading forward Links into memory..."
    @forwards = {}
    @forwardsdb = PStore.new(forwardsFile)
    @forwardsdb.transaction do |db|
      db.roots.each {|key| @forwards[key] = db[key]}
    end 

    puts "Loading backward Links into memory..."
    @backwards = {}
    @backwardsdb = PStore.new(backwardsFile)
    @backwardsdb.transaction do |db|
      db.roots.each {|key| @backwards[key] = db[key]}
    end 
    return true
  end
  
  def save
    puts "Flushing data into relations database..."
    @relationsdb.transaction do |db|
      @relations.each {|key, val|  db[key] = val}
    end
    puts "Flushing data into ForwardLinks database..."
    @forwardsdb.transaction do |db|
      @forwards.each {|key, val| db[key] = @forwards[key]}
    end
    puts "Flushing data into BackwardLinks database..."
    @backwardsdb.transaction do |db|
      @backwards.each {|key, val| db[key] = @backwards[key]}
    end 
  end
  
  def close
    @linksdb.close
    @relationsdb.close
    @forwardsdb.close
    @backwardsdb.close
  end

  def inspect
    # This overrides the default behavior of showing all the LinkStore elements (huge!)
    "<LinkStore_#{id}>"
  end
  
  def size
    @linksdb.size
  end
  
  def newLink(linkId, relId, srcId, destId)
    linkId = newID  if linkId.nil?	# generate SemanticID if needed
    link = CSemanticLink.new(linkId, relId, srcId, destId)
    store(link)
  end

  def store(link)
    # Stores the link into the LinkStore, first verifying that the referenced concepts exist
    raise "Argument must be a CSemanticLink."  if link.class != CSemanticLink
    raise "LinkID #{link.semanticID} is already taken."  if @linksdb[link.semanticID]
    raise "ConceptID #{link.rel} is not defined."  if !link.relation
    raise "ConceptID #{link.src} is not defined."  if !link.source
    raise "ConceptID #{link.dest} is not defined."  if !link.destination
    @linksdb[link.semanticID] = link.to_s
    if @relations[link.rel]
      @relations[link.rel] << link.semanticID
    else
      @relations[link.rel] = [link.semanticID]
    end
    if @forwards[link.src]
      @forwards[link.src] << link.semanticID
    else
      @forwards[link.src] = [link.semanticID]
    end
    if @backwards[link.dest]
      @backwards[link.dest] << link.semanticID
    else
      @backwards[link.dest] = [link.semanticID]
    end
    return link
  end

  def fetch(linkId)
    # Returns the requested CSemanticLink object.  To minimize type conversions, we
    # allow 'linkId' to be  Integer or String.
    buf = @linksdb[linkId.to_s]		# format: "(linkId relId srcId destId)"
    if buf
      arr = buf.delete("()").split
      CSemanticLink.new(*arr)
    end
  end

  def delete(linkId)
    linkId = linkId.to_s
    link = fetch(linkId)
    if link
      @linksdb.delete(linkId)
      @relations[link.rel].delete(linkId)
      @forwards[link.src].delete(linkId)
      @backwards[link.dest].delete(linkId)
    end
    return link
  end

  # Helper Functions

  def newID
    @maxID += 1
    @maxID.to_s
  end

  def initialize_maxID
    # Note: This may get unacceptably slow if the number of concepts stored gets too large
    @maxID = @linksdb.keys.max {|a,b| a.to_i <=> b.to_i}.to_i
  end

end

