# nmzdoc.rb - libraries for making documents of Namazu client
# $Id: nmzdoc.rb,v 1.4 2000/12/03 16:21:27 ryu Exp $
#
# nmzdoc.rb is copyrighted free software by OHSHIMA Ryunosuke.
# You can use/redistribute/modify it under the terms of Namazu or Ruby.


class Namazu
  module DocumentHash
  end # DocumentHash

  Document = Hash
end

class Hash
  include Namazu::DocumentHash
end

class Namazu
  module SummaryArray
    attr_accessor(:querystring, :hitnum, :hitref, :whence, :maxlength)

    public
    def get_whence(whence = nil)
      newwhence = 0
      (newwhence = whence) if (whence and (whence > newwhence))
      newwhence
    end

    public
    def get_maxlength(maxlength = nil)
      newmaxlength = length() - whence()
      (newmaxlength = 0) if (newmaxlength < 0)
      (newmaxlength = maxlength) if (maxlength and (maxlength < newmaxlength))
      newmaxlength
    end

    public
    def get(whence, maxlength)
      whence = get_whence(whence)
      maxlength = get_maxlength(maxlength)
      if ((whence == 0) and (maxlength == length()))
	self
      else
	summary = self.class().new(maxlength)
	summary[whence, maxlength] = self[whence, maxlength]
	summary.querystring = querystring()
	summary.hitnum = hitnum()
	summary.hitref = hitref()
	summary.whence = whence
	summary.maxlength = maxlength
	summary
      end
    end
  end # SummaryArray

  Summary = Array
end

class Array
  include Namazu::SummaryArray
end

class Namazu
  class Config
    def initialize(confname = nil)
      @config = {}
      unless confname
	tmpconfname = []
	tmpconfname.push(File.join(ENV['HOME'], '.namazurc')) if ENV['HOME']
	tmpconfname.push(ENV['NAMAZURC']) if ENV['NAMAZURC']
	tmpconfname.push(File.join('', 'usr', 'local', 'etc', 'namazurc', 'namazurc'))
	for confname in tmpconfname
	  break if FileTest.file?(confname)
	end
      end
      if FileTest.readable?(confname)
	open(confname) do |conffile|
	  conffile.each_line() do |line|
	    cindex = line.index(/\s*\#/)
	    if cindex
	      line[cindex .. line.length() - 1] = ''
	    end
	    if /REPLACE\s+(\S+)\s+(\S+)/i =~ line
	      @config['replace'] = [] unless @config['replace']
	      @config['replace'].push([$1, $2])
	    elsif /(\w+)\s+(.+)/ =~ line
	      @config[$1.downcase()] = $2
	    end
	  end
	end
      end
    end
    attr_reader(:config)

    public
    def show_config()
      @config.each() do |name, value|
	if value.kind_of?(Array)
	  value.each() do |val|
	    if val.kind_of?(Array)
	      puts(name.capitalize() + "\t: " + val.join("\t"))
	    else
	      puts(name.capitalize() + "\t: " + val)
	    end
	  end
	else
	  puts(name.capitalize() + "\t: " + value)
	end
      end
    end

    public
    def [](arg)
      @config[arg]
    end
  end # Config

  class Summarizer
    def initialize(indices, options = {})
      @indices = indices
      @options = options
      @summary = nil
    end
    attr_reader(:summary)

    public
    def get_whence(whence = nil)
      if whence
	@summary.get_whence(whence)
      elsif @options['whence']
	@summary.get_whence(@options['whence'].to_i())
      else
	@summary.get_whence()
      end
    end

    public
    def get_maxlength(maxlength = nil)
      if maxlength
	@summary.get_maxlength(maxlength)
      elsif @options['max']
	@summary.get_maxlength(@options['max'].to_i())
      else
	@summary.get_maxlength()
      end
    end

    public
    def query(querystring)
      summary = nil
      if querystring
	results = @indices.collect() do |index|
	  index.query(querystring)
	end
	if results.find() do |result| result end
	  summary = []
	  summary.querystring = querystring
	  summary.hitref = results.collect() do |result|
	    result ? result.to_s() : ' (no result) '
	  end . join(' ::: ')
	  index = doc = docid = nmzfieldall = nil
	  results.each_with_index() do |result, i|
	    next unless result
	    index = @indices[i]
	    nmzfieldall = index.fieldall()
	    nmzfieldall.open()
	    result.scoreresult().each() do |docid, scoreresultitem|
	      doc = {}
	      doc['docid'] = docid
	      doc['score'] = scoreresultitem[0]
	      doc['time'] = scoreresultitem[1]
	      doc.update(nmzfieldall.get(docid))
	      summary.push(doc)
	    end
	    nmzfieldall.close()
	  end
	  summary.hitnum = summary.length()
	  if @options['whence']
	    summary.whence = summary.get_whence(@options['whence'].to_i())
	  else
	    summary.whence = summary.get_whence()
	  end
	  if @options['max']
	    summary.maxlength = summary.get_maxlength(@options['max'].to_i())
	  else
	    summary.maxlength = summary.get_maxlength()
	  end
	end
      end
      @summary = summary
    end

    alias summarize query

    public
    def sort!(options = nil)
      if options
	@options = options
      end
      summary = @summary
      summary.sort!() { |a, b| b['score'] <=> a['score'] }
      summary.sort!() { |a, b| b['time'] <=> a['time'] } if @options['late']
      summary.sort!() { |a, b| a['time'] <=> b['time'] } if @options['early']
      if @options['sort']
	if (/date/i =~ @options['sort'])
	  summary.sort!() { |a, b| b['time'] <=> a['time'] }
	  summary.reverse!() if (/early/i =~ @options['sort'])
	elsif /field:([^:]*)(:.*)?/i =~ @options['sort']
	  field = $1
	  if (field == 'size')
	    summary.sort!() { |a, b| b['size'].to_i() <=> a['size'].to_i() }
	  else
	    summary.sort!() { |a, b| b[field] <=> a[field] }
	  end
	end
	summary.reverse!() if (/ascending/i =~ @options['sort'])
      end
      summary.reverse!() if @options['ascending']
      summary
    end
  end # Summarizer

  class Formatter
    def initialize(indices, options = {}, config = {})
      @indices = indices
      @options = options
      @config = config

      if @config['template']
	@templatedirname = @config['template']
	@templatedirname += File::Separator unless @templatedirname[-1] == File::Separator[0]
      else
	@templatedirname = @indices[0].dirname()
      end
    end

    private
    def read_template(templatename, querystring = nil)
      template = nil
      open(@templatedirname + templatename) do |file|
	template = file.read()
      end
      template.gsub!(/<form.*<\/form>/im, '') unless @options['form']
      template.gsub!(/\{cgi\}/i, File.basename($0))
      template.gsub!(/\$\{(namazu::)?([^\}]+)\}/, '#{\2}')
      if querystring
	querystring = querystring.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
	template.gsub!(/(<input[^>]+name="query"[^>]*)>/im,
			  '\1 value="'+ querystring + '">')
	template.gsub!(/<\/title>/i, ': &lt;' + querystring + '&gt;\&')
      end
      @options.each() do |key, value|
	template.gsub!(/<select[^>]+name="#{key}".*?<\/select>/im) do
	  |selections|
	  selections.gsub!(/\s*selected\s*/im, ' ')
	  unless selections.gsub!(/(<option[^>]+value="#{value}"[^>]*)>/im,
				  '\1 selected>')
	    selections.sub!(/<\/select>/im,
			    '<option value="' + value.to_s() +
			    '" selected>' + value.to_s() + "\n" + '\&')
	  end
	  selections
	end
      end
      template
    end

    private
    def references2s(nmzsummary)
      result = ''
      result.concat("<h2>Results:</h2>\n\n")
      result.concat('<p>References: ')
      result.concat(nmzsummary.hitref())
      result.concat("</p>\n\n")
      result
    end

    private
    def hitnum2s(nmzsummary)
      result = ''
      if @options['count']
	result << '<strong><!-- HIT -->' << nmzsummary.hitnum().to_s() << "<!-- HIT --></strong>\n"
      elsif (not @options['list'])
	result << '<p><strong> Total <!-- HIT -->' << nmzsummary.hitnum().to_s() << "<!-- HIT --> documents matching your query.</strong></p>\n\n"
      end
      result
    end

    private
    def lists2s(nmzsummary)
      result = ''

      resulttemplate = nil
      if @options['list']
	resulttemplate = '<dt>#{uri}'
      else
	if @options['result']
	  resulttemplate = read_template('NMZ.result.' << @options['result'])
	elsif @options['short']
	  resulttemplate = read_template('NMZ.result.short')
	else
	  resulttemplate = read_template('NMZ.result.normal')
	end
      end

      tmpresult = nil
      (result << "<dl>\n") if @options['html']
      counter = nmzsummary.whence() + 1
      uri = title = score = i = author = date = summary = size = nil
      for listno in (1 .. nmzsummary.maxlength())
	uri = nmzsummary[counter - 1]['uri']
	if @options['decode-uri']
	  uri.tr!('+', ' ')
	  uri.gsub!(/((?:%[0-9a-fA-F]{2})+)/n) do
	    [$1.delete('%')].pack('H*')
	  end
	end
	if (@options['replace'] and @config['replace'])
	  @config['replace'].each() do |value|
	    uri.sub!(*value)
	  end
	end
	title = nmzsummary[counter - 1]['subject']
	score = nmzsummary[counter - 1]['score'].to_i().to_s()
	i = (score.length() - 1) % 3 + 1
	while (i < score.length())
	  score[i, 0] = ','
	  i += 4
	end
	author = nmzsummary[counter - 1]['from']
	date = nmzsummary[counter - 1]['date']
	summary = nmzsummary[counter - 1]['summary']
	size = nmzsummary[counter - 1]['size']
	i = (size.length() - 1) % 3 + 1
	while (i < size.length())
	  size[i, 0] = ','
	  i += 4
	end
	eval("tmpresult = %Q\x1f" + resulttemplate + "\n\x1f")
	result << tmpresult
	counter += 1
      end
      (result << "</dl>\n") if @options['html']
      result
    end

    private
    def listsummary2s(nmzsummary)
      result = ''
      startlistno = nmzsummary.whence() + 1
      endlistno = nmzsummary.whence() + nmzsummary.maxlength()
      result << '<p><strong>Current List: ' << startlistno.to_s() << ' - ' <<
	endlistno.to_s() << "</strong><br></p>\n"
      result
    end

    private
    def page2s(nmzsummary)
      result = ''
      unless @options['max']
	result << "<p><strong>Page:</strong> <strong>[1]</strong><br></p>\n"
      else
	result << "<p><strong>Page:</strong>\n"

	tmpoptions = @options.dup()
	querystring = nmzsummary.querystring().gsub(/([^ a-zA-Z0-9_.-]+)/n) do
	  '%' << $1.unpack('H2' * $1.size).join('%').upcase()
	end
	querystring.tr!(' ', '+')

	startindex = nmzsummary.whence()
	step = @options['max'].to_i()
	i = startindex
	while (i > 0)
	  i -= step
	end
	if (i < startindex)
	  tmpoptions['whence'] = startindex - step
	  result << ' <a href="' << File.basename($0) <<
	    '?query=' << querystring << '&amp;'
	  result << tmpoptions.collect() do |opt|
	    opt.join('=')
	  end . join('&amp;') << '">[prev]</a>' << "\n"
	end
	counter = 1
	while (i < nmzsummary.hitnum())
	  if (i == startindex)
	    result << ' <strong>[' << counter.to_s() << ']</strong>' << "\n"
	  else
	    tmpoptions['whence'] = i
	    result << ' <a href="' << File.basename($0) <<
	      '?query=' << querystring << '&amp;'
	    result << tmpoptions.collect() do |opt|
	      opt.join('=')
	    end . join('&amp;') << '">[' << counter.to_s() << ']</a>' << "\n"
	  end
	  i += step
	  counter += 1
	end
	if (i > (startindex + step))
	  tmpoptions['whence'] = startindex + step
	  result << ' <a href="' << File.basename($0) <<
	    '?query=' << querystring << '&amp;'
	  result << tmpoptions.collect() do |opt|
	    opt.join('=')
	  end . join('&amp;') << '">[next]</a>' << "\n"
	end
	result << "<br></p>\n"
      end
      result
    end

    private
    def html2text(html)
      text = html.dup()
      text.gsub!(/<!--.*?-->/im, '')
      text.gsub!(/<.*?>/im, '')
      text.gsub!(/\n{3,}/im, "\n\n")
      text
    end

    public
    def format(nmzsummary = nil)
      result = ''

      result.concat(read_template('NMZ.head', (nmzsummary ? nmzsummary.querystring() : nil))) if @options['html']

      unless nmzsummary
	result.concat(read_template('NMZ.body'))
      else
	if (@options['references'] and
	    (not @options['list']) and (not @options['count']))
	  result.concat(references2s(nmzsummary))
	end
	result.concat(hitnum2s(nmzsummary))
	unless @options['count']
	  unless (nmzsummary.hitnum() > 0)
	    result.concat(read_template('NMZ.tips', nmzsummary.querystring()))
	  else
	    result.concat(lists2s(nmzsummary))
	    result.concat(listsummary2s(nmzsummary)) unless @options['list']
	    result.concat(page2s(nmzsummary)) if @options['page']
	  end
	end
      end

      result.concat(read_template('NMZ.foot', (nmzsummary ? nmzsummary.querystring() : nil))) if @options['html']

      result = html2text(result) unless @options['html']

      result
    end
  end # Formatter

  class CGIFormatter < Formatter
    def initialize(indices, options = {}, config = {})
      @indices = indices
      cgioptions = [
	'max', 'whence', 'list', 'short', 'result', 'late', 'early',
	'sort', 'ascending', 'html', 'references', 'page', 'form',
	'replace', 'decode-uri',
      ]
      options.delete_if() do |key, value| not cgioptions.include?(key) end
      options.each() do |key, value|
	next unless value.kind_of?(String)
	options[key] = value.to_i() if /^[\-\+]*\d+$/ =~ value
	options[key] = true if /^(true|on)$/i =~ value
	options[key] = false if /^(false|off)$/i =~ value
	options[key] = nil if /^nil$/i =~ value
      end
      @options = options

      @config = config
      if @config['template']
	@templatedirname = @config['template']
	@templatedirname += File::Separator unless @templatedirname[-1] == File::Separator[0]
      else
	@templatedirname = @indices[0].dirname()
      end
    end

    public
    def format(nmzsummary = nil)
      result = "Content-Type: text/html\n\n"
      result.concat(super)
      result
    end
  end # CGIFormatter

  class XhtmlBasicCGIFormatter < CGIFormatter
    private
    def references2s(nmzsummary)
      result = ''
      result << nmzsummary.hitref()
      result << "<br>\n"
      result
    end

    private
    def hitnum2s(nmzsummary)
      result = ''
      if @options['count']
	result << nmzsummary.hitnum().to_s() << "\n"
      elsif (not @options['list'])
	result << 'Total <!-- HIT -->' << nmzsummary.hitnum().to_s() << "<!-- HIT --> hits.<br>\n"
      end
      result
    end

    private
    def listsummary2s(nmzsummary)
      result = ''
      result
    end

    private
    def page2s(nmzsummary)
      result = ''
      if @options['max']
	tmpoptions = @options.dup()
	querystring = nmzsummary.querystring().gsub(/([^ a-zA-Z0-9_.-]+)/n) do
	  '%' << $1.unpack('H2' * $1.size).join('%').upcase()
	end
	querystring.tr!(' ', '+')

	startindex = nmzsummary.whence()
	step = @options['max'].to_i()
	i = startindex
	while (i > 0)
	  i -= step
	end

	if (i < startindex)
	  tmpoptions['whence'] = startindex - step
	  result << '<a href="' << File.basename($0) <<
	    '?query=' << querystring << '&amp;'
	  result << tmpoptions.collect() do |opt|
	    opt.join('=')
	  end . join('&amp;') << '" accesskey="*">*&lt;=</a>' << "\n"
	end
	startlistno = nmzsummary.whence() + 1
	endlistno = nmzsummary.whence() + nmzsummary.maxlength()
	result << '[' << startlistno.to_s() << '-' << endlistno.to_s() << "]\n"
	while (i < nmzsummary.hitnum())
	  i += step
	end
	if (i > (startindex + step))
	  tmpoptions['whence'] = startindex + step
	  result << '<a href="' << File.basename($0) <<
	    '?query=' << querystring << '&amp;'
	  result << tmpoptions.collect() do |opt|
	    opt.join('=')
	  end . join('&amp;') << '" accesskey="#">=&gt;#</a>' << "\n"
	end
	result << "<br>\n"
      end
      result
    end

    public
    def format(nmzsummary = nil)
      result = "Content-Type: text/html\n\n"
      result.concat(read_template('NMZ.head', nmzsummary ? nmzsummary.querystring(): nil)) if @options['html']
      unless nmzsummary
	result.concat(read_template('NMZ.body'))
      else
	result.concat(hitnum2s(nmzsummary))
	unless (@options['count'])
	  unless (nmzsummary.hitnum() > 0)
	    result.concat(read_template('NMZ.tips', nmzsummary.querystring()))
	  else
	    result.concat(page2s(nmzsummary)) if @options['page']
	    result.concat(lists2s(nmzsummary))
	  end
	end
	if (@options['references'] and 
	    (not @options['list']) and (not @options['count']))
	  result.concat(references2s(nmzsummary))
	end
      end
      result.concat(read_template('NMZ.foot', nmzsummary ? nmzsummary.querystring() : nil)) if @options['html']
      result
    end
  end # XhtmlBasicCGIFormatter
end # Namazu
