#include <vectrex.h>

#define PREFER_ACCURATE 1 /* don't define, to avoid longs and take some shortcuts in the arithmetic */
#define SLOWMO 1 /* change to 20 to slow down rotation for debugging */

/*
 * The graphics here are an approximation to the missing game Cube Quest.  It was
 * not one of the original GCE games but did come from around that time. The
 * developer, Paul Allen Newell <pnewell@cs.cmu.edu>, prototyped a game for a
 * laserdisc system using the Vectrex, but the Vectrex version was far from
 * complete and not really very playable.
 * Cube Quest was later realized in 1983/84 as a hybrid vector/raster arcade
 * game by Simutrek, but the author was not entirely happy with it, as it was
 * rushed out before it had been fully developed.

 * There are videos of the Vectrex version of Cube Quest such as:
 * https://www.youtube.com/watch?v=n8vRh4XAsDg 

 * From the latter video it's clear that the intention was to make it a multigame,
 * with each movement across the cube being permitted only if you complete
 * another mission, with the subgames being like Black Widow, Terror Tubes
 * etc - in fact also looking a lot like some of ClockworkRobot's smaller games.

 * Several Youtube videos of the laser version of Cube Quest are available, such as:
 *   https://www.youtube.com/watch?v=nNGBFEeEvKM and https://www.youtube.com/watch?v=xsfq2PXTxSA

 * Also worth a read:
 *   http://www.ataricompendium.com/archives/interviews/paul_allen_newell/interview_paul_allen_newell.html

 * This rewrite is currently very much a Work-In-Progress (WIP) - there is no game logic yet.
 * As discussed on the Facebook Vectrex forum, it would be nice to finish this rewrite to be
 * compatible with the Vectrex Cube Quest (which was partially released at a retro gaming show,
 * but has never been dumped and released to the public), and then to go on and add new original
 * minigames in the spirit of the arcade Cube Quest.

 * This must be compiled in the Vide environment using gcc's -O option in order to acheive acceptable
 * frame rates.

 * The first version of this code used pre-computed lookup tables for the animation, but
 * after some experimentation, it was found that the benefit in speed did not justify the
 * added complexity or amount of RAM used.  Table-driving the animations may still be an
 * option but if we go that route it needs to be done differently from before.
 *
 */

#define MAX_BRIGHTNESS 0x7fU
#define BRIGHT 0x7fU
#define NORMAL 0x3fU
#define DIM 0x28U // adjusted for my vectrex, not for the emulator
#define MIN_SCALE 0x0fU
#define PREFERRED_SCALE 0x78U
#define MAX_SCALE 0xf0U
#define SCREEN_X_ORIGIN (-80)   /* where to place the lower left corner of the cube */

static int beam_x = 0, beam_y = 0;
static long int target_x = 0L, target_y = 0L;
static int vectors_drawn = 0;
static unsigned int Old_Intensity = 0;

// wrap move and draw procedures to allow automatic insertion of Reset0Ref
// in order to keep image stable.
static inline void Move(int y, int x) {
  target_x += (long)x; target_y += (long)y;
}

static void Line(long SuppressMask, long BelongsIn, unsigned int Intensity, int y, int x) {
  // If the line belongs in a particular plane, and the suppression mask is set for
  // that plane, then the line is not drawn.  Planes are defined perpendicular to
  // the given axis.
  long int dx, dy;
  if (Old_Intensity != Intensity) {
    Intensity_a(Old_Intensity = Intensity);    // repeated procedure call overhead is worth avoiding
  }
  if ((beam_x != target_x) || (beam_y != target_y)) {
    if (vectors_drawn >= 2) {// 150 cycles saved per increment. optimum appearance is 2
      Reset0Ref(); beam_x = 0; beam_y = 0; vectors_drawn = 0;
    }
    dy = target_y-(long)beam_y; dx = target_x-(long)beam_x;
    if (dx > 127L || dx < -128L || dy > 127L || dy < -128L) {
      // Too big for a single move. So halve the distance and move twice.
      Moveto_d((int)(dy>>1), (int)(dx>>1)); dx -= dx>>1; dy -= dy>>1; // bring the move back into range
    }
    Moveto_d((int)dy, (int)dx);
    beam_x = (int)target_x; beam_y = (int)target_y;
  }

  if ((SuppressMask&BelongsIn) == 0) Draw_Line_d(y, x); else Moveto_d(y, x); // might be able to optimise this by calling Move()
  vectors_drawn += 1;
  beam_x = (int)(target_x)+x; beam_y = (int)target_y+y;
  target_x = (long)beam_x; target_y = (long)beam_y;
}

// The rot sets in...
#define ROTATION_STEPS 32
int rotation = (0==0);

// This is a dimetric projection.  If we want a trimetric projection,
// to copy the slight tilt of GCE's Cube Quest, then we'll need a Z parameter
// as well; but for now this is good enough...

#ifdef PREFER_ACCURATE
#define ONE 16384
  typedef long POINT;
  typedef long long WPOINT; /* Wide point */
#define S 0
  const long sine[ROTATION_STEPS+1] = {
#define fpsin(theta) sine[theta]
#define fpcos(theta) sine[(int)ROTATION_STEPS-theta]
#define DIV 14  /* sin/cos now in range 0..16k-1 */
#else
#define ONE 16383
  typedef int POINT; // in case we need to switch to long ...
  typedef long WPOINT;
#define S 7
  const int sine[ROTATION_STEPS+1] = {
#define fpsin(theta) sine[theta]
#define fpcos(theta) sine[(int)ROTATION_STEPS-theta]
#define DIV 7  /* sin/cos now in range 0..63 */
#endif
        0>>S,    803>>S,   1605>>S,   2404>>S,   3196>>S,   3980>>S,   4756>>S,   5519>>S,
     6269>>S,   7005>>S,   7723>>S,   8423>>S,   9102>>S,   9759>>S,  10393>>S,  11002>>S, 
    11585>>S,  12139>>S,  12665>>S,  13159>>S,  13622>>S,  14053>>S,  14449>>S,  14810>>S,
    15136>>S,  15426>>S,  15678>>S,  15892>>S,  16069>>S,  16206>>S,  16305>>S,  16364>>S, ONE>>S /* scaled 0:1 -> 0:16384 */
  };
  // because of the way that the sin table was created, it's possible that the calculated cos
  // value is off by 1/64th of 90 degrees!  A mismatch between sin and cos for the same angle
  // could explain the 'wobble' seen during rotations. Need to re-calculate this table...


static inline void map2dYX_to_dimetric(POINT x, POINT y, POINT *screeny, POINT *screenx) {
  //for every 20 x added, X' increases by 50.
  //for every 20 y added, X' increases by 30
  //for every 20 y added, Y' increases by 20
  //for every 20 x added, Y' increases by -10

  // X' = x*50/20 + y*30/20
  // Y' = y*20/20 - x*10/20

  // X' = x*5/2 + y*3/2
  // Y' = y - x/2

  POINT tmp = x>>1L; // accept 1 pt of rounding error for now...
#ifdef PREFER_ACCURATE
  *screenx = (int)(((long)x*5L)>>1L) + y + (y>>1); *screeny = y - tmp;
#else
  *screenx = tmp*5 + y + (y>>1); *screeny = y - tmp;
#endif
}

// generated similarly...
static inline void map2dZY_to_dimetric(POINT z, POINT y, POINT *screeny, POINT *screenx) {
  *screenx = (y>>1)+y;
  *screeny = (z<<1)+y;
}

static inline void map2dZX_to_dimetric(POINT z, POINT x, POINT *screeny, POINT *screenx) {
  *screeny = (z<<1) - (x>>1);
  //*screenx = (x*5)>>1;
#ifdef PREFER_ACCURATE
  *screenx = (int)(((long)x*5L)>>1L);
#else
  *screenx = (x<<1) + (x>>1); //I think that's right.  Untested so far.
#endif
}

#define BASE_Y (0)
#define BASE_X (0)
#define FORTY_Y (40)
#define SIXTY_X (60)
#define MINUSTWENTY_Y (-20)
#define HUNDRED_X (100)

#define X0   1L
#define X1   2L
#define X2   4L
#define Y0   8L
#define Y1  16L
#define Y2  32L
#define Z0  64L
#define Z1 128L
#define Z2 256L

static inline long Suppress(int axis, int plane) {
  int planebit = 1<<plane;                        // group of 3 bits: 210
  return (long)planebit << ((long)(axis-'X')*3L); // 3 groups: ZYX
}


static void draw_cube(long SuppressMask) {
  // The suppress mask tells the cube drawing code to not draw specific planes,
  // It defines an axis and a plane within that axis.  Every line-drawing call
  // in the cube drawing procedure checks to see if it is disabled, which it
  // will be if the corresponding bit is set.  A line may belong to multiple
  // planes and faces, so it will act on the OR of three separate tests, which
  // it can do efficiently with a single bitmask.

  // The face rotation will now be done separately from the cube drawing.

  // Calling 'draw cube' with Suppress == 0 will draw the entire cube.

  //                  X   Y   Z
  // Suppress mask:   111 111 111
  //                  012 012 012

  int screen_x_origin = SCREEN_X_ORIGIN;
  int z = 0;

  // precompute commonly used values for speed...
  // although doing these on each plane is a bit wasteful!  Could be done
  // once and stored in tables for each rotation angle...

#define Hidden DIM
#define Visible NORMAL

// I know. At one point these were variables.  I'll fix it uo soon...
#define forty_y FORTY_Y
#define sixty_x SIXTY_X
#define minustwenty_y MINUSTWENTY_Y
#define hundred_x HUNDRED_X
#define minusforty_y (-forty_y)
#define minussixty_x (-sixty_x)
#define twenty_y (-minustwenty_y)
#define minushundred_x (-hundred_x)
#define half_of_forty_y (forty_y>>1)
#define half_of_sixty_x (sixty_x>>1)
#define half_of_minustwenty_y (minustwenty_y>>1)
#define half_of_hundred_x (hundred_x>>1)
#define half_of_minusforty_y (minusforty_y>>1)
#define half_of_minussixty_x (minussixty_x>>1)
#define half_of_twenty_y (twenty_y>>1)
#define half_of_minushundred_x (minushundred_x>>1)

/////////////////////////////////////////////////////////////////// Z = 0

  z = 0; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;

  Move(40,screen_x_origin);

  Move(BASE_Y, BASE_X);
                           // Key: 'Z0' means the plane that can rotate around the Z axis, i.e. the X/Y plane, level 0 (top)
  Line(SuppressMask, X0|Z0, Visible, forty_y, sixty_x);           // horizontal front left corner to rear left corner

  Line(SuppressMask, Y2|Z0, Visible, minustwenty_y, hundred_x);   // horizontal rear left corner to rear right corner
  Move(half_of_twenty_y, half_of_minushundred_x);
    Line(SuppressMask, X1|Z0, Visible, minusforty_y, minussixty_x);
    Move(forty_y,sixty_x);                                        // back to front horizontal cross-member
  Move(half_of_minustwenty_y, half_of_hundred_x);

  Line(SuppressMask, X2|Z0, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner

  Move(half_of_forty_y, half_of_sixty_x);
    Line(SuppressMask, Y1|Z0, Visible, twenty_y, minushundred_x);
    Move(minustwenty_y,hundred_x);                                //right to left cross-member
  Move(half_of_minusforty_y, half_of_minussixty_x);

  Line(SuppressMask, Y0|Z0, Visible, twenty_y, minushundred_x);   // horizontal front (right to left)
  Move(minustwenty_y, hundred_x);

/////////////////////////////////////////////////////////////////// Z = 1

  z = 1; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;

  Move(0,screen_x_origin);

  Move(BASE_Y, BASE_X);

  Line(SuppressMask, X0|Z1, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner

  Line(SuppressMask, Y2|Z1, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner

  Move(half_of_twenty_y, half_of_minushundred_x);
  Line(SuppressMask, X1|Z1, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member

  Move(half_of_minustwenty_y, half_of_hundred_x);

  Line(SuppressMask, X2|Z1, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner

  Move(half_of_forty_y, half_of_sixty_x);
    Line(SuppressMask, Y1|Z1, Hidden, twenty_y, minushundred_x);
    Move(minustwenty_y,hundred_x);                                //right to left cross-member
  Move(half_of_minusforty_y, half_of_minussixty_x);

  Line(SuppressMask, Y0|Z1, Visible, twenty_y, minushundred_x); // horizontal front (right to left)

  Move(minustwenty_y, hundred_x);

/////////////////////////////////////////////////////////////////// Z = 2
  z = 2; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;

  Move(-40,screen_x_origin);

  Move(BASE_Y, BASE_X);
  
  { // VERTICALS
    Line(SuppressMask, X0|Y0, Visible, 80, 0); Move(-80, 0); // Leftmost pillar
  }

  Line(SuppressMask, X0|Z2, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner

  { // VERTICALS
    Line(SuppressMask, X0|Y2, Hidden, 80, 0); Move(-80, 0); // back-left corner vertical
    Move(-20, -30);
    Line(SuppressMask, X0|Y1, Hidden, 80, 0); Move(-80, 0); // hidden left face center vertical
    Move(20, 30);
  }

  Line(SuppressMask, Y2|Z2, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner

  Move(half_of_twenty_y, half_of_minushundred_x);
  Line(SuppressMask, X1|Z2, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member

  { // VERTICALS
    Line(SuppressMask, X1|Y2, Hidden, 80, 0); Move(-80, 0); // rear face center vertical
    Move(-20,-30); Line(SuppressMask, X1|Y1, Hidden, 80, 0); Move(-80, 0); Move(20,30); // center vertical
  }

  Move(half_of_minustwenty_y, half_of_hundred_x);
  { // VERTICALS
    Line(SuppressMask, Y2|X2, Visible, 80, 0); Move(-80, 0); // back right corner vertical
  }

  Line(SuppressMask, X2|Z2, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner

  Move(half_of_forty_y, half_of_sixty_x);
    Line(SuppressMask, Y1|Z2, Hidden, twenty_y, minushundred_x);
    Move(minustwenty_y,hundred_x);                          //right to left cross-member
    if (z == 2) {Line(SuppressMask, X2|Y1, Visible, 80, 0); Move(-80, 0);} // right face center vertical
  Move(half_of_minusforty_y, half_of_minussixty_x);

  if (z == 2) {Line(SuppressMask, X2|Y0, Visible, 80, 0); Move(-80, 0);} // front right corner vertical

  Line(SuppressMask, Y0|Z2, Visible, twenty_y, minushundred_x); // horizontal front (right to left)

  Move(half_of_minustwenty_y, half_of_hundred_x);
    if (z == 2) {Line(SuppressMask, X1|Y0, Visible, 80, 0); Move(-80, 0);} // front center vertical
  Move(half_of_minustwenty_y, half_of_hundred_x);

/////////////////////////////////////////////////////////////////////////////// end
}

static int delay=0;
static int theta = 0;
static int turn_direction = 1;

void draw_rotating_plane(int axis, int plane) {
  int screen_x_origin = SCREEN_X_ORIGIN;
  int X, Y,  X_ll = -20, Y_ll = -20, X_ul = -20, Y_ul = 20, X_ur = 20, Y_ur = 20;
  POINT p0x,p0y,p1x,p1y,p2x,p2y;

  Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;

  if (axis == 'X') {
    Move(plane*-10-40, screen_x_origin+plane*50);
  } else if (axis == 'Y') {
    Move(-40+plane*20, screen_x_origin+plane*30);
  } else {
    Move(40-plane*40, -80);
  }

  // calculating this on the fly is a bit more expensive than the pre-computed version
  // (42K cycles per frame) but I'm tempted to live with it.  It only reduces the frame
  // rate below optimum during a short animation sequence which in the game will not run
  // very often...

  // (and if you cheat by doing the rotation only during the scaling-up, it always
  // keeps to around 30K cycles per frame - if necessary, design that into the game...)

  // (X_ll,Y_ll) -> 
  X = (int)((((WPOINT)X_ll * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ll * (WPOINT)fpsin(theta)) >> DIV));
  Y = (int)((((WPOINT)Y_ll * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ll * (WPOINT)fpsin(theta)) >> DIV));
  X += 20; Y += 20;
  // and map to dimetric projection...
  if (axis == 'X') {
    map2dZY_to_dimetric(Y,X, &p0y,&p0x);
  } else if (axis == 'Y') {
    map2dZX_to_dimetric(Y,X, &p0y,&p0x);
  } else {
    map2dYX_to_dimetric(Y,X, &p0y,&p0x);
  }
  // (X_ul,Y_ul) -> 
  X = (int)((((WPOINT)X_ul * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ul * (WPOINT)fpsin(theta)) >> DIV));
  Y = (int)((((WPOINT)Y_ul * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ul * (WPOINT)fpsin(theta)) >> DIV));
  X += 20; Y += 20;
  if (axis == 'X') {
    map2dZY_to_dimetric(Y,X, &p1y,&p1x);
  } else if (axis == 'Y') {
    map2dZX_to_dimetric(Y,X, &p1y,&p1x);
  } else {
    map2dYX_to_dimetric(Y,X, &p1y,&p1x);
  }
  // (X_ur,Y_ur) -> 
  X = (int)((((WPOINT)X_ur * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ur * (WPOINT)fpsin(theta)) >> DIV));
  Y = (int)((((WPOINT)Y_ur * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ur * (WPOINT)fpsin(theta)) >> DIV));
  X += 20; Y += 20;
  if (axis == 'X') {
    map2dZY_to_dimetric(Y,X, &p2y,&p2x);
  } else if (axis == 'Y') {
    map2dZX_to_dimetric(Y,X, &p2y,&p2x);
  } else {
    map2dYX_to_dimetric(Y,X, &p2y,&p2x);
  }
  Move((int)p0y, (int)p0x);

  Line(0, 0, BRIGHT, (int)(p1y-p0y), (int)(p1x-p0x)); // p0 to p1
    Move((int)(p0y-p1y)>>1, (int)(p0x-p1x)>>1);
      Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); Move((int)(p1y-p2y), (int)(p1x-p2x));
    Move((int)(p1y-p0y)>>1, (int)(p1x-p0x)>>1); 
  Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); // p1 to p2
    Move((int)(p1y-p2y)>>1, (int)(p1x-p2x)>>1);
      Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); Move((int)(p1y-p0y), (int)(p1x-p0x));
    Move((int)(p2y-p1y)>>1, (int)(p2x-p1x)>>1);
  Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); // p3 (-p1) to p0
  Line(0, 0, BRIGHT, (int)(p1y-p2y), (int)(p1x-p2x)); // p2 to p3 (-p1)

  delay = delay+1;
  if (delay == SLOWMO) { // set delay to something like 20 to see the details of the rotation wobble
    theta = (theta + turn_direction) & 31;
    if (theta == 0) {
      rotation = (0 != 0); // tell the level above that we've finished a 90 degree rotation
      turn_direction = 0 - turn_direction;
    }
    delay = 0;
  }
}

int main(void)
{
  static long countdown = 0L;
  static char *message = "X AXIS  PLANE 0\x80";
  int demo_axis = 'Z', demo_plane = 0;
  int rotate_level;
  unsigned int scale = MIN_SCALE;
  int delta_scale = 2;

  // Precompute the corners of the square under rotation and fill the table with the delta-moves.
  // from one corner to the next.

  rotate_level = 0; rotation = (0==0);
  theta = 0;
  for (;;) {
    Wait_Recal();
    Intensity_a(Old_Intensity = BRIGHT);                /* set some brightness */
    message[0] = (char)demo_axis; message[14] = (char)demo_plane + '0';
    Print_Str_d(-80, -84, message);
    VIA_t1_cnt_lo = scale;                              /* set scale factor */
    if (rotation) {
      draw_cube(Suppress(demo_axis, demo_plane));
      draw_rotating_plane(demo_axis, demo_plane);
    } else {
      draw_cube(0); // 0 means no suppression
    }
    scale = scale + (unsigned int)delta_scale;
    if (scale >= PREFERRED_SCALE) {
      if (countdown < 0L) {
        //delta_scale = 0-delta_scale; // for zooming in and zooming out...
        rotate_level = ((rotate_level + 1) % 3) - 10; // 0 1 2
        delta_scale = 0;
        rotation = (0==0);
        countdown = 96L;
      }
    } else if (scale <= MIN_SCALE) {
      delta_scale = 0-delta_scale;
    }
    countdown -= 1L;
    if (countdown == 0L) {
      // this is just a trivial animation until the game logic is added...
      rotate_level += 10; delta_scale = 1; scale = MIN_SCALE; rotation = (0==0);
      demo_plane = (demo_plane + 1) % 3;
      if (demo_plane == 0) demo_axis = ((demo_axis-'X' + 1) % 3) + 'X'; // 3 for x y z
    }
  };
  return 0;
}