

#if defined(_MSC_VER) && (_MSC_VER >= 1400)
#define _CRT_SECURE_NO_DEPRECATE 1
#endif

#define _WIN32_DCOM

#define _WIN32_WINNT 0x0500
#include "tsystem.h"

#include <windows.h>
#include <objbase.h>
#include "texception.h"
#include "tiio_avi.h"
#include "tsound.h"
#include "tconvert.h"

#include "timageinfo.h"
#include "trasterimage.h"

#include <type_traits>

//------------------------------------------------------------------------------

namespace {

#define DibPtr(lpbi) (LPBYTE)(DibColors(lpbi) + (UINT)(lpbi)->biClrUsed)
#define DibColors(lpbi) ((LPRGBQUAD)((LPBYTE)(lpbi) + (int)(lpbi)->biSize))

//-----------------------------------------------------------

void WideChar2Char(LPCWSTR wideCharStr, char *str, int strBuffSize) {
  WideCharToMultiByte(CP_ACP, 0, wideCharStr, -1, str, strBuffSize, 0, 0);
}

//------------------------------------------------------------------------------

std::string buildAVIExceptionString(int rc) {
  switch (rc) {
  case AVIERR_BADFORMAT:
    return "The file couldn't be read, indicating a corrupt file or an "
           "unrecognized format.";
  case AVIERR_MEMORY:
    return "The file could not be opened because of insufficient memory.";
  case AVIERR_FILEREAD:
    return "A disk error occurred while reading the file.";
  case AVIERR_FILEOPEN:
    return "A disk error occurred while opening the file.";
  case REGDB_E_CLASSNOTREG:
    return "According to the registry, the type of file specified in "
           "m_aviFileOpen does not have a handler to process it.";
  case AVIERR_UNSUPPORTED:
    return "Format unsupported";
  case AVIERR_INTERNAL:
    return "Internal error";
  case AVIERR_NODATA:
    return "No data";
  default:
    return "Unable to create avi.";
  }
}

//------------------------------------------------------------------------------

std::string SplitFourCC(DWORD fcc) {
  std::string s;
  s += (char((fcc & 0x000000ff) >> 0));
  s += (char((fcc & 0x0000ff00) >> 8));
  s += (char((fcc & 0x00ff0000) >> 16));
  s += (char((fcc & 0xff000000) >> 24));

  return s;
}

//-----------------------------------------------------------

TRaster32P DIBToRaster(UCHAR *pDIBImage, int width, int height) {
  assert(pDIBImage);
  if (!pDIBImage) return TRaster32P();

  // BITMAPINFOHEADER *bihdr = reinterpret_cast<BITMAPINFOHEADER *>(pDIBImage);
  UCHAR *rawData = pDIBImage;

  TRaster32P rasOut32(width, height);
  // ULONG imgSize = bihdr->biSizeImage;

  int ly = rasOut32->getLy();
  int n  = 0;
  rasOut32->lock();
  while (n < ly) {
    TPixel32 *pix    = rasOut32->pixels(n);
    TPixel32 *endPix = pix + rasOut32->getLx();
    while (pix < endPix) {
      pix->b = *rawData;
      rawData++;
      pix->g = *rawData;
      rawData++;
      pix->r = *rawData;
      rawData++;
      pix->m = 255;
      ++pix;
      // rawData += 3;
    }
    ++n;
  }
  rasOut32->unlock();
  return rasOut32;
}

//-----------------------------------------------------------

int getPrevKeyFrame(PAVISTREAM videoStream, int index) {
  return AVIStreamFindSample(videoStream, index, FIND_PREV | FIND_KEY);
}

//-----------------------------------------------------------

bool isAKeyFrame(PAVISTREAM videoStream, int index) {
  return index == getPrevKeyFrame(videoStream, index);
}

};  // end of namespace

//===========================================================
//
//  TImageWriterAvi
//
//===========================================================

class TImageWriterAvi final : public TImageWriter {
public:
  int m_frameIndex;

  TImageWriterAvi(const TFilePath &path, int frameIndex, TLevelWriterAvi *lwa)
      : TImageWriter(path), m_frameIndex(frameIndex), m_lwa(lwa) {
    m_lwa->addRef();
  }
  ~TImageWriterAvi() { m_lwa->release(); }

  bool is64bitOutputSupported() override { return false; }
  void save(const TImageP &img) override { m_lwa->save(img, m_frameIndex); }

private:
  TLevelWriterAvi *m_lwa;

  // not implemented
  TImageWriterAvi(const TImageWriterAvi &);
  TImageWriterAvi &operator=(const TImageWriterAvi &src);
};

//===========================================================
//
//  TLevelWriterAvi;
//
//===========================================================

#ifdef _WIN32

TLevelWriterAvi::TLevelWriterAvi(const TFilePath &path, TPropertyGroup *winfo)
    : TLevelWriter(path, winfo)
    , m_aviFile(0)
    , m_videoStream(0)
    , m_audioStream(0)
    , m_bitmapinfo(0)
    , m_outputFmt(0)
    , m_hic(0)
    , m_initDone(false)
    , IOError(0)
    , m_st(0)
    , m_bpp(32)
    , m_maxDataSize(0)
    , m_buffer(0)
    , m_firstframe(-1) {
  CoInitializeEx(0, COINIT_MULTITHREADED);

  m_frameRate = 12.;

  AVIFileInit();

  if (TSystem::doesExistFileOrLevel(m_path)) TSystem::deleteFile(m_path);

  int rc = AVIFileOpenW(&m_aviFile, m_path.getWideString().c_str(),
                        OF_CREATE | OF_WRITE, 0);
  if (rc != 0) throw TImageException(getFilePath(), "Unable to create file");
  m_delayedFrames.clear();
}

//-----------------------------------------------------------

TLevelWriterAvi::~TLevelWriterAvi() {
  // controllo che non ci siano ancora dei frame in coda nel codec (alcuni codec
  // sono asincroni!)
  while (!m_delayedFrames.empty()) {
    LONG lSampWritten, lBytesWritten;
    int frameIndex = m_delayedFrames.front();
    DWORD flagsOut = 0;
    DWORD flagsIn  = !frameIndex ? AVIIF_KEYFRAME : 0;
    BITMAPINFOHEADER outHeader;
    void *bufferOut = 0;
    int res =
        compressFrame(&outHeader, &bufferOut, frameIndex, flagsIn, flagsOut);
    if (res != ICERR_OK) {
      if (bufferOut) _aligned_free(bufferOut);
      break;
    }
    if (AVIStreamWrite(m_videoStream, frameIndex, 1, bufferOut,
                       outHeader.biSizeImage, flagsOut, &lSampWritten,
                       &lBytesWritten)) {
      if (bufferOut) _aligned_free(bufferOut);
      break;
    }
    m_delayedFrames.pop_front();
    if (bufferOut) _aligned_free(bufferOut);
  }

  if (m_st) {
    doSaveSoundTrack();
    m_st = 0;
  }

  if (m_hic) {
    ICCompressEnd(m_hic);
    ICClose(m_hic);
  }

  if (m_bitmapinfo) free(m_bitmapinfo);
  if (m_outputFmt) free(m_outputFmt);

  if (m_videoStream) AVIStreamClose(m_videoStream);

  if (m_audioStream) AVIStreamClose(m_audioStream);

  if (m_aviFile) AVIFileClose(m_aviFile);

  AVIFileExit();
  CoUninitialize();
  if (!m_delayedFrames.empty())
    throw TImageException(
        getFilePath(),
        "error compressing frame " + std::to_string(m_delayedFrames.front()));
}

//-----------------------------------------------------------

void TLevelWriterAvi::searchForCodec() {
  if (!m_properties) m_properties = new Tiio::AviWriterProperties();

  TEnumProperty *p = (TEnumProperty *)m_properties->getProperty("Codec");
  assert(p);
  std::wstring codecName = p->getValue();

  //--------  // cerco compressorName fra i codec

  HIC hic = 0;
  ICINFO icinfo;
  memset(&icinfo, 0, sizeof(ICINFO));
  bool found = false;
  if (codecName != L"Uncompressed") {
    char descr[2048], name[2048];
    DWORD fccType = 0;

    BITMAPINFO inFmt;
    memset(&inFmt, 0, sizeof(BITMAPINFO));

    inFmt.bmiHeader.biSize  = sizeof(BITMAPINFOHEADER);
    inFmt.bmiHeader.biWidth = inFmt.bmiHeader.biHeight = 100;
    inFmt.bmiHeader.biPlanes                           = 1;
    inFmt.bmiHeader.biCompression                      = BI_RGB;
    found                                              = false;

    for (int bpp = 32; (bpp >= 24) && !found; bpp -= 8) {
      inFmt.bmiHeader.biBitCount = bpp;
      for (int i = 0; ICInfo(fccType, i, &icinfo); i++) {
        // Skip blacklisted DLLs
        char driver[2048];
        WideChar2Char(icinfo.szDriver, driver, sizeof(driver));
        if (TSystem::isDLLBlackListed(
                QString::fromStdString(std::string(driver)))) {
          continue;
        }

        hic = ICOpen(icinfo.fccType, icinfo.fccHandler, ICMODE_COMPRESS);

        ICGetInfo(hic, &icinfo,
                  sizeof(ICINFO));  // Find out the compressor name
        WideChar2Char(icinfo.szDescription, descr, sizeof(descr));
        WideChar2Char(icinfo.szName, name, sizeof(name));

        std::string compressorName;
        compressorName = std::string(name) + " '" + std::to_string(bpp) + "' " +
                         std::string(descr);

        if (hic) {
          if (ICCompressQuery(hic, &inFmt, NULL) != ICERR_OK) {
            ICClose(hic);
            continue;  // Skip this compressor if it can't handle the format.
          }
          if (::to_wstring(compressorName) == codecName) {
            found = true;
            m_bpp = bpp;
            break;
          }
          ICClose(hic);
        }
      }
    }
  }
  m_hic = hic;
}

//-----------------------------------------------------------

void TLevelWriterAvi::createBitmap(int lx, int ly) {
  const RGBQUAD NOMASK = {0x00, 0x00, 0x00, 0x00};
  m_bitmapinfo =
      (BITMAPINFO *)calloc(1, sizeof(BITMAPINFOHEADER) + 255 * sizeof(RGBQUAD));

  m_bitmapinfo->bmiHeader.biSize          = sizeof(BITMAPINFOHEADER);
  m_bitmapinfo->bmiHeader.biWidth         = lx;
  m_bitmapinfo->bmiHeader.biHeight        = ly;
  m_bitmapinfo->bmiHeader.biPlanes        = 1;
  m_bitmapinfo->bmiHeader.biBitCount      = m_bpp;
  m_bitmapinfo->bmiHeader.biCompression   = BI_RGB;
  m_bitmapinfo->bmiHeader.biSizeImage     = lx * ly * m_bpp / 8;
  m_bitmapinfo->bmiHeader.biXPelsPerMeter = 0;
  m_bitmapinfo->bmiHeader.biYPelsPerMeter = 0;
  m_bitmapinfo->bmiHeader.biClrUsed       = 0;
  m_bitmapinfo->bmiHeader.biClrImportant  = 0;

  m_bitmapinfo->bmiColors[0] = NOMASK;
  m_bitmapinfo->bmiColors[1] = NOMASK;
  m_bitmapinfo->bmiColors[2] = NOMASK;

  m_buffer = _aligned_malloc(m_bitmapinfo->bmiHeader.biSizeImage, 128);
}

//-----------------------------------------------------------

TImageWriterP TLevelWriterAvi::getFrameWriter(TFrameId fid) {
  if (IOError != 0)
    throw TImageException(m_path, buildAVIExceptionString(IOError));
  if (!fid.getLetter().isEmpty()) return TImageWriterP(0);
  int index            = fid.getNumber() - 1;
  TImageWriterAvi *iwa = new TImageWriterAvi(m_path, index, this);
  return TImageWriterP(iwa);
}

//-----------------------------------------------------------

void TLevelWriterAvi::save(const TImageP &img, int frameIndex) {
  CoInitializeEx(0, COINIT_MULTITHREADED);

  if (m_firstframe < 0) m_firstframe = frameIndex;
  int index = frameIndex - m_firstframe;
  TRasterImageP image(img);
  int lx = image->getRaster()->getLx();
  int ly = image->getRaster()->getLy();

  int pixelSize = image->getRaster()->getPixelSize();

  if (pixelSize != 4)
    throw TImageException(getFilePath(), "Unsupported pixel type");

  QMutexLocker sl(&m_mutex);

  if (!m_initDone) {
    searchForCodec();
    createBitmap(lx, ly);

    DWORD num = DWORD(tround(m_frameRate * 100.0));
    DWORD den = 100;

    AVISTREAMINFO psi;
    memset(&psi, 0, sizeof(AVISTREAMINFO));
    psi.fccType = streamtypeVIDEO;
    if (m_hic) {
      ICINFO icinfo;
      ICGetInfo(m_hic, &icinfo, sizeof(ICINFO));
      psi.fccHandler = icinfo.fccHandler;
    }
    psi.dwScale        = den;
    psi.dwRate         = num;
    psi.dwQuality      = ICQUALITY_DEFAULT;
    psi.rcFrame.right  = lx;
    psi.rcFrame.bottom = ly;
    ::strcpy(psi.szName, m_path.getName().c_str());
    int rc;

    if (AVIFileCreateStream(m_aviFile, &(m_videoStream), &(psi)))
      throw TImageException(getFilePath(), "Unable to create video stream");

    if (!m_hic) {
      if (AVIStreamSetFormat(m_videoStream, 0, &m_bitmapinfo->bmiHeader,
                             m_bitmapinfo->bmiHeader.biSize))
        throw TImageException(getFilePath(), "unable to set format");
    } else {
      m_outputFmt = (BITMAPINFO *)calloc(
          1, sizeof(BITMAPINFOHEADER) + 255 * sizeof(RGBQUAD));
      m_outputFmt->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

      rc = ICCompressGetFormat(m_hic, m_bitmapinfo, m_outputFmt);
      if (rc != ICERR_OK)
        throw TImageException(getFilePath(),
                              "Error codec (ec = " + std::to_string(rc) + ")");

      ICCOMPRESSFRAMES icf;
      memset(&icf, 0, sizeof icf);

      icf.dwFlags     = (DWORD)&icf.lKeyRate;
      icf.lStartFrame = index;
      icf.lFrameCount = 0;
      icf.lQuality    = ICQUALITY_DEFAULT;
      icf.lDataRate   = 0;
      icf.lKeyRate    = (DWORD)(num / den);
      icf.dwRate      = num;
      icf.dwScale     = den;
      ICSendMessage(m_hic, ICM_COMPRESS_FRAMES_INFO, (WPARAM)&icf,
                    sizeof(ICCOMPRESSFRAMES));

      m_maxDataSize = ICCompressGetSize(m_hic, m_bitmapinfo, m_outputFmt);

      rc = ICCompressBegin(m_hic, m_bitmapinfo, m_outputFmt);
      if (rc != ICERR_OK)
        throw TImageException(getFilePath(), "Error starting codec (ec = " +
                                                 std::to_string(rc) + ")");

      if (AVIStreamSetFormat(m_videoStream, 0, &m_outputFmt->bmiHeader,
                             m_outputFmt->bmiHeader.biSize))
        throw TImageException(getFilePath(), "unable to set format");
    }
    m_initDone = true;
  }

  // copio l'immagine nella bitmap che ho creato in createBitmap
  image->getRaster()->lock();
  void *buffin = image->getRaster()->getRawData();
  assert(buffin);

  TRasterGR8P raux;
  if (m_bpp == 24) {
    int newlx = lx * 3;
    raux      = TRasterGR8P(newlx, ly);
    raux->lock();
    UCHAR *buffout24 = (UCHAR *)raux->getRawData();  // new UCHAR[newlx*ly];
    TPixel *buffin32 = (TPixel *)buffin;
    buffin           = buffout24;
    for (int i = 0; i < ly; i++) {
      UCHAR *pix = buffout24 + newlx * i;
      for (int j = 0; j < lx; j++, buffin32++) {
        *pix++ = buffin32->b;
        *pix++ = buffin32->g;
        *pix++ = buffin32->r;
      }
    }
    raux->unlock();
  }
  memcpy(m_buffer, buffin, lx * ly * m_bpp / 8);
  image->getRaster()->unlock();
  raux = TRasterGR8P();

  LONG lSampWritten, lBytesWritten;

  lSampWritten  = 0;
  lBytesWritten = 0;

  if (!m_hic) {
    if (AVIStreamWrite(m_videoStream, index, 1, m_buffer,
                       m_bitmapinfo->bmiHeader.biSizeImage, AVIIF_KEYFRAME,
                       &lSampWritten, &lBytesWritten)) {
      throw TImageException(getFilePath(), "error writing frame");
    }
  } else {
    BITMAPINFOHEADER outHeader;
    void *bufferOut = 0;

    DWORD flagsOut = 0;
    DWORD flagsIn  = !index ? ICCOMPRESS_KEYFRAME : 0;
    int res = compressFrame(&outHeader, &bufferOut, index, flagsIn, flagsOut);
    if (res != ICERR_OK) {
      if (bufferOut) _aligned_free(bufferOut);
      throw TImageException(getFilePath(),
                            "error compressing frame " + std::to_string(index));
    }

    if (outHeader.biCompression == '05xd' ||
        outHeader.biCompression == '05XD' ||
        outHeader.biCompression == 'divx' || outHeader.biCompression == 'DIVX')
      if (outHeader.biSizeImage == 1 && *(char *)bufferOut == 0x7f) {
        m_delayedFrames.push_back(index);
        if (bufferOut) _aligned_free(bufferOut);
        return;
      }
    if (!m_delayedFrames.empty()) {
      m_delayedFrames.push_back(index);
      index = m_delayedFrames.front();
      m_delayedFrames.pop_front();
    }
    if (AVIStreamWrite(m_videoStream, index, 1, bufferOut,
                       outHeader.biSizeImage, !index ? flagsOut : 0,
                       &lSampWritten, &lBytesWritten)) {
      if (bufferOut) _aligned_free(bufferOut);
      throw TImageException(getFilePath(), "error writing frame");
    }
    if (bufferOut) _aligned_free(bufferOut);
  }
}

//-----------------------------------------------------------

int TLevelWriterAvi::compressFrame(BITMAPINFOHEADER *outHeader,
                                   void **bufferOut, int frameIndex,
                                   DWORD flagsIn, DWORD &flagsOut) {
  *bufferOut    = _aligned_malloc(m_maxDataSize, 128);
  *outHeader    = m_outputFmt->bmiHeader;
  DWORD chunkId = 0;
  if (flagsIn) flagsOut = AVIIF_KEYFRAME;
  int res = ICCompress(m_hic, flagsIn, outHeader, *bufferOut,
                       &m_bitmapinfo->bmiHeader, m_buffer, &chunkId, &flagsOut,
                       frameIndex, frameIndex ? 0 : 0xFFFFFF, 0, NULL, NULL);
  return res;
}

#else

TLevelWriterAvi::TLevelWriterAvi(const TFilePath &path) : TLevelWriter(path) {}
TLevelWriterAvi::~TLevelWriterAvi() {}
TImageWriterP TLevelWriterAvi::getFrameWriter(TFrameId) {
  throw TImageException(getFilePath(), "not supported");
  return TImageWriterP(iwa);
}
void TImageWriterAvi::save(const TImageP &) {
  throw TImageException(m_path, "AVI file format not supported");
}

#endif

//-----------------------------------------------------------

void TLevelWriterAvi::saveSoundTrack(TSoundTrack *st) {
  if (!m_aviFile) throw TException("unable to save soundtrack");

  if (!st) throw TException("null reference to soundtrack");

  m_st =
      st;  // prima devo aver salvato tutto il video, salvo l'audio alla fine!
}

//-----------------------------------------------------------

void TLevelWriterAvi::doSaveSoundTrack() {
  WAVEFORMATEX waveinfo;
  int ret;
  LONG lSampWritten, lBytesWritten;

  AVISTREAMINFO audioStreamInfo;
  memset(&audioStreamInfo, 0, sizeof(AVISTREAMINFO));

  audioStreamInfo.fccType    = streamtypeAUDIO;
  audioStreamInfo.fccHandler = 0;
  audioStreamInfo.dwFlags    = 0;
  audioStreamInfo.dwCaps     = 0;
  audioStreamInfo.wPriority  = 0;
  audioStreamInfo.wLanguage  = 0;
  audioStreamInfo.dwScale    = 1;

  audioStreamInfo.dwRate = m_st->getSampleRate();

  audioStreamInfo.dwStart               = 0;
  audioStreamInfo.dwLength              = 0;
  audioStreamInfo.dwInitialFrames       = 0;
  audioStreamInfo.dwSuggestedBufferSize = 0;
  audioStreamInfo.dwQuality             = (ULONG)-1;
  audioStreamInfo.dwSampleSize          = 0;
  audioStreamInfo.rcFrame.left          = 0;
  audioStreamInfo.rcFrame.top           = 0;
  audioStreamInfo.rcFrame.right         = 0;
  audioStreamInfo.rcFrame.bottom        = 0;
  audioStreamInfo.dwEditCount           = 0;
  audioStreamInfo.dwFormatChangeCount   = 0;
  audioStreamInfo.szName[0]             = 0;

  waveinfo.wFormatTag      = WAVE_FORMAT_PCM;  // WAVE_FORMAT_DRM
  waveinfo.nChannels       = m_st->getChannelCount();
  waveinfo.nSamplesPerSec  = m_st->getSampleRate();
  waveinfo.wBitsPerSample  = m_st->getBitPerSample();
  waveinfo.nBlockAlign     = waveinfo.nChannels * waveinfo.wBitsPerSample >> 3;
  waveinfo.nAvgBytesPerSec = waveinfo.nSamplesPerSec * waveinfo.nBlockAlign;
  waveinfo.cbSize          = sizeof(WAVEFORMATEX);

  const UCHAR *buffer = m_st->getRawData();

  if (AVIFileCreateStream(m_aviFile, &m_audioStream, &audioStreamInfo))
    throw TException("error creating soundtrack stream");

  if (AVIStreamSetFormat(m_audioStream, 0, &waveinfo, sizeof(WAVEFORMATEX)))
    throw TException("error setting soundtrack format");

  LONG count   = m_st->getSampleCount();
  LONG bufSize = m_st->getSampleCount() * m_st->getSampleSize();

  if (ret = AVIStreamWrite(m_audioStream, 0, count, (char *)buffer, bufSize,
                           AVIIF_KEYFRAME, &lSampWritten, &lBytesWritten))
    throw TException("error writing soundtrack samples");
}

//===========================================================
//
//  TImageReaderAvi
//
//===========================================================

class TImageReaderAvi final : public TImageReader {
public:
  int m_frameIndex;

  TImageReaderAvi(const TFilePath &path, int index, TLevelReaderAvi *lra)
      : TImageReader(path), m_lra(lra), m_frameIndex(index) {
    m_lra->addRef();
  }
  ~TImageReaderAvi() { m_lra->release(); }

  TImageP load() override { return m_lra->load(m_frameIndex); }
  TDimension getSize() const { return m_lra->getSize(); }
  TRect getBBox() const { return TRect(); }

private:
  TLevelReaderAvi *m_lra;

  // not implemented
  TImageReaderAvi(const TImageReaderAvi &);
  TImageReaderAvi &operator=(const TImageReaderAvi &src);
};

//===========================================================
//
//  TLevelReaderAvi
//
//===========================================================

#ifdef _WIN32
TLevelReaderAvi::TLevelReaderAvi(const TFilePath &path)
    : TLevelReader(path)
#ifdef _WIN32
    , m_srcBitmapInfo(0)
    , m_dstBitmapInfo(0)
    , m_hic(0)
    , IOError(0)
    , m_prevFrame(-1)
    , m_decompressedBuffer(0)
#endif
{
  CoInitializeEx(0, COINIT_MULTITHREADED);

  AVIFileInit();

  int rc = AVIStreamOpenFromFileW(&m_videoStream, path.getWideString().c_str(),
                                  streamtypeVIDEO, 0, OF_READ, 0);
  if (rc != 0) {
    IOError = rc;
    throw TImageException(m_path, buildAVIExceptionString(IOError));
  }
  LONG size       = sizeof(BITMAPINFO);
  m_srcBitmapInfo = (BITMAPINFO *)calloc(1, size);
  m_dstBitmapInfo = (BITMAPINFO *)calloc(1, size);

  rc = AVIStreamReadFormat(m_videoStream, 0, m_srcBitmapInfo, &size);
  if (rc) throw TImageException(getFilePath(), "error reading info.");

  *m_dstBitmapInfo                         = *m_srcBitmapInfo;
  m_dstBitmapInfo->bmiHeader.biCompression = 0;

  AVISTREAMINFO si;
  rc = AVIStreamInfo(m_videoStream, &si, sizeof(AVISTREAMINFO));
  if (rc) throw TImageException(getFilePath(), "error reading info.");

  m_info              = new TImageInfo();
  m_info->m_frameRate = si.dwRate / double(si.dwScale);
  m_info->m_lx        = si.rcFrame.right - si.rcFrame.left;
  m_info->m_ly        = si.rcFrame.bottom - si.rcFrame.top;

  int bpp = m_srcBitmapInfo->bmiHeader.biBitCount;
  switch (bpp) {
  case 32: {
    m_info->m_bitsPerSample  = 8;
    m_info->m_samplePerPixel = 4;
    m_dstBitmapInfo->bmiHeader.biSizeImage =
        m_dstBitmapInfo->bmiHeader.biWidth *
        m_dstBitmapInfo->bmiHeader.biHeight * 4;
    break;
  }
  case 24: {
    m_info->m_bitsPerSample  = 8;
    m_info->m_samplePerPixel = 3;
    m_dstBitmapInfo->bmiHeader.biSizeImage =
        m_dstBitmapInfo->bmiHeader.biWidth *
        m_dstBitmapInfo->bmiHeader.biHeight * 3;
    break;
  }
  case 16: {
    m_info->m_bitsPerSample  = 8;
    m_info->m_samplePerPixel = 2;
    // chiedo al decoder di decomprimerla in un'immagine a 24 bit (sperando che
    // lo permetta)
    m_dstBitmapInfo->bmiHeader.biBitCount = 24;
    m_dstBitmapInfo->bmiHeader.biSizeImage =
        m_dstBitmapInfo->bmiHeader.biWidth *
        m_dstBitmapInfo->bmiHeader.biHeight * 3;
    break;
  }
  default: {
    throw TImageException(getFilePath(),
                          "unable to find a decompressor for this format");
  }
  }

  Tiio::AviWriterProperties *prop = new Tiio::AviWriterProperties();
  m_info->m_properties            = prop;

  if (m_srcBitmapInfo->bmiHeader.biCompression != 0) {
    // ci sono dei codec (es. Xvid) che non decomprimono bene dentro immagini a
    // 32 bit.
    m_dstBitmapInfo->bmiHeader.biBitCount = 24;
    m_dstBitmapInfo->bmiHeader.biSizeImage =
        m_dstBitmapInfo->bmiHeader.biWidth *
        m_dstBitmapInfo->bmiHeader.biHeight * 3;

    ICINFO icinfo;
    memset(&icinfo, 0, sizeof(ICINFO));
    ICInfo(ICTYPE_VIDEO, si.fccHandler, &icinfo);

    char driver[2048];
    WideChar2Char(icinfo.szDriver, driver, sizeof(driver));
    if (!TSystem::isDLLBlackListed(driver))
      m_hic = ICOpen(icinfo.fccType, icinfo.fccHandler, ICMODE_DECOMPRESS);
    if (!m_hic) {
      m_hic = findCandidateDecompressor();
      if (!m_hic)
        throw TImageException(getFilePath(),
                              "unable to find a decompressor for this format");
    }

    DWORD result = ICDecompressQuery(m_hic, m_srcBitmapInfo, m_dstBitmapInfo);
    if (result != ICERR_OK)
      throw TImageException(getFilePath(),
                            "unable to find a decompressor for this format");

    result = ICDecompressBegin(m_hic, m_srcBitmapInfo, m_dstBitmapInfo);
    if (result != ICERR_OK)
      throw TImageException(getFilePath(),
                            "unable to initialize the decompressor");

    char descr[2048], name[2048];
    ICGetInfo(m_hic, &icinfo, sizeof(ICINFO));  // Find out the compressor name
    WideChar2Char(icinfo.szDescription, descr, sizeof(descr));
    WideChar2Char(icinfo.szName, name, sizeof(name));
    std::string compressorName;
    compressorName = std::string(name) + " '" +
                     std::to_string(m_dstBitmapInfo->bmiHeader.biBitCount) +
                     "' " + std::string(descr);
    TEnumProperty *p =
        (TEnumProperty *)m_info->m_properties->getProperty("Codec");
    p->setValue(::to_wstring(compressorName));
    m_decompressedBuffer =
        _aligned_malloc(m_dstBitmapInfo->bmiHeader.biSizeImage, 128);
  } else {
    m_hic = 0;
    TEnumProperty *p =
        (TEnumProperty *)m_info->m_properties->getProperty("Codec");
    p->setValue(L"Uncompressed");
    m_decompressedBuffer = _aligned_malloc(si.dwSuggestedBufferSize, 128);
  }
}

//------------------------------------------------

TLevelReaderAvi::~TLevelReaderAvi() {
  if (m_hic) {
    ICDecompressEnd(m_hic);
    ICClose(m_hic);
  }

  if (m_srcBitmapInfo) free(m_srcBitmapInfo);
  if (m_dstBitmapInfo) free(m_dstBitmapInfo);

  if (m_videoStream) AVIStreamClose(m_videoStream);

  AVIFileExit();
  _aligned_free(m_decompressedBuffer);
}

HIC TLevelReaderAvi::findCandidateDecompressor() {
  ICINFO info        = {0};
  BITMAPINFO srcInfo = *m_srcBitmapInfo;
  BITMAPINFO dstInfo = *m_dstBitmapInfo;

  for (DWORD id = 0; ICInfo(ICTYPE_VIDEO, id, &info); ++id) {
    info.dwSize = sizeof(
        ICINFO);  // I don't think this is necessary, but just in case....

    // Skip blacklisted DLLs
    char driver[2048];
    WideChar2Char(info.szDriver, driver, sizeof(driver));
    if (TSystem::isDLLBlackListed(
            QString::fromStdString(std::string(driver)))) {
      continue;
    }

    HIC hic = ICOpen(info.fccType, info.fccHandler, ICMODE_DECOMPRESS);

    if (!hic) continue;

    DWORD result = ICDecompressQuery(hic, &srcInfo, &dstInfo);

    if (result == ICERR_OK) {
      // Check for a codec that doesn't actually support what it says it does.
      // We ask the codec whether it can do a specific conversion that it can't
      // possibly support.  If it does it, then we call BS and ignore the codec.
      // The Grand Tech Camera Codec and Panasonic DV codecs are known to do
      // this.
      //
      // (general idea from Raymond Chen's blog)

      BITMAPINFOHEADER testSrc = {// note: can't be static const since
                                  // IsBadWritePtr() will get called on it
                                  sizeof(BITMAPINFOHEADER),
                                  320,
                                  240,
                                  1,
                                  24,
                                  0x2E532E42,
                                  320 * 240 * 3,
                                  0,
                                  0,
                                  0,
                                  0};

      DWORD res = ICDecompressQuery(hic, &testSrc, NULL);

      if (ICERR_OK == res) {  // Don't need to wrap this, as it's OK if testSrc
                              // gets modified.
        ICINFO info = {sizeof(ICINFO)};
        ICGetInfo(hic, &info, sizeof info);

        // Okay, let's give the codec a chance to redeem itself. Reformat the
        // input format into
        // a plain 24-bit RGB image, and ask it what the compressed format is.
        // If it produces
        // a FOURCC that matches, allow it to handle the format. This should
        // allow at least
        // the codec's primary format to work. Otherwise, drop it on the ground.

        if (m_srcBitmapInfo) {
          BITMAPINFOHEADER unpackedSrc = {sizeof(BITMAPINFOHEADER),
                                          m_srcBitmapInfo->bmiHeader.biWidth,
                                          m_srcBitmapInfo->bmiHeader.biHeight,
                                          1,
                                          24,
                                          BI_RGB,
                                          0,
                                          0,
                                          0,
                                          0,
                                          0};

          unpackedSrc.biSizeImage =
              ((unpackedSrc.biWidth * 3 + 3) & ~3) * abs(unpackedSrc.biHeight);

          LONG size = ICCompressGetFormatSize(hic, &unpackedSrc);

          if (size >= sizeof(BITMAPINFOHEADER)) {
            BITMAPINFOHEADER tmp;
            if (ICERR_OK == ICCompressGetFormat(hic, &unpackedSrc, &tmp) &&
                tmp.biCompression == m_srcBitmapInfo->bmiHeader.biCompression)
              return hic;
          }
        }
      } else
        return hic;
    }
    ICClose(hic);
  }
  return NULL;
}

//-----------------------------------------------------------

TLevelP TLevelReaderAvi::loadInfo() {
  if (IOError) throw TImageException(m_path, buildAVIExceptionString(IOError));
  int nFrames = AVIStreamLength(m_videoStream);
  if (nFrames == -1) return TLevelP();
  TLevelP level;
  for (int i = 1; i <= nFrames; i++) level->setFrame(i, TImageP());
  return level;
}

//-----------------------------------------------------------

TImageReaderP TLevelReaderAvi::getFrameReader(TFrameId fid) {
  if (IOError != 0)
    throw TImageException(m_path, buildAVIExceptionString(IOError));
  if (!fid.getLetter().isEmpty()) return TImageReaderP(0);
  int index = fid.getNumber() - 1;

  TImageReaderAvi *ira = new TImageReaderAvi(m_path, index, this);
  return TImageReaderP(ira);
}

//------------------------------------------------------------------------------

TDimension TLevelReaderAvi::getSize() {
  QMutexLocker sl(&m_mutex);
  AVISTREAMINFO psi;
  AVIStreamInfo(m_videoStream, &psi, sizeof(AVISTREAMINFO));
  return TDimension(psi.rcFrame.right - psi.rcFrame.left,
                    psi.rcFrame.bottom - psi.rcFrame.top);
}

//------------------------------------------------

TImageP TLevelReaderAvi::load(int frameIndex) {
  AVISTREAMINFO si;
  int rc = AVIStreamInfo(m_videoStream, &si, sizeof(AVISTREAMINFO));
  if (rc) throw TImageException(getFilePath(), "error reading info.");
  void *bufferOut = 0;

  if (!m_hic) {
    rc = readFrameFromStream(m_decompressedBuffer, si.dwSuggestedBufferSize,
                             frameIndex);
    if (rc) {
      throw TImageException(m_path, "unable read frame " +
                                        std::to_string(frameIndex) +
                                        "from video stream.");
    }
  } else {
    int prevKeyFrame = m_prevFrame == frameIndex - 1
                           ? frameIndex
                           : getPrevKeyFrame(m_videoStream, frameIndex);
    while (prevKeyFrame <= frameIndex) {
      bufferOut       = _aligned_malloc(si.dwSuggestedBufferSize, 128);
      DWORD bytesRead = si.dwSuggestedBufferSize;
      rc              = readFrameFromStream(bufferOut, bytesRead, prevKeyFrame);
      if (rc) {
        _aligned_free(bufferOut);
        throw TImageException(m_path, "unable read frame " +
                                          std::to_string(frameIndex) +
                                          "from video stream.");
      }

      DWORD res = decompressFrame(bufferOut, bytesRead, m_decompressedBuffer,
                                  prevKeyFrame, frameIndex);

      _aligned_free(bufferOut);
      if (res != ICERR_OK) {
        throw TImageException(
            m_path, "error decompressing frame " + std::to_string(frameIndex));
      }
      prevKeyFrame++;
    }
  }

  int width  = m_srcBitmapInfo->bmiHeader.biWidth;
  int height = m_srcBitmapInfo->bmiHeader.biHeight;

  int bpp     = m_dstBitmapInfo->bmiHeader.biBitCount;
  m_prevFrame = frameIndex;
  switch (bpp) {
  case 32: {
    TRasterPT<TPixelRGBM32> ret;
    ret.create(width, height);
    ret->lock();
    memcpy(ret->getRawData(), m_decompressedBuffer, width * height * 4);
    ret->unlock();
    return TRasterImageP(ret);
  }
  case 24: {
    TRasterImageP i(DIBToRaster((UCHAR *)m_decompressedBuffer, width, height));
    return i;
  }
  default: {
    throw TImageException(m_path,
                          std::to_string(bpp) + " to 32 bit not supported\n");
  }
  }
  return TRasterImageP();
}

//------------------------------------------------

int TLevelReaderAvi::readFrameFromStream(void *bufferOut, DWORD &bufferSize,
                                         int frameIndex) const {
  assert(bufferOut && bufferSize > 0);
  LONG bytesReaded = 0;
  LONG samplesRead = 0;

  int rc = AVIStreamRead(m_videoStream, frameIndex, 1, bufferOut, bufferSize,
                         &bytesReaded, &samplesRead);
  if (!rc) {
    assert(samplesRead == 1);  // must have read a frame!!!
    assert(bytesReaded <=
           (LONG)bufferSize);  // must have read a number of bytes
    // less than or equal to what we expect
    bufferSize = bytesReaded;
  }
  return rc;
}

//------------------------------------------------

DWORD TLevelReaderAvi::decompressFrame(void *srcBuffer, int srcSize,
                                       void *dstBuffer, int currentFrame,
                                       int desiredFrame) {
  BITMAPINFOHEADER srcHeader = m_srcBitmapInfo->bmiHeader;
  BITMAPINFOHEADER dstHeader = m_dstBitmapInfo->bmiHeader;
  srcHeader.biSizeImage      = srcSize;

  DWORD dwFlags = 0;

  if (!isAKeyFrame(m_videoStream, currentFrame))
    dwFlags |= ICDECOMPRESS_NOTKEYFRAME;
  if (currentFrame < desiredFrame) dwFlags |= ICDECOMPRESS_PREROLL;

  DWORD res = ICDecompress(m_hic, dwFlags, &srcHeader, srcBuffer, &dstHeader,
                           dstBuffer);
  return res;
}

#else
TLevelReaderAvi::TLevelReaderAvi(const TFilePath &path)::TLevelReader(path) {}
TLevelReaderAvi::~TLevelReaderAvi() {}
TLevelP TLevelReaderAvi::loadInfo() { return TLevelP(); }
TImageReaderP TLevelReaderAvi::getFrameReader(TFrameId fid) {
  throw TImageException(m_path, "AVI not supported");
  return TImageReaderP(0);
}
TDimension TLevelReaderAvi::getSize() { return TDimension(); }
TImageP TLevelReaderAvi::load(int frameIndex) {
  throw TImageException(m_path, "AVI not supported");
  return TImageP(0);
}
#endif

//===========================================================
//
//  Tiio::AviWriterProperties
//
//===========================================================

#ifdef _WIN32

namespace {
BOOL safe_ICInfo(DWORD fccType, DWORD fccHandler, ICINFO *lpicinfo) {
#ifdef _MSC_VER
  __try {
    return ICInfo(fccType, fccHandler, lpicinfo);
  } __except (EXCEPTION_EXECUTE_HANDLER) {
  }
  return FALSE;
#else
  return ICInfo(fccType, fccHandler, lpicinfo);
#endif
}

LRESULT safe_ICClose(HIC hic) {
#ifdef _MSC_VER
  __try {
    if (hic) {
      return ICClose(hic);
    }
  } __except (EXCEPTION_EXECUTE_HANDLER) {
  }
#else
  if (hic) {
    return ICClose(hic);
  }
#endif
  return ICERR_OK;
}

#ifdef _MSC_VER
typedef std::unique_ptr<std::remove_pointer_t<HIC>, decltype(&safe_ICClose)>
    hic_t;
#else
typedef std::unique_ptr<std::remove_pointer<HIC>::type, decltype(&safe_ICClose)>
    hic_t;
#endif

hic_t safe_ICOpen(DWORD fccType, DWORD fccHandler, UINT wMode) {
#ifdef _MSC_VER
  HIC const hic = [fccType, fccHandler, wMode]() -> HIC {
    __try {
      return ICOpen(fccType, fccHandler, wMode);
    } __except (EXCEPTION_EXECUTE_HANDLER) {
    }
    return nullptr;
  }();
#else
  HIC hic = nullptr;
  try {
    hic = ICOpen(fccType, fccHandler, wMode);
  } catch (...) {
  }
#endif
  return hic_t(hic, safe_ICClose);
}

LRESULT safe_ICGetInfo(hic_t const &hic, ICINFO *picinfo, DWORD cb) {
#ifdef _MSC_VER
  __try {
    return ICGetInfo(hic.get(), picinfo, cb);
  } __except (EXCEPTION_EXECUTE_HANDLER) {
  }
  return 0;  // return copied size in bytes (0 means an error)
#else
  return ICGetInfo(hic.get(), picinfo, cb);
#endif
}

LRESULT safe_ICCompressQuery(hic_t const &hic, BITMAPINFO *lpbiInput,
                             BITMAPINFO *lpbiOutput) {
#ifdef _MSC_VER
  __try {
    return ICCompressQuery(hic.get(), lpbiInput, lpbiOutput);
  } __except (EXCEPTION_EXECUTE_HANDLER) {
  }
  return ICERR_INTERNAL;
#else
  return ICCompressQuery(hic.get(), lpbiInput, lpbiOutput);
#endif
}
}  // namespace

Tiio::AviWriterProperties::AviWriterProperties() : m_codec("Codec") {
  if (m_defaultCodec.getRange().empty()) {
    char descr[2048], name[2048];
    DWORD fccType = 0;
    ICINFO icinfo;
    BITMAPINFO inFmt;

    m_defaultCodec.addValue(L"Uncompressed");

    memset(&inFmt, 0, sizeof(BITMAPINFO));
    inFmt.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    inFmt.bmiHeader.biWidth       = 100;
    inFmt.bmiHeader.biHeight      = 100;
    inFmt.bmiHeader.biPlanes      = 1;
    inFmt.bmiHeader.biCompression = BI_RGB;
    for (int bpp = 32; bpp >= 24; bpp -= 8) {
      inFmt.bmiHeader.biBitCount = bpp;
      for (int i = 0;; i++) {
        memset(&icinfo, 0, sizeof icinfo);
        if (!safe_ICInfo(fccType, i, &icinfo)) {
          break;
        }

        // Skip blacklisted DLLs
        char driver[2048];
        WideChar2Char(icinfo.szDriver, driver, sizeof(driver));
        if (TSystem::isDLLBlackListed(
                QString::fromStdString(std::string(driver)))) {
          continue;
        }

        auto const hic =
            safe_ICOpen(icinfo.fccType, icinfo.fccHandler, ICMODE_QUERY);
        if (!hic) {
          break;
        }

        // Find out the compressor name
        if (safe_ICGetInfo(hic, &icinfo, sizeof(ICINFO)) == 0) {
          break;
        }

        WideChar2Char(icinfo.szDescription, descr, sizeof(descr));
        WideChar2Char(icinfo.szName, name, sizeof(name));
        if ((strstr(name, "IYUV") != 0) ||
            ((strstr(name, "IR32") != 0) && (bpp == 24))) {
          continue;
        }
        // Give up to load codecs once the blackmagic codec is found -
        // as it seems to cause crash for unknown reasons (issue #138)
        if (strstr(descr, "Blackmagic") != 0) break;

        std::string compressorName;
        compressorName = std::string(name) + " '" + std::to_string(bpp) + "' " +
                         std::string(descr);

        // per il momento togliamo i codec indeo
        if (std::string(compressorName).find("Indeo") != -1) {
          continue;
        }

        if (safe_ICCompressQuery(hic, &inFmt, nullptr) != ICERR_OK) {
          continue;  // Skip this compressor if it can't handle the format.
        }

        m_defaultCodec.addValue(::to_wstring(compressorName));
        if (compressorName.find("inepak") != -1) {
          m_defaultCodec.setValue(::to_wstring(compressorName));
        }
      }
    }
  }
  m_codec = m_defaultCodec;
  bind(m_codec);
}

void Tiio::AviWriterProperties::updateTranslation() {
  m_codec.setQStringName(tr("Codec"));
  m_codec.setItemUIName(L"Uncompressed", tr("Uncompressed"));
}

TEnumProperty Tiio::AviWriterProperties::m_defaultCodec =
    TEnumProperty("Codec");

#endif
