;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; window manager
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define desktop #nil)


;; public API
define desktop-empty?()
  {{null? desktop} or {not desktop[has-any-windows?:]}}


;; internal API
define create-desktop()
  ;; basic box object
  define lay-box flexlay:new-box()
  define closed-windows #nil  ;; list
  ;; temp list for "close-window:", to not process close requests twice
  define closing-windows #nil  ;; list
  define self-obj #nil
  define wait-for-mouse-up #f
  define pass-mouse-on-window-activate #t
  ;;
  define append-closed-window(win)
    {closed-windows := cons(win closed-windows)}
  ;;
  define closed?(win)
    {any-true? memq(win closed-windows)}
  ;;
  define remove-closed-windows()
    define win
    while {not-null? closed-windows}
      {win := car(closed-windows)}
      win[desktop-set!: #nil]
      self-obj[remove-child: win]
      {closed-windows := cdr(closed-windows)}
  ;;
  define closing?(win)
    {any-true? memq(win closing-windows)}
  ;;
  define append-closing-window(win)
    {closing-windows := cons(win closing-windows)}
  ;;
  define remove-closing-window(win)
    if {any-true? memq(win closing-windows)}
      cond
        {car(closing-windows) eq? win}
          {closing-windows := cdr(closing-windows)}
        else
          define tail #nil
          while {pair? closing-windows}
            if {not {car(closing-windows) eq? win}}
              {tail := cons(car(closing-windows) tail)}
            {closing-windows := cdr(closing-windows)}
          {closing-windows := reverse!(tail)}
  ;;
  define pass-mouse-event(cbox evt)
    if {not-null? cbox}
      then
        define ox evt[x:]
        define oy evt[y:]
        try-finally
          lambda ()
            {evt[x:] := {evt[gx:] - cbox[final-x:]}}
            {evt[y:] := {evt[gy:] - cbox[final-y:]}}
            cbox[handle-event: evt]
          lambda ()
            {evt[x:] := ox}
            {evt[y:] := oy}
  ;;
  set! self-obj
    method-lambda desktop-widget
      %process-closed-windows: (self)
        remove-closed-windows()
        self
      fix-dimensions: (self)
        {lay-box[pref-width:] := lgfx:window-width}
        {lay-box[pref-height:] := lgfx:window-height}
        {lay-box[min-width:] := lgfx:window-width}
        {lay-box[min-height:] := lgfx:window-height}
        {lay-box[max-width:] := lgfx:window-width}
        {lay-box[max-height:] := lgfx:window-height}
        {lay-box[final-width:] := lgfx:window-width}
        {lay-box[final-height:] := lgfx:window-height}
        self
      debug-id: (self)
        ;debug-id
        -666
      super: (self)
        lay-box
      type: (self)
        desktop:
      layout-box: (self)
        lay-box
      %parent-box-set!: (self box)
        error "cannot set parent for desktop widget"
      desktop: (self)
        #nil
      desktop-set!: (self value)
        error "cannot set desktop for desktop widget"
      top-widget?: (self)
        #t
      parent: (self)
        #nil
      ;; should return boolean.
      ;; return #t if can be activated.
      selectable?: (self)
        #f
      handle-property: (self name value)
        error "desktop has no properties (yet)"
      option: (self name)
        case name
          (pass-mouse-on-window-activate:)
            pass-mouse-on-window-activate
          else
            error string-append("invalid option name: " symbol->string(name))
      option-set!: (self name value)
        case name
          (pass-mouse-on-window-activate:)
            assert {boolean? value} "invalid value for option: pass-mouse-on-window-activate:"
            {pass-mouse-on-window-activate := value}
          else
            error string-append("invalid option name: " symbol->string(name))
        self
      ;; do not call this for ignored widgets!
      paint-desktop: (self)
        define old-gofs-x lgfx:global-offset-x
        define old-gofs-y lgfx:global-offset-y
        remove-closed-windows()
        try-finally
          lambda ()
            define cbox self[first-child:]
            while {not-null? cbox}
              if {not {closed?(cbox) or cbox[ignore?:]}}
                then
                  gset! lgfx:global-offset-x cbox[final-x:]
                  gset! lgfx:global-offset-y cbox[final-y:]
                  cbox[paint-with-clip:]
              {cbox := cbox[next-sibling:]}
          lambda ()
            gset! lgfx:global-offset-x old-gofs-x
            gset! lgfx:global-offset-y old-gofs-y
        remove-closed-windows()
        self
      ;; propagate event to children
      ;; return value doesn't matter.
      sink-event: (self evt)
        define cbox
        if evt[alive?:]
          case evt[type:]
            (BROADCAST)
              ;; broadcasts are sent to all windows
              define win-act self[active-window:]
              if {not-null? win-act}
                win-act[handle-event: evt]
              {cbox := self[first-child:]}
              while {{not-null? cbox} and evt[alive?:]}
                if {not {cbox eq? win-act}}
                  cbox[handle-event: evt]
                {cbox := cbox[next-sibling:]}
            (MOUSE-UP)
              if wait-for-mouse-up
                then
                  {wait-for-mouse-up := {not-zero? mouse-button-state-last}}
                  evt[cancel!:]
                else
                  pass-mouse-event self[active-window:] evt
              evt[cancel!:]
            (MOUSE-DOWN MOUSE-MOTION MOUSE-WHEEL)
              if wait-for-mouse-up
                evt[cancel!:]
                pass-mouse-event self[active-window:] evt
            else
              if wait-for-mouse-up
                evt[cancel!:]
                else
                  {cbox := self[active-window:]}
                  if {not-null? cbox}
                    cbox[handle-event: evt]
        #void
      ;; event handler. should pass the event to the destination.
      ;; return value doesn't matter.
      handle-event: (self evt)
        ;remove-closed-windows()
        if evt[alive?:]
          case evt[type:]
            (KEY-DOWN)
              ;if sdl:names-equal?("C-x" evt[keysym:])
              ;  (sdl:post-quit-event)
              ;  self[sink-event: evt]
              self[sink-event: evt]
            (MOUSE-DOWN)
              if {zero? mouse-button-state}
                then
                  define win self[find-window-at: evt[gx:] evt[gy:]]
                  cond
                    {null? win}
                      {wait-for-mouse-up := #t}
                      evt[cancel!:]
                    win[active-window?:]
                      pass-mouse-event win evt
                    else
                      ;printf "activate!\n"
                      self[activate-window: win]
                      if pass-mouse-on-window-activate
                        pass-mouse-event win evt
                        {wait-for-mouse-up := #t}
                      if evt[alive?:]
                        evt[eat!:]
                else
                  self[sink-event: evt]
            else
              self[sink-event: evt]
        ;remove-closed-windows()
      ;;
      ;; desktop API
      ;;
      has-any-windows?: (self)
        define cbox self[first-child:]
        while {{not-null? cbox} and closed?(cbox)}
          {cbox := cbox[next-sibling:]}
        {not-null? cbox}
      ;; first-level child
      find-window-at: (self lx ly)
        define cbox self[last-child:]
        while {{not-null? cbox} and {not cbox[ignore?:]} and
               {not closed?(cbox)} and
               {not cbox[xy-in-box?: {lx - cbox[final-x:]}
                                     {ly - cbox[final-y:]}]}}
          {cbox := cbox[prev-sibling:]}
        cbox
      ;;
      find-window-by-id: (self id)
        define res #nil
        define cbox self[first-child:]
        while {{null? res} and {not-null? cbox}}
          if {cbox[id:] eqv? id}
            {res := cbox}
          {cbox := cbox[next-sibling:]}
        res
      ;;
      active-window: (self)
        define cbox self[last-child:]
        while {{not-null? cbox} and closed?(cbox)}
          {cbox := cbox[prev-sibling:]}
        cbox
      activate-window: (self win)
        if {{not-null? win} and {not closed?(win)}}
          then
            assert {win[parent-box:] eq? self} "cannot activate alien window"
            assert {win[desktop:] eq? self} "cannot activate alien window"
            define prev-top-win self[active-window:]
            if {not {prev-top-win eq? win}}
              then
                need-repaint!()
                prev-top-win[%send-window-blur-event:]
                self[remove-child: win]
                self[append-child: win]
                win[%send-window-focus-event:]
        self
      append-window: (self win)
        if {not-null? win}
          then
            assert {not closed?(win)} "cannot reappend recently closed window" ;FIXME!
            assert {null? win[parent-box:]} "cannot append alien window"
            assert {null? win[desktop:]} "cannot append alien window"
            need-repaint!()
            if {zero? win[final-width:]}
              do-layout win
            define prev-top-win self[active-window:]
            if {not-null? prev-top-win}
              prev-top-win[%send-window-blur-event:]
            self[append-child: win]
            ;printf "added; parent-box=%d\n" box-debug-id(invoke(win parent-box:))
            win[desktop-set!: desktop]
            win[%send-window-focus-event:]
        self
      close-window: (self win)
        if {{not-null? win} and {not closed?(win)} and {not closing?(win)}}
          then
            ;printf "close-window: %d\n" box-debug-id(win)
            ;printf "tbox=%d\n" box-debug-id(invoke(win parent-box:))
            assert {win[parent-box:] eq? self} "cannot close alien window"
            need-repaint!()
            append-closing-window win
            try-finally
              lambda ()
                define top-win self[active-window:]
                if {top-win eq? win}
                  top-win[%send-window-blur-event:]
                append-closed-window win
                {top-win := self[active-window:]}
                if {not-null? top-win}
                  top-win[%send-window-focus-event:]
              lambda ()
                remove-closing-window win
        self
      ;;
      close-all-windows: (self)
        define win self[first-child:]
        while {not-null? win}
          if {not closed?(win)}
            self[close-window: win]
          {win := win[next-sibling:]}
      ;;
      else
        lay-box


;; public API
define close-all-windows()
  if {not-null? desktop}
    then
      desktop[close-all-windows:]
      desktop[%process-closed-windows:]


;; public API
define append-window(win)
  if {null? desktop}
    gset! desktop create-desktop()
  desktop[fix-dimensions:]
  assert {{not-null? win} and win[box?:] and {null? win[parent-box:]}}
    "invalid window root box"
  win[activate-default-widget:]
  desktop[append-window: win]
  win


;; public API
define center-window(win)
  define desk win[desktop:]
  assert {not-null? desk} "window is not on the desktop"
  define dw desk[final-width:]
  define dh desk[final-height:]
  define ww win[final-width:]
  define wh win[final-height:]
  {win[final-x:] := {{dw - ww} div 2}}
  {win[final-y:] := {{dh - wh} div 2}}
  win


define find-window-by-id(id)
  if {not-null? desktop}
    desktop[find-window-by-id: id]
    #nil


;; public API
define draw-desktop()
  if {not-null? desktop}
    then
      need-repaint-reset!()
      define stt (if debug-render-time ticks-msec() 0)
      desktop[fix-dimensions:]
      desktop[paint-desktop:]
      if debug-render-time
        then
          {stt := {ticks-msec() - stt}}
          printf "RENDER time: %,d milliseconds.\n" stt
