require './observable.rb'
require 'set'
require 'forwardable'
require 'procvar'

class List
  include Observable
  include Enumerable
  extend Forwardable

  def_delegators :selary, :[], :at, :last, :first, :each, :length
  def_delegators :@ary, :rindex

  def_observables :@ary, *instance_methods.grep(/!$/)
  def_observables :@ary, *[:[]=, :<<]
  def_observables :@ary, *%w|delete delete_if delete_at replace|
  def_observables :@ary, *%w|push pop shift unshift clear fill taint|

  def self.dependencies
    @dependencies ||= Hash.new {|h,k| h[k] = []}
  end
  def self.def_update sym, *deps, &block
    dependencies[sym] ||= Set.new
    dependencies[sym] += deps
    define_method "update_#{sym}".to_sym, &block if block_given?
    eval <<-EOF
      define_method sym do
        if (tmp = update(:#{sym})) != :no_update
          @cache_#{sym} = tmp
        else
          @cache_#{sym}
        end
      end
    EOF
  end

  def_update :selected, :selproc, :ary do
    if @selproc
      select_indices &@selproc
    else
      (0...@ary.length).to_a
    end
  end
  def_update :current, :position, :selary do
    position and selary[position]
  end
  def_update :position, :curpos do
    selected.rindex curpos
  end
  def_update :curpos, :selected, :set_curpos, :last_pos do
    return nil unless @curpos and not selected.empty?
    i = nil
    last = selected.select {|x| @ary[x] == @last_current}
    unless updates[:last_pos] <= updates[:set_curpos] or last.empty?
      i = last.sort {|a,b| (a - @curpos).abs <=> (b - @curpos).abs}.first
    else
      f,e = 0, selected.length
      i = selected.length / 2
      while f != e and (selary[i] == @last_current or selected[i] != @curpos)
        if @curpos < selected[i]
          e = i - 1
        else
          f = i + 1
        end
        i = (e - f) / 2 + f
      end
    end
    @last_current = selary[i]
    update_time :last_pos
    @curpos = selected[i]
  end
  def_update :selary, :selected do
    selected.map {|s| @ary[s]}
  end

  def initialize *args, &block
    add_observation do |meth|
      update_time :ary
    end
    @ary = Array.new *args, &block
  end

  def load *args, &block
    c = current
    @ary = Array.new *args, &block
    update_time :ary
    @curpos = rindex c
    self
  end

  def select i
    @curpos = i.nil? ? i : selected[i]
    update_time :set_curpos
    position
  end

  def next
    i = inc_pos
    current
  end
  def prev
    i = dec_pos
    current
  end
  def inc_pos
    p = position
    if p.nil?
      @curpos = selected.first
    else
      @curpos = selected[p + 1]
    end
    update_time :set_curpos
    position
  end
  def dec_pos
    p = position
    if p.nil?
      @curpos = selected.last
    elsif p.zero?
      @curpos = nil
    else
      @curpos = selected[p - 1]
    end
    update_time :set_curpos
    position
  end

  def select! &block
    @selproc = block
    update_time :selproc
    self
  end

  def inspect
    "%d/%d %s" % [length, @ary.length, selected.map {|x| x == curpos ? "*#{@ary[x]}" : @ary[x]}.inspect]
  end
  private
  def select_indices &block
    newlst = []
    @ary.each_with_index do |x,i|
      newlst << i if yield x
    end
    newlst
  end

  def update_time sym
    updates[sym] = Time.now
  end
  def update sym, &block
    up = updates[sym].nil?
    self.class.dependencies[sym].each do |d|
      if respond_to? d
        send d
      else
        update d
      end
      up = (up or updates[d].nil? or updates[d] > updates[sym])
    end
    if up
      s = "update_#{sym}".to_sym
      r = nil
      if block_given?
        r = yield
      elsif respond_to? s
        r = send s
      end
      update_time sym
      r
    else
      :no_update
    end
  end

  def updates
    @updates ||= {}
  end
end
