/** @file
  BrotliCompress Compress/Decompress tool (BrotliCompress)

  Copyright (c) 2020, ByoSoft Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

/* Command line interface for Brotli library. */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>

#include "./brotli/c/common/constants.h"
#include "./brotli/c/common/version.h"
#include <brotli/decode.h>
#include <brotli/encode.h>

#if !defined(_WIN32)
#include <unistd.h>
#include <utime.h>
#else
#include <io.h>
#include <share.h>
#include <sys/utime.h>

#if !defined(__MINGW32__)
#define STDIN_FILENO _fileno(stdin)
#define STDOUT_FILENO _fileno(stdout)
#define S_IRUSR S_IREAD
#define S_IWUSR S_IWRITE
#endif

#define fopen ms_fopen
#define open ms_open

#if defined(_MSC_VER) && (_MSC_VER >= 1400)
#define fseek _fseeki64
#define ftell _ftelli64
#endif

static FILE* ms_fopen(const char* FileName, const char* Mode) {
  FILE* Result;
  Result = NULL;
  fopen_s(&Result, FileName, Mode);
  return Result;
}

static int ms_open(const char* FileName, int Oflag, int Pmode) {
  int Result;
  Result = -1;
  _sopen_s(&Result, FileName, Oflag | O_BINARY, _SH_DENYNO, Pmode);
  return Result;
}
#endif  /* WIN32 */


#ifndef _MAX_PATH
#define _MAX_PATH 500
#endif

#define DEFAULT_LGWIN 22
#define DECODE_HEADER_SIZE 0x10
#define GAP_MEM_BLOCK 0x1000
size_t ScratchBufferSize = 0;
static const size_t kFileBufferSize  = 1 << 19;

static void Version(void) {
  int Major;
  int Minor;
  int Patch;
  Major = BROTLI_VERSION >> 24;
  Minor = (BROTLI_VERSION >> 12) & 0xFFF;
  Patch = BROTLI_VERSION & 0xFFF;
  printf("BrotliCompress %d.%d.%d\n", Major, Minor, Patch);
}

static void Usage() {
  printf("Usage: %s [OPTION]... [FILE]...\n", __FILE__);
  printf(
"Options:\n"
"  -e, --compress              compress\n"
"  -d, --decompress            decompress\n"
"  -h, --help                  display this help and exit\n");
  printf(
"  -o FILE, --output=FILE      output file (only if 1 input file)\n");
 printf(
"  -g NUM, --gap=NUM           scratch memory gap level (1-16)\n");
  printf(
"  -q NUM, --quality=NUM       compression level (%d-%d)\n",
          BROTLI_MIN_QUALITY, BROTLI_MAX_QUALITY);
  printf(
"  -v, --version               display version and exit\n");
}

static int64_t FileSize(const char* Path) {
  FILE *FileHandle;
  int64_t RetVal;
  FileHandle = fopen(Path, "rb");

  if (FileHandle == NULL) {
    printf ("Failed to open file [%s]\n", Path);
    return -1;
  }
  if (fseek(FileHandle, 0L, SEEK_END) != 0) {
    printf ("Failed to seek file [%s]\n", Path);
    fclose(FileHandle);
    return -1;
  }
  RetVal = ftell(FileHandle);
  if (fclose(FileHandle) != 0) {
    printf ("Failed to close file [%s]\n", Path);
    return -1;
  }
  return RetVal;
}

static BROTLI_BOOL HasMoreInput(FILE *FileHandle) {
  return feof(FileHandle) ? BROTLI_FALSE : BROTLI_TRUE;
}

int OpenFiles(char *InputFile, FILE **InHandle, char *OutputFile, FILE **OutHandle) {
  *InHandle = NULL;
  *OutHandle = NULL;
  *InHandle = fopen(InputFile, "rb");
  if (*InHandle == NULL) {
    printf("Failed to open input file [%s]\n", InputFile);
    return BROTLI_FALSE;
  }

  *OutHandle = fopen(OutputFile, "wb+");
  if (*OutHandle == NULL) {
    printf("Failed to open output file [%s]\n", OutputFile);
    fclose(*InHandle);
    return BROTLI_FALSE;
  }
  return BROTLI_TRUE;
}

int CompressFile(char *InputFile, uint8_t *InputBuffer, char *OutputFile, uint8_t *OutputBuffer, int Quality, int Gap) {
  int64_t InputFileSize;
  FILE *InputFileHandle;
  FILE *OutputFileHandle;
  BrotliEncoderState *EncodeState;
  uint32_t LgWin;
  BROTLI_BOOL IsEof;
  size_t AvailableIn;
  const uint8_t *NextIn;
  size_t AvailableOut;
  uint8_t *NextOut;
  uint8_t *Input;
  uint8_t *Output;
  size_t TotalOut;
  size_t OutSize;
  uint32_t SizeHint;
  BROTLI_BOOL IsOk;
  AvailableIn = 0;
  IsEof = BROTLI_FALSE;
  Input = InputBuffer;
  Output = OutputBuffer;
  IsOk = BROTLI_TRUE;
  LgWin = DEFAULT_LGWIN;

  InputFileSize = FileSize(InputFile);

  IsOk = OpenFiles(InputFile, &InputFileHandle, OutputFile, &OutputFileHandle);
  if (!IsOk) {
    return IsOk;
  }

  fseek (OutputFileHandle, DECODE_HEADER_SIZE, SEEK_SET);

  EncodeState = BrotliEncoderCreateInstance(NULL, NULL, NULL);
  if (!EncodeState) {
    printf("Out of memory\n");
    IsOk = BROTLI_FALSE;
    goto Finish;
  }
  BrotliEncoderSetParameter(EncodeState, BROTLI_PARAM_QUALITY, (uint32_t)Quality);

  if (InputFileSize >= 0) {
    LgWin = BROTLI_MIN_WINDOW_BITS;
    while (BROTLI_MAX_BACKWARD_LIMIT(LgWin) < InputFileSize) {
      LgWin++;
      if (LgWin == BROTLI_MAX_WINDOW_BITS) {
        break;
      }
    }
  }
  BrotliEncoderSetParameter(EncodeState, BROTLI_PARAM_LGWIN, LgWin);
  if (InputFileSize > 0) {
    SizeHint = InputFileSize < (1 << 30)? (uint32_t)InputFileSize : (1u << 30);
    BrotliEncoderSetParameter(EncodeState, BROTLI_PARAM_SIZE_HINT, SizeHint);
  }

  AvailableIn = 0;
  NextIn = NULL;
  AvailableOut = kFileBufferSize;
  NextOut = Output;
  for (;;) {
    if (AvailableIn == 0 && !IsEof) {
      AvailableIn = fread(Input, 1, kFileBufferSize, InputFileHandle);
      NextIn = Input;
      if (ferror(InputFileHandle)) {
        printf("Failed to read input [%s]\n", InputFile);
        IsOk = BROTLI_FALSE;
        goto Finish;
      }
      IsEof = !HasMoreInput(InputFileHandle);
    }

    if (!IsEof){
      do{
        if (!BrotliEncoderCompressStream(EncodeState,
        BROTLI_OPERATION_FLUSH,
        &AvailableIn, &NextIn, &AvailableOut, &NextOut, &TotalOut)) {
          printf("Failed to compress data [%s]\n", InputFile);
          IsOk = BROTLI_FALSE;
          goto Finish;
        }
        OutSize = (size_t)(NextOut - Output);
        if (OutSize > 0) {
          fwrite(Output, 1, OutSize, OutputFileHandle);
          if (ferror(OutputFileHandle)) {
            printf("Failed to write output [%s]\n", OutputFile);
            IsOk = BROTLI_FALSE;
            goto Finish;
          }
        }
        NextOut = Output;
        AvailableOut = kFileBufferSize;
      }
      while (AvailableIn > 0 || BrotliEncoderHasMoreOutput(EncodeState));
    }
    else{
      do{
        if (!BrotliEncoderCompressStream(EncodeState,
        BROTLI_OPERATION_FINISH,
        &AvailableIn, &NextIn, &AvailableOut, &NextOut, &TotalOut)) {
          printf("Failed to compress data [%s]\n", InputFile);
          IsOk = BROTLI_FALSE;
          goto Finish;
        }
        OutSize = (size_t)(NextOut - Output);
        if (OutSize > 0) {
          fwrite(Output, 1, OutSize, OutputFileHandle);
          if (ferror(OutputFileHandle)) {
            printf("Failed to write output [%s]\n", OutputFile);
            IsOk = BROTLI_FALSE;
            goto Finish;
          }
        }
        NextOut = Output;
        AvailableOut = kFileBufferSize;
      }
      while (AvailableIn > 0 || BrotliEncoderHasMoreOutput(EncodeState));
    }
    if (BrotliEncoderIsFinished(EncodeState)){
      break;
    }
  }

Finish:
  if (EncodeState) {
    BrotliEncoderDestroyInstance(EncodeState);
  }
  if (InputFileHandle) {
    fclose(InputFileHandle);
  }
  if (OutputFileHandle) {
    fclose(OutputFileHandle);
  }
  return IsOk;
}

/* Default BrotliAllocFunc */
void* BrotliAllocFunc(void* Opaque, size_t Size) {
  *(size_t *)Opaque = *(size_t *) Opaque + Size;
  return malloc(Size);
}

/* Default BrotliFreeFunc */
void BrotliFreeFunc(void* Opaque, void* Address) {
  free(Address);
}

int DecompressFile(char *InputFile, uint8_t *InputBuffer, char *OutputFile, uint8_t *OutputBuffer, int Quality, int Gap) {
  FILE *InputFileHandle;
  FILE *OutputFileHandle;
  BrotliDecoderState *DecoderState;
  BrotliDecoderResult Result;
  size_t AvailableIn;
  const uint8_t *NextIn;
  size_t AvailableOut;
  uint8_t *NextOut;
  uint8_t *Input;
  uint8_t *Output;
  size_t OutSize;
  BROTLI_BOOL IsOk;
  AvailableIn = 0;
  Input = InputBuffer;
  Output = OutputBuffer;
  IsOk = BROTLI_TRUE;

  IsOk = OpenFiles(InputFile, &InputFileHandle, OutputFile, &OutputFileHandle);
  if (!IsOk) {
        return IsOk;
  }
  fseek(InputFileHandle, DECODE_HEADER_SIZE, SEEK_SET);

  DecoderState = BrotliDecoderCreateInstance(BrotliAllocFunc, BrotliFreeFunc, &ScratchBufferSize);
  if (!DecoderState) {
    printf("Out of memory\n");
    IsOk = BROTLI_FALSE;
    goto Finish;
  }
  /* This allows decoding "large-window" streams. Though it creates
       fragmentation (new builds decode streams that old builds don't),
       it is better from used experience perspective. */
  BrotliDecoderSetParameter(DecoderState, BROTLI_DECODER_PARAM_LARGE_WINDOW, 1u);

  AvailableIn = 0;
  NextIn = NULL;
  AvailableOut = kFileBufferSize;
  NextOut = Output;
  Result = BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT;
  for (;;) {
    if (Result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) {
      if (!HasMoreInput(InputFileHandle)) {
        printf("Corrupt input [%s]\n", InputFile);
        IsOk = BROTLI_FALSE;
        goto Finish;
      }
      AvailableIn = fread(Input, 1, kFileBufferSize, InputFileHandle);
      NextIn = Input;
      if (ferror(InputFileHandle)) {
        printf("Failed to read input [%s]\n", InputFile);
        IsOk = BROTLI_FALSE;
        goto Finish;
      }
    } else if (Result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
      OutSize = (size_t) (NextOut - Output);
      if (OutSize > 0) {
        fwrite(Output, 1, OutSize, OutputFileHandle);
        if (ferror(OutputFileHandle)) {
          printf("Failed to write output [%s]\n", OutputFile);
          IsOk = BROTLI_FALSE;
          goto Finish;
        }
      }
      AvailableOut = kFileBufferSize;
      NextOut = Output;
    } else if (Result == BROTLI_DECODER_RESULT_SUCCESS) {
      OutSize = (size_t) (NextOut - Output);
      if (OutSize > 0) {
        fwrite(Output, 1, OutSize, OutputFileHandle);
        if (ferror(OutputFileHandle)) {
          printf("Failed to write output [%s]\n", OutputFile);
          IsOk = BROTLI_FALSE;
          goto Finish;
        }
      }
      AvailableOut = 0;
      if (AvailableIn != 0 || HasMoreInput(InputFileHandle)) {
        printf("Corrupt input [%s]\n", InputFile);
        IsOk = BROTLI_FALSE;
        goto Finish;
      }
    } else {
      printf("Corrupt input [%s]\n", InputFile);
      IsOk = BROTLI_FALSE;
      goto Finish;
    }
    if (!HasMoreInput(InputFileHandle) && Result == BROTLI_DECODER_RESULT_SUCCESS ) {
      break;
    }
    Result = BrotliDecoderDecompressStream(DecoderState, &AvailableIn, &NextIn, &AvailableOut, &NextOut, 0);
  }
Finish:
  if (DecoderState) {
    BrotliDecoderDestroyInstance(DecoderState);
  }
  if (InputFileHandle) {
    fclose(InputFileHandle);
  }
  if (OutputFileHandle) {
    fclose(OutputFileHandle);
  }
  return IsOk;
}

int main(int argc, char** argv) {
  BROTLI_BOOL CompressBool;
  BROTLI_BOOL DecompressBool;
  char *OutputFile;
  char *InputFile;
  char OutputTmpFile[_MAX_PATH];
  FILE *OutputHandle;
  int Quality;
  int Gap;
  int OutputFileLength;
  int InputFileLength;
  int Ret;
  size_t InputFileSize;
  uint8_t *Buffer;
  uint8_t *InputBuffer;
  uint8_t *OutputBuffer;
  int64_t Size;

  InputFile = NULL;
  OutputFile = NULL;
  CompressBool = BROTLI_FALSE;
  DecompressBool = BROTLI_FALSE;
  //
  //Set default Quality and Gap
  //
  Quality = 9;
  Gap = 1;
  InputFileSize = 0;
  Ret = 0;

  if (argc < 2) {
    Usage();
    return 1;
  }
  if (strcmp(argv[1], "-h") == 0 || strcmp (argv[1], "--help") == 0 ) {
    Usage();
    return 0;
  }
  if (strcmp(argv[1], "-v") == 0 || strcmp (argv[1], "--version") == 0 ) {
    Version();
    return 0;
  }
  while (argc > 1) {
    if (strcmp(argv[1], "-e") == 0 || strcmp(argv[1], "--compress") == 0 ) {
      CompressBool = BROTLI_TRUE;
      if (DecompressBool) {
        printf("Can't use -e/--compress with -d/--decompess on the same time\n");
        return 1;
      }
      argc--;
      argv++;
      continue;
    }
    if (strcmp(argv[1], "-d") == 0 || strcmp(argv[1], "--decompress") == 0 ) {
      DecompressBool = BROTLI_TRUE;
      if (CompressBool) {
        printf("Can't use -e/--compress with -d/--decompess on the same time\n");
        return 1;
      }
      argc--;
      argv++;
      continue;
    }
    if (strcmp(argv[1], "-o") == 0 || strncmp(argv[1], "--output", 8) == 0) {
      if (strcmp(argv[1], "-o") == 0) {
        OutputFileLength = strlen(argv[2]);
        if (OutputFileLength > _MAX_PATH) {
          printf ("The file path %s is too long\n", argv[2]);
          return 1;
        }
        OutputFile = argv[2];
        if (OutputFile == NULL) {
          fprintf(stderr, "Input file can't be null\n");
          return 1;
        }
        argc--;
        argv++;
      } else {
        OutputFileLength = strlen(argv[1] - 9);
        OutputFile = (char *)argv[1] + 9;
      }
      argc--;
      argv++;
      continue;
    }
    if (strcmp(argv[1], "-q") == 0 || strncmp(argv[1], "--quality", 9) == 0) {
      if (strcmp(argv[1], "-q") == 0) {
        Quality = strtol(argv[2], NULL, 16);
        argc--;
        argv++;
      } else {
        Quality = strtol((char *)argv[1] + 10, NULL, 16);
      }
      argc--;
      argv++;
      continue;
    }
    if (strcmp(argv[1], "-g") == 0 || strncmp(argv[1], "--gap", 5) == 0) {
      if (strcmp(argv[1], "-g") == 0) {
        Gap = strtol(argv[2], NULL, 16);
        argc--;
        argv++;
      } else {
        Gap = strtol((char *)argv[1] + 6, NULL, 16);
      }
      argc--;
      argv++;
      continue;
    }
    if (argc > 1) {
      InputFileLength = strlen(argv[1]);
      if (InputFileLength > _MAX_PATH - 1) {
        printf ("The file path %s is too long\n", argv[2]);
        return 1;
      }
      InputFile = argv[1];
      if (InputFile == NULL) {
       printf("Input file can't be null\n");
       return 1;
      }
      argc--;
      argv++;
    }
  }

  Buffer = (uint8_t*)malloc(kFileBufferSize * 2);
  if (!Buffer) {
    printf("Out of memory\n");
    goto Finish;
  }
  memset(Buffer, 0, kFileBufferSize*2);
  InputBuffer = Buffer;
  OutputBuffer = Buffer + kFileBufferSize;
  if (CompressBool) {
    //
    // Compress file
    //
    Ret = CompressFile(InputFile, InputBuffer, OutputFile, OutputBuffer, Quality, Gap);
    if (!Ret) {
      printf ("Failed to compress file [%s]\n", InputFile);
      goto Finish;
    }
    //
    // Decompress file for get Outputfile size
    //
    strcpy (OutputTmpFile, OutputFile);
    if (strlen(InputFile) + strlen(".tmp") < _MAX_PATH) {
      strcat(OutputTmpFile, ".tmp");
    } else {
      printf ("Output file path is too long[%s]\n", OutputFile);
      Ret = BROTLI_FALSE;
      goto Finish;
    }
    memset(Buffer, 0, kFileBufferSize*2);
    Ret = DecompressFile(OutputFile, InputBuffer, OutputTmpFile, OutputBuffer, Quality, Gap);
    if (!Ret) {
      printf ("Failed to decompress file [%s]\n", OutputFile);
      goto Finish;
    }
    remove (OutputTmpFile);

    //
    // fill decoder header
    //
    InputFileSize = FileSize(InputFile);
    Size = (int64_t)InputFileSize;
    OutputHandle = fopen(OutputFile, "rb+"); /* open output_path file and add in head info */
    fwrite(&Size, 1, sizeof(int64_t), OutputHandle);
    ScratchBufferSize += Gap * GAP_MEM_BLOCK; /* there is a memory gap between IA32 and X64 environment*/
    ScratchBufferSize += kFileBufferSize * 2;
    Size = (int64_t) ScratchBufferSize;
    fwrite(&Size, 1, sizeof(int64_t), OutputHandle);
    if (fclose(OutputHandle) != 0) {
      printf("Failed to close output file [%s]\n", OutputFile);
      Ret = BROTLI_FALSE;
      goto Finish;
    }
  } else {
    Ret = DecompressFile(InputFile, InputBuffer, OutputFile, OutputBuffer, Quality, Gap);
    if (!Ret) {
      printf ("Failed to decompress file [%s]\n", InputFile);
      goto Finish;
    }
  }
  Finish:
  if (Buffer != NULL) {
    free (Buffer);
  }
  return !Ret;
}
