﻿using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using System;

public class okaypeg_main: UdonSharpBehaviour
{

/*
 * Usage:
 *
 *  okaypeg_main should have 1 instance in a world.
 *  Clients of the compressor should call the following:
 *
 * -----------------------------------------------------------------------------
 *   
 *  _opg_claim_and_clear( okaypeg_store full, okaypeg_store delta )
 *   
 *   Clears the image, and gives localplayer ownership of the image storage.
 *   
 *   full must not be null.
 *   delta can be null if you don't want delta compression.
 *   
 *   returns void
 *
 * -----------------------------------------------------------------------------
 *
 *  bool _opg_upload( RenderTexture rt, int area_w, int area_h,
 *                    okaypeg_store full, okaypeg_store delta )
 *   
 *   Compresses and uploads the texture 'rt' to the two store objects.
 *   
 *   full must not be null.
 *   delta can be null if you don't want delta compression.
 *
 *   returns 1 if operation passed, returns 0 if the system is busy and you
 *   should try to call this function again the next frame or at a later time.
 *
 * -----------------------------------------------------------------------------
 *
 *  void _opg_decode( okaypeg_store main, okaypeg_store delta, 
 *                    Color clear_colour, RenderTexture rt )
 *
 *   Repeatedly call this to download & decompress the image into rt.
 *   Clear colour is used when the image data is blank.
 *   
 */

void Start() { _opg_init(); }
void Update() { _opg_update(); }

public void _opg_claim_and_clear( okaypeg_store full, okaypeg_store delta )
{
   Networking.SetOwner( Networking.LocalPlayer, full.gameObject );
   if( delta != null )
   {
      Networking.SetOwner( Networking.LocalPlayer, delta.gameObject );
      
      int total_blocks = 256/8;
      total_blocks *= total_blocks;

      for( int i=0; i<total_blocks; i ++ )
         _diff_map[i] = 0;
   }

   int my_id = VRCPlayerApi.GetPlayerId( Networking.LocalPlayer );

   int v1 = -1;
   if( delta != null )
      v1 = delta._version;

   full._version = Mathf.Max( full._version, v1 ) + 10;
   full._data = new byte[1]{5};
   full.RequestSerialization();

   if( delta != null )
   {
      delta._data = new byte[1]{8};
      delta._version = full._version-1;
      delta.RequestSerialization();
   }
}

public bool _opg_upload( RenderTexture rt, int area_w, int area_h,
                         okaypeg_store full, okaypeg_store delta )
{
   if( _compressor_stage != k_compressor_stage_offline ) return false;

   if( full == null )
   {  
      LOG( LOGC_ERR, "Reap null store (full)" );
      return false;
   }

   if( delta == null ) 
   {
      LOG( LOGC_ERR, "Reap null store (delta)" );
      return false;
   }

   _store_full = full;
   _store_delta = delta;

   _opg_ingest( rt, area_w, area_h );
   _compressor_stage = k_compressor_stage_diff;
   return true;
}

public void _opg_decode( okaypeg_store main, okaypeg_store delta, 
                         Color clear_colour, RenderTexture rt )
{
   bool main_update  = main._version > main._local_version;
   bool delta_update = false;

   if( delta != null )
   {
      delta_update = (delta._version > delta._local_version) &&
                     (delta._version > main._version);
   }

   if( main_update )
   {
      if( _opg_store_event( main, false, clear_colour, rt ) )
      {
         main._local_version = main._version;

         if( delta != null )
         {
            /* 
             * the local delta version gets rolled back to this mainline
             * version, since its overrides it. in case the delta image
             * has a from-the-future timestamp, it will reacquire in the
             * next decode loop.
             */
            delta._local_version = main._version;
         }
      }
   }

   if( delta_update )
   {
      if( _opg_store_event( delta, true, clear_colour, rt ) )
      {
         delta._local_version = delta._version;
      }
   }
}

bool _opg_store_event( okaypeg_store data, bool is_delta, 
                       Color clear_colour, RenderTexture rt )
{
   if( data._data.Length < 10 ) /* clear command */
   {
      if( is_delta ) 
      {
         LOG( LOGC_LOW, "Nothing to do." );
      }
      else
      {
         Color cc = clear_colour;
         cc.r = Mathf.Pow( cc.r, 1.0f/2.2f );
         cc.g = Mathf.Pow( cc.g, 1.0f/2.2f );
         cc.b = Mathf.Pow( cc.b, 1.0f/2.2f );
         _m_solid.SetVector( "_Color", cc );
         VRCGraphics.Blit( null, rt, _m_solid );
      }

      return true;
   }

   return _opg_load_data( data, is_delta, rt );
}

[Header("Compressor")]

okaypeg_store _store_full, _store_delta;
RenderTexture _rt_decode_dest = null;
RenderTexture _rt0, _rt1; /* Front/back buffers */

RenderTexture _rt_ingest, _rt_ref, _rtdiff0, _rtdiff1;
Texture2D _t_diff_sink; /* Read from GPU */

int[] _diff_map = new int[32*32];
Color[] _raw_diff_mask = new Color[32*32];

Texture2D _t_gpu_sink; /* Texture connection between GPU and CPU */

public Material _m_yuv, _m_rgb, _m_dct, _m_diff, _m_clear, _m_solid, _m_resamp;
public float _compressor_full_compress_target_s = 2.0f;

float _frametime_s = 1.0f/30.0f;

const int k_compressor_budget_min = 10;
const int k_compressor_budget_max = 500;
int _compressor_budget = k_compressor_budget_min;
float _compressor_spent_ms = 0.0f;

RenderTexture _create_pipeline_buffer()
{
   RenderTexture rt = new RenderTexture( 
      256, 256,                      /* size */
      0,                             /* depth  bits */
      RenderTextureFormat.ARGBHalf,  /* format */
      RenderTextureReadWrite.Linear);/* gamma */

   rt.useMipMap = false;
   rt.autoGenerateMips = false;

   return rt;
}

public void _opg_init()
{
   _t_gpu_sink = new Texture2D( 256, 256, TextureFormat.RGBAHalf, 
                                -1, true, false );
   _t_gpu_sink.filterMode = FilterMode.Bilinear;
   _t_gpu_sink.wrapMode = TextureWrapMode.Clamp;

   _rt0 = _create_pipeline_buffer();
   _rt1 = _create_pipeline_buffer();

   /* Diff buffers */
   _rt_ref = new RenderTexture( 256, 256,
                                0,
                                RenderTextureFormat.ARGB32,
                                RenderTextureReadWrite.sRGB );
   _rt_ref.useMipMap = false;
   _rt_ref.autoGenerateMips = false;
   _rt_ref.filterMode = FilterMode.Point;

   _rt_ingest = new RenderTexture( 256, 256,
                                0,
                                RenderTextureFormat.ARGB32,
                                RenderTextureReadWrite.sRGB );
   _rt_ingest.useMipMap = false;
   _rt_ingest.autoGenerateMips = false;
   _rt_ingest.filterMode = FilterMode.Point;

   _rtdiff0 = new RenderTexture( 256/8, 256,
                                 0,
                                 RenderTextureFormat.ARGB32,
                                 RenderTextureReadWrite.Linear );
   _rtdiff0.useMipMap = false;
   _rtdiff0.autoGenerateMips = false;
   _rtdiff0.filterMode = FilterMode.Point;

   _rtdiff1 = new RenderTexture( 256/8, 256/8,
                                 0,
                                 RenderTextureFormat.ARGB32,
                                 RenderTextureReadWrite.Linear );
   _rtdiff1.useMipMap = false;
   _rtdiff1.autoGenerateMips = false;
   _rtdiff1.filterMode = FilterMode.Point;

   _t_diff_sink = new Texture2D( 256/8, 256/8, TextureFormat.ARGB32, 
                                 -1, true, false );
   _t_diff_sink.filterMode = FilterMode.Point;
   _t_diff_sink.wrapMode = TextureWrapMode.Clamp;
}

const int k_compressor_stage_offline = 0;
const int k_compressor_stage_begin = 1;
const int k_compressor_stage_begin_decompress = 2;
const int k_compressor_stage_block_compress = 3;
const int k_compressor_stage_block_decompress = 4;
const int k_compressor_stage_upload = 5;
const int k_compressor_stage_end = 6;
const int k_compressor_stage_diff = 7;
const int k_compressor_stage_store_wait = 8;
string[] kSTR_compressor_stage = new string[]
{
   "OFFLINE",
   "BEGIN C",
   "BEGIN D",
   "BLOCK C",
   "BLOCK D",
   "UPLOAD ",
   "END    ",
   "DIFF   ",
   "STALLED"
};

const int k_compressor_mode_full = 0;
const int k_compressor_mode_delta = 1;
string[] kSTR_compressor_mode = new string[]
{
   "FULL ",
   "DELTA"
};

int _compressor_stage = k_compressor_stage_offline;
int _compressor_mode = k_compressor_mode_full;

/* Block compressor stage */
int _block_channel = 0;
int _block_x = 0;
int _block_y = 0;
int _block_total_x = 0;
int _block_total_y = 0;
int _null_block_count = 0;
int _block_processed = 0;
int _block_total = 0;

Color[] _pixels;
Color[] _raw_image = new Color[ 256*256 ];

int[] _zigzag = new int[64]
{
   0*256+0, 1*256+0, 0*256+1, 0*256+2, 1*256+1, 2*256+0, 3*256+0, 2*256+1,
   1*256+2, 0*256+3, 0*256+4, 1*256+3, 2*256+2, 3*256+1, 4*256+0, 5*256+0,
   4*256+1, 3*256+2, 2*256+3, 1*256+4, 0*256+5, 0*256+6, 1*256+5, 2*256+4,
   3*256+3, 4*256+2, 5*256+1, 6*256+0, 7*256+0, 6*256+1, 5*256+2, 4*256+3,
   3*256+4, 2*256+5, 1*256+6, 0*256+7, 1*256+7, 2*256+6, 3*256+5, 4*256+4,
   5*256+3, 6*256+2, 7*256+1, 7*256+2, 6*256+3, 5*256+4, 4*256+5, 3*256+6,
   2*256+7, 3*256+7, 4*256+6, 5*256+5, 6*256+4, 7*256+3, 7*256+4, 6*256+5,
   5*256+6, 4*256+7, 5*256+7, 6*256+6, 7*256+5, 7*256+6, 6*256+7, 7*256+7,
};

void _blit( string name, RenderTexture src, RenderTexture dst, Material mat )
{
   VRCGraphics.Blit( src, dst, mat );
}

void _compressor_calculate_budget()
{
   float target_s = _compressor_full_compress_target_s;
   float update_count = target_s / _frametime_s;
   int p = (int)Mathf.Ceil((float)_block_total / update_count);
   _compressor_budget = Mathf.Clamp( p, k_compressor_budget_min,
                                        k_compressor_budget_max );
}

bool _opg_load_data( okaypeg_store data, bool is_delta, RenderTexture rt_dest )
{
   if( _compressor_stage != k_compressor_stage_offline ) return false;

   if( is_delta )
   {
      _compressor_mode = k_compressor_mode_delta;
      _store_full = null;
      _store_delta = data;
   }
   else
   {
      _compressor_mode = k_compressor_mode_full;
      _store_full = data;
      _store_delta = null;
   }
   
   _rt_decode_dest = rt_dest;
   _compressor_stage = k_compressor_stage_begin_decompress;
   return true;
}

bool _opg_store_safe()
{
   if( Networking.IsClogged ) /* be patient */
      return false;

   okaypeg_store dest = null;
   if( _compressor_mode == k_compressor_mode_delta ) dest = _store_delta;
   else                                              dest = _store_full;

   dest._version = Mathf.Max( _store_delta._version+1, _store_full._version+1 );
   dest._data = new byte[ _scratch_bytes ];
   Buffer.BlockCopy( _scratch_buffer, 0, dest._data, 0, _scratch_bytes );
   dest.RequestSerialization();

   LOG( LOGC_LOW, "Stored onto object (" + dest._version.ToString() + ")" );
   return true;
}

public void _opg_update()
{
   float cftime = Time.deltaTime;
   
   if( _compressor_stage == k_compressor_stage_block_compress ||
       _compressor_stage == k_compressor_stage_block_decompress )
       cftime -= _compressor_spent_ms/1000.0f;

   _frametime_s = Mathf.Lerp( _frametime_s, cftime, Time.deltaTime );

   if( _compressor_stage == k_compressor_stage_offline )
   {
   }
   else if( _compressor_stage == k_compressor_stage_begin )
   {
      LOG( LOGC_LOW, "Compressor: begin" );
      if( _compressor_mode == k_compressor_mode_full )
         VRCGraphics.Blit( _rt_ingest, _rt_ref );

      /* Transform RGB to YUV */
      _blit( "yuv", _rt_ingest, _rt0, _m_yuv );

      /* 2 pass DCT */
      _m_dct.SetInteger( "_Stage", 0 );
      _m_dct.SetInteger( "_Axis", 0 );
      _blit( "dct0", _rt0, _rt1, _m_dct );
      _m_dct.SetInteger( "_Axis", 1 );
      _blit( "dct1", _rt1, _rt0, _m_dct );

      _m_dct.SetInteger( "_Stage", 2 );
      _m_dct.SetInteger( "_Axis", 0 );
      VRCGraphics.Blit( _rt0, _rt1, _m_dct );

      _t_gpu_sink.ReadPixels( new Rect(0,0,256,256), 0,0, false );
      _pixels = _t_gpu_sink.GetPixels(0);

      _compressor_stage = k_compressor_stage_block_compress;
      LOG( LOGC_LOW, "Compressor: block_compress" );

      _block_channel = 0;
      _block_x = 0;
      _block_y = 0;
      _block_total_x = 256/8;
      _block_total_y = 256/8;
      _null_block_count = 0;
      _block_total = _block_total_x*_block_total_y*3;
      _block_processed = 0;

      _scratch_bytes = 0;
      _th_initwrite();
      _compressor_calculate_budget();
   }
   else if( _compressor_stage == k_compressor_stage_begin_decompress )
   {
      LOG( LOGC_LOW, "Compressor: begin decompress" );
      _block_channel = 0;
      _block_x = 0;
      _block_y = 0;
      _block_total_x = 256/8;
      _block_total_y = 256/8;
      _null_block_count = -1;
      _block_total = _block_total_x*_block_total_y*3;
      _block_processed = 0;

      _compressor_stage = k_compressor_stage_block_decompress;
      _th_initread();

      okaypeg_store src = null;
      if( _compressor_mode == k_compressor_mode_delta ) src = _store_delta;
      else                                              src = _store_full;
      Buffer.BlockCopy( src._data, 0, _scratch_buffer, 0, src._data.Length );
      LOG( LOGC_LOW, "Harvested data from object" );
      _compressor_calculate_budget();
   }
   else if( _compressor_stage == k_compressor_stage_block_compress )
   {
      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
      sw.Start();

      for( int _=0; _<_compressor_budget; _ ++ )
      {
         int py = _block_y*8 * 256;
         int px = _block_x*8;

         bool write_block = true;
         
         if( (_compressor_mode == k_compressor_mode_delta) &&
             (_block_channel == 0) )
         {
            if( _diff_map[ _block_y*32 + _block_x ] == 0 )
            {
               write_block = false;
               _null_block_count ++;
            }
            else
            {
               _th_store( _null_block_count );
               _null_block_count = 0;
            }
         }
         
         if( write_block )
         {
            int basis = py+px;

            for( int i=0; i<64; i ++ )
            {
               int index = basis+_zigzag[i];
               float v = Mathf.Round( _pixels[index][_block_channel] * 127.0f );
               _th_store( (int)v );
            }

            _block_channel ++;
         }
         else
            _block_channel = 3;

         _block_processed ++;

         if( _block_channel >= 3 )
         {
            _block_channel = 0;
            _block_x ++;

            if( _block_x >= _block_total_x )
            {
               _block_x = 0;
               _block_y ++;

               if( _block_y >= _block_total_y )
               {
                  _block_y = 0;

                  if( _compressor_mode == k_compressor_mode_delta )
                     _th_store( _null_block_count );

                  _th_finishwrite();
                  
                  
                  float score = (float)(_scratch_bytes*100)/(float)(256*256*3);
                  string stat = _scratch_bytes.ToString() + 
                                " bytes (" + score.ToString("0.00") + "%)\n";
                  LOG( LOGC_OK, "Compression finished: " + stat );

                  if( _th_trunced )
                     LOG( LOGC_WARN, "Truncation occured in th compressor" );
                  
                  if( _opg_store_safe() )
                     _compressor_stage = k_compressor_stage_offline;
                  else
                  {
                     LOG( LOGC_WARN, "Network is 'clogged' so we'll wait" );
                     _compressor_stage = k_compressor_stage_store_wait;
                  }
                  return;
               }
            }
         }
      }

      sw.Stop();
      _compressor_spent_ms = sw.ElapsedMilliseconds;
   }
   else if( _compressor_stage == k_compressor_stage_store_wait )
   {
      if( _opg_store_safe() )
         _compressor_stage = k_compressor_stage_offline;
   }
   else if( _compressor_stage == k_compressor_stage_block_decompress )
   {
      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
      sw.Start();

      for( int _=0; _<_compressor_budget; _ ++ )
      {
         int py = _block_y*8 * 256;
         int px = _block_x*8;
         int basis = py+px;

         if( (_compressor_mode == k_compressor_mode_delta) &&
             (_block_channel == 0) )
         {
            if( _null_block_count < 0 )
               _null_block_count = _th_read();

            _null_block_count --;

            float mask = 0.0f;
            if( _null_block_count < 0 ) mask = 1.0f;
            _raw_diff_mask[ _block_y*32 + _block_x ][0] = mask;
         }

         if( _null_block_count < 0 )
         {
            for( int i=0; i<64; i ++ )
            {
               float v = ((float)_th_read()) / 127.0f;

               int index = basis+_zigzag[i];
               _raw_image[index][_block_channel] = v;
               _raw_image[index][3] = 1.0f;
            }

            _block_channel ++;
         }
         else
            _block_channel = 3;

         _block_processed ++;

         if( _block_channel >= 3 )
         {
            _block_channel = 0;
            _block_x ++;

            if( _block_x >= _block_total_x )
            {
               _block_x = 0;
               _block_y ++;

               if( _block_y >= _block_total_y )
               {
                  _block_y = 0;

                  _compressor_stage = k_compressor_stage_upload;
                  LOG( LOGC_OK, "Readback decompression finished" );
                  return;
               }
            }
         }
      }

      sw.Stop();
      _compressor_spent_ms = sw.ElapsedMilliseconds;
   }
   else if( _compressor_stage == k_compressor_stage_upload )
   {
      _t_diff_sink.SetPixels( _raw_diff_mask );
      _t_diff_sink.Apply();

      _t_gpu_sink.SetPixels( _raw_image );
      _t_gpu_sink.Apply();
      VRCGraphics.Blit( _t_gpu_sink, _rt1 );

      _m_dct.SetInteger( "_Stage", 2 );
      _m_dct.SetInteger( "_Axis", 1 );
      _blit( "iquantize", _rt1, _rt0, _m_dct );

      _m_dct.SetInteger( "_Stage", 1 );
      _m_dct.SetInteger( "_Axis", 0 );
      _blit( "idct0", _rt0, _rt1, _m_dct );
      _m_dct.SetInteger( "_Axis", 1 );
      _blit( "idct1", _rt1, _rt0, _m_dct );

      if( _compressor_mode == k_compressor_mode_delta )
      {
         _blit( "rgb", _rt0, _rt1, _m_rgb );
         _m_clear.SetTexture( "_Mask", _t_diff_sink );
         _blit( "mask", _rt1, _rt_decode_dest, _m_clear );
      }
      else
      {
         _blit( "rgb", _rt0, _rt_decode_dest, _m_rgb );
      }

      LOG( LOGC_OK, "Decode finished" );
      _compressor_stage = k_compressor_stage_end;
   }
   else if( _compressor_stage == k_compressor_stage_end )
   {
      _compressor_stage = k_compressor_stage_offline;
   }
   else if( _compressor_stage == k_compressor_stage_diff )
   {
      /* Compute diff horizontally */
      _m_diff.SetInteger( "_Axis", 0 );
      _m_diff.SetTexture( "_Reference", _rt_ref );
      _m_diff.SetTexture( "_Target", _rt_ingest );
      _blit( "diff0", null, _rtdiff0, _m_diff );

      /* Sum up diff rows */
      _m_diff.SetInteger( "_Axis", 1 );
      _m_diff.SetTexture( "_Target", _rtdiff0 );
      VRCGraphics.Blit( null, _rtdiff1, _m_diff );

      _t_diff_sink.ReadPixels( new Rect(0,0,256/8,256/8), 0,0, false );
      Color[] _diff_pixels = _t_diff_sink.GetPixels(0);

      int diff_count = 0,
          total_blocks = 256/8;
      total_blocks *= total_blocks;

      for( int i=0; i<total_blocks; i ++ )
      {
         if( _diff_pixels[i].r > 0.0001f )
         {
            _diff_map[i] |= 1;
            diff_count ++;
         }
      }

      if( diff_count == 0 )
      {
         _compressor_stage = k_compressor_stage_end;
         LOG( LOGC_LOW, "Image is the same. Not compressing new delta." );
      }
      else
      {
         LOG( LOGC_LOW, "Compressor diffs: " + diff_count.ToString() );
         _compressor_stage = k_compressor_stage_begin;
         if( diff_count > 128 )
         {
            _compressor_mode = k_compressor_mode_full;

            for( int i=0; i<total_blocks; i ++ )
               _diff_map[i] = 0;
         }
         else
            _compressor_mode = k_compressor_mode_delta;
      }
   }
}

/* Take any texture and map it into the 256x256 one */
void _opg_ingest( RenderTexture rt, int w, int h )
{
   _m_resamp.SetVector( "_src_rect", new Vector4(256,256,w,h) );
   VRCGraphics.Blit( rt, _rt_ingest, _m_resamp );
}

/* -------------------------------------------------------------------------- */

/* simple entropy compressor 'th'
 * Large numbers cost more bytes to store, small numbers cost less. runs of 
 * zeros are the very very best case. */

byte[] _scratch_buffer = new byte[ 256*256*3*3 + 128 ];
int _scratch_bytes = 0;

bool _th_trunced = false;
int _th_zeros = 0;
int _th_3bit = -999;

void _th_initwrite()
{
   _th_zeros = 0;
   _th_trunced = false;
}

void _th_3bit_straggle()
{
   if( _th_3bit != -999 )
   {
      if( _th_3bit >= 0 ) _th_3bit ++;

      /* fallback to storing as 7 bit number */
      byte bits = (byte)(_th_3bit + 64);
      _scratch_buffer[ _scratch_bytes ++ ] = (byte)(0x80 | bits);

      _th_3bit = -999;
   }
}

void _th_store( int value )
{
   if( value < -32768 || value > 32767 )
   {
      _th_trunced = true;
      if( value > 0 ) value =  32767;
      else            value = -32768;
   }

   if( value == 0 )
   {
      _th_3bit_straggle();
      _th_zeros ++;

      if( _th_zeros == 64 )
      {
         _scratch_buffer[ _scratch_bytes ++ ] = 0x3f; /* 00111111 */
         _th_zeros = 0;
      }
   }
   else
   {
      if( _th_zeros > 0 )
      {
         _scratch_buffer[ _scratch_bytes ++ ] = (byte)(_th_zeros -1);
         _th_zeros = 0;
      }

      /* for now we dont bother with the 2x packed 3 bit case */
      if( value >= -4 && value <= 4 )
      {
         if( value > 0 ) /* don't need 0 */
            value --;

         if( _th_3bit == -999 )
         {
            _th_3bit = value;
         }
         else
         {
            byte bits = (byte)(0x40 | (_th_3bit + 4) | ((value + 4) << 3));
            _scratch_buffer[ _scratch_bytes ++ ] = bits;
            _th_3bit = -999;
         }
      }
      else
      {
         _th_3bit_straggle();

         /* 7 bit number except -64 (reserved code word) */
         if( value >= -63 && value <= 63 )
         {
            byte bits = (byte)(value + 64);
            _scratch_buffer[ _scratch_bytes ++ ] = (byte)(0x80 | bits);
         }
         else
         {
            /* 16 bit number worse case */
            _scratch_buffer[ _scratch_bytes ++ ] = 0x80;

            int bits = value + 32768;
            byte lower = (byte)( bits & 0xff),
                 upper = (byte)((bits>>8) & 0xff);

            _scratch_buffer[ _scratch_bytes ++ ] = lower;
            _scratch_buffer[ _scratch_bytes ++ ] = upper;
         }
      }
   }
}

void _th_finishwrite()
{
   if( _th_zeros > 0 )
   {
      _scratch_buffer[ _scratch_bytes ++ ] = (byte)(_th_zeros -1);
   }
   else _th_3bit_straggle();
}

/* READING */

int _th_readptr = 0;
void _th_initread()
{
   _th_zeros = 0;
   _th_readptr = 0;
   _th_3bit = -999;
}

int _th_read()
{
   if( _th_3bit != -999 )
   {
      int value = _th_3bit;
      if( value >= 0 ) value ++;

      _th_3bit = -999;
      return value;
   }

   if( _th_zeros > 0 )
   {
      _th_zeros --;
      return 0;
   }
   else
   {
      byte raw = _scratch_buffer[ _th_readptr ++ ],
           code = (byte)((uint)raw & 0xC0);
      if( code == 0x00 )
      {
         _th_zeros = raw & 0x3F;
         return 0;
      }
      else
      { 
         if( code == 0x40 )
         {
            int value = (raw & 0x7) - 4;
            _th_3bit = ((raw & 0x38) >> 3) - 4;

            if( value >= 0 ) value ++;
            return value;
         }
         else
         {
            if( raw == 0x80 )
            {
               int lower = (int)_scratch_buffer[ _th_readptr ++ ],
                   upper = (int)_scratch_buffer[ _th_readptr ++ ];
               return (lower | (upper<<8)) - 32768;
            }
            else
               return (int)(raw & 0x7F) -64;
         }
      }
   }
}

/* -------------------------------------------------------------------------- */

const string LOGC_LOW  = "<color=#ADADAD>LOW | ";
const string LOGC_ERR  = "<color=#B84139>ERR | ";
const string LOGC_WARN = "<color=#DEC521>WARN| ";
const string LOGC_OK   = "<color=#69D128>OK  | ";

void LOG( string type, string msg )
{
#if UNITY_EDITOR
	Debug.Log( msg );
#endif
}
}
