/*
 * The Amiga version of raypaint will open a 16 gray level black & white
 * screen or a 8/32 color screen (depending on whether hires is required for
 * the requested screen width or not) of the appropriate size. It will use
 * hires, lace, overscan, or autoscroll to accommodate the entire picture.
 * Both PAL and NTSC machines should be catered for.
 * 
 * If you have a three button mouse, the middle button should work as
 * described in the README file. If you do not, pressing both mouse
 * buttons will do the same thing (remember to deactivate any background
 * programs like DMouse that use pressing both mouse buttons to flip
 * between screens, etc). Some of these programs can be fooled by pressing
 * the right mouse button first.
 * 
 * The code will only work on version 2.0 or higher of AmigaDOS.
 * 
 * Send comments and bug reports to:
 * 
 * Kriton Kyrimis	UUCP:     pythia!theseas!kriton!kyrimis
 *	 		INTERNET: kyrimis@theseas.ntua.gr
 */

#include <stdio.h>
#include <exec/types.h>
#include <exec/nodes.h>
#include <exec/lists.h>
#include <exec/ports.h>
#include <dos/dos.h>
#include <intuition/intuition.h>
#include <intuition/classes.h>
#include <graphics/gfxbase.h>
#include <graphics/view.h>
#include <graphics/graphint.h>
#include <utility/tagitem.h>

#ifdef __SASC
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/intuition.h>
#include <proto/graphics.h>
#endif
#ifdef __GNUC__
#include <inline/exec.h>
#include <inline/dos.h>
#include <inline/intuition.h>
#include <inline/graphics.h>
#endif

#define MAX_X 362
#define MAX_Y_NTSC 241
#define MAX_Y_PAL 283

#define MAPX(x) ((x) + offsetx)
#define MAPY(y) (sh - (y) - 1 - offsety)

#define UNMAPX(x) ((x) - offsetx)
#define UNMAPY(y) (sh - (y) - 1 - offsety)

static void Cleanup(void);
static int IsPAL(void);
static int CalcWidth(int);
static int CalcHeight(int, int);
static void MouseHandler(void);
static void HandleIDCMP(void);
static void StripIntuiMessages(struct MsgPort *, struct Window *);
static struct MsgPort *CreatePort(UBYTE *, LONG);
static void DeletePort(struct MsgPort *);
static void SetupGrayMap(void);
static void SetupColorMap(int);

struct GfxBase       *GfxBase = NULL;
struct IntuitionBase *IntuitionBase = NULL;

static struct NewScreen screen = {
  0, 0, 0, 0, 0, 0, 1, 0, AUTOSCROLL | CUSTOMSCREEN, NULL, NULL, NULL, NULL
};

UWORD DriPens[] = { (UWORD)~0 };

static struct TagItem ScreenTags[] = {
#define DISPIDTAG 0
  {SA_DisplayID, NULL},
  {SA_Overscan, OSCAN_VIDEO},
  {SA_Pens, (ULONG)DriPens},
  {TAG_DONE, NULL}
};

static struct Image img = {
  0, 0, 10, 10, 0, NULL, 0, 0, NULL
};

static struct Gadget CloseGadget = {
  NULL, 0, 0, 10, 10, GFLG_GADGHCOMP | GFLG_GADGIMAGE, GACT_RELVERIFY,
  GTYP_BOOLGADGET, (APTR)&img, NULL, NULL, 0, NULL, 1, NULL
};

static struct NewWindow window = {
  0, 0, 0, 0, 0, 1, 0,
  WFLG_SMART_REFRESH | WFLG_BACKDROP | WFLG_BORDERLESS | WFLG_RMBTRAP,
  &CloseGadget, NULL, NULL, NULL, NULL, 0, 0, 0, 0, CUSTOMSCREEN
};

static volatile struct Screen *s = NULL;
static volatile struct Window *w = NULL;
static struct RastPort *rp = NULL;
static struct ViewPort *vp = NULL;

static int maxx, maxy, sw, sh, offsetx, offsety;
static volatile int Done = 0;
static struct Task *HandlerTask = NULL;
static volatile int LMB=0;
static volatile int MMB=0;
static volatile int RMB=0;
static volatile struct MsgPort *HandlerPort;

static int gray, max_colors;

void
GraphicsInit(int xsize, int ysize, char *name, int gray)
{
  BYTE pal;

  atexit(Cleanup);
  if ((GfxBase =
      (struct GfxBase *) OpenLibrary ("graphics.library", 0L)) == NULL){
    fprintf (stderr, "Can't open graphics.library\n");
    Cleanup();
    exit(RETURN_FAIL);
  }

  if ((IntuitionBase =
      (struct IntuitionBase *) OpenLibrary ("intuition.library", 0L)) == NULL){
    fprintf (stderr, "Can't open intuition.library\n");
    Cleanup();
    exit(RETURN_FAIL);
  }

  pal = IsPAL();
  maxx = xsize - 1;
  maxy = ysize - 1;
  sw = CalcWidth(xsize);
  screen.Width = sw;
  sh = CalcHeight(ysize, pal);
  screen.Height = sh;
  screen.DefaultTitle = name;
  offsetx = (sw - xsize) / 2;
  offsety = (sh - ysize) / 2;
  if (pal) {
    screen.ViewModes =
      ((sh > MAX_Y_PAL) ? LACE : 0) | ((sw > MAX_X) ? HIRES : 0);
  }else{
    screen.ViewModes =
      ((sh > MAX_Y_NTSC) ? LACE : 0) | ((sw > MAX_X) ? HIRES : 0);
  }
  window.Width = sw, window.Height = sh;
  ScreenTags[DISPIDTAG].ti_Data = screen.ViewModes;
  if (pal) {
    ScreenTags[DISPIDTAG].ti_Data |= PAL_MONITOR_ID;
  }else{
    ScreenTags[DISPIDTAG].ti_Data |= NTSC_MONITOR_ID;
  }
  if (gray) {
    screen.Depth = 4;
  }else{
    if (screen.ViewModes & HIRES) {
      screen.Depth = 3;
    }else{
      screen.Depth = 5;
    }
  }
  if ((s = OpenScreenTagList (&screen, ScreenTags)) == NULL){
    fprintf (stderr, "Can't open screen 1\n");
    Cleanup();
    exit(RETURN_FAIL);
  }
  ShowTitle (s, FALSE);
  window.Screen = s;
  if ((w = OpenWindow (&window)) == NULL){
    fprintf (stderr, "Can't open window\n");
    Cleanup();
    exit(RETURN_FAIL);
  }
  vp = &(s->ViewPort);
  rp = w->RPort;

  if (gray) {
    SetupGrayMap();
  }else{
    SetupColorMap(screen.Depth);
  }

  HandlerTask =
    CreateTask("Raypaint Mouse Handler", 1, (APTR)MouseHandler, 10000);
}

static void
SetupGrayMap()
{
  int i;

  gray = 1;
  for (i=0; i<16; i++) {
    SetRGB4 (vp, i,  i, i, i);
  }
}

static void
SetupColorMap(int depth)
{
  int col;
  int r, g, b, red, green, blue;
  double one_over_gamma = 0.4;

  gray = 0;
  if (depth == 3) {
    max_colors = 2;
  }else{
    max_colors = 3;
  }
  col = 0;
  for (r = 0; r < max_colors; ++r) {
    red = 15 * r / (max_colors-1);
    for (g = 0; g < max_colors; ++g) {
      green = 15 * g / (max_colors-1);
      for (b = 0; b < max_colors; ++b) {
        blue = 15 * b / (max_colors-1);
	SetRGB4 (vp, col++,  red, green, blue);
      }
    }
  }
}

static int
IsPAL(void)
{
  struct Screen *s;
  ULONG mode;

  s = LockPubScreen("Workbench");
  mode = GetVPModeID(&(s->ViewPort));
  UnlockPubScreen(NULL, s);
  if ((mode & PAL_MONITOR_ID) == PAL_MONITOR_ID){
    return TRUE;
  }
  if ((mode & NTSC_MONITOR_ID) == NTSC_MONITOR_ID){
    return FALSE;
  }
  /* Used to be this simple... */
  return (GfxBase->DisplayFlags & PAL) ? TRUE : FALSE;
}

static int
CalcWidth(int width)
{
  if (width <= 320) return 320;
  if (width <= MAX_X) return width;
  if (width <= 640) return 640;
  return width;
}

static int
CalcHeight(int height, int pal)
{
  if (pal) {
    if (height <= 256) return 256;
    if (height <= MAX_Y_PAL) return height;
    if (height <= 512) return 512;
    return height;
  }else{
    if (height <= 200) return 200;
    if (height <= MAX_Y_NTSC) return height;
    if (height <= 400) return 400;
    return height;
  }
}

/*
 * Draw the pixel at (xp, yp) in the color given by the rgb-triple,
 * 0 indicating 0 intensity, 255 max intensity.
 */
void
GraphicsDrawPixel(int xp, int yp, unsigned char color[3])
{
  int val;

  if (Done) {
    Cleanup();
    exit(RETURN_OK);
  }

  if (gray) {
    val = (((0.35*(double)(color[0]) +
	     0.55*(double)(color[1]) +
	     0.10*(double)(color[2])) * 15.0) / 255.0) + 0.5;
  }else{
    val = color[0] / 255.0 * (max_colors-1) + 0.5;
    val *= max_colors;
    val += color[1] / 255.0 * (max_colors-1) + 0.5;
    val *= max_colors;
    val += color[2] / 255.0 * (max_colors-1) + 0.5;
  }
  SetAPen(rp, val);
  WritePixel(rp, MAPX(xp), MAPY(yp));
}

/*
 * Draw the rect with lower left corner (xp, yp) and upper right
 * corner (xp+ys, yp+ys).  The colors of the l-l, l-r, u-r, and u-l
 * corners are given as arrays of unsigned chars as above.
 */
void
GraphicsDrawRectangle(int xp, int yp, int xs, int ys,
                      unsigned char ll[3], unsigned char lr[3], 
                      unsigned char ur[3], unsigned char ul[3])
{
  int val;

  if (Done) {
    Cleanup();
    exit(RETURN_OK);
  }
  if (gray) {
    val = (((0.35*(double)(ll[0]) +
	     0.55*(double)(ll[1]) +
	     0.10*(double)(ll[2])) * 15.0) / 255.0) + 0.5;
  }else{
    val = ll[0] / 255.0 * (max_colors-1) + 0.5;
    val *= max_colors;
    val += ll[1] / 255.0 * (max_colors-1) + 0.5;
    val *= max_colors;
    val += ll[2] / 255.0 * (max_colors-1) + 0.5;
  }
  SetAPen(rp, val);
  RectFill(rp, MAPX(xp), MAPY(yp+ys), MAPX(xp+xs), MAPY(yp));
}

int
GraphicsRedraw(void)
{
  /* This is a SMART_REFRESH window;
     We don't have to worry about redrawing it */
  return 0;
}

void
GraphicsGetMousePos(int *x, int *y)
{
  *x = UNMAPX(w->MouseX);
  *y = UNMAPY(w->MouseY);
  if (*x < 0) {
    *x = 0;
  }
  if (*x > maxx) {
    *x = maxx;
  }
  if (*y < 0) {
    *y = 0;
  }
  if (*y > maxy) {
    *y = maxy;
  }
}

int
GraphicsLeftMouseEvent(void)
{
  return LMB;
}

int
GraphicsRightMouseEvent(void)
{
  return RMB;
}

int
GraphicsMiddleMouseEvent(void)
{
  return MMB || (LMB && RMB);
}

static void
Cleanup(void)
{
  if (HandlerTask) {
    Signal(HandlerTask, 1 << HandlerPort->mp_SigBit);
    Delay(2);
    HandlerTask = NULL;
  }
  if (w) {
    CloseWindow(w);
    w = NULL;
  }
  if (s) {
    CloseScreen(s);
    s = NULL;
  }
  if (GfxBase) {
    CloseLibrary((struct Library *)GfxBase);
  }
  if (IntuitionBase) {
    CloseLibrary ((struct Library *)IntuitionBase);
  }
}

static void
MouseHandler(void)
{
  ULONG winsignalmask, portsignalmask, signals;
  struct MsgPort *IDCMPPort;
  struct IntuitionBase *IntuitionBase;

  IntuitionBase =
    (struct IntuitionBase *) OpenLibrary ("intuition.library", 0L);
  IDCMPPort = CreatePort("RayPaint IDCMP Port", 0);
  w->UserPort = IDCMPPort;
  ModifyIDCMP(w, IDCMP_GADGETUP | IDCMP_MOUSEBUTTONS);

  HandlerPort = CreatePort("RayPaint Handler Port", 0);

  for(;;){
    winsignalmask  = 1L << w->UserPort->mp_SigBit;
    portsignalmask = 1L << HandlerPort->mp_SigBit;

    signals = Wait(winsignalmask | portsignalmask);
    if (signals & winsignalmask) {
      HandleIDCMP();
    }
    if (signals & portsignalmask){
      Forbid();
      StripIntuiMessages(w->UserPort, w);
      w->UserPort = NULL;
      ModifyIDCMP(w, 0);
      Permit();
      DeletePort(IDCMPPort);
      DeletePort(HandlerPort);
      CloseLibrary ((struct Library *)IntuitionBase);
      Done = 1;
      return;
    }
  }
}

static void
HandleIDCMP(void)
{
  struct IntuiMessage *message;
  ULONG class;
  USHORT code;

  while (message = (struct IntuiMessage *)GetMsg(w->UserPort)) {
    class = message->Class;
    code = message->Code;

    switch(class) {
      case IDCMP_GADGETUP:
        Done = 1;
        break;
      case IDCMP_MOUSEBUTTONS:
        switch(code) {
	  case SELECTUP:
	    LMB = 0;
	    break;
	  case SELECTDOWN:
	    LMB = 1;
	    break;
	  case MENUUP:
	    RMB = 0;
	    break;
	  case MENUDOWN:
	    RMB = 1;
	    break;
	  case MIDDLEUP:
	    MMB = 0;
	    break;
	  case MIDDLEDOWN:
	    MMB = 1;
	    break;
        }
        break;
    }
    ReplyMsg((struct Message *)message);
  }
}

static void
StripIntuiMessages(struct MsgPort *mp, struct Window *win)
{
  struct IntuiMessage *msg;
  struct Node *succ;

  msg = (struct IntuiMessage *)mp->mp_MsgList.lh_Head;

  while (succ = msg->ExecMessage.mn_Node.ln_Succ) {
    if (msg->IDCMPWindow == win) {
      Remove((struct Node*)msg);
      ReplyMsg((struct Message *)msg);
    }
    msg = (struct IntuiMessage *)succ;
  }
}

static struct MsgPort *
CreatePort(UBYTE *name, LONG pri)
{
  struct MsgPort *newmp;

  if (newmp = CreateMsgPort()) {
    newmp->mp_Node.ln_Name = name;
    newmp->mp_Node.ln_Pri = pri;
    AddPort(newmp);
  }
  return newmp;
}

static void
DeletePort(struct MsgPort *mp)
{
  if (mp) {
    if (mp->mp_Node.ln_Name) {
      RemPort(mp);
    }
    DeleteMsgPort(mp);
  }
}
