module Starscope::Exportable

def cscope_mark(rec)

def cscope_mark(rec)
  case rec[:tbl]
  when :end
    case rec[:type]
    when :func
      ret = '}'
    else
      return ''
    end
  when :file
    ret = '@'
  when :defs
    ret = case rec[:type]
          when :func
            '$'
          when :class, :module
            'c'
          when :type
            't'
          else
            'g'
          end
  when :calls
    ret = '`'
  when :requires
    ret = '~"'
  when :imports
    ret = '~<'
  when :assigns
    ret = '='
  else
    return ''
  end
  "\t#{ret}"
end

def cscope_output(line, prev, offset, record)

def cscope_output(line, prev, offset, record)
  buf = ''
  buf << CSCOPE_GLOBAL_HACK_STOP if record[:type] == :func && record[:tbl] == :defs
  record[:name][0...-1].each do |key|
    # output previous components of the name (ie the Foo in Foo::bar) as unmarked symbols
    key = key.to_s.sub(/\W+$/, '')
    next if key.empty?
    index = line.index(key, prev)
    index = line.index(key, index + 1) while index && index + key.length < offset && !valid_index?(line, index, key)
    next unless index && index + key.length < offset
    buf << cscope_plaintext(line, prev, index) << "\n"
    buf << "#{strip_unicode(key)}\n"
    prev = index + key.length
  end
  buf << cscope_plaintext(line, prev, offset) << "\n"
  buf << cscope_mark(record) << strip_unicode(record[:key]) << "\n"
  buf << CSCOPE_GLOBAL_HACK_START if record[:type] == :func && record[:tbl] == :end
  buf
rescue ArgumentError
  # invalid utf-8 byte sequence in the line, oh well
  strip_unicode(line)
end

def cscope_plaintext(line, start, stop)

def cscope_plaintext(line, start, stop)
  ret = line.slice(start, stop - start)
  ret.lstrip! if start == 0
  ret.rstrip! if stop == line.length
  ret.gsub!(/\s+/, ' ')
  ret.empty? ? ' ' : strip_unicode(ret)
rescue ArgumentError
  # invalid utf-8 byte sequence in the line, oh well
  strip_unicode(line)
end

def ctag_ext_tags(rec, file)

def ctag_ext_tags(rec, file)
  tag = {}
  # these extensions are documented at http://ctags.sourceforge.net/FORMAT
  case rec[:type]
  when :func
    tag['kind'] = 'f'
  when :module, :class
    tag['kind'] = 'c'
  end
  tag['language'] = file[:lang]
  tag
end

def ctag_line(rec, file, path_prefix)

def ctag_line(rec, file, path_prefix)
  line = line_for_record(rec).gsub('/', '\/')
  path = File.join(path_prefix, rec[:file])
  ret = "#{rec[:name][-1]}\t#{path}\t/^#{line}$/"
  ext = ctag_ext_tags(rec, file)
  unless ext.empty?
    ret << ';"'
    ext.sort.each do |k, v|
      ret << "\t#{k}:#{v}"
    end
  end
  ret
end

def db_by_line

def db_by_line
  db = {}
  @tables.each do |tbl, records|
    records.each do |record|
      next unless record[:line_no]
      record[:tbl] = tbl
      db[record[:file]] ||= {}
      db[record[:file]][record[:line_no]] ||= []
      db[record[:file]][record[:line_no]] << record
    end
  end
  db
end

def export(format, path = nil)

def export(format, path = nil)
  case format
  when :ctags
    path ||= CTAGS_DEFAULT_PATH
  when :cscope
    path ||= CSCOPE_DEFAULT_PATH
  else
    raise UnknownExportFormatError
  end
  @output.normal("Exporting to '#{path}' in format '#{format}'...")
  path_prefix = Pathname.getwd.relative_path_from(Pathname.new(path).dirname.expand_path)
  File.open(path, 'wb') do |file|
    export_to(format, file, path_prefix)
  end
  @output.normal('Export complete.')
end

def export_cscope(file, _path_prefix)

ftp://ftp.eeng.dcu.ie/pub/ee454/cygwin/usr/share/doc/mlcscope-14.1.8/html/cscope.html
def export_cscope(file, _path_prefix)
  buf = ''
  files = []
  db_by_line.each do |filename, lines|
    next if lines.empty?
    buf << "\t@#{filename}\n\n"
    buf << "0 #{CSCOPE_GLOBAL_HACK_START}\n"
    files << filename
    func_count = 0
    lines.sort.each do |line_no, records|
      line = line_for_record(records.first)
      toks = tokenize_line(line, records)
      next if toks.empty?
      prev = 0
      buf << line_no.to_s << ' '
      toks.each do |offset, record|
        next if offset < prev # this probably indicates an extractor bug
        # Don't export nested functions, cscope barfs on them since C doesn't
        # have them at all. Skipping tokens is easy; since prev isn't updated
        # they get turned into plain text automatically.
        if record[:type] == :func
          case record[:tbl]
          when :defs
            func_count += 1
            next unless func_count == 1
          when :end
            func_count -= 1
            next unless func_count == 0
          end
        end
        buf << cscope_output(line, prev, offset, record)
        prev = offset + record[:key].length
      end
      buf << cscope_plaintext(line, prev, line.length) << "\n\n"
    end
  end
  buf << "\t@\n"
  header = "cscope 15 #{Dir.pwd} -c "
  offset = format("%010d\n", header.length + 11 + buf.bytes.count)
  file.print(header)
  file.print(offset)
  file.print(buf)
  file.print("#{@meta[:paths].length}\n")
  @meta[:paths].each { |p| file.print("#{p}\n") }
  file.print("0\n")
  file.print("#{files.length}\n")
  buf = ''
  files.each { |f| buf << "#{f}\n" }
  file.print("#{buf.length}\n#{buf}")
end

def export_ctags(file, path_prefix)

def export_ctags(file, path_prefix)
  file.puts <<~HEADER
    !_TAG_FILE_FORMAT	2	/extended format/
    !_TAG_FILE_SORTED	1	/0=unsorted, 1=sorted, 2=foldcase/
    !_TAG_PROGRAM_AUTHOR	Evan Huus /eapache@gmail.com/
    !_TAG_PROGRAM_NAME	Starscope //
    !_TAG_PROGRAM_URL	https://github.com/eapache/starscope //
    !_TAG_PROGRAM_VERSION	#{Starscope::VERSION}	//
  HEADER
  defs = (@tables[:defs] || {}).sort_by { |x| x[:name][-1].to_s }
  defs.each do |record|
    file.puts ctag_line(record, @meta[:files][record[:file]], path_prefix)
  end
end

def export_to(format, io, path_prefix)

def export_to(format, io, path_prefix)
  case format
  when :ctags
    export_ctags(io, path_prefix)
  when :cscope
    export_cscope(io, path_prefix)
  else
    raise UnknownExportFormatError
  end
end

def strip_unicode(str)

def strip_unicode(str)
  str.encode(ASCII, invalid: :replace, undef: :replace, replace: '')
end

def tokenize_line(line, records)

def tokenize_line(line, records)
  toks = {}
  records.each do |record|
    key = record[:name][-1].to_s
    # use the column if we have it, otherwise fall back to scanning
    index = record[:col] || line.index(key)
    index = line.index(key, index + 1) while index && !valid_index?(line, index, key)
    next if index.nil?
    # Strip trailing non-word characters, otherwise cscope barfs on
    # function names like `include?`
    if key =~ /^\W*$/
      next unless %i[defs end].include?(record[:tbl])
    else
      key.sub!(/\W+$/, '')
    end
    record[:key] = key
    toks[index] = record
  end
  toks.sort
end

def valid_index?(line, index, key)

def valid_index?(line, index, key)
  # index is valid if the key exists at it, and the prev/next chars are not word characters
  ((line[index, key.length] == key) &&
   (index == 0 || line[index - 1] !~ /[[:word:]]/) &&
   (index + key.length == line.length || line[index + key.length] !~ /[[:word:]]/))
end