/*
  This file is part of CDO. CDO is a collection of Operators to
  manipulate and analyse Climate model Data.

  Copyright (C) 2003-2020 Uwe Schulzweida, <uwe.schulzweida AT mpimet.mpg.de>
  See COPYING file for copying and redistribution conditions.

  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 of the License.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#ifdef HAVE_LIBCURL
#include <curl/curl.h>
#endif

#include <cerrno>
#include <cstring>

#include <cdi.h>

#include "gridreference.h"
#include "process_int.h"
#include "cdo_output.h"
#include <mpim_grid.h>
#include "cdi_lockedIO.h"

// callback function for curl for writing the network retrieved grid file
#ifdef HAVE_LIBCURL
static size_t
writeData(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
  return fwrite(ptr, size, nmemb, stream);
}
#endif

// code from grid_tools.2
static int
downloadGridfile(const char *uri, const char *basename)
{
  int rval = 1;
#ifdef HAVE_LIBCURL
  // As curl_easy_init calls non-thread safe curl_global_init the libcurl
  // developer advice to call curl_global_init first and before potential thread spawning.

  int curlflags = CURL_GLOBAL_DEFAULT;

#ifdef CURL_GLOBAL_ACK_EINTR
  curlflags |= CURL_GLOBAL_ACK_EINTR;
#endif

  auto ret = curl_global_init(curlflags);
  if (ret != 0)
    {
      fprintf(stderr, "ERROR: %s!\n", curl_easy_strerror(ret));
      return -1;
    }

  auto hd = curl_easy_init();
  if (hd == nullptr)
    {
      fprintf(stderr, "ERROR: could not get curl handler.\n");
      return -1;
    }
  else
    {
      auto fp = fopen(basename, "w");
      if (fp == nullptr)
        {
          fprintf(stderr, "ERROR: could not open local output file %s. %s.\n", basename, strerror(errno));
          return -1;
        }

      // curl_easy_setopt(hd, CURLOPT_VERBOSE, 1);
      curl_easy_setopt(hd, CURLOPT_URL, uri);
      curl_easy_setopt(hd, CURLOPT_WRITEFUNCTION, writeData);
      curl_easy_setopt(hd, CURLOPT_WRITEDATA, fp);
      ret = curl_easy_perform(hd);
      fclose(fp);
      if (ret == 0)
        {
          /*
          int ihead;
          curl_easy_getinfo(hd, CURLINFO_HEADER_SIZE, &ihead);
          printf("ihead %d\n", ihead);
          */
          char *ctype;
          curl_easy_getinfo(hd, CURLINFO_CONTENT_TYPE, &ctype);

          if (strstr(ctype, "html") == nullptr)  // no html content
            {
              double length;
              curl_easy_getinfo(hd, CURLINFO_SIZE_DOWNLOAD, &length);
              if (gridVerbose) cdoPrint("File %s downloaded - size: %.0lf byte", basename, length);
              rval = 0;
            }
          else
            {
              int status = remove(basename);
              if (status == -1) perror(basename);
              if (gridVerbose) cdoPrint("The requested URL was not found on this server!");
            }
        }
      else
        {
          int status = remove(basename);
          if (status == -1) perror(basename);
          fprintf(stderr, "ERROR: %s. Download %s failed.\n\n", curl_easy_strerror(ret), basename);
        }

      curl_easy_cleanup(hd);
    }
#else
  (void) uri;
  (void) basename;

  cdoWarning("CURL support not compiled in!");
#endif

  return rval;
}

// Search for filename.
static int
searchFile(const char *directory, const char *filename)
{
#ifdef HAVE_SYS_STAT_H
  struct stat buf;
  if (stat(directory, &buf) == 0)
    {
      if (stat(filename, &buf) == 0)
        {
          if (buf.st_size != 0 && !(buf.st_mode&S_IFDIR)) return 0;
        }
    }
  else
    {
      perror(directory);
    }
#endif

  return 1;
}

static int
gridFromURI(char *griduri, char *gridpath, char *filename, bool lgriduri)
{
  int status = -1;

  if (gridSearchDir != nullptr)
    {
      strcpy(gridpath, gridSearchDir);
      strcat(gridpath, filename);
      if (gridVerbose) cdoPrint("Search for horizontal grid file \"%s\"", gridpath);

      // scan directory given by environment variable
      status = searchFile(gridSearchDir, gridpath);
    }

  if (status != 0 && lgriduri)
    {
      if (gridVerbose) cdoPrint("Download horizontal grid file %s to %s", griduri, filename);
      status = downloadGridfile(griduri, filename);
    }

  return status;
}

static int
gridFromFile(int gridID1, char *gridpath)
{
  int gridID2 = -1;

  if (gridVerbose) cdoPrint("Horizontal grid file used: %s", gridpath);

  const auto gridsize = gridInqSize(gridID1);

  int position = 0;
  cdiInqKeyInt(gridID1, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDINREFERENCE, &position);

  openLock();
  const auto streamID = streamOpenRead(gridpath);
  if (streamID < 0) cdiOpenError(streamID, "Open failed on horizontal grid file >%s<", gridpath);
  openUnlock();

  const auto vlistID = streamInqVlist(streamID);
  const auto ngrids = vlistNgrids(vlistID);
  if (position > 0 && position <= ngrids)
    {
      const auto gridID = vlistGrid(vlistID, position - 1);
      if (gridInqSize(gridID) == gridsize)
        gridID2 = gridDuplicate(gridID);
      else
        cdoWarning("Grid size %zu on position %d do not match! Reference=%s", gridsize, position, gridpath);
    }
  else if (position == 0)
    {
      for (int grididx = 0; grididx < ngrids; ++grididx)
        {
          const auto gridID = vlistGrid(vlistID, grididx);
          if (gridInqSize(gridID) == gridsize)
            {
              gridID2 = gridDuplicate(gridID);
              break;
            }
        }
    }
  else
    cdoWarning("Number of grid in reference %d not available! Reference=%s", position, gridpath);

  if (gridID2 != -1)
    {
      unsigned char uuidOfHGrid1[CDI_UUID_SIZE] = { 0 };
      unsigned char uuidOfHGrid2[CDI_UUID_SIZE] = { 0 };

      int length = CDI_UUID_SIZE;
      cdiInqKeyBytes(gridID1, CDI_GLOBAL, CDI_KEY_UUID, uuidOfHGrid1, &length);
      length = CDI_UUID_SIZE;
      cdiInqKeyBytes(gridID2, CDI_GLOBAL, CDI_KEY_UUID, uuidOfHGrid2, &length);

      if (uuidOfHGrid1[0] != 0 && uuidOfHGrid2[0] != 0 && memcmp(uuidOfHGrid1, uuidOfHGrid2, CDI_UUID_SIZE) != 0)
        cdoWarning("UUID of horizontal grids differ!");

      int number1 = 0, number2 = 0;
      cdiInqKeyInt(gridID1, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDUSED, &number1);
      cdiInqKeyInt(gridID2, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDUSED, &number2);
      if (number1 > 0 && number2 > 0 && number1 != number2) cdoWarning("Number of grid used of horizontal grids differ!");
    }

  streamClose(streamID);

  return gridID2;
}

static int
referenceToGrid(int gridID1)
{
  int gridID2 = -1;

  char griduri[8192] = { 0 }, gridpath[8192] = { 0 };

  int length = 0;
  if (CDI_NOERR == cdiInqKeyLen(gridID1, CDI_GLOBAL, CDI_KEY_REFERENCEURI, &length))
    cdiInqKeyString(gridID1, CDI_GLOBAL, CDI_KEY_REFERENCEURI, griduri, &length);

  if (griduri[0] == 0)
    {
      cdoWarning("Reference to horizontal grid not available!");
    }
  else
    {
      auto lgriduri = true;

      auto filename = strrchr(griduri, '/');
      if (filename == nullptr)
        {
          filename = griduri;
          lgriduri = false;
        }
      else
        {
          filename++;
        }

      strcpy(gridpath, "./");
      strcat(gridpath, filename);
      if (gridVerbose) cdoPrint("Search for horizontal grid file \"%s\"", gridpath);

      // scan local directory for file
      auto status = searchFile("./", gridpath);
      if (status != 0) status = gridFromURI(griduri, gridpath, filename, lgriduri);
      if (status == 0) gridID2 = gridFromFile(gridID1, gridpath);
    }

  return gridID2;
}

RefGrid
dereferenceGrid(int gridID)
{
  RefGrid reference;

  int number_of_grid_used = 0;
  cdiInqKeyInt(gridID, CDI_GLOBAL, CDI_KEY_NUMBEROFGRIDUSED, &number_of_grid_used);
  if (number_of_grid_used > 0)
    {
      reference.exists = true;
      reference.gridID = referenceToGrid(gridID);
      reference.isValid = (reference.gridID != -1);
      reference.notFound = (reference.gridID == -1);
    }

  return reference;
}
