/* bzflag
 * Copyright (c) 1993-2017 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "common.h"
#include <math.h>
#include "global.h"
#include "Pack.h"
#include "Teleporter.h"
#include "Intersect.h"
#include "MeshTransform.h"


const char* Teleporter::typeName = "Teleporter";


Teleporter::Teleporter()
{
  backLink = NULL;
  frontLink = NULL;
  return;
}


Teleporter::Teleporter(const float* p, float a, float w,
		       float b, float h, float _border, bool _horizontal,
		       bool drive, bool shoot, bool rico) :
			 Obstacle(p, a, w, b, h, drive, shoot, rico),
			 border(_border), horizontal(_horizontal)
{
  finalize();
  return;
}


Teleporter::~Teleporter()
{
  delete backLink;
  delete frontLink;
  return;
}


Obstacle* Teleporter::copyWithTransform(const MeshTransform& xform) const
{
  float newPos[3], newSize[3], newAngle;
  memcpy(newPos, pos, sizeof(float[3]));
  memcpy(newSize, origSize, sizeof(float[3]));
  newAngle = angle;

  MeshTransform::Tool tool(xform);
  bool flipped;
  tool.modifyOldStyle(newPos, newSize, newAngle, flipped);

  Teleporter* copy =
    new Teleporter(newPos, newAngle, newSize[0], newSize[1], newSize[2],
		   border, horizontal, driveThrough, shootThrough, ricochet);

  copy->setName(name);

  return copy;
}

void Teleporter::finalize()
{
  origSize[0] = size[0];
  origSize[1] = size[1];
  origSize[2] = size[2];

  if (!horizontal) {
    size[1] = origSize[1] + (border * 2.0f);
    size[2] = origSize[2] + border;
  }

  // the same as the default Obstacle::getExtents(), except
  // that we use the larger of the border half-width and size[0].
  float sizeX = border * 0.5f;
  if (size[0] > sizeX) {
    sizeX = size[0];
  }
  float xspan = (fabsf(cosf(angle)) * sizeX) + (fabsf(sinf(angle)) * size[1]);
  float yspan = (fabsf(cosf(angle)) * size[1]) + (fabsf(sinf(angle)) * sizeX);
  extents.mins[0] = pos[0] - xspan;
  extents.maxs[0] = pos[0] + xspan;
  extents.mins[1] = pos[1] - yspan;
  extents.maxs[1] = pos[1] + yspan;
  extents.mins[2] = pos[2];
  extents.maxs[2] = pos[2] + size[2];

  makeLinks();

  return;
}


void Teleporter::makeLinks()
{
  int i;

  // make the new pointers to floats,
  // the MeshFace will delete[] them
  float **fvrts = new float*[4];
  float **bvrts = new float*[4];
  float **ftxcds = new float*[4];
  float **btxcds = new float*[4];
  for (i = 0; i < 4; i++) {
    fvrts[i] = fvertices[i];
    bvrts[i] = bvertices[i];
    ftxcds[i] = texcoords[i];
    btxcds[i] = texcoords[i];
  }

  // get the basics
  const float* p = getPosition();
  const float a = getRotation();
  const float w = getWidth();
  const float b = getBreadth();
  const float br = getBorder();
  const float h = getHeight();

  // setup the texcoord coordinates
  const float xtxcd = 1.0f;
  float ytxcd;
  if ((b - br) > 0.0f) {
    ytxcd = h / (2.0f * (b - br));
  } else {
    ytxcd = 1.0f;
  }
  texcoords[0][0] = 0.0f;
  texcoords[0][1] = 0.0f;
  texcoords[1][0] = xtxcd;
  texcoords[1][1] = 0.0f;
  texcoords[2][0] = xtxcd;
  texcoords[2][1] = ytxcd;
  texcoords[3][0] = 0.0f;
  texcoords[3][1] = ytxcd;

  const float cos_val = cosf(a);
  const float sin_val = sinf(a);

  if (!horizontal) {
    const float params[4][2] =
      {{-1.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
    float wlen[2] = { (cos_val * w), (sin_val * w) };
    float blen[2] = { (-sin_val * (b - br)), (cos_val * (b - br)) };

    for (i = 0; i < 4 ;i++) {
      bvrts[i][0] = p[0] + (wlen[0] + (blen[0] * params[i][0]));
      bvrts[i][1] = p[1] + (wlen[1] + (blen[1] * params[i][0]));
      bvrts[i][2] = p[2] + ((h - br) * params[i][1]);
    }
    backLink = new MeshFace(NULL, 4, bvrts, NULL, btxcds,
			    NULL, -1, false, false, true, true, false);

    for (i = 0; i < 4 ;i++) {
      fvrts[i][0] = p[0] - (wlen[0] + (blen[0] * params[i][0]));
      fvrts[i][1] = p[1] - (wlen[1] + (blen[1] * params[i][0]));
      fvrts[i][2] = p[2] + ((h - br) * params[i][1]);
    }
    frontLink = new MeshFace(NULL, 4, fvrts, NULL, ftxcds,
			     NULL, -1, false, false, true, true, false);
  }
  else {
    float xlen = w - br;
    float ylen = b - br;
    bvrts[0][0] = p[0] + ((cos_val * xlen) - (sin_val * ylen));
    bvrts[0][1] = p[1] + ((cos_val * ylen) + (sin_val * xlen));
    bvrts[0][2] = p[2] + h - br;
    bvrts[1][0] = p[0] + ((cos_val * xlen) - (sin_val * -ylen));
    bvrts[1][1] = p[1] + ((cos_val * -ylen) + (sin_val * xlen));
    bvrts[1][2] = p[2] + h - br;
    bvrts[2][0] = p[0] + ((cos_val * -xlen) - (sin_val * -ylen));
    bvrts[2][1] = p[1] + ((cos_val * -ylen) + (sin_val * -xlen));
    bvrts[2][2] = p[2] + h - br;
    bvrts[3][0] = p[0] + ((cos_val * -xlen) - (sin_val * ylen));
    bvrts[3][1] = p[1] + ((cos_val * ylen) + (sin_val * -xlen));
    bvrts[3][2] = p[2] + h - br;
    backLink = new MeshFace(NULL, 4, bvrts, NULL, btxcds,
			    NULL, -1, false, false, true, true, false);

    for (i = 0; i < 4; i++) {
      memcpy(fvrts[i], bvrts[3 - i], sizeof(float[3])); // reverse order
      fvrts[i][2] = p[2] + h; // change the height
    }
    frontLink = new MeshFace(NULL, 4, fvrts, NULL, ftxcds,
			     NULL, -1, false, false, true, true, false);
  }

  return;
}


const char* Teleporter::getType() const
{
  return typeName;
}


const char* Teleporter::getClassName() // const
{
  return typeName;
}


bool Teleporter::isValid() const
{
  if (!backLink->isValid() || !frontLink->isValid()) {
    return false;
  }
  return Obstacle::isValid();
}


float Teleporter::intersect(const Ray& r) const
{
  // expand to include border
  return timeRayHitsBlock(r, getPosition(), getRotation(),
			  getWidth(), getBreadth(), getHeight());
}


void Teleporter::getNormal(const float* p1, float* n) const
{
  // get normal to closest border column (assume column is circular)
  const float* p2 = getPosition();
  const float c = cosf(-getRotation());
  const float s = sinf(-getRotation());
  const float b = 0.5f * getBorder();
  const float d = getBreadth() - b;
  const float j = (c * (p1[1] - p2[1]) + s * (p1[0] - p2[0]) > 0.0f) ? d : -d;
  float cc[2];
  cc[0] = p2[0] + (s * j);
  cc[1] = p2[1] + (c * j);
  getNormalRect(p1, cc, getRotation(), b, b, n);
}


bool Teleporter::inCylinder(const float* p, float radius, float height) const
{
  return (p[2]+height) >= getPosition()[2] &&
	  p[2] <= getPosition()[2] + getHeight() &&
	  testRectCircle(getPosition(), getRotation(),
			 getWidth(), getBreadth(), p, radius);
}


bool Teleporter::inBox(const float* p, float a,
		       float dx, float dy, float dz) const
{
  const float tankTop = p[2] + dz;
  const float teleTop = getExtents().maxs[2];
  const float crossbarBottom = teleTop - getBorder();

  if ((p[2] < crossbarBottom) && (tankTop >= getPosition()[2])) {
    // test individual border columns
    const float c = cosf(getRotation());
    const float s = sinf(getRotation());
    const float d = getBreadth() - (0.5f * getBorder());
    const float r = 0.5f * getBorder();
    float o[2];
    o[0] = getPosition()[0] - (s * d);
    o[1] = getPosition()[1] + (c * d);
    if (testRectRect(p, a, dx, dy, o, getRotation(), r, r)) {
      return true;
    }
    o[0] = getPosition()[0] + (s * d);
    o[1] = getPosition()[1] - (c * d);
    if (testRectRect(p, a, dx, dy, o, getRotation(), r, r)) {
      return true;
    }
  }

  if ((p[2] < teleTop) && (tankTop >= crossbarBottom)) {
    // test crossbar
    if (testRectRect(p, a, dx, dy, getPosition(),
		     getRotation(), getWidth(), getBreadth())) {
      return true;
    }
  }

  return false;
}


bool Teleporter::inMovingBox(const float* oldP, float UNUSED(oldAngle),
			     const float* p, float a,
			     float dx, float dy, float dz) const
{
  float minPos[3];
  minPos[0] = p[0];
  minPos[1] = p[1];
  if (oldP[2] < p[2]) {
    minPos[2] = oldP[2];
  } else {
    minPos[2] = p[2];
  }
  dz += fabsf(oldP[2] - p[2]);
  return inBox(minPos, a, dx, dy, dz);
}


bool Teleporter::isCrossing(const float* p, float a,
			    float dx, float dy, float /* dz */, float* plane) const
{
  // if not inside or contained then not crossing
  const float* p2 = getPosition();
  if (!testRectRect(p, a, dx, dy,
		p2, getRotation(), getWidth(), getBreadth() - getBorder()) ||
		p[2] < p2[2] || p[2] > p2[2] + getHeight() - getBorder())
    return false;
  if (!plane) return true;

  // it's crossing -- choose which wall is being crossed (this
  // is a guestimate, should really do a careful test).  just
  // see which wall the point is closest to.
  const float a2 = getRotation();
  const float c = cosf(-a2);
  const float s = sinf(-a2);
  const float x = c * (p[0] - p2[0]) - s * (p[1] - p2[1]);
  float pw[2];
  plane[0] = ((x < 0.0f) ? -cosf(a2) : cosf(a2));
  plane[1] = ((x < 0.0f) ? -sinf(a2) : sinf(a2));
  pw[0] = p2[0] + getWidth() * plane[0];
  pw[1] = p2[1] + getWidth() * plane[1];

  // now finish off plane equation
  plane[2] = 0.0f;
  plane[3] = -(plane[0] * pw[0] + plane[1] * pw[1]);
  return true;
}


// return true if ray goes through teleporting part
float Teleporter::isTeleported(const Ray& r, int& face) const
{
  // get t's for teleporter with and without border
  const float tb = intersect(r);
  const float t = timeRayHitsBlock(r, getPosition(),
			getRotation(), getWidth(),
			getBreadth() - getBorder(), getHeight() - getBorder());

  // if intersection with border is before one without then doesn't teleport
  // (cos it hit the border first).  also no teleport if no intersection.
  if ((tb >= 0.0f && t - tb > 1e-6) || t < 0.0f)
    return -1.0f;

  // get teleport position.  if above or below teleporter then no teleportation.
  float p[3];
  r.getPoint(t, p);
  p[2] -= getPosition()[2];
  if (p[2] < 0.0f || p[2] > getHeight() - getBorder()) {
    return -1.0f;
  }

  // figure out which face:  rotate intersection into teleporter space,
  //	if to east of teleporter then face 0 else face 1.
  const float x = cosf(-getRotation()) * (p[0] - getPosition()[0]) -
		sinf(-getRotation()) * (p[1] - getPosition()[1]);
  face = (x > 0.0f) ? 0 : 1;
  return t;
}


float Teleporter::getProximity(const float* p, float radius) const
{
  // make sure tank is sufficiently close
  if (!testRectCircle(getPosition(), getRotation(),
			getWidth(), getBreadth() - getBorder(),
			p, 1.2f * radius)) {
    return 0.0f;
  }

  // transform point to teleporter space
  // translate origin
  float pa[3];
  pa[0] = p[0] - getPosition()[0];
  pa[1] = p[1] - getPosition()[1];
  pa[2] = p[2] - getPosition()[2];

  // make sure not too far above or below teleporter
  if (pa[2] < -1.2f * radius ||
      pa[2] > getHeight() - getBorder() + 1.2f * radius)
    return 0.0f;

  // rotate and reflect into first quadrant
  const float c = cosf(-getRotation()), s = sinf(-getRotation());
  const float x = fabsf(c * pa[0] - s * pa[1]);
  const float y = fabsf(c * pa[1] + s * pa[0]);

  // get proximity to face
  float t = 1.2f - x / radius;

  // if along side then trail off as point moves away from faces
  if (y > getBreadth() - getBorder()) {
    float f = (float)(2.0 / M_PI) * atan2f(x, y - getBreadth() + getBorder());
    t *= f * f;
  }
  else if (pa[2] < 0.0f) {
    float f = 1.0f + pa[2] / (1.2f * radius);
    if (f >= 0.0f && f <= 1.0f) t *= f * f;
  }
  else if (pa[2] > getHeight() - getBorder()) {
    float f = 1.0f - (pa[2] - getHeight() + getBorder()) / (1.2f * radius);
    if (f >= 0.0f && f <= 1.0f) t *= f * f;
  }

  return t > 0.0f ? (t > 1.0f ? 1.0f : t) : 0.0f;
}


bool Teleporter::hasCrossed(const float* p1, const float* p2, int& f) const
{
  // check above/below teleporter
  const float* p = getPosition();
  if ((p1[2] < p[2] && p2[2] < p[2]) ||
	(p1[2] > p[2] + getHeight() - getBorder() &&
	 p2[2] > p[2] + getHeight() - getBorder()))
    return false;

  const float c = cosf(-getRotation()), s = sinf(-getRotation());
  const float x1 = c * (p1[0] - p[0]) - s * (p1[1] - p[1]);
  const float x2 = c * (p2[0] - p[0]) - s * (p2[1] - p[1]);
  const float y2 = c * (p2[1] - p[1]) + s * (p2[0] - p[0]);
  if (x1 * x2 < 0.0f && fabsf(y2) <= getBreadth() - getBorder()) {
    f = (x1 > 0.0f) ? 0 : 1;
    return true;
  }
  return false;
}


void Teleporter::getPointWRT(const Teleporter& t2, int face1, int face2,
			     const float* pIn, const float* dIn, float aIn,
			     float* pOut, float* dOut, float* aOut) const
{
  // transforming pIn to pOut, going from tele1(this) to tele2
  const Teleporter& tele1 = *this;
  const Teleporter& tele2 = t2;

  const float* p1 = tele1.getPosition();
  const float* p2 = tele2.getPosition();
  const float* s1 = tele1.getSize();
  const float* s2 = tele2.getSize();

  const fvec3  pos1 (p1[0], p1[1], p1[2]);
  const fvec3 size1 (s1[0], s1[1], s1[2]);
  const fvec3  pos2 (p2[0], p2[1], p2[2]);
  const fvec3 size2 (s2[0], s2[1], s2[2]);

  const float bord1 = tele1.getBorder();
  const float bord2 = tele2.getBorder();

  // y & z axis scaling factors  (relative active areas)
  const fvec2 dims1(size1.y - bord1, size1.z - bord1);
  const fvec2 dims2(size2.y - bord2, size2.z - bord2);
  const fvec2 dimsScale = (dims2 / dims1);

  // adjust the angles for the faces
  // NOTE: if (face1 == face2), there's an extra 180 degrees spin
  const float pi = float(M_PI);
  const float radians1 = tele1.getRotation() + ((face1 == 0) ? 0.0f : pi);
  const float radians2 = tele2.getRotation() + ((face2 == 1) ? 0.0f : pi);

  // intermediate output position
  fvec3 p(pIn[0], pIn[1], pIn[2]);

  // translate to origin, and revert rotation
  p -= pos1;
  p = p.rotateZ(-radians1);

  // fixed x offset, and scale y & z coordinates
  p.x = -size2.x;
  p.y *= dimsScale.x;
  p.z *= dimsScale.y;

  // apply rotation, translate to new position
  p = p.rotateZ(+radians2);
  p += pos2;

  // final output position
  pOut[0] = p.x;
  pOut[1] = p.y;
  pOut[2] = p.z;

  // fill in output angle and direction variables, if requested
  const float a = radians2 - radians1;
  if (aOut) {
    *aOut = aIn + a;
  }
  if (dOut && dIn) {
    const float c = cosf(a);
    const float s = sinf(a);
    const float dx = dIn[0];
    const float dy = dIn[1];
    dOut[0] = (c * dx) - (s * dy);
    dOut[1] = (c * dy) + (s * dx);
    dOut[2] = dIn[2];
  }
}


bool Teleporter::getHitNormal(const float* pos1, float azimuth1,
			      const float* pos2, float azimuth2,
			      float width, float breadth, float,
			      float* normal) const
{
  return Obstacle::getHitNormal(pos1, azimuth1,
			pos2, azimuth2, width, breadth,
			getPosition(), getRotation(), getWidth(), getBreadth(),
			getHeight(), normal) >= 0.0f;
}



void* Teleporter::pack(void* buf) const
{
  buf = nboPackStdString(buf, name);

  buf = nboPackVector(buf, pos);
  buf = nboPackFloat(buf, angle);
  buf = nboPackVector(buf, origSize);
  buf = nboPackFloat(buf, border);

  unsigned char horizontalByte = horizontal ? 1 : 0;
  buf = nboPackUByte(buf, horizontalByte);

  unsigned char stateByte = 0;
  stateByte |= isDriveThrough() ? _DRIVE_THRU : 0;
  stateByte |= isShootThrough() ? _SHOOT_THRU : 0;
  stateByte |= canRicochet()    ? _RICOCHET   : 0;
  buf = nboPackUByte(buf, stateByte);

  return buf;
}


const void* Teleporter::unpack(const void* buf)
{
  buf = nboUnpackStdString(buf, name);

  buf = nboUnpackVector(buf, pos);
  buf = nboUnpackFloat(buf, angle);
  buf = nboUnpackVector(buf, size);
  buf = nboUnpackFloat(buf, border);

  unsigned char horizontalByte;
  buf = nboUnpackUByte(buf, horizontalByte);
  horizontal = (horizontalByte == 0) ? false : true;

  unsigned char stateByte;
  buf = nboUnpackUByte(buf, stateByte);
  driveThrough = (stateByte & _DRIVE_THRU) != 0;
  shootThrough = (stateByte & _SHOOT_THRU) != 0;
  ricochet     = (stateByte & _RICOCHET)   != 0;

  finalize();

  return buf;
}


int Teleporter::packSize() const
{
  int fullSize = 0;
  fullSize += nboStdStringPackSize(name);
  fullSize += sizeof(float[3]); // pos
  fullSize += sizeof(float);    // rotation
  fullSize += sizeof(float[3]); // size
  fullSize += sizeof(float);    // border
  fullSize += sizeof(uint8_t);  // horizontal
  fullSize += sizeof(uint8_t);  // state bits
  return fullSize;
}


void Teleporter::print(std::ostream& out, const std::string& indent) const
{
  out << indent << "teleporter";
  if (name.size() > 0) {
    out << " " << name;
  }
  out << std::endl;
  const float *_pos = getPosition();
  out << indent << "  position " << _pos[0] << " " << _pos[1] << " "
				 << _pos[2] << std::endl;
  out << indent << "  size " << origSize[0] << " " << origSize[1] << " "
			     << origSize[2] << std::endl;
  out << indent << "  rotation " << ((getRotation() * 180.0) / M_PI)
				 << std::endl;
  out << indent << "  border " << getBorder() << std::endl;
  if (horizontal) {
    out << indent << "  horizontal" << std::endl;
  }
  if (ricochet) {
    out << indent << "  ricochet" << std::endl;
  }
  out << indent << "end" << std::endl;
  return;
}


static void outputFloat(std::ostream& out, float value)
{
  char buffer[32];
  snprintf(buffer, 30, " %.8f", value);
  out << buffer;
  return;
}

void Teleporter::printOBJ(std::ostream& out, const std::string& UNUSED(indent)) const
{
  int i;
  float verts[8][3] = {
    {-1.0f, -1.0f, 0.0f},
    {+1.0f, -1.0f, 0.0f},
    {+1.0f, +1.0f, 0.0f},
    {-1.0f, +1.0f, 0.0f},
    {-1.0f, -1.0f, 1.0f},
    {+1.0f, -1.0f, 1.0f},
    {+1.0f, +1.0f, 1.0f},
    {-1.0f, +1.0f, 1.0f}
  };
  float norms[6][3] = {
    {0.0f, -1.0f, 0.0f}, {+1.0f, 0.0f, 0.0f},
    {0.0f, +1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f},
    {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, +1.0f}
  };
  float txcds[4][2] = {
    {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}
  };
  MeshTransform xform;
  const float degrees = getRotation() * (float)(180.0 / M_PI);
  const float zAxis[3] = {0.0f, 0.0f, +1.0f};
  xform.addScale(getSize());
  xform.addSpin(degrees, zAxis);
  xform.addShift(getPosition());
  xform.finalize();
  MeshTransform::Tool xtool(xform);
  for (i = 0; i < 8; i++) {
    xtool.modifyVertex(verts[i]);
  }
  for (i = 0; i < 6; i++) {
    xtool.modifyNormal(norms[i]);
  }

  out << "# OBJ - start tele" << std::endl;
  out << "o bztele_" << getObjCounter() << std::endl;

  for (i = 0; i < 8; i++) {
    out << "v";
    outputFloat(out, verts[i][0]);
    outputFloat(out, verts[i][1]);
    outputFloat(out, verts[i][2]);
    out << std::endl;
  }
  for (i = 0; i < 4; i++) {
    out << "vt";
    outputFloat(out, txcds[i][0]);
    outputFloat(out, txcds[i][1]);
    out << std::endl;
  }
  for (i = 0; i < 6; i++) {
    out << "vn";
    outputFloat(out, norms[i][0]);
    outputFloat(out, norms[i][1]);
    outputFloat(out, norms[i][2]);
    out << std::endl;
  }
  out << "usemtl telefront" << std::endl;
  out << "f -7/-4/-5 -6/-3/-5 -2/-2/-5 -3/-1/-5" << std::endl;
  out << "usemtl teleback" << std::endl;
  out << "f -5/-4/-3 -8/-3/-3 -4/-2/-3 -1/-1/-3" << std::endl;
  out << "usemtl telerim" << std::endl;
  out << "f -5/-4/-2 -6/-3/-2 -7/-2/-2 -8/-1/-2" << std::endl;
  out << "f -4/-4/-1 -3/-3/-1 -2/-2/-1 -1/-1/-1" << std::endl;
  out << "f -8/-4/-6 -7/-3/-6 -3/-2/-6 -4/-1/-6" << std::endl;
  out << "f -6/-4/-4 -5/-3/-4 -1/-2/-4 -2/-1/-4" << std::endl;

  out << std::endl;

  incObjCounter();

  return;
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 8 ***
// c-basic-offset: 2 ***
// indent-tabs-mode: t ***
// End: ***
// ex: shiftwidth=2 tabstop=8
