#ifndef __DTSOUTPUTSTREAM_H
#define __DTSOUTPUTSTREAM_H

#include <cassert>
#include <vector>
#include <fstream>

#include "DTSShape.h"
#include "DTSQuaternion.h"
#include "DTSMatrix.h"
#include "DTSEndian.h"

namespace DTS
{
   // -------------------------------------------------------------------------
   // class OutputStream
   // --------------------------------------------------------------------------

   /*!   The DTS file format stores in separate buffers values of 8, 16 or 32
         bits of length, in order to speed up loading in non-Intel platforms.
         In order to create a DTS file you should create the three buffers in
         memory and then begin writting the data using helper functions. This
         mimics the way the data is retrieved in the engine.
     
         This class provides the needed functionality. Simply create a
         OutputStream object and write the data using the "write" method,
         that stores the values in the correct buffer. The "flush" method
         creates finally the file in the format expected by the V12 engine.
     
         The class knowns about all DTS namespace simple objects and can
         save them (operator <<) in the expected format.
     
      *** WARNING ***   THIS CODE IS NOT PORTABLE
     
         This code assumes a little-endian 32 bits machine
   */

   class OutputStream
   {
   public:
      
      //! Initialize the stream. The version number is written in the header
      OutputStream (std::ostream &, int DTSVersion = 24) ;
      ~OutputStream () ;
      
      //! Wite "count" dwords (= count x 4 bytes)
      void write (const int   *, int count) ;

      //! Write "count" words (= count x 2 bytes)
      void write (const short *, int count) ; 

      //! Write "count" bytes 
      void write (const char  *, int count) ;
      
      //! Write one dword 
      OutputStream & operator << (const int   value) { write(&value, 1) ; return *this ; }

      //! Write one word
      OutputStream & operator << (const short value) { write(&value, 1) ; return *this ; }
      OutputStream & operator << (const unsigned short value) { write((const short*)&value, 1) ; return *this ; }

      //! Write one byte
      OutputStream & operator << (const char  value) { write(&value, 1) ; return *this ; }
      
      //! Write one dword (1 float == 32 bits)
      OutputStream & operator << (const float value) 
      { 
         write((int *)&value, 1) ; 
         return *this ; 
      }

      //! Write one byte (provided for convenience)
      OutputStream & operator << (const unsigned char value) 
      { 
         write((char *)&value, 1) ; 
         return *this ; 
      }

      //! Write a string (1 byte len first, then n byte-sized characters)
      OutputStream & operator << (const std::string &s) ;

      // Operators to write some usual structs
      
      OutputStream & operator << (const Point &) ;     
      OutputStream & operator << (const Point2D &) ;     
      OutputStream & operator << (const Box &) ;
      OutputStream & operator << (const Quaternion &) ;
      OutputStream & operator << (const Node &) ;
      OutputStream & operator << (const Object &) ;
      OutputStream & operator << (const Decal &) ;
      OutputStream & operator << (const IFLMaterial &) ;
      OutputStream & operator << (const DecalState &) ;
      OutputStream & operator << (const DetailLevel &) ;
      OutputStream & operator << (const ObjectState &) ;
      OutputStream & operator << (const Subshape &) ;
      OutputStream & operator << (const Trigger &) ;
      OutputStream & operator << (const Primitive &) ;
      OutputStream & operator << (const Mesh &) ;
      OutputStream & operator << (const Cluster &) ;
      OutputStream & operator << (const Matrix<4,4> &) ;
      
      //! Stores checkpoint values in the streams
      void storeCheck (int checkPoint = -1) ;

      //! When you're finished, call this method to create the file
      //! You'll still need to write the sequence data to the stream, next
      void flush () ;
      
   private:
      
      std::ostream & out ;
      int DTSVersion ;
      int checkCount ;
      
      // Pointers to the 3 memory buffers
      
      int   * Buffer32 ;
      short * Buffer16 ;
      char  * Buffer8  ;
      
      // Values allocated in each buffer
      
      unsigned long Allocated32 ;
      unsigned long Allocated16 ;
      unsigned long Allocated8 ;
      
      // Values actually used in each buffer
      
      unsigned long Used32 ;
      unsigned long Used16 ;
      unsigned long Used8 ;
   };
   
   // --------------------------------------------------------------------------
   //    Construction and destruction
   // --------------------------------------------------------------------------
   
   inline OutputStream::OutputStream (std::ostream & s, int version)
      : out(s), DTSVersion(version)
   {
      // Versions 17 and down don't use the buffers
      
      assert (version >= 18) ;
      
      // Create buffers with some unused space in
      
      Buffer32 = new int   [64] ;
      Buffer16 = new short [128] ;
      Buffer8  = new char  [256] ;
      
      Allocated32 = 64 ;
      Allocated16 = 128 ;
      Allocated8  = 256 ;
      
      Used32 = 0 ;
      Used16 = 0 ;
      Used8  = 0 ;

      checkCount = 0 ;
   }
   
   inline OutputStream::~OutputStream()
   {
      delete [] Buffer8 ;
      delete [] Buffer16 ;
      delete [] Buffer32 ;
   }
   
   // --------------------------------------------------------------------------
   //    Write functions
   // --------------------------------------------------------------------------

   /*! All 32 bits dwords must be written to the internal 32-bits buffer
       so they can be reversed in a single pass in big-endian machines */
   inline void OutputStream::write (const int * data, int count)
   {
      assert (count > 0) ;
      assert (data != 0) ;
      
      if (Used32 + count > Allocated32)
      {
         // Not enough space allocated. Alloc a bigger buffer,
         // with a multiple of 64 number of elements, but enough
         // to add the new data, and swap the old buffer for it
         
         Allocated32 = ((Used32 + count) & ~63) + 64 ;
         assert (Allocated32 > Used32 + count) ;
         int * NewBuffer32 = new int[Allocated32] ;
         memcpy (NewBuffer32, Buffer32, sizeof(int) * Used32) ;
         delete [] Buffer32 ;
         Buffer32 = NewBuffer32 ;
      }
      
      // Add the new data to the buffer
      
      memcpy (Buffer32 + Used32, data, sizeof(int) * count) ;
      Used32 += count ;
   }
   
   // The other write methods are exactly the same, except for the buffer used
   
   inline void OutputStream::write (const short * data, int count)
   {
      assert (count > 0) ;
      assert (data != 0) ;
      
      if (Used16 + count > Allocated16)
      {
         Allocated16 = ((Used16 + count) & ~63) + 64 ;
         assert (Allocated16 > Used16 + count) ;
         short * NewBuffer16 = new short[Allocated16] ;
         memcpy (NewBuffer16, Buffer16, sizeof(short) * Used16) ;
         delete [] Buffer16 ;
         Buffer16 = NewBuffer16 ;
      }
      
      memcpy (Buffer16 + Used16, data, sizeof(short) * count) ;
      Used16 += count ;
   }
   
   inline void OutputStream::write (const char * data, int count)
   {
      assert (count > 0) ;
      assert (data != 0) ;
      
      if (Used8 + count > Allocated8)
      {
         Allocated8 = ((Used8 + count) & ~63) + 64 ;
         assert (Allocated8 > Used8 + count) ;
         char * NewBuffer8 = new char[Allocated8] ;
         memcpy (NewBuffer8, Buffer8, sizeof(char) * Used8) ;
         delete [] Buffer8 ;
         Buffer8 = NewBuffer8 ;
      }
      
      memcpy (Buffer8 + Used8, data, sizeof(char) * count) ;
      Used8 += count ;
   }
   
   // --------------------------------------------------------------------------
   //    Write operators
   // --------------------------------------------------------------------------
   
   inline OutputStream & OutputStream::operator << (const Point &p)
   { 
      return (*this) << p.x() << p.y() << p.z() ; 
   }
   
   inline OutputStream & OutputStream::operator << (const Point2D &p)
   { 
      return (*this) << p.x() << p.y() ; 
   }
   
   inline OutputStream & OutputStream::operator << (const Box &b)
   { 
      return (*this) << b.min << b.max ; 
   }
   
   inline OutputStream & OutputStream::operator << (const Quaternion &q)
   {
      return (*this) << (short)(q.x() * 32767.0f) 
                     << (short)(q.y() * 32767.0f)
                     << (short)(q.z() * 32767.0f)
                     << (short)(q.w() * 32767.0f) ;
   }

   inline OutputStream & OutputStream::operator << (const Node &n)
   {
      return (*this) << n.name  << n.parent << n.firstObject 
                     << n.child << n.sibling ;
   }
   
   inline OutputStream & OutputStream::operator << (const Object &o)
   {
      return (*this) << o.name << o.numMeshes << o.firstMesh
                     << o.node << o.sibling   << o.firstDecal ;
   }
   
   inline OutputStream & OutputStream::operator << (const Decal &d)
   {
      return (*this) << d.name   << d.numMeshes << d.firstMesh 
                     << d.object << d.sibling ;
   }

   inline OutputStream & OutputStream::operator << (const IFLMaterial &m)
   {
      return (*this) << m.name << m.slot << m.firstFrame 
                     << m.time << m.numFrames ;
   }

   inline OutputStream & OutputStream::operator << (const Subshape &s)
   {
      assert (0 && "Subshapes should be written in block") ;
   }

   inline OutputStream & OutputStream::operator << (const Trigger &d)
   {
      return (*this) << d.state << d.pos ;
   }

   inline OutputStream & OutputStream::operator << (const DecalState &d)
   {
      return (*this) << d.frame ;
   }

   inline OutputStream & OutputStream::operator << (const ObjectState &o)
   {
      return (*this) << o.vis << o.frame << o.matFrame ;
   }

   inline OutputStream & OutputStream::operator << (const DetailLevel &d)
   {
      return (*this) << d.name << d.subshape << d.objectDetail << d.size
                     << d.avgError << d.maxError << d.polyCount ;
   }

   inline OutputStream & OutputStream::operator << (const Primitive &p)
   {
      return (*this) << p.firstElement << p.numElements << p.type ;
   }

   inline OutputStream & OutputStream::operator << (const Mesh &m)
   {
      m.save(*this) ;
      return *this ;
   }

   inline OutputStream & OutputStream::operator << (const Cluster &c)
   {
      return *this << c.startPrimitive << c.endPrimitive << c.normal << c.k << c.frontCluster << c.backCluster;
   }

   inline OutputStream & OutputStream::operator << (const std::string &s) 
   {
      std::string::const_iterator pos = s.begin() ;
      while (pos != s.end()) (*this) << *pos++ ;
      (*this) << '\x00' ;
      return *this ;
   }

   template <class type>
   inline OutputStream & operator << (OutputStream & out, const std::vector<type> &v) 
   {
      std::vector<type>::const_iterator pos = v.begin() ;
      while (pos != v.end())
      {
         out << *pos++ ;
      }
      return out ;
   }

   inline OutputStream & OutputStream::operator << (const Matrix<4,4> &m)
   { 
     for (int r = 0 ; r < 4 ; r++)
        for (int c = 0 ; c < 4 ; c++)
            (*this) << m[r][c];
      return *this;
   }
   
   // --------------------------------------------------------------------------
   //  Checkpoints and alignment
   // --------------------------------------------------------------------------
   
   /*! Sometimes during the storing process, a checkpoint value is
       stored to all the buffers at the same time. This check helps
       to determine if the files are not corrupted. */

   inline void OutputStream::storeCheck(int checkPoint)
   {
      if (checkPoint >= 0)
         assert (checkPoint == checkCount) ;

      (*this) << (int)   checkCount ;
      (*this) << (short) checkCount ;
      (*this) << (char)  checkCount ;

      checkCount++ ;
   }

   // --------------------------------------------------------------------------
   //    Flush method
   // --------------------------------------------------------------------------
   
   /*! The file format wants a header with the version number, total size
       of all three buffers (in dwords), and the offsets of the 16-bits
       and 8-bits buffers, followed with the buffer data itself.

       There is additional information that is stored after this data,
       but we don't know about it here */

   inline void OutputStream::flush ()
   {
      int totalSize ;
      int offset16 ;
      int offset8 ;
      int i;
      
      // Force all buffers to have a size multiple of 4 bytes
      
      if    (Used16 & 0x0001) (*this) << (short)0 ;
      while (Used8  & 0x0003) (*this) << (char) 0 ;
      
      // Compute the header values (in dwords)
      
      offset16  = Used32 ;
      offset8   = Used32 + Used16/2 ;
      totalSize = Used32 + Used16/2 + Used8/4 ;

      // Fix endian...
      if (!isLittleEndian())
      {
         for (i=0; i<Used16; i++)
            Buffer16[i] = FIX_ENDIAN(Buffer16[i]);
         for (i=0; i<Used32; i++)
            Buffer32[i] = FIX_ENDIAN(Buffer32[i]);
      }

      // Write the resulting data to the file

      std::streampos pos = out.tellp() ;
      out.write ((char *) &FIX_ENDIAN(DTSVersion), 4) ;
      out.write ((char *) &FIX_ENDIAN(totalSize), 4) ;
      out.write ((char *) &FIX_ENDIAN(offset16), 4) ;
      out.write ((char *) &FIX_ENDIAN(offset8), 4) ;
      out.write ((char *) Buffer32, 4 * Used32) ;
      out.write ((char *) Buffer16, 2 * Used16) ;
      out.write (Buffer8, Used8) ;
      std::streampos endpos = out.tellp() ;

      assert ((endpos - pos) == (std::streampos)(4*Used32 + 2*Used16 + 1*Used8 + 16)) ;

   }
   
}

#endif