#!/usr/bin/env ruby

require 'curses'
require 'thread'
require 'logger'

$log = Logger.new('wm.log')

module Raggle
  module Curses
    class Rect
      attr_accessor :x, :y, :w, :h

      class InvalidValueError < Exception
      end

      def check_vals
        raise MissingValueError, 'missing X coordinate' unless (@x)
        raise MissingValueError, 'missing Y coordinate' unless (@y)
        raise MissingValueError, 'missing width' unless (@w)
        raise MissingValueError, 'missing height' unless (@h)
        raise InvalidValueError, 'invalid width' unless (@w >= 0)
        raise InvalidValueError, 'invalid height' unless (@h >= 0)
      end
      
      def initialize(*args)
        @x, @y, @w, @h = args
        check_vals
      end

      def intersects?(r)
        check_vals

        [r.x, r.x + r.w].select { |v| 
          (@x .. (@x + @w)).include?(v) 
        }.size > 0 && [r.y, r.y + r.w].select { |v| 
          (@y .. (@y + @h)).include?(v) 
        }.size > 0
      end

      def intersection(r)
        check_vals
        return nil unless intersects?(r)
        'TODO!'
      end

      # broken!
      def union(r)
        s, ary = self, []
        ary = [0, 1].inject([]) { |a, i| a[i] = (s[i] < r[i]) ? s[i] : r[i]; a }
        ary = [2, 3].inject(ary) { |a, i| a[i] = (s[i] > r[i]) ? s[i] : r[i]; a }
        Rect.new(*ary)
      end

      alias :bounding_box :union

      def to_a
        check_vals
        [@x, @y, @w, @h]
      end

      def to_s
        "[#{to_a.join(',')}]"
      end

      def [](i)
        check_vals
        [@x, @y, @w, @h][i]
      end

      def []=(i, v)
        check_vals
        ary = [:xr=, :xa=, :yr=, :ya=, :wr=, :wa=, :hr=, :ha=]
        call(ary[i], v) if ary[i]
      end
    end
    
    Constraint = Struct.new(:min_r, :min_a, :max_r, :max_a)
    
    class Geometry 
      attr_accessor :xr, :xa, :yr, :ya, :wr, :wa, :hr, :ha, :constraints

      class MissingValueError < Exception
      end

      class InvalidValueError < Exception
      end

      class BoundsError < Exception
      end

      # special value for centered elements
      # TODO
      CENTER = 'center'

      def check_vals
        raise MissingValueError, 'missing X coordinate' unless (@xr || @xa)
        raise MissingValueError, 'missing Y coordinate' unless (@yr || @ya)
        raise MissingValueError, 'missing width' unless (@wr || @wa)
        raise MissingValueError, 'missing height' unless (@hr || @ha)
        raise MissingValueError, 'invalid width' if (@wr && @wr < 0)
        raise MissingValueError, 'invalid height' if (@hr && @hr < 0)
        
        # check constraints
        c_w, c_h = @constraints[:w], @constraints[:h]
        if ((!c_w.min_r || c_w.min_r == 0) && c_w.min_a && c_w.min_a < 0)
          raise InvalidValueError, 'invalid width constraint'
        end
        if ((!c_h.min_r || c_h.min_r == 0) && c_h.min_a && c_h.min_a < 0)
          raise InvalidValueError, 'invalid height constraint'
        end
      end

      def initialize(*args)
        @xr, @xa, @yr, @ya, @wr, @wa, @hr, @ha = args
        @constraints = { :w => Constraint.new, :h => Constraint.new }
        check_vals
      end

      def to_a
        check_vals
        [@xr, @xa, @yr, @ya, @wr, @wa, @hr, @ha]
      end

      def to_s
        "[#{to_a.join(',')}]"
      end

      def [](i)
        ary = self.to_a
        raise BoundsError, 'array index out of bounds' if (i >= ary.size || i < 0 - ary.size)
        ary[i]
      end

      def []=(i, v)
        ary = [:xr=, :xa=, :yr=, :ya=, :wr=, :wa=, :hr=, :ha=]
        raise BoundsError, 'array index out of bounds' if (i >= ary.size || i < 0 - ary.size)
        call(ary[i], v) if ary[i]
      end
        
      def to_rect(parent_width, parent_height)
        check_vals
        p_w, p_h = parent_width, parent_height
        ret = [0] * 4

        # TODO: ret[0] = @xr ? p_w * (@xr == @@CENTER ? 0.5 : @xr) : nil

        # calculate x, y
        ret[0] = p_w * @xr if @xr
        ret[0] += @xa if @xa
        ret[1] = p_h * @yr if @yr
        ret[1] += @ya if @ya

        # calculate w, h
        ret[2] = p_w * @wr if @wr
        ret[2] += @wa if @wa
        ret[3] = p_h * @hr if @hr
        ret[3] += @ha if @ha

        # check width constraints
        if (c_w = @constraints[:w]).any?
          min_w, max_w = 0, p_w
          
          min_w = if c_w.min_r && c_w.min_a
            c_w.min_r * p_w + c_w.min_a
          elsif c_w.min_r
            c_w.min_r * p_w
          elsif c_w.min_a
            c_w.min_a
          end

          max_w = if c_w.max_r && c_w.max_a
            c_w.max_r * p_w + c_w.max_a
          elsif c_w.max_r
            c_w.max_r * p_w
          elsif c_w.max_a
            c_w.max_a
          end

          raise BoundsError, 'width constraint out of bounds' if min_w > max_w
          ret[2] = min_w if ret[2] < min_w
          ret[2] = max_w if ret[2] > max_w
        end

        # check height constraints
        if (c_h = @constraints[:h]).any?
          min_h, max_h = 0, p_h
          
          # calculate min height
          min_h = if c_h.min_r && c_h.min_a
            c_h.min_r * p_h + c_h.min_a
          elsif c_h.min_r
            c_h.min_r * p_h
          elsif c_h.min_a
            c_h.min_a
          end

          # calculate max height
          max_h = if c_h.max_r && c_h.max_a
            c_h.max_r * p_h + c_h.max_a
          elsif c_h.max_r
            c_h.max_r * p_h
          elsif c_h.max_a
            c_h.max_a
          end

          raise BoundsError, 'height constraint out of bounds' if min_h > max_h
          ret[3] = min_h if ret[3] < min_h
          ret[3] = max_h if ret[3] > max_h
        end

        # check return values
        raise BoundsError, 'X out of bounds' unless (0..p_w).include?(ret[0])
        raise BoundsError, 'Y out of bounds' unless (0..p_h).include?(ret[1])
        raise BoundsError, 'width out of bounds' if (ret[2] > p_w || ret[0] + ret[2] > p_w)
        puts "DEBUG: pw = #{p_w}, ph = #{p_h}, ret = " << ret.join(',')
        raise BoundsError, "height out of bounds (h = #{ret[3]}, ph = #{p_h}" \
          if (ret[3] > p_h || ret[1] + ret[3] > p_h)

        # return result
        Rect.new(*ret)
      end

      def resize(*args)
        @wr, @wa, @hr, @ha = args
        check_vals
      end

      def move(*args)
        @xr, @xa, @yr, @ya = args
        check_vals
      end
    end

    Layout = Struct.new(:visible, :modal, :expand, :border)

    class Window
      attr_accessor :layer, :parent, :layout, :rect, :geom, :win, :title

      MODAL_LAYER = 1000

      def do_refresh
        if visible?
          # actual curses update here
          # TODO: draw contents and shit here too
          @win.box(0, 0) # if @layout.border
          if @title
            @win.setpos(0, 2)
            @win.addstr " #{@title} "
          end
          @win.refresh
          # $log.info('do_refresh') { "#@layer: #@rect" }
        end
      end

      def refresh(child = nil)
        @parent.refresh(self)
      end

      def calc_rect
        @geom.to_rect(@parent.rect.w, @parent.rect.h)
      end

      def close
        # TODO: lots of stuff (cleanup, etc)
        @parent.rm_win(self) if @parent
      end

      def initialize(parent, geom, title = nil, modal = false)
        @parent, @geom = parent, geom
        @layout = Layout.new(false, modal, true, true)
        @layer = @parent ? @parent.top_layer : 0
        @title = title

        # derive rectangle from parent geometry
        @rect = self.calc_rect
        @win = @parent.subwin(*@rect) if @parent
        @win.setpos(1, 1) if @win && @layout.border

        # if the window is modal, then do some extra processing
        if @layout.modal
          # find parent that has the modal queue
          win = self
          win = @parent.parent until (!win || win.respond_to?(:modal_queue))
          raise "Couldn't find modal queue" unless win

          # add myself to the modal queue
          # (this operation will block until I'm the only modal window)
          win.modal_queue << self

          # set my layer to the MODAL_LAYER
          @layer = MODAL_LAYER
        end

        # register with parent (usually the WM)
        @parent.add_win(self) if @parent
      end

      def subwin(x, y, w, h)
        @win ? @win.subwin(h, w, y, x) : nil
      end


      def hide
        @layout.visible = false
      end

      def show
        @layout.visible = true
        self.refresh
      end

      def raise
        @layer = @parent.top_layer + 1
      end

      def lower
        @layer = @parent.bottom_layer - 1
      end

      def modal?
        @layout.modal
      end

      def visible?
        @layout.visible
      end

      def expand?
        @layout.expand
      end

      def on_resize
        @rect = self.calc_rect
        @win.resize(@rect.h, @rect.w)
        self.refresh
      end

      def resize(wr, wa, hr, ha)
        @geom.resize(wr, wa, hr, ha)
        self.on_resize
      end

      def on_move
        @rect = self.calc_rect
        @win.move(@rect.y, @rect.x)
        self.refresh
      end

      def move(xr, xa, yr, ya)
        @geom.move(xr, xa, yr, ya)
        self.on_move
      end

      alias :top_layer :layer
      alias :bottom_layer :layer
    end

    class ListWindow < Window
      attr_accessor :items

      def do_refresh
        @win.setpos(0, 1)
        @items.each { |str| @win.addstr(' ' << str) }
        super
      end

      def <<(str)
        @items << str
        self.refresh
      end

      def initialize(*args)
        @items = []
        super(*args)
      end
    end

    class WindowManager < Window
      attr_accessor :wins, :sw, :sh, :modal_queue

      def add_win(win)
        @wins << win
      end

      def rm_win(win)
        @wins.delete(win)
      end

      def refresh(child = nil)
        update_wins = []

        if child
          # get a list of all visible non-modal windows that are above the
          # child
          tmp = @wins.select { |win|
            win.visible? && win.layer >= child.layer && !win.modal? 
          }.sort { |a, b| a.layer <=> b.layer }

          # check clipping on each window
          update_wins = [child]
          tmp.each_index do |i| 
            0.upto(i - 1) do |j| 
              if tmp[i].rect.intersects?(tmp[j].rect)
                update_wins << tmp[i] 
                break
              end
            end
          end
        else
          @win.refresh
          update_wins = @wins.select { |win| 
            win.visible? && !win.modal? 
          }.sort { |a, b| 
            a.layer <=> b.layer 
          }
        end

        update_wins.each { |win| win.do_refresh }
      end

      def resize(wr, wa, hr, ha)
        @sw = wa || ::Curses::cols
        @sh = ha || ::Curses::lines
        super
      end

      def calc_rect
        puts "DEBUG: wm calc_rect called"
        w, h = @sw || ::Curses::cols, @sh || ::Curses::lines
        ret = @geom.to_rect(w, h) # FIXME: should be curses w/h
        puts "DEBUG: calc_rect(): wm.rect = #{ret}"
        ret
      end
        

      DFLT_GEOM = [0.0, nil, 0.0, nil, 1.0, nil, 1.0, nil]

      def initialize(parent_curses_win = nil, geom = nil)
        geom ||= Geometry.new(*DFLT_GEOM)

        super(nil, geom)

        puts "DEBUG: rect = #{@rect.to_s}, geom = #{@geom.to_s}"
        @win = parent_curses_win
        @wins = []
        @modal_queue = SizedQueue.new(1)
      end

      def top_layer
        wins.inject(0) { |r, w| !w.modal? && w.layer > r ? w.layer : r}
      end

      def bottom_layer
        wins.inject(1000) { |r, w| !w.modal? && w.layer < r ? w.layer : r }
      end
    end
  end
end

if __FILE__ == $0
  # test rect and geometry classes
  rect = Raggle::Curses::Rect.new(5, 4, 10, 15)
  geom = Raggle::Curses::Geometry.new(0.25, nil, 0.25, nil, 0.5, nil, nil, 5)

  # debugging output
  puts "DEBUG: geom[5] = #{geom[5]}, geom.to_rect = #{geom.to_rect(80, 24).to_s}"
  puts "DEBUG union (broken): " << rect.union(geom.to_rect(80, 24)).to_s

  # create window manager and a test window
  scr = Curses::init_screen
  wm = Raggle::Curses::WindowManager.new(scr)

  wins = [ 
    [0,     nil, 0,   nil, 0.300, nil, 0.999, 0   ],   # feed window
    [0.300, nil, 0,   nil, 0.700, nil, 0.5,   nil ],   # item window
    [0.300, nil, 0.5, nil, 0.700, nil, 0.5,   0   ],   # desc window
  ].inject([]) do |ret, geom_ary|
    g = Raggle::Curses::Geometry.new(*geom_ary)
    ret << Raggle::Curses::ListWindow.new(wm, g)
    ret[-1].win << 'poop'
    ret[-1].title = 'askdljflaksjfas'
    ret[-1].show
    10.times { ret[-1] << "poop kdlfjaajdsfjkas\n" }
    ret
  end

  puts "wm.rect = #{wm.rect}"
  win = Raggle::Curses::Window.new(wm, geom)
  # win.win.setpos(1, 1)
  # win.win.addstr "\n asdfklajsdflkadsj"
  # win.show
  # win.win.getch
  # Thread.new { loop { win.move(nil, rand(wm.rect.w - win.rect.w), nil,
  # rand(wm.rect.h - win.rect.h)); wm.refresh; sleep 0.5 } }
  wm.win.getch
  wm.win.clear
  wm.win.refresh
  wm.resize(nil, 80, nil, 30)
  wm.win.clear
  wm.refresh
  wm.win.getch
  Curses::close_screen
end

