{- |
  This module provides pixel-oriented graphics operations.
  It defines an 'ImageBuffer' type, which is a thin layer
  over a GTK+ 'Pixbuf'. It then provides functions for
  doing pixel-oriented graphics on this without having to
  do a lot of bit-twiddling math by hand. It also
  provides for quickly and easily loading and saving
  images, and displaying them on the screen.

  In the simplest case, you can import this module alone
  and never need to touch the actual GTK modules;
  however, this module provides access to the underlying
  GTK resources as well, in case you want to use it as
  part of a larger GTK program.

  Note that /displaying/ an image on screen requires
  you to run the GTK event loop. If you only want to
  load, save and process image files, you can completely
  ignore the GTK event loop. (You do still need to call
  'init_system', however.)
-}

module Graphics.EasyRaster.GTK
    (
      -- * System initialisation
      init_system,

      -- * Types
      Coord, Channel,
      ImageFileType (..), type_string,
      ImageBuffer (),

      -- * Image buffer creation
      ib_new, ib_load,

      -- * Image buffer output
      ib_save,

      -- * Image buffer queries
      ib_size, ib_coords, ib_coords2, ib_valid_coords,

      -- * Pixel I/O
      ib_write_pixel, ib_read_pixel,

      -- * Image display
      ib_display, ib_canvas,

      -- * GTK event loop
      process_event, wait_event, main_loop,

      -- * Low-level GTK access
      ib_pixbuf

      -- * Example code

      {- |
        The following short program should give you an idea
        of how to use this library. It draws a simple graphic
        and saves it to disk.

        > module Main where
        >
        > import Graphics.EasyRaster.GTK
        >
        > main = do
        >   init_system
        >   ibuf <- ib_new (640, 480)
        >   mapM_ (\p -> ib_write_pixel ibuf p (colour p)) (ib_coords ibuf)
        >   ib_save ibuf IFT_PNG "Example1.png"
        >
        > colour :: (Coord, Coord) -> (Channel, Channel, Channel)
        > colour (x, y) = (floor $ fromIntegral x / 640 * 255, floor $ fromIntegral y / 480 * 255, 0)
      -}
    )
  where

import Data.Word
import Data.Array.IO
import Data.Array.Base (unsafeWrite, unsafeRead)
import Graphics.UI.Gtk
import Graphics.UI.Gtk.Gdk.GC -- Necessary with gtk-0.11.2 for some reason.

{- |
  Initialise the GTK runtime system. This function /must/
  be called before any other functions in this module.
  (If not, your program will likely summarily crash with
  an obscure error to @stderr@.)

  Note that Gtk2hs by default will crash with a warning
  message if the program is compiled with the threaded
  RTS; this module disables that warning. However, be
  warned: You must /only/ call the GTK-based functions
  from thread 0.
-}
init_system :: IO ()
init_system = unsafeInitGUIForThreadedRTS >> return ()

{- |
  Process one GTK event if there are any waiting, otherwise
  do nothing.
-}
process_event :: IO ()
process_event = mainIterationDo False >> return ()

{- |
  Process one GTK event. Wait for an event to arrive if
  necessary.
-}
wait_event :: IO ()
wait_event = mainIterationDo True >> return ()

{- |
  Run the GTK \"main loop\" until GTK's 'mainQuit'
  function is called. (Note that this means that
  thread 0 can do no other work while the main
  loop is running.)
-}
main_loop :: IO ()
main_loop = mainGUI

-- | The type of pixel coordinates.
type Coord   = Int

-- | The type of pixel channels.
type Channel = Word8

{- |
  An @ImageBuffer@ is a space-efficient grid of 24-bit
  RGB pixels (i.e., 8 bits per channel) that the GTK
  library can also access. (The latter gives us the
  ability to load\/save\/display the graphics, rather
  than just manipulate it in memory.)
-}
data ImageBuffer = ImageBuffer
  {
    pixbuf :: Pixbuf,
    sizeX, sizeY :: Coord,
    rowstride :: Coord,
    raw :: PixbufData Coord Channel
  }

{- |
  Used to indicate image file type.
-}
data ImageFileType =
    IFT_PNG | -- ^ PNG image.
    IFT_JPEG  -- ^ JPEG image.
  deriving (Eq, Enum, Bounded, Read, Show)

{- |
  Convert an 'ImageFileType' to a plain string (all
  lower-case).
-}
type_string :: ImageFileType -> String
type_string IFT_PNG  = "png"
type_string IFT_JPEG = "jpeg"

{- |
  Make a new 'ImageBuffer' of the specified size. Valid
  coordinates will be in the range (0,0) to (x-1, y-1),
  with (0,0) being the top-left corner of the image.
-}
ib_new :: (Int,Int) -> IO ImageBuffer
ib_new (x,y) = do
  core <- pixbufNew ColorspaceRgb False 8 x y
  build core

{- |
  Load a graphics file into a newly created 'ImageBuffer'.
  The file type is determined automatically (any type that
  GTK+ supports), as is the size for the 'ImageBuffer'.
  (This can be queried later.)
-}
ib_load :: FilePath -> IO ImageBuffer
ib_load f = do
  core <- pixbufNewFromFile f
  build core

{- |
  Save an 'ImageBuffer' into a file with the specified
  file type. Note that the filename should include an
  appropriate suffix (e.g., @.png@). The type of file is
  determined by the 'ImageFileType' argument, /not/ by
  the filename.
-}
ib_save :: ImageBuffer -> ImageFileType -> FilePath -> IO ()
ib_save buf t f = do
  pixbufSave (pixbuf buf) f (type_string t) []
  return ()

{- |
  Returns a GTK+ canvas object which displays the graphcs
  in the 'ImageBuffer' and will automatically repaint
  itself in response to damage.
-}
ib_canvas :: ImageBuffer -> IO DrawingArea
ib_canvas ib = do
    canvas <- drawingAreaNew
    canvas `onExposeRect` (redraw canvas)
    return canvas
  where
    redraw canvas _ = do
      dc <- widgetGetDrawWindow canvas
      gc <- gcNew dc
      drawPixbuf dc gc (pixbuf ib) 0 0 0 0 (-1) (-1) RgbDitherNone 0 0
      return ()

{- |
  This is a quick shortcut for displaying an image
  on screen. Given an 'ImageBuffer' and a window
  title, this will display the image in a new window.
  Clicking the window's close button will call GTK's
  'mainQuit' function. For example,

  > ib_display buf "My Window Title"
  > main_loop

  will display @buf@ on screen and halt the program
  (or at least thread 0) until the user clicks the
  close button. Crude, but useful for quick testing.
-}
ib_display :: ImageBuffer -> String -> IO ()
ib_display ib title = do
  window <- windowNew
  windowSetTitle window title
  windowSetDefaultSize window (sizeX ib) (sizeY ib)
  canvas <- ib_canvas ib
  containerAdd window canvas
  window `onDestroy` mainQuit
  widgetShowAll window

build :: Pixbuf -> IO ImageBuffer
build core = do
  sx <- pixbufGetWidth     core
  sy <- pixbufGetHeight    core
  s  <- pixbufGetRowstride core
  r  <- pixbufGetPixels    core
  return $ ImageBuffer
    {
      pixbuf = core,
      sizeX = sx,
      sizeY = sy,
      rowstride = s,
      raw = r
    }

{- |
  Return the underlying GTK 'Pixbuf' used by an 'ImageBuffer'.
  (In case you want to do something to it with GTK.)
-}
ib_pixbuf :: ImageBuffer -> Pixbuf
ib_pixbuf = pixbuf

{- |
  Query the size of an 'ImageBuffer'. This returns the width
  and height in pixels. Valid coordinates for this @ImageBuffer@
  are from (0,0) to (x-1, y-1), where (x,y) is the value returned
  from @ib_size@.

  Note that this is a /pure/ function (since the size of an
  'ImageBuffer' is immutable).
-}
ib_size :: ImageBuffer -> (Coord,Coord)
ib_size buf = (sizeX buf, sizeY buf)

{- |
  Determine whether the specified pixel coordinates are valid
  for this 'ImageBuffer' (i.e., check they're not off the
  edge of the image).
-}
ib_valid_coords :: ImageBuffer -> (Coord,Coord) -> Bool
ib_valid_coords buf (x,y) = x < sizeX buf && y < sizeY buf

{- |
  Return a list containing all valid coordinates for this 'ImageBuffer'.
  Useful when you want to process all pixels in the image; now you
  can just use 'mapM' or whatever.
-}
ib_coords :: ImageBuffer -> [(Coord,Coord)]
ib_coords buf = [ (x,y) | y <- [0 .. (sizeY buf) - 1], x <- [0 .. (sizeX buf) - 1] ]

{- |
  Return all valid coordinates as a list of lists instead of a single
  flat list. (In case you need to do something at the end of each row.)
-}
ib_coords2 :: ImageBuffer -> [[(Coord,Coord)]]
ib_coords2 buf = [ [ (x,y) | x <- [0 .. (sizeX buf) - 1] ] | y <- [0 .. (sizeY buf) - 1] ]

{- |
  Overwrite the specified pixel with the specified colour (R, G, B).

  Note that pixel coordinates are /not/ checked, for efficiency reasons.
  However, supplying invalid pixel coordinates may result in behaviour ranging
  from corrupted graphical output to random program crashes. See
  "Graphics.EasyRaster.GTK.Paranoid" for a version of this function with
  bounds checking turned on.
-}
ib_write_pixel :: ImageBuffer -> (Coord,Coord) -> (Channel,Channel,Channel) -> IO ()
ib_write_pixel buf (x,y) (r,g,b) = do
  -- Caution: No range checks!!
  let p = y * rowstride buf + x * 3
  unsafeWrite (raw buf) (p + 0) r
  unsafeWrite (raw buf) (p + 1) g
  unsafeWrite (raw buf) (p + 2) b

{- |
  Read the current (R, G, B) colour valid of the specified pixel.

  Note that pixel coordinates are /not/ checked, for efficiency reasons.
  However, supplying invalid pixel coordinates will at best result in
  gibberish being returned, and at worst a segmentation fault. See
  "Graphics.EasyRaster.Paranoid" for a version of this function with
  bounds checking turned on.
-}
ib_read_pixel :: ImageBuffer -> (Coord,Coord) -> IO (Channel,Channel,Channel)
ib_read_pixel buf (x,y) = do
  -- Cation: No range checks!!
  let p = y * rowstride buf + x * 3
  r <- unsafeRead (raw buf) (p + 0)
  g <- unsafeRead (raw buf) (p + 1)
  b <- unsafeRead (raw buf) (p + 2)
  return (r,g,b)