/*
 * C++ TGA library.
 * Copyright (C) 2008 - 2018 René Rebe, ExactCODE GmbH Germany
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2. A copy of the GNU General
 * Public License can be found in the file LICENSE.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANT-
 * ABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * Alternatively, commercial licensing options are available from the
 * copyright holder ExactCODE GmbH Germany.
 *
 */

#include <iostream>

#include "tga.hh"

#include "Colorspace.hh"

#include <string.h>
#include <ctype.h>

#include <Endianess.hh>
#include <stdint.h>

using Exact::EndianessConverter;
using Exact::LittleEndianTraits;

/*
  Header
  Footer
 */

typedef struct
{
  uint8_t IDLength;
  uint8_t ColorMapType;
  uint8_t ImageType;
  EndianessConverter<uint16_t,LittleEndianTraits> ColorMapIndex;
  EndianessConverter<uint16_t,LittleEndianTraits> ColorMapLength;
  uint8_t ColorMapEntrySize;
  
  EndianessConverter<uint16_t,LittleEndianTraits> ImageXOrigin;
  EndianessConverter<uint16_t,LittleEndianTraits> ImageYOrigin;
  EndianessConverter<uint16_t,LittleEndianTraits> ImageWidth;
  EndianessConverter<uint16_t,LittleEndianTraits> ImageHeight;
  uint8_t ImageDepth;
  uint8_t ImageDescriptor;
} __attribute__((packed)) TGAHeader;

typedef struct
{
  EndianessConverter<uint32_t,LittleEndianTraits> ExtensionOffset;
  EndianessConverter<uint32_t,LittleEndianTraits> DeveloperOffset;
  
  char Signature[16];
  uint8_t ReservedCharacter;
  uint8_t ZeroTerminator;
} __attribute__((packed)) TGAFooter;

// TODO: testing only, use regular iterators, move to Colorspace, share with BMP
static void colorspace_bgr8_to_rgb8(uint8_t* begin, uint8_t* end, const int bytes = 3)
{
  for (uint8_t* it = begin; it < end; it += bytes)
    std::swap(it[0], it[2]);
}


int TGACodec::readImage(std::istream* stream, Image& image, const std::string& decompress)
{
  Args args(decompress);
  const bool debug = args.containsAndRemove("debug");
  bool rle = false;

  /*
  if (!stream->seekg(26, std::ios::end))
    return false;
  
  TGAFooter footer;
  
  if (!stream->read((char*)&footer, sizeof(footer)))
    return false;
  
  */
  
  TGAHeader header;
  if (!stream->read((char*)&header, sizeof(header))) {
     stream->seekg(0);
     return false;
  }
  
  // TODO: differentiate between "maybe be old TGA" and definetly
  //       should be new-style TGA with footer
  int ImageDepth = header.ImageDepth;
  switch (ImageDepth)
    {
    case 1:
    case 8:
    case 16:
    case 24:
    case 32:
      break;
    default:
      if (debug) std::cerr << "TGA: unknown depth" << std::endl;
      stream->seekg(0);
      return false;
    };

  // TODO: harden checks for "maybe TGA", ...
  
  switch (header.ImageType)
    {
    case 9: // RLE, color mapped
    case 10: // RLE, true color
      rle = true;
    case 1: // color mapped
    case 2: // true color
      image.spp = 3;
      break;
    
    case 11: // RLE, b&w
      rle = true;
    case 3: // b&w
      image.spp = 1;
      break;
      
    default:
      if (debug) std::cerr << "TGA: unknown type" << std::endl;
      stream->seekg(0);
      return false;
    }

  if (debug)
    std::cerr << "TGA: " << (int)header.IDLength << ", " << (int)header.ImageType
	      << ", " << ImageDepth << ", " << (int)header.ColorMapType
	      << ", " << header.ImageWidth << ", " << header.ImageHeight
	      << ", " << header.ImageDescriptor << std::endl;
  
  if (ImageDepth == 32)
    image.spp = 4;
  
  image.bps = ImageDepth / image.spp;
  image.setResolution(0, 0); // TODO
  
  image.resize(header.ImageWidth, header.ImageHeight);
  
  if (header.ColorMapType == 1) {
    // seek to color map
    stream->seekg(sizeof(header) + header.IDLength);
  }

  // seek to image data
  stream->seekg(sizeof(header) + header.IDLength);
  
  const int bytes = ImageDepth / 8;
  if (!rle) {
    const bool topdown = (header.ImageDescriptor >> 5) & 1;
    const int beg = topdown ? 0 : image.h - 1, end = topdown ? image.h : -1,
      inc = topdown ? 1 : -1;
    const int stride = image.stride();
    uint8_t* data = image.getRawData();
    for (int y = beg; y != end; y += inc) {
      stream->read((char*)data + y * stride, stride);
      if (ImageDepth >= 24)
	colorspace_bgr8_to_rgb8(data + y * stride, data + (y+1) * stride, bytes);
    }
  } else {
    uint8_t* data = image.getRawData();
    
    // new spec says RLE "should" not overrun scanlines, but old spec did
    for (int i = 0; i < image.stride() * image.h;)
      {
	int t = stream->get();
	uint8_t n = (t & 0x7f) + 1;
	const bool rle = t & 0x80;
        if (t == EOF) {
	  std::cerr << "tga: eof" << std::endl;
	  return true; // false;
        }
	if (rle) { // RLE
	  stream->read((char*)data + i, bytes);
	  if (ImageDepth >= 24)
	    colorspace_bgr8_to_rgb8(data + i, data + i + bytes, bytes);
	  i += bytes;
	  for (--n; n > 0 && i < image.stride() * image.h; --n)
	    for (int j = 0; j < bytes; ++j, ++i)
	      data[i] = data[i - bytes];
	} else { // RAW
	  stream->read((char*)data + i, n * bytes);
	  if (ImageDepth >= 24)
	    colorspace_bgr8_to_rgb8(data + i, data + i + n * bytes, bytes);
	  i += n * bytes;
	}
      }
  }
  
  return true;
  
  stream->seekg(0);
  return false;
}

bool TGACodec::writeImage(std::ostream* stream, Image& image, int quality,
			  const std::string& compress)
{
  Args args(compress);
  const bool rle = args.containsAndRemove("rle");
  
  if (image.bps != 8) {
    std::cerr << "TGA: unsupported bps: " << (int)image.bps << std::endl;
    return false;
  }
      
  TGAHeader header = {};
  //header.IDLength = 0;
  //header.ColorMapType = 0;
  if (!rle)
    header.ImageType = image.spp == 1 ? 3 /*Truecolor*/ : 2/*bw*/;
  else
    header.ImageType = image.spp == 1 ? 11 /*bw*/ : 10/*Truecolor*/;
  //header.ColorMapIndex = 0;
  //header.ColorMapLength = 0;
  //header.ColorMapEntrySize = 0;
  //header.ImageXOrigin = 0;
  //header.ImageYOrigin = 0;
  header.ImageWidth = image.width();
  header.ImageHeight = image.height();
  header.ImageDepth = image.spp * image.bps;
  header.ImageDescriptor = 0x20; // top-left
  stream->write((char*)&header, sizeof(header));
  
  struct accessor {
    accessor(uint8_t* ptr, const uint8_t bytes)
      : ptr(ptr), bytes(bytes) {
    }
    
    uint32_t get(unsigned int i) {
      uint32_t v = 0;
      uint8_t* data = ptr + i * bytes;
      for (uint8_t j = 0; j < bytes; ++j)
	v = v << 8 | data[j];
      return v;
    }
    
    void write(std::ostream* stream, int i, int n) {
      for (; n > 0; --n) {
	uint32_t v = get(i++);
	for (int j = bytes; j > 0; --j) {
	  *stream << (char)(v & 0xff);
	  v >>= 8;
	}
      }
    }
    
    uint8_t* ptr;
    uint8_t bytes;
  };

  const unsigned stride = image.stride();
  for (int y = 0; y < image.height(); ++y) {
    uint8_t* data = image.getRawData() + y * stride;
    accessor row(data, image.spp);
    if (!rle) {
      row.write(stream, 0, image.w);
    } else {
      // scan the whole stride - find repeating run
      for (int i = 0; i < image.w;) {
	int j;
	for (j = i + 1; j < image.w; ++j) {
	  if (row.get(j - 1) == row.get(j)
	      //y&& j + 1 < image.w && row.get(j + 1) == row.get(j)
	      ) {
	    --j; // first repeating
	    break;
	  }
	}
	
	// store non-repeating:
	for (unsigned rawlen = j - i; rawlen > 0;) {
	  unsigned len = rawlen < 0x80 ? rawlen : 0x80;
	  *stream << (char)(len - 1); // high bit not set for uncompressed
	  row.write(stream, i, len);
	  i += len;
	  rawlen -= len;
	}
	// find length of repeating run
	for (i = j; j < image.w; ++j) {
	  if (row.get(i) != row.get(j))
	    break;
	}
	
	for (int runlen = j - i; runlen > 1;) {
	  unsigned len = runlen < 0x80 ? runlen : 0x80;
	  *stream << (char)(0x80 | (len - 1));
	  row.write(stream, i, 1);
	  i += len;
	  runlen -= len;
	}
      }
    }
  }
  
  TGAFooter footer = {};
  //footer.ExtensionOffset = 0;
  //footer.DeveloperOffset = 0;
  memcpy(footer.Signature, "TRUEVISION-XFILE", sizeof(footer.Signature));
  footer.ReservedCharacter = '.';
  //footer.ZeroTerminator = 0;
  stream->write((char*)&footer, sizeof(footer));
  
  return true;
}

TGACodec tga_codec;
