// originally by Andy Sloane for the Arduboy
// https://www.a1k0n.net/2017/02/01/arduboy-teapot.html

#include <Wire.h>
#include "Tiny4kOLED.h"

#include <avr/pgmspace.h>
#include "draw.h"
#include "sincos.h"
#include "vec.h"
#include "teapot.h"


const int MPU_addr = 0x68;    // I2C address of the MPU-6050
uint8_t screen_[1024];
int32_t angle_A_ = 0, angle_B_ = 0, angle_C_ = -16384;
int16_t scale_ = 1024 + 500;
uint16_t frame_ = 0;
static Vec216 verts[mesh_NVERTS];  // rotated, projected screen space vertices


void setup(void) {
  Wire.begin();
  oled.begin(128, 64, sizeof(tiny4koled_init_128x64br), tiny4koled_init_128x64br);

  // Initialize the MPU-6050 and test if it is connected.
  Wire.beginTransmission( MPU_addr);
  Wire.write( 0x6B);  // PWR_MGMT_1 register
  Wire.write( 0);     // set to zero (wakes up the MPU-6050)
  auto error = Wire.endTransmission();
}

void ReadInput() {
  static bool manual_control = false;

  Wire.beginTransmission( MPU_addr);
  Wire.write( 0x43);  // starting with register 0x43 (GYRO_XOUT_H)
  Wire.endTransmission( false);
  Wire.requestFrom( MPU_addr, 6);  // request a total of 6 bytes
  int16_t gx = Wire.read()<<8 | Wire.read();  // 0x43 (GYRO_XOUT_H)  & 0x44 (GYRO_XOUT_L)
  int16_t gy = Wire.read()<<8 | Wire.read();  // 0x45 (GYRO_YOUT_H)  & 0x46 (GYRO_YOUT_L)
  int16_t gz = Wire.read()<<8 | Wire.read();  // 0x47 (GYRO_ZOUT_H)  & 0x48 (GYRO_ZOUT_L)

  if (gx || gy || gz) {
    manual_control = true;
    angle_A_ += gx / 16;
    angle_B_ += gy / 16;
    angle_C_ += gz / 16;
  }

  if (!manual_control) {
    angle_A_ += 499;
    angle_B_ += 331;
    angle_C_ += 229;
  }
}


// down-convert signed 1.10 precision to signed 0.7 precision
// (scaling down from -1024..1024 to -127..127)
static int8_t RescaleR(int16_t x) {
  return ((uint32_t) x*127 + 512) >> 10;
}

void DrawObject() {
  // construct rotation matrix
  int16_t cA, sA, cB, sB, cC, sC;

  GetSinCos((angle_A_ >> 6) & 1023, &sA, &cA);
  GetSinCos((angle_B_ >> 6) & 1023, &sB, &cB);
  GetSinCos((angle_C_ >> 6) & 1023, &sC, &cC);

  // rotate about X axis by C, then Y axis by B, then Z axis by A
  //     [            cA*cB,             cB*sA,    sB]
  // R = [-cA*sB*sC - cC*sA,  cA*cC - sA*sB*sC, cB*sC]
  //     [-cA*cC*sB + sA*sC, -cA*sC - cC*sA*sB, cB*cC]

  // local coordinate frame given rotation values
  // spend some time up front to get an accurate rotation matrix before
  // rounding to 0.7 fixed point
  // the 32-bit math is only done once per frame, and then we can do
  // all the per-vertex stuff in 8-bit math 
  Mat338 rotation_matrix(Vec38(
        RescaleR((int32_t) cA*cB >> 10),
        RescaleR((int32_t) cB*sA >> 10),
        RescaleR(sB)),
      Vec38(
        RescaleR(((int32_t) -cA*sB*sC >> 10) - (int32_t) cC*sA >> 10),
        RescaleR((int32_t) cA*cC - ((int32_t) sA*sB*sC >> 10) >> 10),
        RescaleR((int32_t) cB*sC >> 10)),
      Vec38(
        RescaleR(((int32_t) -cA*cC*sB >> 10) + (int32_t) sA*sC >> 10),
        RescaleR((int32_t) -cA*sC - ((int32_t) cC*sA*sB >> 10) >> 10),
        RescaleR((int32_t) cB*cC >> 10)));

  int8_t sortaxis = 0, sortaxisz = rotation_matrix.z.x;
  if (abs(rotation_matrix.z.y) > abs(sortaxisz)) {
    sortaxis = 1;
    sortaxisz = rotation_matrix.z.y;
  }
  if (abs(rotation_matrix.z.z) > abs(sortaxisz)) {
    sortaxis = 2;
    sortaxisz = rotation_matrix.z.z;
  }

  rotation_matrix.RotateAndProject(mesh_vertices, mesh_NVERTS, scale_, verts);

  // rotate and project all vertices
  /*
  {
    Vec216 *vertptr = verts;
    for (uint16_t j = 0; j < 3*mesh_NVERTS; j += 3) {
      Vec38 obj_vert(
          pgm_read_byte_near(mesh_vertices + j),
          pgm_read_byte_near(mesh_vertices + j + 1),
          pgm_read_byte_near(mesh_vertices + j + 2));
      Vec38 world_vert(
          Fx.dot(obj_vert),
          Fy.dot(obj_vert),
          Fz.dot(obj_vert));
      world_vert.project(scale_, vertptr++);
    }
  }
  */

  // back-face cull and sort faces
  for (uint16_t i = 0; i < mesh_NFACES; i++) {
    uint16_t jf = i;
    // use face sort order depending on which axis is most facing toward camera
    if (sortaxisz < 0) {
      jf = mesh_NFACES - 1 - i;
    }
    if (sortaxis == 1) {
      jf = pgm_read_byte_near(mesh_ysort_faces + jf);
    } else if (sortaxis == 2) {
      jf = pgm_read_byte_near(mesh_zsort_faces + jf);
    }
    jf *= 3;
    uint8_t fa = pgm_read_byte_near(mesh_faces + jf),
            fb = pgm_read_byte_near(mesh_faces + jf + 1),
            fc = pgm_read_byte_near(mesh_faces + jf + 2);
    Vec216 sa = verts[fb] - verts[fa];
    Vec216 sb = verts[fc] - verts[fa];
    if ((int32_t) sa.x * sb.y > (int32_t) sa.y * sb.x) {  // check winding order
      continue;  // back-facing
    }

    int8_t illum = rotation_matrix.CalcIllumination(mesh_normals + jf);
    uint8_t pat[4];
    GetDitherPattern(illum, pat);
    FillTriangle(
        verts[fa].x, verts[fa].y,
        verts[fb].x, verts[fb].y,
        verts[fc].x, verts[fc].y,
        pat, screen_);
  }
}


class Stars {
  static const int kNumStars = 30;
  uint16_t wyhash16_x;

  private:
  // https://github.com/littleli/go-wyhash16/blob/master/wyhash16.go
  uint32_t hash16(uint32_t input, uint32_t key) {
    uint32_t hash = input * key;
    return ((hash >> 16) ^ hash) & 0xFFFF;
  }

  uint16_t wyhash16() {
    wyhash16_x += 0xfc15;
    return hash16(wyhash16_x, 0x2ab);
  }

  public:
  void Draw() {
    wyhash16_x = 0xabcd;  // reseed our private PRNG
    for (uint8_t i = 0; i < kNumStars; i++) {
      uint16_t randval = wyhash16();
      uint8_t xpos = randval ^ (randval >> 8);
      uint8_t ypos = (randval >> 8) & 63;
      uint8_t xspeed = 1 + ((randval + (randval >> 13)) & 7);
 
      uint16_t page = (ypos >> 3) << 7;
      uint8_t mask = 1 << (ypos & 7);
      uint8_t x = xpos - xspeed * frame_;
      x >>= 1;
      screen_[page + x] |= mask;
    }
  }
};

Stars stars_;

void loop(void) {
  memset(screen_, 0, 1024);
  stars_.Draw();
  ReadInput();
  DrawObject();

  uint8_t *addr = screen_;
  for (uint8_t y = 0; y < 8; y++) {
    oled.setCursor(0, y);
    oled.startData();
    for (uint8_t i = 0; i < 128; i++)
      oled.sendData(*addr++);
    oled.endData();
  }
  frame_++;
}