/*
===============================================================================

  FILE:  lasreadpoint.cpp
  
  CONTENTS:
  
    see corresponding header file
  
  PROGRAMMERS:

    info@rapidlasso.de  -  https://rapidlasso.de

  COPYRIGHT:

    (c) 2007-2022, rapidlasso GmbH - fast tools to catch reality

    This is free software; you can redistribute and/or modify it under the
    terms of the Apache Public License 2.0 published by the Apache Software
    Foundation. See the COPYING file for more information.

    This software is distributed WITHOUT ANY WARRANTY and without even the
    implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  
  CHANGE HISTORY:
  
    see corresponding header file
  
===============================================================================
*/

#include "lasreadpoint.hpp"

#include "arithmeticdecoder.hpp"
#include "lasmessage.hpp"
#include "lasreaditemraw.hpp"
#include "lasreaditemcompressed_v1.hpp"
#include "lasreaditemcompressed_v2.hpp"
#include "lasreaditemcompressed_v3.hpp"
#include "lasreaditemcompressed_v4.hpp"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits>
#include <exception>


LASreadPoint::LASreadPoint(U32 decompress_selective)
{
  point_size = 0;
  instream = 0;
  num_readers = 0;
  readers = 0;
  readers_raw = 0;
  readers_compressed = 0;
  dec = 0;
  layered_las14_compression = FALSE;
  // used for chunking
  chunk_size = U32_MAX;
  chunk_count = 0;
  current_chunk = 0;
  number_chunks = 0;
  tabled_chunks = 0;
  chunk_totals = 0;
  chunk_starts = 0;
  // used for selective decompression (new LAS 1.4 point types only)
  this->decompress_selective = decompress_selective;
  // used for seeking
  point_start = 0;
  seek_point = 0;
  // used for error and warning reporting
  last_error = 0;
  last_warning = 0;
}

BOOL LASreadPoint::setup(U32 num_items, const LASitem* items, const LASzip* laszip)
{
  U32 i;

  // is laszip exists then we must use its items
  if (laszip)
  {
    if (num_items == 0) return FALSE;
    if (items == 0) return FALSE;
    if (num_items != laszip->num_items) return FALSE;
    if (items != laszip->items) return FALSE;
  }

  // delete old entropy decoder
  if (dec)
  {
    delete dec;
    dec = 0;
    layered_las14_compression = FALSE;
  }

  // is the content compressed?
  if (laszip && laszip->compressor)
  {
    // create new entropy decoder (if requested)
    switch (laszip->coder)
    {
    case LASZIP_CODER_ARITHMETIC:
      dec = new ArithmeticDecoder();
      break;
    default:
      // entropy decoder not supported
      return FALSE;
    }
    // maybe layered compression for LAS 1.4 
    layered_las14_compression = (laszip->compressor == LASZIP_COMPRESSOR_LAYERED_CHUNKED);
    if (layered_las14_compression && ((num_items == 0) || (items[0].type != LASitem::POINT14)))
    {
      return FALSE;
    }
  }
 
  // initizalize the readers
  readers = 0;
  num_readers = num_items;

  // disable chunking
  chunk_size = U32_MAX;

  // always create the raw readers
  readers_raw = new LASreadItem*[num_readers]();
  for (i = 0; i < num_readers; i++)
  {
    switch (items[i].type)
    {
    case LASitem::POINT10:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_POINT10_LE();
      else
        readers_raw[i] = new LASreadItemRaw_POINT10_BE();
      break;
    case LASitem::GPSTIME11:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_GPSTIME11_LE();
      else
        readers_raw[i] = new LASreadItemRaw_GPSTIME11_BE();
       break;
    case LASitem::RGB12:
    case LASitem::RGB14:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_RGB12_LE();
      else
        readers_raw[i] = new LASreadItemRaw_RGB12_BE();
      break;
    case LASitem::BYTE:
    case LASitem::BYTE14:
      readers_raw[i] = new LASreadItemRaw_BYTE(items[i].size);
      break;
    case LASitem::POINT14:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_POINT14_LE();
      else
        readers_raw[i] = new LASreadItemRaw_POINT14_BE();
      break;
    case LASitem::RGBNIR14:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_RGBNIR14_LE();
      else
        readers_raw[i] = new LASreadItemRaw_RGBNIR14_BE();
      break;
    case LASitem::WAVEPACKET13:
    case LASitem::WAVEPACKET14:
      if (Endian::IS_LITTLE_ENDIAN)
        readers_raw[i] = new LASreadItemRaw_WAVEPACKET13_LE();
      else
        readers_raw[i] = new LASreadItemRaw_WAVEPACKET13_BE();
      break;
    default:
      return FALSE;
    }
    if (items[i].size > (U32_MAX - point_size))
    {
      return FALSE;
    }
    point_size += items[i].size;
  }

  if (dec)
  {
    readers_compressed = new LASreadItem*[num_readers]();
    // seeks with compressed data need a seek point
    if (seek_point)
    {
      delete [] seek_point[0];
      delete [] seek_point;
      seek_point = 0;
    }
    if (layered_las14_compression)
    {
      if (point_size > (U32_MAX/2))
      {
        return FALSE;
      }
      if ((point_size*2) <= 22)
      {
        return FALSE;
      }
    }
    seek_point = new U8*[num_items]();
    if (layered_las14_compression)
    {
      // because combo LAS 1.0 - 1.4 point struct has padding
      seek_point[0] = new U8[(point_size*2)];
      // because extended_point_type must be set
      seek_point[0][22] = 1;
    }
    else
    {
      seek_point[0] = new U8[point_size];
    }
    if (!seek_point[0]) return FALSE;
    for (i = 0; i < num_readers; i++)
    {
      switch (items[i].type)
      {
      case LASitem::POINT10:
        if (items[i].version == 1)
          readers_compressed[i] = new LASreadItemCompressed_POINT10_v1(dec);
        else if (items[i].version == 2)
          readers_compressed[i] = new LASreadItemCompressed_POINT10_v2(dec);
        else
          return FALSE;
        break;
      case LASitem::GPSTIME11:
        if (items[i].version == 1)
          readers_compressed[i] = new LASreadItemCompressed_GPSTIME11_v1(dec);
        else if (items[i].version == 2)
          readers_compressed[i] = new LASreadItemCompressed_GPSTIME11_v2(dec);
        else
          return FALSE;
        break;
      case LASitem::RGB12:
        if (items[i].version == 1)
          readers_compressed[i] = new LASreadItemCompressed_RGB12_v1(dec);
        else if (items[i].version == 2)
          readers_compressed[i] = new LASreadItemCompressed_RGB12_v2(dec);
        else
          return FALSE;
        break;
      case LASitem::BYTE:
        if (items[i].version == 1)
          readers_compressed[i] = new LASreadItemCompressed_BYTE_v1(dec, items[i].size);
        else if (items[i].version == 2)
          readers_compressed[i] = new LASreadItemCompressed_BYTE_v2(dec, items[i].size);
        else
          return FALSE;
        break;
      case LASitem::POINT14:
        if ((items[i].version == 3) || (items[i].version == 2)) // version == 2 from lasproto
          readers_compressed[i] = new LASreadItemCompressed_POINT14_v3(dec, decompress_selective);
        else if (items[i].version == 4)
          readers_compressed[i] = new LASreadItemCompressed_POINT14_v4(dec, decompress_selective);
        else
          return FALSE;
        break;
      case LASitem::RGB14:
        if ((items[i].version == 3) || (items[i].version == 2)) // version == 2 from lasproto
          readers_compressed[i] = new LASreadItemCompressed_RGB14_v3(dec, decompress_selective);
        else if (items[i].version == 4)
          readers_compressed[i] = new LASreadItemCompressed_RGB14_v4(dec, decompress_selective);
        else
          return FALSE;
        break;
      case LASitem::RGBNIR14:
        if ((items[i].version == 3) || (items[i].version == 2)) // version == 2 from lasproto
          readers_compressed[i] = new LASreadItemCompressed_RGBNIR14_v3(dec, decompress_selective);
        else if (items[i].version == 4)
          readers_compressed[i] = new LASreadItemCompressed_RGBNIR14_v4(dec, decompress_selective);
        else
          return FALSE;
        break;
      case LASitem::BYTE14:
        if ((items[i].version == 3) || (items[i].version == 2)) // version == 2 from lasproto
          readers_compressed[i] = new LASreadItemCompressed_BYTE14_v3(dec, items[i].size, decompress_selective);
        else if (items[i].version == 4)
          readers_compressed[i] = new LASreadItemCompressed_BYTE14_v4(dec, items[i].size, decompress_selective);
        else
          return FALSE;
        break;
      case LASitem::WAVEPACKET13:
        if (items[i].version == 1)
          readers_compressed[i] = new LASreadItemCompressed_WAVEPACKET13_v1(dec);
        else
          return FALSE;
        break;
      case LASitem::WAVEPACKET14:
        if (items[i].version == 3)
          readers_compressed[i] = new LASreadItemCompressed_WAVEPACKET14_v3(dec, decompress_selective);
        else if (items[i].version == 4)
          readers_compressed[i] = new LASreadItemCompressed_WAVEPACKET14_v4(dec, decompress_selective);
        else
          return FALSE;
        break;
      default:
        return FALSE;
      }
      if (i)
      {
        if (layered_las14_compression)
        {
          // because combo LAS 1.0 - 1.4 point struct has padding
          seek_point[i] = seek_point[i-1]+(2*items[i-1].size);
        }
        else
        {
          seek_point[i] = seek_point[i-1]+items[i-1].size;
        }
      }
    }
    if (laszip->compressor != LASZIP_COMPRESSOR_POINTWISE)
    {
      if (laszip->chunk_size) chunk_size = laszip->chunk_size;
      number_chunks = U32_MAX;
    }
  }
  return TRUE;
}

BOOL LASreadPoint::init(ByteStreamIn* instream)
{
  if (!instream) return FALSE;
  this->instream = instream;

  U32 i;
  for (i = 0; i < num_readers; i++)
  {
    ((LASreadItemRaw*)(readers_raw[i]))->init(instream);
  }

  if (dec)
  {
    chunk_count = chunk_size;
    point_start = 0;
    readers = 0;
  }
  else
  {
    point_start = instream->tell();
    readers = readers_raw;
  }

  return TRUE;
}

BOOL LASreadPoint::seek(const U64 current, const U64 target)
{
  if (!instream->isSeekable()) return FALSE;
  U64 delta = 0;
  if (dec)
  {
    if (point_start == 0)
    {
      init_dec();
      chunk_count = 0;
    }
    if (chunk_starts)
    {
      U32 target_chunk;
      if (chunk_totals)
      {
        if (number_chunks == 0)
        {
          return FALSE;
        }
        target_chunk = search_chunk_table(target, 0, number_chunks);
        chunk_size = (U32)(chunk_totals[target_chunk+1]-chunk_totals[target_chunk]);
        delta = target - chunk_totals[target_chunk];
      }
      else
      {
        target_chunk = (U32)(target / chunk_size);
        delta = target%chunk_size;
      }
      if (target_chunk >= tabled_chunks)
      {
        if (current_chunk < (tabled_chunks-1))
        {
          dec->done();
          current_chunk = (tabled_chunks-1);
          instream->seek(chunk_starts[current_chunk]);
          init_dec();
          chunk_count = 0;
        }
        delta += (chunk_size*(target_chunk-current_chunk) - chunk_count);
      }
      else if (current_chunk != target_chunk || current > target)
      {
        dec->done();
        current_chunk = target_chunk;
        instream->seek(chunk_starts[current_chunk]);
        init_dec();
        chunk_count = 0;
      }
      else
      {
        delta = target - current;
      }
    }
    else if (current > target)
    {
      dec->done();
      instream->seek(point_start);
      init_dec();
      delta = target;
    }
    else if (current < target)
    {
      delta = target - current;
    }
    while (delta)
    {
      if (!read(seek_point))
      {
        return FALSE;
      }
      delta--;
    }
  }
  else
  {
    if (current != target)
    {
      instream->seek(point_start+(I64)point_size*target);
    }
  }
  return TRUE;
}

BOOL LASreadPoint::read(U8* const * point)
{
  U32 i;
  U32 context = 0;

  try
  {
    if (dec)
    {
      if (chunk_count == chunk_size)
      {
        if (point_start != 0)
        {
          dec->done();
          current_chunk++;
          // check integrity
          if (current_chunk < tabled_chunks)
          {
            I64 here = instream->tell();
            if (chunk_starts[current_chunk] != here)
            {
              // previous chunk was corrupt
              current_chunk--;
              throw 4711;
            }
          }
        }
        init_dec();
        if (current_chunk == tabled_chunks) // no or incomplete chunk table?
        {
          if (current_chunk >= number_chunks)
          {
            number_chunks += 256;
            chunk_starts = (I64*)realloc_las(chunk_starts, sizeof(I64)*((U64)number_chunks+1));
          }
          chunk_starts[tabled_chunks] = point_start; // needs fixing
          tabled_chunks++;
        }
        else if (chunk_totals) // variable sized chunks?
        {
          if (((U64)current_chunk + 1) <= (U64)number_chunks)
          {
            chunk_size = (U32)(chunk_totals[current_chunk+1]-chunk_totals[current_chunk]);
          }
          else
          {
            chunk_size = 0;
          }
        }
        chunk_count = 0;
      }
      chunk_count++;

      if (readers)
      {
        for (i = 0; i < num_readers; i++)
        {
          readers[i]->read(point[i], context);
        }
      }
      else
      {
        for (i = 0; i < num_readers; i++)
        {
          readers_raw[i]->read(point[i], context);
        }
        if (layered_las14_compression)
        {
          // for layered compression 'dec' only hands over the stream
          dec->init(instream, FALSE);
          // read how many points are in the chunk
          U32 count;
          instream->get32bitsLE((U8*)&count);
          // read the sizes of all layers
          for (i = 0; i < num_readers; i++)
          {
            ((LASreadItemCompressed*)(readers_compressed[i]))->chunk_sizes();
          }
          for (i = 0; i < num_readers; i++)
          {
            ((LASreadItemCompressed*)(readers_compressed[i]))->init(point[i], context);
          }
          if (DEBUG_OUTPUT_NUM_BYTES_DETAILS) fprintf(stderr, "\n");
        }
        else
        {
          for (i = 0; i < num_readers; i++)
          {
            ((LASreadItemCompressed*)(readers_compressed[i]))->init(point[i], context);
          }
          dec->init(instream);
        }
        readers = readers_compressed;
      }
    }
    else
    {
      for (i = 0; i < num_readers; i++)
      {
        readers[i]->read(point[i], context);
      }
    }
  }
  catch (I32 exception) 
  {
    // create error string
    if (last_error == 0) last_error = new CHAR[128];
    // report error
    if (exception == EOF)
    {
      // end-of-file
      if (dec)
      {
        snprintf(last_error, 128, "end-of-file during chunk with index %u", current_chunk);
      }
      else
      {
        snprintf(last_error, 128, "end-of-file");
      }
    }
    else
    {
      // decompression error
      snprintf(last_error, 128, "chunk with index %u of %u is corrupt", current_chunk, tabled_chunks);
      // if we know where the next chunk starts ...
      if ((current_chunk+1) < tabled_chunks)
      {
        // ... try to seek to the next chunk
        instream->seek(chunk_starts[(current_chunk+1)]);
        // ... ready for next LASreadPoint::read()
        chunk_count = chunk_size;
      }
    }
    return FALSE;
  }
  catch (const std::exception& e)
  {
    if (last_error == 0) last_error = new CHAR[128];
    snprintf(last_error, 128, "chunk with index %u is corrupt or requests excessive memory: %s", current_chunk, e.what());
    return FALSE;
  }
  return TRUE;
}

BOOL LASreadPoint::check_end()
{
  if (readers == readers_compressed)
  {
    if (dec)
    {
      dec->done();
      current_chunk++;
      // check integrity
      if (current_chunk < tabled_chunks)
      {
        I64 here = instream->tell();
        if (chunk_starts[current_chunk] != here) // then last chunk was corrupt
        {
          // create error string
          if (last_error == 0) last_error = new CHAR[128];
          // report error
          snprintf(last_error, 128, "chunk with index %u of %u is corrupt", current_chunk, tabled_chunks);
          return FALSE;
        }
      }
    }
  }
  return TRUE;
}

BOOL LASreadPoint::done()
{
  instream = 0;
  return TRUE;
}

BOOL LASreadPoint::init_dec()
{
  // maybe read chunk table (only if chunking enabled)

  if (number_chunks == U32_MAX)
  {
    if (!read_chunk_table())
    {
      return FALSE;
    }
    current_chunk = 0;
    if (chunk_totals)
    {
      chunk_size = (number_chunks > 0) ? (U32)chunk_totals[1] : 0;
    }
  }

  point_start = instream->tell();
  readers = 0;

  return TRUE;
}

BOOL LASreadPoint::read_chunk_table()
{
  // read the 8 bytes that store the location of the chunk table
  I64 chunk_table_start_position;
  try { instream->get64bitsLE((U8*)&chunk_table_start_position); } catch(...)
  {
    return FALSE;
  }

  // this is where the chunks start
  I64 chunks_start = instream->tell();

  // was compressor interrupted before getting a chance to write the chunk table?
  if ((chunk_table_start_position + 8) == chunks_start)
  {
    // no choice but to fail if adaptive chunking was used
    if (chunk_size == U32_MAX)
    {
      // create error string
      if (last_error == 0) last_error = new CHAR[128];
      // report error
      snprintf(last_error, 128, "compressor was interrupted before writing adaptive chunk table of LAZ file");
      return FALSE;
    }
    // otherwise we build the chunk table as we read the file
    number_chunks = 256;
    chunk_starts = (I64*)malloc_las(sizeof(I64) * (number_chunks + 1));
    if (chunk_starts == 0)
    {
      return FALSE;
    }
    chunk_starts[0] = chunks_start;
    tabled_chunks = 1;
    // create warning string
    if (last_warning == 0) last_warning = new CHAR[128];
    // report warning
    snprintf(last_warning, 128, "compressor was interrupted before writing chunk table of LAZ file");
    return TRUE;
  }

  // maybe the stream is not seekable
  if (!instream->isSeekable())
  {
    // no choice but to fail if adaptive chunking was used
    if (chunk_size == U32_MAX)
    {
      return FALSE;
    }
    // then we cannot seek to the chunk table but won't need it anyways
    number_chunks = 0;
    tabled_chunks = 0;
    return TRUE;
  }

  if (chunk_table_start_position == -1)
  {
    // the compressor was writing to a non-seekable stream and wrote the chunk table start at the end
    if (!instream->seekEnd(8))
    {
      return FALSE;
    }
    try { instream->get64bitsLE((U8*)&chunk_table_start_position); } catch(...)
    {
      return FALSE;
    }
  }

  // read the chunk table
  try
  {
    // seek to where the chunk table
    instream->seek(chunk_table_start_position);
    // fail if we did not manage to seek there
    I64 where_are_we_now = instream->tell();
    if (where_are_we_now != chunk_table_start_position)
    {
      throw 1;
    }
    U32 version;
    instream->get32bitsLE((U8*)&version);
    // fail if the version is wrong
    if (version != 0)
    {
      throw 1;
    }
    instream->get32bitsLE((U8*)&number_chunks);
    if (chunk_totals) delete [] chunk_totals;
    chunk_totals = 0;
    if (chunk_starts) free(chunk_starts);
    chunk_starts = 0;
    if (((U64)number_chunks + 1) > (U64)(std::numeric_limits<size_t>::max() / sizeof(I64)))
    {
      throw 1;
    }
    if (number_chunks > 100000000)
    {
      LASMessage(LAS_WARNING, "chunk table declares %u chunks, which is implausibly many", number_chunks);
    }
    if (chunk_size == U32_MAX)
    {
      chunk_totals = new U64[(U64)number_chunks+1];
      if (chunk_totals == 0)
      {
        throw 1;
      }
      chunk_totals[0] = 0;
    }
    chunk_starts = (I64*)malloc_las(sizeof(I64) * ((U64)number_chunks + 1));
    if (chunk_starts == 0)
    {
      throw 1;
    }
    chunk_starts[0] = chunks_start;
    tabled_chunks = 1;
    if (number_chunks > 0)
    {
      U64 i;
      dec->init(instream);
      IntegerCompressor ic(dec, 32, 2);
      ic.initDecompressor();
      for (i = 1; i <= number_chunks; i++)
      {
        if (chunk_size == U32_MAX) chunk_totals[i] = ic.decompress((i>1 ? (U32)chunk_totals[i-1] : 0), 0);
        chunk_starts[i] = ic.decompress((i>1 ? (U32)(chunk_starts[i-1]) : 0), 1);
        tabled_chunks++;
      }
      dec->done();
      for (i = 1; i <= number_chunks; i++)
      {
        if (chunk_size == U32_MAX) chunk_totals[i] += chunk_totals[i-1];
        chunk_starts[i] += chunk_starts[i-1];
        if (chunk_starts[i] <= chunk_starts[i-1])
        {
          throw 1;
        }
      }
    }
  }
  catch (...)
  {
    // something went wrong while reading the chunk table
    if (chunk_totals) delete [] chunk_totals;
    chunk_totals = 0;
    // no choice but to fail if adaptive chunking was used
    if (chunk_size == U32_MAX)
    {
      return FALSE;
    }
    // did we not even read the number of chunks
    if (number_chunks == U32_MAX)
    {
      // then compressor was interrupted before getting a chance to write the chunk table
      number_chunks = 256;
      chunk_starts = (I64*)malloc_las(sizeof(I64) * ((U64)number_chunks + 1));
      if (chunk_starts == 0)
      {
        return FALSE;
      }
      chunk_starts[0] = chunks_start;
      tabled_chunks = 1;
    }
    else
    {
#pragma warning(push)
#pragma warning(disable : 6011)
      // otherwise fix as many additional chunk_starts as possible
      U32 i;
      for (i = 1; i < tabled_chunks; i++)
      {
        chunk_starts[i] += chunk_starts[i-1];
      }
#pragma warning(pop)
    }
    // create warning string
    if (last_warning == 0) last_warning = new CHAR[128];
    // first seek to the end of the file
    instream->seekEnd();
    // get position of last byte
    I64 last_position = instream->tell();
    // warn if last byte position is before chunk table start position
    if (last_position <= chunk_table_start_position)
    {
      // report warning
      if (last_position == chunk_table_start_position)
      {
        snprintf(last_warning, 128, "chunk table is missing. improper use of LAZ compressor?");
      }
      else
      {
        snprintf(last_warning, 128, "chunk table and %lld bytes are missing. LAZ file truncated during copy or transfer?", chunk_table_start_position - last_position);
      }
    }
    else
    {
      // report warning
      snprintf(last_warning, 128, "corrupt chunk table");
    }
  }
  if (!instream->seek(chunks_start))
  {
    return FALSE;
  }
  return TRUE;
}

U32 LASreadPoint::search_chunk_table(const U64 index, const U32 lower, const U32 upper)
{
  if (lower + 1 == upper) return lower;
  U32 mid = (lower+upper)/2;
  if (index >= chunk_totals[mid])
    return search_chunk_table(index, mid, upper);
  else
    return search_chunk_table(index, lower, mid);
}

LASreadPoint::~LASreadPoint()
{
  U32 i;

  if (readers_raw)
  {
    for (i = 0; i < num_readers; i++)
    {
      delete readers_raw[i];
    }
    delete [] readers_raw;
  }

  if (readers_compressed)
  {
    for (i = 0; i < num_readers; i++)
    {
      delete readers_compressed[i];
    }
    delete [] readers_compressed;
  }

  if (dec)
  {
    delete dec;
  }

  if (chunk_totals) delete [] chunk_totals;
  if (chunk_starts) free(chunk_starts);

  if (seek_point)
  {
    delete [] seek_point[0];
    delete [] seek_point;
  }

  if (last_error) delete [] last_error;
  if (last_warning) delete [] last_warning;
}
