#  Copyright (C) 1997,1998 Kikutani Makoto (kikutani@debian.or.jp)

require "slanglib"
include Slanglib

module Slang
  SLANG_VERSION = 51
  SL_MOUSE_B0 = 0x1101
  SL_MOUSE_B1 = 0x1102
  SL_MOUSE_B2 = 0x1103
  
  PAGER_QUIT = 0x1111
  PAGER_KEY_UP = 0x1112
  PAGER_KEY_DOWN = 0x1113
  PAGER_NPAGE = 0x1114
  PAGER_PPAGE = 0x1115
  PAGER_BOB = 0x1116
  PAGER_EOB = 0x1117
  PAGER_REDRAW = 0x1118
  PAGER_KEY_RIGHT = 0x1119
  PAGER_KEY_LEFT = 0x111a
  PAGER_SEARCH_FWD = 0x111b
  PAGER_SEARCH_BWD = 0x111c
  PAGER_SEARCH_NEXT = 0x111d

  SL_MOUSE_B3 = 0x111e
  SL_MOUSE_B4 = 0x111f

  BRANCH = 3
  LAST_THREAD = 2
  SAME_LEVEL_THREAD = 1
  BLANK = 0

  class SlangSearch < StandardError; end

  $slang_mouse_mode = FALSE
  $after_mini_message = 0

  # same as sl_reset
  # is this necessary ?
  def slang_reset
    slsmg_gotorc(sltt_screen_rows - 1, 0)
    slsmg_refresh
    sltt_set_mouse_mode(0, 1) if $slang_mouse_mode
    sl_reset_tty
    slsmg_reset_smg
  end

  def slang_suspend
    sltt_set_mouse_mode(0, 1) if $slang_mouse_mode
    #reset_tty
    #slsmg_reset_smg
    slsmg_suspend_smg
  end
  
  def slang_resume
    # init_tty(-1, 0, 1)
    # slsmg_init_smg
    slsmg_resume_smg
    sltt_set_mouse_mode(1, 1) if $slang_mouse_mode
  end


  def init_terminal(tty, smg)
    slsig_block_signals
    sltt_get_terminfo
    $old_row = sltt_screen_rows
    $old_col = sltt_screen_cols
    # SLkp_init assumes that SLtt_get_terminfo has been called. 
    if tty && -1 == slkp_init
      slsig_unblock_signals
      return FALSE
    end
    
    sl_init_signals
    
    slang_init_tty(-1, 0, 1) if tty
    sltty_set_suspend_state(1) if tty
    slsmg_init_smg if smg
    
    slsig_unblock_signals
    
    sl_init_colors if sl_color_terminal?
    
    sltt_set_mouse_mode(1, 1) if $slang_mouse_mode
    
    return TRUE
  end
  
  def mouse_on
    $slang_mouse_mode = TRUE
    sl_mouse_on
  end
  
  def mouse_off
    $slang_mouse_mode = FALSE
    sl_mouse_off
  end
  
  # the following minibuffer functions are retrieved from 'most' sources
  def mini_message(what, how)
    @mini_buf = what
    if how
      @beep_mini = TRUE
    else 
      @beep_mini = FALSE
    end
  end

  def select_minibuffer
    slsmg_gotorc(sltt_screen_rows - 1, 0)
    slsmg_erase_eol
    # slsmg_refresh
    @last_message = nil
    @minibuffer_selected = TRUE
  end
  
  def exit_minibuffer
   # slsmg_gotorc(sltt_screen_rows - 1, 0)
   # slsmg_erase_eol
   @minibuffer_selected = FALSE   
  end

  def put_message_1(what)
    select_minibuffer
    slsmg_write_string(what)
    # exit_minibuffer
    @last_message = what
  end

  def flush_message(msg)
    slsmg_gotorc(sltt_screen_rows - 1, 0)
    slsmg_write_string(msg)
    slsmg_erase_eol
    slsmg_refresh
  end

  def put_message
    sltt_beep if @beep_mini
    
    put_message_1(@mini_buf)
    
    if @beep_mini
      slsmg_refresh
      sleep 0.5
    end
    @beep_mini = TRUE
    @mini_buf = nil
    $after_mini_message = 1
  end

  def sl_put_mini_message(msg, how)
    mini_message(msg, how)
    select_minibuffer
    put_message
    # exit_minibuffer
  end

  def clear_minibuffer
    @mini_buf = nil
    @Beep_Mini = FALSE
    put_message
  end
  
  def check_minibuffer
    if @mini_buf
      put_message
    else
      if @last_message != global_msg
	put_message_1(global_msg)
      end
    end
  end

  def completion(str)
    if $completion_hook
      if str != $completion_str
	$completion_str = str
	begin
	  $completion_array = $completion_hook.call(str)
	rescue
	  $completion_array = []
	end
      end
    end
    return str if $completion_array == []
    $completion_index = 0 if $completion_index >= $completion_array.size
    s = $completion_array[$completion_index]
    $completion_index += 1
    s
  end

  def sl_read_mini(prompt, ini=nil, noecho=nil)
    select_minibuffer
    $completion_str = nil
    $completion_array = []
    len, str = sl_read_line(prompt, ini, noecho)
    exit_minibuffer
    return len, str
  end

  def sl_read_mini_with_compl(prompt, ini=nil, noecho=nil)
    select_minibuffer
    $completion_str = nil
    $completion_index = 0
    len, str = sl_read_line(prompt, ini, noecho)
    exit_minibuffer
    return len, str
  end
  
  def sl_read_mini_with_file_compl(prompt, ini=nil, noecho=nil)
    select_minibuffer
    $completion_str = nil
    $completion_index = 0
    $completion_hook = Proc.new{|s| Dir.glob(File.expand_path(s + "*"))}
    len, str = sl_read_line(prompt, ini, noecho)
    exit_minibuffer
    return len, str
  end
  
  def sl_read_mini_key(msg, keys)
    select_minibuffer
    slsmg_write_string(msg)
    slsmg_refresh
    while TRUE
      k = slang_getkey
      break if keys.include?(k)
      if k == 0x07 # CTL-G
	k = -1
	break
      end
    end
    exit_minibuffer
    k
  end
  
  
  class Line_element
    def initialize(s='')
      @id = 0
      @data = s
      @attribute = nil
      @continued_line = nil
      @continued_top = nil
      @long_line = nil
      @special_write = nil
    end
    attr("id", TRUE)
    attr("data", TRUE)
    attr("attribute", TRUE)
    attr("special_write", TRUE)
    attr("continued_line", TRUE)
    attr("continued_top", TRUE)
    attr("long_line", TRUE)
  end

  class Tree_element
    def initialize(p = nil)
      @id = 0
      @parent = p
      @node = nil
      @uncollapsed = FALSE
      @depth = 0
      @hier_list = []
      @dval = {}
      @last_element = FALSE
      @num_of_children = 0
      @fake_thread = nil
    end
    attr("id", TRUE)
    attr("parent", TRUE)
    attr("node", TRUE)
    attr("uncollapsed", TRUE)
    attr("depth", TRUE)
    attr("hier_list", TRUE)
    attr("last_element", TRUE)
    attr("num_of_children", TRUE)
    attr("fake_thread", TRUE)

    def set_val(key, val)
      @dval[key] = val
    end

    def get_val(key)
      @dval[key]
    end
  end

  class Pager
    def initialize(row_min, row_max, col_min, col_max, 
		   lelement=nil, offset=0)
      @rmin = row_min
      @rmax = row_max
      @cmin = col_min
      @cmax = col_max
      @cursor_move = nil
      @cursor_min = 0 # not used
      @cursor_max = 0 # not used
      if lelement
	@lelement = lelement
      else
	@lelement = Line_element
      end
      @with_mini_buffer = nil
      @revcolor = SL_BG_BLUE + SL_YELLOW # BG=BLUE, FG=YELLOW
      @current_row = 0
      @col_start = 0
      @scrw = Scroll_window.new
      @key_actions = {}
      @bar = 0
      @tilde = 1
      @screen_shift_rate = 3/4.0
      @upd_obj = self
      init_key_actions    
      special_key_actions
      @hlines = {}
      @lines = []
      @lines_num = 0
      @inhibit_horiz_scroll = nil
      @search_dir = 1
      @search_str = nil
      @default_cursor_pos = []
      @offset = offset
      @highlight_rex = nil
      build_lines
    end
    
    attr("scrw")
    attr("rmin", TRUE)
    attr("rmax", TRUE)
    attr("cmin", TRUE)
    attr("cmax", TRUE)
    attr("current_row", TRUE)
    attr("revcolor", TRUE)
    attr("screen_shift_rate", TRUE)
    attr("bar", TRUE)
    attr("tilde", TRUE)
    attr("inhibit_horiz_scroll", TRUE)
    attr("lines")
    attr("default_cursor_pos", TRUE)
    attr("offset")
    attr :highlight_rex
    attr :highlight_color, true

    def get_current_line
      return line_num() + @current_row - @offset - 1
    end

    def set_highlight_rex(rex)
      @highlight_rex = rex
    end

    def set_cursor(cursor_move, cursor_min, cursor_max)
      @cursor_move = cursor_move
      @cursor_min = cursor_min # not used
      @cursor_max = cursor_max
      if @cursor_move
	@current_row = @cursor_min 
      end
    end

    def set_mini_buffer(v)
      @with_mini_buffer = v
      if v == TRUE
	@cursor_max = @rmax - 2
      else
	@cursor_max = @rmax - 1
      end
    end

    def set_bar(n)
      @bar = n
      @scrw.set_bar(@bar)
    end

    def set_tilde(n)
      @tilde = n
      @scrw.set_tilde(@tilde)
    end

    def init_lines
      @scrw.init_lines(@lines_num)
    end
    private :init_lines

    def build_lines
      # in this method
      # @hlines[id] will be filled with @lelement
      # @lines_num should be updated
      # you shoud call init_lines at the end of this method
    end

    def add_line(str, a=nil)
      if str.kind_of?(String)
	l = @lelement.new(str)
      else
	l = str
      end
      id = @scrw.add_line(l)
      return nil if 0 == id
      l.id = id
      l.attribute = a if a
      @lines[@lines_num] = l
      @lines_num += 1
      return l
    end
    private :add_line

    def replace_line(old, str, a=nil)
      if str.kind_of?(String)
	l = @lelement.new(str)
      else
	l = str
      end
      newid = @scrw.replace_line(old.id, l, @current_row)
      return nil unless newid
      l.id = newid
      l.attribute = a if a
      @hlines[newid] = l # to avoid being erased by GC
      @hlines.delete(old.id)
      newid
    end
    private :replace_line

    def draw_bottom
    end
    private :draw_bottom

    def normal_color
      if sl_color_terminal?
	slsmg_set_color(@scrw.get_normal_color)
      else
	slsmg_normal_video
      end
    end
    private :normal_color

    # usually used for bottom line
    def reverse_color
      if sl_color_terminal?
	slsmg_set_color(@revcolor)
      else
	slsmg_reverse_video
      end
    end
    private :reverse_color

    def cursor_color
      if sl_color_terminal?
	slsmg_set_color(@scrw.get_cursor_line_color)
      else
	slsmg_reverse_video
      end
    end
    private :cursor_color

    def update_display
      slsig_block_signals
      begin
	normal_color
	
	if @cursor_move
	  res = update_region(@rmin, @rmax, @cmin, @cmax, @col_start, @current_row)
	  unless res
	    if @with_mini_buffer
	      sl_put_mini_message('something wrong in update_display', TRUE)
	    end
	    return FALSE 
	  end
	  draw_bottom
	  # normal_color
	  if @with_mini_buffer
	    unless @minibuffer_selected
	      slsmg_gotorc(sltt_screen_rows-1, 0) 
	      slsmg_erase_eol
	    end
	  end
	  slsmg_gotorc(@current_row, @cmin) 
	else
	  res = update_region(@rmin, @rmax, @cmin, @cmax, @col_start, -1)
	  unless res
	    if @with_mini_buffer
	      sl_put_mini_message('something wrong in update_display', TRUE)
	    end
	    return FALSE 
	  end
	  draw_bottom
	  if @with_mini_buffer
	    unless @minibuffer_selected
	      slsmg_gotorc(sltt_screen_rows-1, 0) 
	      slsmg_erase_eol
	    end
	  end
	  if @default_cursor_pos != []
	    slsmg_gotorc(@default_cursor_pos[0], @default_cursor_pos[1])
	  else
	    slsmg_gotorc(@rmin, @cmin)
	  end
	end
	
	slsmg_refresh
	return TRUE
      ensure
	slsig_unblock_signals
      end
    end
    
    def goto_bob
      while TRUE
	break if @scrw.scroll_pageup == -1 
      end
      if @cursor_move
	@current_row = @rmin
      end
    end

    def goto_eob
      while TRUE
#	break if @scrw.scroll_pagedown == -1
	break unless next_page
      end
    end

    def scroll_prev_n(n)
      @scrw.scroll_prev_n(n)
    end

    def back_scr
      if !@cursor_move
	scroll_prev_n(1)
      else
	if @current_row > @rmin
	  @current_row -= 1
	else
	  scroll_prev_n(1)
	end
      end
    end

    def line_num
      @scrw.line_num
    end

    def num_lines
      @scrw.num_lines
    end

    def scroll_next_n(n)
      @scrw.scroll_next_n(n)
    end

    def forw_scr
      if !@cursor_move
	scroll_next_n(1)
      else
	if @scrw.line_num+@current_row < @rmin + @scrw.num_lines
	  if @current_row < @cursor_max
	    @current_row += 1
	  else
	    scroll_next_n(1)
	  end
	end
      end
    end
    
    def next_page
      if @scrw.line_num + (@rmax-@rmin) - 1 < @scrw.num_lines
	@scrw.scroll_pagedown
	@scrw.top_window_line_is_current_line
	if @scrw.line_num+@current_row > @scrw.num_lines
	  if @cursor_move
	    @current_row = @scrw.num_lines - @scrw.line_num + @rmin
	  end
	end
	return TRUE
      else # already last page
	if @cursor_move
	  @current_row = @scrw.num_lines - @scrw.line_num + @rmin
	end
	return FALSE
      end      
    end
    
    def prev_page
      if @scrw.scroll_pageup < 0 # already top page
	@current_row = @rmin if @cursor_move
      end
    end
    
    def key_actions_add(keys, action = Proc.new)
      keys.each{|k| @key_actions[k] = action}
    end
    private :key_actions_add

    def post_key_hook(k)
      exit_minibuffer if $after_mini_message == 0
      $after_mini_message -= 1 if $after_mini_message > 0
    end
    private :post_key_hook

    def undefined_key_hook(k)
    end
    private :undefined_key_hook
    
    def key_actions_call(k)
      if p = @key_actions[k]
	p.call
	post_key_hook(k)
      else
	undefined_key_hook(k)
	sltt_beep
      end
      @do_break
    end
    private :key_actions_call

    # method for mouse click 
    def mouse_b0(c, r)
    end
    private :mouse_b0

    def mouse_b1_status(c, r)
    end
    private :mouse_b1_status

    def mouse_b1(c, r)
    end
    private :mouse_b1

    def mouse_b2(c, r)
    end
    private :mouse_b2

    def do_search_next(research)
      cur_line = @scrw.line_num - 1
      @scrw.find_set_nth_line(cur_line)
      n = 0
      while TRUE
	# assume data is string!
	line = @scrw.find_current_line.data
	if @search_str =~ line
	  break if n != 0
	  break unless research
	end
	if @search_dir > 0
	  res = @scrw.find_next_line
	else
	  res = @scrw.find_prev_line
	end
	unless res 
	  raise SlangSearch
	end
	n += @search_dir
      end
      goto_bob
      @scrw.scroll_next_n(cur_line + n)
    end
    
    def do_search_fwd
      len, str = sl_read_mini("Forward Search(regexp): ")
      begin
	@search_str = Regexp.new(str)
      rescue RegxpError
        sl_put_mini_message("invalid regexp", TRUE)
        return
      end                           
      
      if len > 0
	@search_dir = 1
	do_search_next(FALSE)
      end
    end
    
    def do_search_bwd
      len, str = sl_read_mini("Backward Search(regexp): ")
      begin
	@search_str = Regexp.new(str)
      rescue RegxpError
        sl_put_mini_message("invalid regexp", TRUE)
        return
      end                           
      if len > 0
	@search_dir = -1
	do_search_next(FALSE)
      end
    end
    
    # basic pager actions
    def init_key_actions
      key_actions_add([PAGER_QUIT]) do
	@do_break = TRUE # may be overridden by subclass 
      end

      key_actions_add([SL_KEY_RIGHT, PAGER_KEY_RIGHT]) do
	unless @inhibit_horiz_scroll
	  @col_start += (@cmax - @cmin) * @screen_shift_rate
	  # tmp, newp = slsmg_set_screen_start (0, @screen_start)
	end
      end

      key_actions_add([SL_KEY_LEFT, PAGER_KEY_LEFT]) do
	unless @inhibit_horiz_scroll
	  @col_start -= (@cmax - @cmin) * @screen_shift_rate
	  @col_start = 0 if @col_start < 0
	  # tmp, newp = slsmg_set_screen_start (0, @screen_start)
	end
      end

      key_actions_add([SL_KEY_UP, PAGER_KEY_UP]) do
	back_scr
      end

      key_actions_add([SL_KEY_DOWN, PAGER_KEY_DOWN]) do
	forw_scr
      end

      key_actions_add([SL_KEY_NPAGE, PAGER_NPAGE]) do
        next_page
      end

      key_actions_add([SL_KEY_PPAGE, PAGER_PPAGE]) do
	prev_page
      end

      key_actions_add([SL_KEY_HOME, PAGER_BOB]) do
	goto_bob
      end

      key_actions_add([SL_KEY_END, PAGER_EOB]) do
	goto_eob
      end

      key_actions_add([PAGER_REDRAW]) do 
	slsmg_cls
	update_display
      end

      key_actions_add([PAGER_SEARCH_FWD]) do
	begin
	  do_search_fwd
	rescue SlangSearch
	  sl_put_mini_message("not found", TRUE)
	end
      end

      key_actions_add([PAGER_SEARCH_BWD]) do
	begin
	  do_search_bwd
	rescue SlangSearch
	  sl_put_mini_message("not found", TRUE)
	end
      end

      key_actions_add([PAGER_SEARCH_NEXT]) do
	begin
	  do_search_next(TRUE)
	rescue SlangSearch
	  sl_put_mini_message("not found", TRUE)
	end
      end

      # define the behavior of status line click
      key_actions_add([SL_MOUSE_B0]) do
	c, r = sl_get_mouse_rc
	if r == @rmax
	  next_page
	elsif r < @rmax and r >= @rmin
	  mouse_b0(c, r)
	end
      end
      
      key_actions_add([SL_MOUSE_B1]) do
	c, r = sl_get_mouse_rc
	if r == @rmax
          mouse_b1_status(c, r)
	elsif r < @rmax and r >= @rmin
	  mouse_b1(c, r)
	end
      end
      
      key_actions_add([SL_MOUSE_B2]) do
	c, r = sl_get_mouse_rc
	if r == @rmax
	  prev_page
	elsif r < @rmax and r >= @rmin
	  mouse_b2(c, r)
	end
      end      
    end
    private :init_key_actions
    
    def special_key_actions
    end
    private :special_key_actions

    def win_row_resize
      @rmax = sltt_screen_rows - ($old_row - @rmax)
    end

    def win_col_resize
      @cmax = sltt_screen_cols - ($old_col - @cmax)
    end

    def win_resize
      win_row_resize
      win_col_resize
    end

    def post_winch_hook
    end

    # if window size is modified by mouse drag
    def check_winch
      if sl_get_winch_flag == 1
	sltt_get_screen_size
	if $old_row != sltt_screen_rows or
	      $old_col != sltt_screen_cols
	  slsmg_init_smg
	  win_resize
	  post_winch_hook()
	  $old_row = sltt_screen_rows
	  $old_col = sltt_screen_cols
	end
	sl_clr_winch_flag
      end
    end

    def split_long_line(str, len)
      first = TRUE
      lines = []
      l = sl_expand_tab(str).dup
      while l.size > len
        str0 = l[0, len+1]
        # judge if last char is 2nd byte of KANJI
	# the following match is affected by $KCODE value
        if str0 =~ /.$/ and $&.size == 2
          str0[-2, 2] = '  '
          l = l[len-1..-1]
        else
          l = l[len..-1]
        end
        lines << str0.chop
	if first
	  first = FALSE
	  len -= 1 # 2nd and after strings are len -1 for CONTINUE mark
	end
      end
      lines << l if l
      lines
    end

    # wrap a long line into short lines
    def wrap
      @inhibit_horiz_scroll = TRUE
      len = @cmax - @cmin + 1
      num = 0
      return unless @lines[0].data.kind_of?(String)
      @lines.each {|l|
	str = l.data
	if sl_tabbed_len(str) > len
	  al = split_long_line(str, len)
	  if al.size >= 2
	    s0 = @lelement.new(al[0])
	    newid = @scrw.replace_line(l.id, s0, @current_row)
	    if newid
	      s0.id = newid
	      s0.continued_top = TRUE
	      s0.long_line = str
	      s0.attribute = l.attribute
	      s0.special_write = l.special_write
	      @hlines[newid] = s0 # to avoid being erased by GC
	      @hlines.delete(l.id)
	      @lines[num] = s0
	    end
	    now = s0
	    j = 1
	    al[1..-1].each{|s|
	      sn = @lelement.new('+' + s)
	      newid = @scrw.insert_line(now.id, sn)
	      if newid
		sn.id = newid
		sn.continued_line = TRUE
		sn.continued_top = s0
		sn.attribute = l.attribute
		sn.special_write = l.special_write
		@hlines[newid] = sn # to avoid being erased by GC
		@lines[num + j, 0] = [sn]
		@lines_num += 1
		now = sn
	      end
	      j += 1
	    }
	  end
	end
	num += 1
      }
    end

    def unwrap
      @inhibit_horiz_scroll = FALSE
      num = 0
      while l = @lines[num]
	unless l.continued_top
	  num += 1
	else
	  s0 = @lelement.new(l.long_line)
	  newid = @scrw.replace_line(l.id, s0, @current_row)
	  if newid
	    s0.id = newid
	    s0.continued_top = FALSE
	    s0.attribute = l.attribute
	    s0.special_write = l.special_write
	    @hlines[newid] = s0 # to avoid being erased by GC
	    @hlines.delete(l.id)
	    @lines[num] = s0
	  end
	  num += 1
	  while (l = @lines[num]).continued_line
	    @scrw.remove_line(l.id)
	    @hlines.delete(l.id)
	    @lines.delete_at(num)
	    @lines_num -= 1
	  end
	end
      end
    end

    def main_loop
      @do_break = FALSE
      while TRUE
	check_winch
	update_display
	sleep(0.1) if (IO::select([ STDIN ]))
	key_actions_call(slkp_getkey)
	break if @do_break
      end
    end

    def main_loop_c
      @scrw.main_loop(self)
    end

    # called from slmodule.c
    def get_hlarray(line, hr=nil)
      rex = hr || @highlight_rex 
      return nil unless rex
      pa = []
      rex.each do |r|
	color = r[0]
	regexp = r[1]
	search_pos = 0
	line.scan(regexp) do |sa|
	  sa.each do |s|
	    b = line.index(s, search_pos)
	    len = s.length
	    pa << [b, len, color]
	    search_pos = b + len
	  end
	end
      end
      pa.sort
    end                                                                                        

    # write_line is called from slmodule.c/scrW_update_region
    # this method will be usually overriden
    def write_line(l, row, hl_rex=nil, ncol=nil)
      line = l.data
      ncolor = ncol || @scrw.get_normal_color
      if l.special_write
	call l.special_write
      else
	sl_write_string_with_offset(self, line, @col_start, @cmax, hl_rex, ncolor)
      end
    end
    private :write_line

    def update_region(b, e, cb, ce, off, curline)
      return if e <= b || ce <= cb
      @scrw.update_region(self, b, e, cb, ce, off, curline)
    end # update_region
  end

  class Tree_Pager < Pager
    def initialize(row_min, row_max, col_min, col_max, lelement, offset=0)
      @aformat = []
      @uncollapse_all = FALSE
      @dia = FALSE
      super(row_min, row_max, col_min, col_max, lelement, offset)
      set_cursor(TRUE, row_min, row_max-1)
    end

    def build_index
      # similar to build_lines, but not called from new()
    end

    def set_format(f)
      @format = f
    end

    def set_uncollapse_all
      @uncollapse_all = TRUE
    end

    def unset_uncollapse_all
      @uncollapse_all = FALSE
    end

    def set_dia
      @dia = TRUE
    end

    def unset_dia
      @dia = FALSE
    end

    def replace_line(old, l)
      newid = @scrw.replace_line(old.id, l, @current_row)
      return nil unless newid
      l.id = newid
      @hlines[newid] = l # to avoid being erased by GC
      @hlines.delete(old.id)
      l.node = old.node
      l.parent = old.parent
      l.depth = old.depth
      l.uncollapsed = old.uncollapsed
      l.hier_list = old.hier_list
      l.num_of_children = old.num_of_children
      l.last_element = old.last_element
      l.fake_thread = old.fake_thread
      newid
    end
    private :replace_line

    def insert_elements(l)
      # should be called from uncollapse
    end
    private :insert_elements

    def insert_line(id, l, num)
      newid = @scrw.insert_line(id, l)
      return nil unless newid
      l.id = newid
      @hlines[newid] = l # to avoid being erased by GC
      @lines[num, 0] = [l] # insert as the next
      @lines_num += 1
      newid
    end
    private :insert_line

    def uncollapse(num)
      l = @lines[num]

      # insert_elements should be defined in subclass
      uncollapsed_list = insert_elements(num)

      l.num_of_children = uncollapsed_list.size
      l.uncollapsed = TRUE
  
      last = TRUE
      # reverse analysis for tree display
      uncollapsed_list.reverse_each { |r|
        if last
	  hlist = [LAST_THREAD]
	  r.last_element = TRUE
	  last = FALSE
        elsif r == uncollapsed_list[0] 
	  hlist = [BRANCH]
        else
	  hlist = [BRANCH]
        end
        upper_list = l.hier_list
	if upper_list != []
	  if r.parent and not r.parent.last_element
	    upper_list[-1] = SAME_LEVEL_THREAD
	  else
	    upper_list[-1] = BLANK
	  end
	end
        r.hier_list = upper_list + hlist
      }
      if l.last_element
	l.hier_list[-1] = LAST_THREAD
      elsif l.hier_list != []
	l.hier_list[-1] = BRANCH 
      end
      inc = 0
      if @uncollapse_all
	n = num + 1
	uncollapsed_list.each { |l|
	  rl = @lines[n]
	  if rl.node and rl.uncollapsed == FALSE
	    res = uncollapse(n)
	    n += res
	    inc += res
	  end
	  n += 1
	}
      end
      uncollapsed_list.size + inc
    end
    private :uncollapse

    def collapse(num)
      m = @lines[num]
      if m.num_of_children == 0
	m.uncollapsed = FALSE
	return
      end
      cur_depth = m.depth
      num += 1
      while TRUE
        l = @lines[num]
        break if l.nil? # exceed line list
        d = l.depth
        break if d <= cur_depth
        if l.node and l.uncollapsed
	  collapse(num)
        end
        @scrw.remove_line(l.id)
        @hlines.delete(l.id) # to be erased by GC (but work ?)
        @lines.delete_at(num)
	@lines_num -= 1
      end
      m.uncollapsed = FALSE
    end
    private :collapse

    def coll_uncoll
      num = @scrw.line_num + @current_row - 1 - @rmin
      if l = @lines[num]
        if l.node
	  if l.uncollapsed
	    collapse(num)
	  else
	    uncollapse(num)
	  end
        end
      end
    end
    private :coll_uncoll

    # difficult ? yup!
    def write_tree(l, width, row)
      w = 0
      if l.depth == 0
        if l.node
	  if l.uncollapsed
	    slsmg_write_char(32)
	    w += 1
	  else
	    if @dia
	      slsmg_set_color($tree_color) unless row == @current_row
	      slsmg_set_char_set(1)
	      slsmg_write_char(96) # dia
	      slsmg_set_char_set(0)
	    else
	      slsmg_write_char(32)
	    end
	    w += 1
	  end
        else
	  slsmg_write_char(32) 
	  w += 1
        end
      else
        slsmg_write_char(32) 
        w += 1
        slsmg_set_color($tree_color) unless row == @current_row
        slsmg_set_char_set(1)
        for i in 0 .. l.depth-1 do
	  if l.hier_list[i] == SAME_LEVEL_THREAD
	    slsmg_write_char(120) # |
	    slsmg_write_char(32) 
	    w += 2; break if width > 0 and w >= width
	  elsif l.hier_list[i] == BRANCH
	    slsmg_write_char(116) # |-
	    if l.fake_thread and i == l.depth-1
	      slsmg_set_char_set(0) # because of stupid kterm
	      slsmg_write_char(?*) # -
	      slsmg_set_char_set(1)
	    else
	      slsmg_write_char(113) # -
	    end
	    w += 2; break if width > 0 and w >= width
	  elsif l.hier_list[i] == LAST_THREAD
	    slsmg_write_char(109) # L	  
	    if l.fake_thread and i == l.depth-1
	      slsmg_set_char_set(0) # because of stupid kterm
	      slsmg_write_char(?*) # -
	      slsmg_set_char_set(1)
	    else
	      slsmg_write_char(113) # -
	    end
	    w += 2; break if width > 0 and w >= width
	  elsif l.hier_list[i] == BLANK
	    slsmg_write_char(32) 
	    slsmg_write_char(32) 
	    w += 2; break if width > 0 and w >= width
	  end
	end
	
        unless l.uncollapsed
	  if l.node 
	    if @dia
	      slsmg_write_char(96) # dia
	    else
	      slsmg_write_char(113) # -
	    end
	  else
	    slsmg_set_char_set(0) # because of stupid kterm
	    slsmg_write_char(?>) 
	  end
	  w += 1
        end
	slsmg_set_char_set(0)
      end
      normal_color unless row == @current_row
      w # return how many chars are used for tree
    end
    private :write_tree

    # convert format string to array
    # to make write_line faster
    def set_aformat
      @format.split.each {|s|
        format_char = sprintf("%c", s[-1]) # last char
        @aformat << [format_char, s.to_i]
      }
    end
    private :set_aformat
  end
end
