#    concept.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 CSemanticConcept
  attr_reader :semanticID, :name
  
  @@linkstore = nil  if !defined?(@@linkstore)
  
  def CSemanticConcept.linkstore=(lstore)
    raise "Parameter must be a LinkStore object."  if lstore.class != LinkStore
    @@linkstore = lstore
  end

  def CSemanticConcept.linkstore
    @@linkstore
  end
  
  def initialize(cid, cname)
    @semanticID, @name = cid.to_s, cname
  end

  def ==(other)
    true == other.kind_of?(CSemanticConcept) && @semanticID == other.semanticID
  end
  
  def to_s(showDetails=false)
    return "#{@name}"  if !showDetails
    "(c##{@semanticID} '#{@name}' "  +
    "(#{forwardLinks.join(' ')}) " + 
    "(#{backwardLinks.join(' ')})"  +
    ")"
  end
  
  def inspect(showDetails=false)
    return "#c#{@semanticID}"  if !showDetails
    
    puts "[Concept #{@semanticID}: '#{@name}'"
    if forwardLinks
      puts " Forward Links:"
      forwardLinks.each {|link| puts "  #{link.to_s(true)}"}
    end
    if  backwardLinks
      puts " Backward Links:"
      backwardLinks.each {|link| puts "  #{link.to_s(true)}"}
    end
    puts "]"
  end
  
  def forwardLinks
    links = @@linkstore.forwards[@semanticID]
    links  ? links.collect {|x| @@linkstore.fetch(x)} : []
  end
  
  def backwardLinks  
    links = @@linkstore.backwards[@semanticID]
    links  ? links.collect {|x| @@linkstore.fetch(x)} : []
  end
 
end


module ConceptMapMethods
  # Common methods to be included into all ConceptMap classes
  
  def size
    @name2id.size
  end
  
  def isIn?(cname)
    @name2id[cname] != nil
  end

  def index(cname)
    isIn?(cname)  ?  @name2id[cname] : nil
  end
  
  def name(cid)
    @id2name[cid.to_s]
  end
  
  def newConcept(cid, cname)
    cid = newID  if cid.nil?			# generate SemanticID if needed
    raise "ConceptID #{cid} is already taken."  if name(cid)
    c = CSemanticConcept.new(cid, cname)
    store(c)
  end
    
  def store(c)
    raise "Argument must be a CSemanticConcept."  if c.class != CSemanticConcept
    raise "Concept '#{c.name}' is already in ConceptMap"  if isIn?(c.name)
    @name2id[c.name] = c.semanticID
    @id2name[c.semanticID] = c.name
    return c
  end

  def fetch(cname)
    # Retrieves a concept by name
    cid = @name2id[cname]
    CSemanticConcept.new(cid, cname)  if cid
  end

  def fetchByIndex(term)
    # Retrieves a concept by Semantic ID
    cid = term.to_s
    cname = @id2name[cid]
    CSemanticConcept.new(cid, cname)  if cname
  end

  def delete(term)
    c = fetch(term)
    return nil  if c.nil?
    cid = c.semanticID
    if @linkstore.relations[cid] || @linkstore.forwards[cid] || @linkstore.backwards[cid]
      raise "Cannot delete concept because it is still referenced by one or more assertions."
    end
    @name2id.delete(c.name)
    @id2name.delete(cid)
    return c
  end
  
  def deleteByIndex(term)
    c = fetchByIndex(term)
    return nil  if c.nil?
    cid = c.semanticID
    if @linkstore.relations[cid] || @linkstore.forwards[cid] || @linkstore.backwards[cid]
      raise "Cannot delete concept because it is still referenced by one or more assertions."
    end
    @name2id.delete(c.name)
    @id2name.delete(cid)
    return c
  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 = @id2name.keys.max {|a,b| a.to_i <=> b.to_i}.to_i
  end

end	# ConceptMapMethods


class ConceptMapPersistent
  include ConceptMapMethods
  require 'sdbm'

  DBDIR = "./db"
  C2ID_MAPFILE = "#{DBDIR}/concept2id"
  ID2C_MAPFILE = "#{DBDIR}/id2concept"

  attr_reader :name2id, :id2name
 
  def initialize(namesFile=C2ID_MAPFILE, indexFile=ID2C_MAPFILE)
    @name2id = SDBM.open(namesFile)
    @id2name = SDBM.open(indexFile)
    @linkstore = CSemanticConcept.linkstore || raise("Pointer to LinkStore was assigned a null value.")
    initialize_maxID
  end
 
  def close
    @name2id.close
    @id2name.close
  end

end

class ConceptMapInMemory
  # Maps a concept index (CSemanticID) to a concept object (CSemanticConcept).
  # This is the in-memory version, which limits the size of your ConceptMap to how much
  #  system memory is available.
  
  include ConceptMapMethods

  attr_reader :name2id, :id2name
  
  def initialize
    @name2id = {}
    @id2name = {}
    @linkstore = CSemanticConcept.linkstore || raise("Pointer to LinkStore was assigned a null value.")
    initialize_maxID
  end

  def close
  end
  
end

