--
--	sdlraster: SDL-based raster display
--	Copyright (C) 2007  University of Kent
--	Copyright (C) 2007  Adam Sampson <ats@offog.org>
--
--	This library is free software; you can redistribute it and/or
--	modify it under the terms of the GNU Lesser General Public
--	License as published by the Free Software Foundation, either
--	version 2 of the License, or (at your option) any later version.
--
--	This library is distributed in the hope that it will be useful,
--	but WITHOUT ANY WARRANTY; without even the implied warranty of
--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
--	Lesser General Public License for more details.
--
--	You should have received a copy of the GNU Lesser General Public
--	License along with this library.  If not, see
--	<http://www.gnu.org/licenses/>.
--

--** SDL raster interface.
--
-- This module provides an SDL-based portable bitmap ("raster") display.
--
-- sdlraster uses the ring pattern.
-- [@ref RASTER] arrays cycle around a ring of processes.
-- One of the processes is [@ref raster.display] (or [@ref raster.display.simple]),
-- which generates fresh rasters on startup, and draws rasters it receives to
-- the display before passing them around the ring again.
-- The other processes in the ring can do whatever they like to the rasters
-- before passing them on.
--
-- To shut down the ring, a raster of height zero is sent around the
-- ring, replacing one of the existing rasters.
-- Each process passes it on and then exits, aside from the process that sent
-- it which just waits to receive it again before exiting.
-- (You can use [@ref raster.shutdown] to do this.)
--
-- To avoid races with multiple shutdown messages, only one process is permitted
-- to initiate shutdown; that is, if you're using [@ref raster.display.simple],
-- then you mustn't try to shut down the ring from any other process.
--
-- Programs that don't care about input events can use [@ref
-- raster.display.simple], which simply shuts down the ring when the user closes
-- the window.
-- If you want to handle keyboard or mouse input, [@ref raster.display] provides an
-- extra channel which delivers SDL events as [@ref RASTER.EVENT]
-- structures; in that case, you must shut down the ring using [@ref
-- raster.shutdown] when you want to exit.
--
-- (This module also provides the old [@code SDLRaster] and [@code miniraster]
-- interfaces for backwards compatibility, but you should not use them in new
-- code.)
--
-- @module sdlraster

--{{{  includes
#INCLUDE "occSDL.module"
#INCLUDE "raster.module"
#INCLUDE "sdlraster.inc"
--}}}

--{{{  constants
--* The maximum number of initial rasters allowed in the ring.
VAL INT MAX.RASTERS IS 256:

--* The time between event checks, in nanoseconds.
VAL INT EVENT.CHECK.TIME IS 10000:
--}}}

--{{{  raster utilities
--{{{  PROC raster.clear
--* Clear a raster to a solid colour.
PROC raster.clear (RASTER r, VAL INT colour)
  SDL.Surface box:
  INT result:
  SEQ
    occ.SDL.make.surface (r, box)
    ASSERT (box <> 0)
    INITIAL SDL.Rect rect IS 0:
    INITIAL INT c IS colour:
    SDL.FillRect (result, box, rect, c)
    ASSERT (result = 0)
    SDL.FreeSurface (box)
:
--}}}
--}}}

--{{{  PROC raster.one.place.buffer
--* One-place raster.one.place.buffer.
-- @private
PROC raster.one.place.buffer (CHAN RASTER in?, out!, VAL INT width, height)
  INITIAL RASTER r IS MOBILE [height][width]INT:
  SEQ
    raster.clear (r, #000000)

    WHILE (SIZE r) > 0
      SEQ
        out ! r
        in ? r
    out ! r
:
--}}}

--{{{  PROC raster.convert.event
--* Convert an [@ref SDL.Event] into a [@ref RASTER.EVENT].
PROC raster.convert.event (SDL.Event event, RESULT RASTER.EVENT ev)
  BYTE type:
  SEQ
    ev := [-1, 0, 0, 0, 0]
    get.SDL.Event.type (type, event)
    CASE INT type
      SDL.QUIT
        --{{{  quit event
        ev[type] := RASTER.ET.QUIT
        --}}}
      SDL.KEYDOWN, SDL.KEYUP
        --{{{  keyboard event
        SDL.keysym sym:
        INT n:
        SEQ
          CASE INT type
            SDL.KEYDOWN
              ev[type] := RASTER.ET.KEYDOWN
            SDL.KEYUP
              ev[type] := RASTER.ET.KEYUP
          INITIAL SDL.KeyboardEvent ke IS SDL.KeyboardEvent event:
          get.SDL.KeyboardEvent.keysym (sym, ke)
          -- Temporary variable used here to avoid a KRoC bug.
          get.SDL.keysym.sym (n, sym)
          ev[id] := n
          get.SDL.keysym.mod (n, sym)
          ev[mod] := n
        --}}}
      SDL.MOUSEBUTTONDOWN, SDL.MOUSEBUTTONUP
        --{{{  mouse button event
        SEQ
          CASE INT type
            SDL.MOUSEBUTTONDOWN
              ev[type] := RASTER.ET.MOUSEBUTTONDOWN
            SDL.MOUSEBUTTONUP
              ev[type] := RASTER.ET.MOUSEBUTTONUP
          INITIAL SDL.MouseButtonEvent me IS SDL.MouseButtonEvent event:
          BYTE b:
          INT16 n:
          SEQ
            get.SDL.MouseButtonEvent.button (b, me)
            ev[id] := INT b
            -- Temporary variable used here to avoid a KRoC
            -- bug.
            get.SDL.MouseButtonEvent.x (n, me)
            ev[x] := n
            get.SDL.MouseButtonEvent.y (n, me)
            ev[y] := n
          SDLMod mod:
          SEQ
            SDL.GetModState (mod)
            ev[mod] := INT mod
        --}}}
      SDL.MOUSEMOTION
        --{{{  mouse motion event
        SEQ
          ev[type] := RASTER.ET.MOUSEMOTION
          INITIAL SDL.MouseMotionEvent me IS SDL.MouseMotionEvent event:
          INT16 n:
          SEQ
            get.SDL.MouseMotionEvent.x (n, me)
            ev[x] := n
            get.SDL.MouseMotionEvent.y (n, me)
            ev[y] := n
        --}}}
      SDL.VIDEOEXPOSE
        --{{{  window exposure
        ev[type] := RASTER.ET.EXPOSE
        --}}}
      ELSE
        SKIP
:
--}}}

--{{{  PROC raster.events
--* Fetch events from SDL.
-- @private
PROC raster.events (SDL.Surface screen, CHAN RASTER.EVENT out!, CHAN BOOL kill?)
  SDL.Event event:
  TIMER tim:
  INT next.update:

  INITIAL BOOL is.fullscreen IS FALSE:
  --{{{  PROC toggle.fullscreen
  PROC toggle.fullscreen ()
    SEQ
      -- Ignore the error; this won't work on all displays.
      INT result:
      SDL.WM.ToggleFullScreen (result, screen)
      is.fullscreen := NOT is.fullscreen
  :
  --}}}

  SEQ
    --{{{  initialise
    create.SDL.Event (event)
    tim ? next.update
    --}}}

    INITIAL BOOL running IS TRUE:
    WHILE running
      PRI ALT
        tim ? AFTER next.update
          SEQ
            --{{{  check for events
            INITIAL BOOL checking IS TRUE:
            WHILE checking
              INT result:
              SEQ
                SDL.PollEvent (result, event)
                IF
                  result > 0
                    --{{{  decode event
                    RASTER.EVENT ev:
                    SEQ
                      raster.convert.event (event, ev)
                      --{{{  check for special keys
                      VAL INT ANY.ALT IS (KMOD.LALT \/ KMOD.RALT) \/ (KMOD.LMETA \/ KMOD.RMETA):
                      IF
                        (ev[type] = RASTER.ET.KEYDOWN) AND (ev[id] = SDLK.RETURN) AND ((ev[mod] /\ ANY.ALT) <> 0)
                          --{{{  Alt-Enter: toggle full-screen mode
                          toggle.fullscreen ()
                          --}}}
                        (ev[type] = RASTER.ET.KEYDOWN) AND (ev[id] = SDLK.BACKSPACE) AND ((ev[mod] /\ ANY.ALT) <> 0)
                          --{{{  Alt-Backspace: STOP
                          SEQ
                            --{{{  leave full-screen mode first
                            IF
                              is.fullscreen
                                toggle.fullscreen ()
                              TRUE
                                SKIP
                            --}}}
                            STOP
                          --}}}
                        TRUE
                          out ! ev
                      --}}}
                    --}}}
                  TRUE
                    checking := FALSE
            --}}}
            next.update := next.update PLUS EVENT.CHECK.TIME
        BOOL b:
        kill ? b
          running := FALSE

    --{{{  shut down
    INITIAL RASTER.EVENT ev IS [RASTER.ET.SHUTDOWN, 0, 0, 0, 0]:
    out ! ev
    delete.SDL.Event (event)
    --}}}
:
--}}}

--{{{  PROC raster.display
--* Raster display process.
-- Generate a number of rasters on startup, and draw incoming rasters to the
-- display. In addition, report any events received on the display.
--
-- This process provides [@code num.rasters + 1] places of buffering in the
-- ring.
--
-- @param title Window title
-- @param width Width of window
-- @param height Height of window
-- @param num.rasters Initial number of rasters to inject into the ring.
--   This must be at least 1, and no more than [@ref MAX.RASTERS].
-- @param in Raster input
-- @param out Raster output
-- @param events Input events.
--   When it shuts down, the raster will send a [@ref RASTER.ET.SHUTDOWN] event
--   as its last message, so you know when to stop reading from this channel.
PROC raster.display (VAL []BYTE title, VAL INT width, height, num.rasters,
                 CHAN RASTER in?, out!, CHAN RASTER.EVENT events!)
  SDL.Surface screen:
  #PRAGMA SHARED screen
  INT result:
  SEQ
    --{{{  initialise
    ASSERT (num.rasters >= 1)
    ASSERT (num.rasters <= MAX.RASTERS)

    occ.SDL.Init (result, SDL.INIT.VIDEO)
    ASSERT (result = 0)

    MOBILE []BYTE titlez:
    SEQ
      occ.SDL.zero.terminate (title, titlez)
      SDL.WM.SetCaption (titlez, titlez)

    SDL.SetVideoMode (screen, width, height, 32,
                      (SDL.HWSURFACE \/ SDL.DOUBLEBUF) \/ SDL.ANYFORMAT)
    ASSERT (screen <> 0)
    --}}}

    [MAX.RASTERS - 1]CHAN RASTER cs:
    CHAN BOOL kill.ev:
    PAR
      --{{{  raster.one.place.buffers
      PAR i = 0 FOR num.rasters - 1
        raster.one.place.buffer (cs[i]?, cs[i + 1]!, width, height)
      raster.one.place.buffer (cs[num.rasters - 1]?, out!, width, height)
      --}}}
      --{{{  event server
      raster.events (screen, events!, kill.ev?)
      --}}}
      --{{{  update display
      SEQ
        INITIAL BOOL running IS TRUE:
        WHILE running
          RASTER r:
          SEQ
            in ? r

            IF
              (SIZE r) = 0
                running := FALSE
              TRUE
                --{{{  update screen
                SEQ
                  occ.SDL.blit.raster (r, 0, 0, SIZE r[0], SIZE r, screen, 0, 0)
                  SDL.Flip (result, screen)
                  ASSERT (result = 0)
                --}}}

            cs[0] ! r
        kill.ev ! TRUE
      --}}}

    --{{{  shut down
    --}}}
:
--}}}

--{{{  PROC raster.shutdown
--* Shut down a raster ring.
-- You must have read a raster from the ring yourself before calling this.
-- This outputs an empty raster, and then discards incoming rasters until the
-- empty one's received again.
PROC raster.shutdown (CHAN RASTER in?, out!)
  INITIAL RASTER r IS MOBILE [0][0]INT:
  SEQ
    out ! r
    INITIAL BOOL running IS TRUE:
    WHILE running
      SEQ
        in ? r
        running := (SIZE r) <> 0
:
--}}}

--{{{  PROC raster.autoclose
--* Respond to [@ref EVENT.QUIT] messages by shutting down the ring;
-- otherwise just pass rasters through.
-- @private
PROC raster.autoclose (CHAN RASTER in?, out!, CHAN RASTER.EVENT events?)
  INITIAL BOOL running IS TRUE:
  INITIAL BOOL quitting IS FALSE:
  WHILE running
    PRI ALT
      RASTER.EVENT ev:
      events ? ev
        --{{{  event coming in
        CASE ev[type]
          RASTER.ET.QUIT
            quitting := TRUE
          ELSE
            SKIP
        --}}}
      RASTER r:
      in ? r
        --{{{  raster coming in
        SEQ
          IF
            quitting
              SEQ
                PAR
                  raster.shutdown (in?, out!)
                  --{{{  wait for the final event
                  INITIAL BOOL waiting IS TRUE:
                  WHILE waiting
                    RASTER.EVENT ev:
                    SEQ
                      events ? ev
                      waiting := ev[type] <> RASTER.ET.SHUTDOWN
                  --}}}
                running := FALSE
            (SIZE r) = 0
              running := FALSE
            TRUE
              SKIP
          out ! r
        --}}}
:
--}}}

--{{{  PROC raster.display.simple
--* Simple interface for programs that don't need to handle events.
-- This automatically shuts down the ring when the window is closed;
-- otherwise it behaves like [@ref raster.display].
PROC raster.display.simple (VAL []BYTE title, VAL INT width, height, num.buffers,
                        CHAN RASTER in?, out!)
  CHAN RASTER.EVENT events:
  CHAN RASTER thru:
  PAR
    raster.display (title, width, height, num.buffers, in?, thru!, events!)
    raster.autoclose (thru?, out!, events?)
:
--}}}

