//-----------------------------------------------------------------------------
// Product:     OpenCTM tools
// File:        lwo.cpp
// Description: Implementation of the LWO file format importer/exporter.
//-----------------------------------------------------------------------------
// Copyright (c) 2009-2010 Marcus Geelnard
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
//     1. The origin of this software must not be misrepresented; you must not
//     claim that you wrote the original software. If you use this software
//     in a product, an acknowledgment in the product documentation would be
//     appreciated but is not required.
//
//     2. Altered source versions must be plainly marked as such, and must not
//     be misrepresented as being the original software.
//
//     3. This notice may not be removed or altered from any source
//     distribution.
//-----------------------------------------------------------------------------

#include <stdexcept>
#include <fstream>
#include <cstring>
#include <string>
#include "lwo.h"

#ifdef _MSC_VER
  typedef unsigned int uint32;
#else
  #include <stdint.h>
  typedef uint32_t uint32;
#endif

using namespace std;


//------------------------------------------------------------------------------
// Helper functions for reading
//------------------------------------------------------------------------------

/// Read a 16-bit integer, endian independent.
static uint32 ReadU2(istream &aStream)
{
  unsigned char buf[2];
  aStream.read((char *) buf, 2);
  return (((uint32) buf[0]) << 8) |
         ((uint32) buf[1]);
}

/// Read a 32-bit integer, endian independent.
static uint32 ReadU4(istream &aStream)
{
  unsigned char buf[4];
  aStream.read((char *) buf, 4);
  return (((uint32) buf[0]) << 24) |
         (((uint32) buf[1]) << 16) |
         (((uint32) buf[2]) << 8) |
         ((uint32) buf[3]);
}

/// Read a 32-bit floating point scalar, endian independent.
static float ReadF4(istream &aStream)
{
  unsigned char buf[4];
  aStream.read((char *) buf, 4);
  union {
    uint32 i;
    float  f;
  } val;
  val.i = (((uint32) buf[0]) << 24) |
          (((uint32) buf[1]) << 16) |
          (((uint32) buf[2]) << 8) |
          ((uint32) buf[3]);
  return val.f;
}

/// Read a 3 x 32-bit floating point vector, endian independent.
/// Note: We flip the Y & Z axes, to go from our right handed to LightWaves
/// left handed coordinate system.
static Vector3 ReadVEC12(istream &aStream)
{
  unsigned char buf[12];
  Vector3 result;
  aStream.read((char *) buf, 12);
  union {
    uint32 i;
    float  f;
  } val;
  val.i = (((uint32) buf[0]) << 24) |
          (((uint32) buf[1]) << 16) |
          (((uint32) buf[2]) << 8) |
          ((uint32) buf[3]);
  result.x = val.f;
  val.i = (((uint32) buf[4]) << 24) |
          (((uint32) buf[5]) << 16) |
          (((uint32) buf[6]) << 8) |
          ((uint32) buf[7]);
  result.z = val.f;
  val.i = (((uint32) buf[8]) << 24) |
          (((uint32) buf[9]) << 16) |
          (((uint32) buf[10]) << 8) |
          ((uint32) buf[11]);
  result.y = val.f;
  return result;
}

/// Read a non-terminated string from the stream (e.g. a chunk ID string).
static string ReadString(istream &aStream, int aCount)
{
  string result;
  result.resize(aCount);
  aStream.read((char *) &result[0], aCount);
  return result;
}

/// Read a zero terminated string from a stream.
static string ReadStringZ(istream &aStream)
{
  string str;
  getline(aStream, str, '\0');
  if((str.size() & 1) == 0)
    aStream.get();
  return str;
}

/// Read a vertex index (variable size) from a stream.
static uint32 ReadVX(istream &aStream, int * aBytesLeft)
{
  uint32 result = ReadU2(aStream);
  if(result >= 0x0000ff00)
  {
    result = ((result & 255) << 16) | ReadU2(aStream);
    *aBytesLeft -= 4;
  }
  else
    *aBytesLeft -= 2;
  return result;
}


//------------------------------------------------------------------------------
// Helper functions for writing
//------------------------------------------------------------------------------

/// Write a 16-bit integer, endian independent.
static void WriteU2(ostream &aStream, uint32 aValue)
{
  unsigned char buf[2];
  buf[0] = (aValue >> 8) & 255;
  buf[1] = aValue & 255;
  aStream.write((char *) buf, 2);
}

/// Write a 32-bit integer, endian independent.
static void WriteU4(ostream &aStream, uint32 aValue)
{
  unsigned char buf[4];
  buf[0] = (aValue >> 24) & 255;
  buf[1] = (aValue >> 16) & 255;
  buf[2] = (aValue >> 8) & 255;
  buf[3] = aValue & 255;
  aStream.write((char *) buf, 4);
}

/// Write a 32-bit floating point scalar, endian independent.
static void WriteF4(ostream &aStream, float aValue)
{
  unsigned char buf[4];
  union {
    uint32 i;
    float  f;
  } val;
  val.f = aValue;
  buf[0] = (val.i >> 24) & 255;
  buf[1] = (val.i >> 16) & 255;
  buf[2] = (val.i >> 8) & 255;
  buf[3] = val.i & 255;
  aStream.write((char *) buf, 4);
}

/// Write a 3 x 32-bit floating point vector, endian independent.
/// Note: We flip the Y & Z axes, to go from our right handed to LightWaves
/// left handed coordinate system.
static void WriteVEC12(ostream &aStream, Vector3 aValue)
{
  unsigned char buf[12];
  union {
    uint32 i;
    float  f;
  } val;
  val.f = aValue.x;
  buf[0] = (val.i >> 24) & 255;
  buf[1] = (val.i >> 16) & 255;
  buf[2] = (val.i >> 8) & 255;
  buf[3] = val.i & 255;
  val.f = aValue.z;
  buf[4] = (val.i >> 24) & 255;
  buf[5] = (val.i >> 16) & 255;
  buf[6] = (val.i >> 8) & 255;
  buf[7] = val.i & 255;
  val.f = aValue.y;
  buf[8] = (val.i >> 24) & 255;
  buf[9] = (val.i >> 16) & 255;
  buf[10] = (val.i >> 8) & 255;
  buf[11] = val.i & 255;
  aStream.write((char *) buf, 12);
}

/// Write a string to a stream (no zero termination).
static void WriteString(ostream &aStream, const char * aString)
{
  int len = strlen(aString);
  aStream.write(aString, len);
}

/// Write a zero terminated string to a stream.
static void WriteStringZ(ostream &aStream, const char * aString)
{
  int len = strlen(aString) + 1;
  aStream.write(aString, len);
  if(len & 1)
  {
    char zero = 0;
    aStream.write(&zero, 1);
  }
}

/// Write a vertex index (variable size) to a stream.
static void WriteVX(ostream &aStream, uint32 aIndex)
{
  if(aIndex < 0x0000ff00)
    WriteU2(aStream, aIndex);
  else
    WriteU4(aStream, aIndex + 0xff000000);
}

/// Calculate the size of a POLS chunk - take variable size indices into
/// account...
static uint32 CalcPOLSSize(Mesh * aMesh)
{
  uint32 triCount = (uint32) (aMesh->mIndices.size() / 3);
  uint32 size = 4 + triCount * 2;
  for(unsigned int i = 0; i < aMesh->mIndices.size(); ++ i)
  {
    uint32 idx = aMesh->mIndices[i];
    if(idx < 0x0000ff00)
      size += 2;
    else
      size += 4;
  }
  return size;
}

/// Calculate the size of a VMAP chunk - take variable size indices into
/// account, but exclude the name string (at least two bytes)...
static uint32 CalcVMAPSize(Mesh * aMesh, uint32 aDimension)
{
  uint32 size = 6 + aMesh->mVertices.size() * (2 + 4 * aDimension);
  uint32 maxIdx = aMesh->mVertices.size() - 1;
  if(maxIdx >= 0x0000ff00)
    size += (maxIdx - 0x0000feff) * 2;
  return size;
}


//------------------------------------------------------------------------------
// Public functions
//------------------------------------------------------------------------------

/// Import a mesh from an LWO file.
void Import_LWO(const char * aFileName, Mesh * aMesh)
{
  // Open the input file
  ifstream f(aFileName, ios_base::in | ios_base::binary);
  if(f.fail())
    throw runtime_error("Could not open input file.");

  // File header
  if(ReadString(f, 4) != string("FORM"))
    throw runtime_error("Not a valid LWO file (missing FORM chunk).");
  uint32 fileSize = ReadU4(f);
  if(ReadString(f, 4) != string("LWO2"))
    throw runtime_error("Not a valid LWO file (not LWO2 format).");

  // Start with an empty mesh
  aMesh->Clear();
  uint32 pointCount = 0;
  uint32 triangleCount = 0;
  uint32 indexBias = 0;
  bool havePoints = false;

  // Current pivot point (based on current layer)
  Vector3 pivot(0.0f, 0.0f, 0.0f);

  // Iterate all chunks
  while(!f.eof() && (f.tellg() < fileSize))
  {
    // Get chunk ID & size (round size to next nearest even size - all chunks
    // are word aligned)
    string chunkID = ReadString(f, 4);
    uint32 chunkSize = (ReadU4(f) + 1) & 0xfffffffe;

    // Get file position of the chunk start
    size_t chunkStart = f.tellg();

    // Was this a supported chunk?
    if(chunkID == string("TEXT"))
    {
      // Read file comment
      aMesh->mComment = string(ReadStringZ(f));
    }
    else if(chunkID == string("LAYR"))
    {
      // Read layer information
      ReadU2(f);            // number
      ReadU2(f);            // flags
      pivot = ReadVEC12(f); // pivot
      ReadStringZ(f);       // name

      size_t pos = f.tellg();
      if((pos - chunkStart) < chunkSize)
        ReadU2(f);          // parent (optional)
    }
    else if(chunkID == string("PNTS"))
    {
      // Check point count
      uint32 newPoints = chunkSize / 12;
      if((newPoints * 12) != chunkSize)
        throw runtime_error("Not a valid LWO file (invalid PNTS chunk).");

      // Read points (relative to current pivot point)
      aMesh->mVertices.resize(pointCount + newPoints);
      for(uint32 i = pointCount; i < (uint32) aMesh->mVertices.size(); ++ i)
        aMesh->mVertices[i] = ReadVEC12(f) + pivot;
      indexBias = pointCount;
      pointCount += newPoints;
      havePoints = true;
    }
    else if(chunkID == string("POLS"))
    {
      // POLS before PNTS?
      if(!havePoints)
        throw runtime_error("Not a valid LWO file (POLS chunk before PNTS chunk).");

      // Check that we have a FACE or PTCH descriptor.
      string type = ReadString(f, 4);
      if((type == string("FACE")) || (type == string("PTCH")))
      {
        // Perpare for worst case triangle count (a single poly with only
        // 16-bit indices)
        uint32 maxTriCount = (chunkSize - 10) / 2;
        vector<uint32> indices;
        indices.resize(maxTriCount * 3);

        // Read polygons
        uint32 newTris = 0;
        int bytesLeft = (int) chunkSize - 4;
        while(bytesLeft > 0)
        {
          int polyNodes = (int) ReadU2(f) & 1023;
          bytesLeft -= 2;
          if(polyNodes >= 3)
          {
            polyNodes -= 3;
            uint32 idx[3];
            idx[0] = ReadVX(f, &bytesLeft);
            idx[1] = ReadVX(f, &bytesLeft);
            idx[2] = ReadVX(f, &bytesLeft);
            while((polyNodes >= 0) && (bytesLeft >= 0))
            {
              indices[newTris * 3] = idx[0];
              indices[newTris * 3 + 1] = idx[1];
              indices[newTris * 3 + 2] = idx[2];
              ++ newTris;
              if(polyNodes > 0)
              {
                idx[1] = idx[2];
                idx[2] = ReadVX(f, &bytesLeft);
              }
              -- polyNodes;
            }
          }
          else
          {
            // Skip polygons with less than 3 nodes
            for(int i = 0; i < polyNodes; ++ i)
              ReadVX(f, &bytesLeft);
          }
        }

        // Copy all the read indices to the mesh
        aMesh->mIndices.resize((triangleCount + newTris) * 3);
        for(uint32 i = 0; i < newTris; ++ i)
        {
          aMesh->mIndices[(i + triangleCount) * 3] = indices[i * 3] + indexBias;
          aMesh->mIndices[(i + triangleCount) * 3 + 1] = indices[i * 3 + 1] + indexBias;
          aMesh->mIndices[(i + triangleCount) * 3 + 2] = indices[i * 3 + 2] + indexBias;
        }
        triangleCount += newTris;
      }
      else
      {
        // We only support FACE/PTCH type polygons - skip this chunk
        f.seekg(chunkSize - 4, ios_base::cur);
      }
    }
    else if((chunkID == string("VMAP")) || (chunkID == string("VMAD")))
    {
      bool dynamic = (chunkID == string("VMAD"));
      string type = ReadString(f, 4);
      uint32 dimension = ReadU2(f);
      ReadStringZ(f); // Ignore the name

      // How many bytes are currently left to read in this chunk?
      int bytesLeft = (int) chunkSize - ((int) f.tellg() - (int) chunkStart);

      if((type == string("RGB ")) || (type == string("RGBA")))
      {
        // Resize the mesh colors array
        uint32 oldSize = aMesh->mColors.size();
        aMesh->mColors.resize(pointCount);
        for(uint32 i = oldSize; i < pointCount; ++ i)
          aMesh->mColors[i] = Vector4(1.0f, 1.0f, 1.0f, 1.0f);

        // Read all the colors
        while(bytesLeft > 0)
        {
          uint32 idx = ReadVX(f, &bytesLeft) + indexBias;
          if(dynamic)
            ReadVX(f, &bytesLeft); // ignore the face index for VMAD...
          Vector4 col;
          col.x = ReadF4(f);
          col.y = ReadF4(f);
          col.z = ReadF4(f);
          if(dimension == 4)
          {
            col.w = ReadF4(f);
            bytesLeft -= 16;
          }
          else
          {
            col.w = 1.0f;
            bytesLeft -= 12;
          }
          if(idx < aMesh->mColors.size())
            aMesh->mColors[idx] = col;
        }
      }
      else if((type == string("TXUV")))
      {
        // Resize the mesh UV array
        uint32 oldSize = aMesh->mTexCoords.size();
        aMesh->mTexCoords.resize(pointCount);
        for(uint32 i = oldSize; i < pointCount; ++ i)
          aMesh->mTexCoords[i] = Vector2(0.0f, 0.0f);

        // Read all the texture coordinates
        while(bytesLeft > 0)
        {
          uint32 idx = ReadVX(f, &bytesLeft) + indexBias;
          if(dynamic)
            ReadVX(f, &bytesLeft); // ignore the face index for VMAD...
          Vector2 texCoord;
          texCoord.u = ReadF4(f);
          texCoord.v = ReadF4(f);
          bytesLeft -= 8;
          if(idx < aMesh->mTexCoords.size())
            aMesh->mTexCoords[idx] = texCoord;
        }
      }
      else
      {
        // We only support RGB/RGBA & TXUV type VMAPs - skip this chunk
        f.seekg(bytesLeft, ios_base::cur);
      }
    }
    else
    {
      // Just skip this chunk
      f.seekg(chunkSize, ios_base::cur);
    }
  }

  // Post-adjustment: color array (if any)
  if((aMesh->mColors.size() > 0) && (aMesh->mColors.size() < pointCount))
  {
    uint32 oldSize = aMesh->mColors.size();
    aMesh->mColors.resize(pointCount);
    for(uint32 i = oldSize; i < pointCount; ++ i)
      aMesh->mColors[i] = Vector4(1.0f, 1.0f, 1.0f, 1.0f);
  }

  // Post-adjustment: texture coordinate array (if any)
  if((aMesh->mTexCoords.size() > 0) && (aMesh->mTexCoords.size() < pointCount))
  {
    uint32 oldSize = aMesh->mTexCoords.size();
    aMesh->mTexCoords.resize(pointCount);
    for(uint32 i = oldSize; i < pointCount; ++ i)
      aMesh->mTexCoords[i] = Vector2(0.0f, 0.0f);
  }

  // Close the input file
  f.close();
}

/// Export a mesh to an LWO file.
void Export_LWO(const char * aFileName, Mesh * aMesh, Options &aOptions)
{
  // Check if we can support this mesh (too many vertices?)
  if(aMesh->mVertices.size() > 0x00ffffff)
    throw runtime_error("Too large mesh (not supported by the LWO file format).");

  // What should we export?
  bool exportComment = (aMesh->mComment.size() > 0);
  bool exportTexCoords = aMesh->HasTexCoords() && !aOptions.mNoTexCoords;
  bool exportColors = aMesh->HasColors() && !aOptions.mNoColors;

  // Calculate the sizes of the individual chunks
  uint32 textSize = aMesh->mComment.size() + 1;
  if(textSize & 1) ++ textSize;
  uint32 tagsSize = 8;
  uint32 layrSize = 24;
  uint32 pntsSize = (uint32) (aMesh->mVertices.size() * 12);
  uint32 txuvSize = CalcVMAPSize(aMesh, 2) + 20;
  uint32 rgbaSize = CalcVMAPSize(aMesh, 4) + 14;
  uint32 polsSize = CalcPOLSSize(aMesh);

  // Calculate output file size
  uint32 fileSize = 4 +
                    8 + tagsSize +
                    8 + layrSize +
                    8 + pntsSize +
                    8 + polsSize;
  if(exportComment)
    fileSize += 8 + textSize;
  if(exportTexCoords)
    fileSize += 8 + txuvSize;
  if(exportColors)
    fileSize += 8 + rgbaSize;

  // Open the output file
  ofstream f(aFileName, ios_base::out | ios_base::binary);
  if(f.fail())
    throw runtime_error("Could not open output file.");

  // File header
  WriteString(f, "FORM");
  WriteU4(f, fileSize);      // File size (excluding FORM chunk header)
  WriteString(f, "LWO2");

  // TEXT chunk
  if(exportComment)
  {
    WriteString(f, "TEXT");
    WriteU4(f, textSize);
    WriteStringZ(f, aMesh->mComment.c_str());
  }

  // TAGS chunk
  WriteString(f, "TAGS");
  WriteU4(f, tagsSize);
  WriteStringZ(f, "Default");

  // LAYR chunk
  WriteString(f, "LAYR");
  WriteU4(f, layrSize);
  WriteU2(f, 0);                            // number
  WriteU2(f, 0);                            // flags
  WriteVEC12(f, Vector3(0.0f, 0.0f, 0.0f)); // pivot
  WriteStringZ(f, "Layer 1");               // name

  // PNTS chunk
  WriteString(f, "PNTS");
  WriteU4(f, pntsSize);
  for(uint32 i = 0; i < (uint32) aMesh->mVertices.size(); ++ i)
    WriteVEC12(f, aMesh->mVertices[i]);

  // VMAP:TXUV chunk (optional)
  if(exportTexCoords)
  {
    WriteString(f, "VMAP");
    WriteU4(f, txuvSize);
    WriteString(f, "TXUV");                 // type
    WriteU2(f, 2);                          // dimension
    WriteStringZ(f, "Texture coordaintes"); // name
    for(uint32 i = 0; i < (uint32) aMesh->mTexCoords.size(); ++ i)
    {
      WriteVX(f, i);
      WriteF4(f, aMesh->mTexCoords[i].u);
      WriteF4(f, aMesh->mTexCoords[i].v);
    }
  }

  // VMAP:RGBA chunk (optional)
  if(exportColors)
  {
    WriteString(f, "VMAP");
    WriteU4(f, rgbaSize);
    WriteString(f, "RGBA");           // type
    WriteU2(f, 4);                    // dimension
    WriteStringZ(f, "Vertex colors"); // name
    for(uint32 i = 0; i < (uint32) aMesh->mColors.size(); ++ i)
    {
      WriteVX(f, i);
      WriteF4(f, aMesh->mColors[i].x);
      WriteF4(f, aMesh->mColors[i].y);
      WriteF4(f, aMesh->mColors[i].z);
      WriteF4(f, aMesh->mColors[i].w);
    }
  }

  // POLS chunk
  WriteString(f, "POLS");
  WriteU4(f, polsSize);
  WriteString(f, "FACE");
  uint32 triCount = (uint32) (aMesh->mIndices.size() / 3);
  for(uint32 i = 0; i < triCount; ++ i)
  {
    // Polygon node count (always 3)
    WriteU2(f, 3);

    // Write polygon node indices
    for(int j = 0; j < 3; ++ j)
      WriteVX(f, aMesh->mIndices[i * 3 + j]);
  }

  // Close the output file
  f.close();
}
