/*=========================================================================

  Program:   ParaView

  Copyright (c) Kitware, Inc.
  All rights reserved.
  See Copyright.txt or http://www.paraview.org/HTML/Copyright.html for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/

/*
 * Provide support for loading imago images in VR
 */

// de = "SI-17-71"; // collection -- need collectionid
// ds = "Downhole"; // imagery type -- need imagery type id -- in worspaces, multiple definitions
// im = "Wet";      // image type -- in workspace -- multiple definitions
// dp = "0";        // depth -- start depth or enddepth
// da = dataset name

#include "vtkTIFFReader.h"
#include <future>
#include <iostream>
#include <list>
#include <string>
#include <vector>
#include <vtk_jsoncpp.h>
#include <windows.h>
#include <wininet.h>

#pragma comment(lib, "Wininet.lib")

class vtkImagoLoader
{
public:
  // get an image based on an imago URL
  // parses args and then calls the long signature
  bool GetHttpImage(std::string const& fname, std::future<vtkImageData*>& future);

  // get an image based on an holeid and depth
  // parses args and then calls the long signature
  bool GetImage(std::string const& fname, std::future<vtkImageData*>& future);

  // the long signature that does the work
  bool GetImage(std::string const& workspace, std::string const& dataset,
    std::string const& collection, std::string const& imageryType, std::string const& imageType,
    std::string const& depth, std::future<vtkImageData*>& future);

  bool IsCellImageDifferent(std::string const& /*oldimg*/, std::string const& newimg);

  bool Connect();
  bool AcquireToken();
  bool GetContext();

  bool Login(std::string const& uname, std::string const& pw)
  {
    this->UserName = uname;
    this->Password = pw;
    // clear structures
    std::lock_guard<std::mutex> lock(this->Mutex);
    this->APIToken.clear();
    this->CollectionMap.clear();
    this->ImageryMap.clear();
    this->ContextJSON.clear();
    if (!this->Connect() || !this->AcquireToken() || !this->GetContext())
    {
      return false;
    }
    return true;
  }
  bool IsLoggedIn() { return this->ContextJSON.size() > 0; }

  void SetWorkspace(std::string const& val) { this->Workspace = val; }
  void SetDataset(std::string const& val) { this->Dataset = val; }
  void SetImageryType(std::string const& val) { this->ImageryType = val; }
  void SetImageType(std::string const& val) { this->ImageType = val; }

  void GetWorkspaces(std::vector<std::string>& vals);
  void GetDatasets(std::vector<std::string>& vals);
  void GetImageryTypes(std::vector<std::string>& vals);
  void GetImageTypes(std::vector<std::string>& vals);

  vtkImagoLoader();
  virtual ~vtkImagoLoader();

protected:
  bool GetCollection(
    std::string const& collection, std::string const& dataset, Json::Value& resultJSON);
  bool GetImagery(
    std::string const& collection, std::string const& dataset, Json::Value& resultJSON);
  bool ImageryHasDepth(Json::Value imageryJSON, double targetDepth);

  HINTERNET Session = nullptr;
  HINTERNET Connection = nullptr;
  std::string UID;
  std::string APIToken;
  Json::Value ContextJSON;
  std::string UserName;
  std::string Password;
  std::string Workspace;
  std::string Dataset;
  std::string ImageryType;
  std::string ImageType;
  std::atomic<double> LastStartDepth;
  std::atomic<double> LastEndDepth;
  std::list<std::pair<std::string, vtkSmartPointer<vtkImageData>>> ImageCache;
  unsigned int CacheLimit = 20;
  std::map<std::pair<std::string, std::string>, Json::Value> CollectionMap;
  std::map<std::pair<std::string, std::string>, Json::Value> ImageryMap;
  std::mutex Mutex;
};

namespace
{

bool escapeURL(std::string& url, DWORD options = ICU_DECODE | ICU_ENCODE_PERCENT)
{
  DWORD bytes = static_cast<DWORD>(url.size() + 1);
  LPTSTR escapedString = new TCHAR[bytes];
  escapedString[0] = 0;

  // First read will generate the correct byte count for the return string
  //
  bool result = InternetCanonicalizeUrl(url.data(), escapedString, &bytes, options);
  if (!result)
  {
    // Resize the String
    delete[] escapedString;
    escapedString = new TCHAR[bytes];
    escapedString[0] = 0;

    result = InternetCanonicalizeUrl(url.data(), escapedString, &bytes, options);
  }

  if (result)
  {
    url = escapedString;
    delete[] escapedString;
  }

  return result;
}

bool getResult(HINTERNET hRequest, std::vector<char>& result)
{
  static const int bufSize = 10000;
  result.clear();
  std::vector<char> buffer;
  buffer.resize(bufSize);

  while (true)
  {
    DWORD bytesRead;
    bool success;

    success = InternetReadFile(hRequest, buffer.data(), bufSize, &bytesRead);

    // only complete when bytesRead == 0
    if (success && bytesRead == 0)
    {
      break;
    }

    if (!success)
    {
      vtkGenericWarningMacro("InternetReadFile error : " << GetLastError());
      return false;
    }

    if (bytesRead > 0)
    {
      buffer.resize(bytesRead);
      result.insert(result.end(), buffer.begin(), buffer.end());
    }
  }

  return true;
}

bool sendMessage(HINTERNET connection, std::string type, std::string address,
  std::vector<std::string> const& headers, std::string formData, std::vector<char>& result)
{
  PCTSTR rgpszAcceptTypes[] = { "*/*", nullptr };
  HINTERNET hRequest = HttpOpenRequest(connection, (LPCSTR)(type.c_str()),
    (LPCSTR)(address.c_str()), nullptr, nullptr, rgpszAcceptTypes,
    INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NEED_FILE |
      INTERNET_FLAG_SECURE,
    0);
  if (!hRequest)
  {
    vtkGenericWarningMacro("HttpOpenRequest error : " << GetLastError());
    return false;
  }

  for (auto& hdr : headers)
  {
    HttpAddRequestHeaders(hRequest, hdr.c_str(), static_cast<DWORD>(-1),
      HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE);
  }

  std::string frmdata = R"({ "username": "imagodemo1", "password": "ImagoDemo1" })";

  while (!HttpSendRequest(
    hRequest, nullptr, 0, (LPVOID)formData.c_str(), static_cast<DWORD>(formData.size())))
  {
    InternetErrorDlg(GetDesktopWindow(), hRequest, ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED,
      FLAGS_ERROR_UI_FILTER_FOR_ERRORS | FLAGS_ERROR_UI_FLAGS_GENERATE_DATA |
        FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS,
      nullptr);
  }

  if (!getResult(hRequest, result))
  {
    return false;
  }

  InternetCloseHandle(hRequest);
  return true;
}

Json::Value getJSON(std::vector<char> const& result)
{
  Json::Value root;
  {
    Json::CharReaderBuilder builder;
    builder["collectComments"] = false;
    std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
    bool success = reader->parse(result.data(), result.data() + result.size(), &root, nullptr);
    if (!success)
    {
      return false;
    }
  }
  return root;
}
}

//-----------------------------------------------------------------------------
vtkImagoLoader::vtkImagoLoader() = default;

//-----------------------------------------------------------------------------
vtkImagoLoader::~vtkImagoLoader()
{
  if (this->Connection)
  {
    InternetCloseHandle(this->Connection);
    this->Connection = nullptr;
  }
  if (this->Session)
  {
    InternetCloseHandle(this->Session);
    this->Session = nullptr;
  }
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::Connect()
{
  if (!this->Session)
  {
    this->Session = InternetOpen("Mozilla/5.0", INTERNET_OPEN_TYPE_DIRECT, nullptr, nullptr, 0);
    if (!this->Session)
    {
      vtkGenericWarningMacro("InternetOpen error : " << GetLastError());
      return false;
    }
  }

  if (!this->Connection)
  {
    this->Connection = InternetConnect(this->Session, "io.imago.live", INTERNET_DEFAULT_HTTPS_PORT,
      "", "", INTERNET_SERVICE_HTTP, 0, 0);
    if (!this->Connection)
    {
      vtkGenericWarningMacro("InternetConnect error : " << GetLastError());
      return false;
    }
  }

  return true;
}

//-----------------------------------------------------------------------------
void vtkImagoLoader::GetWorkspaces(std::vector<std::string>& vals)
{
  vals.clear();
  if (!this->Connect() || !this->AcquireToken() || !this->GetContext())
  {
    return;
  }

  for (auto& w : this->ContextJSON["workspaces"])
  {
    std::string itname = w["name"].asString();
    if (std::find(vals.begin(), vals.end(), itname) == vals.end())
    {
      vals.push_back(itname);
    }
  }
}

//-----------------------------------------------------------------------------
void vtkImagoLoader::GetDatasets(std::vector<std::string>& vals)
{
  vals.clear();
  if (!this->Connect() || !this->AcquireToken() || !this->GetContext())
  {
    return;
  }

  for (auto& w : this->ContextJSON["workspaces"])
  {
    // is this the correct workspace
    if (this->Workspace.size() == 0 || this->Workspace == w["name"].asString())
    {
      auto& ds = w["datasets"];
      for (auto& d : ds)
      {
        std::string itname = d["name"].asString();
        if (std::find(vals.begin(), vals.end(), itname) == vals.end())
        {
          vals.push_back(itname);
        }
      }
    }
  }
}

//-----------------------------------------------------------------------------
void vtkImagoLoader::GetImageryTypes(std::vector<std::string>& vals)
{
  vals.clear();
  if (!this->Connect() || !this->AcquireToken() || !this->GetContext())
  {
    return;
  }

  for (auto& w : this->ContextJSON["workspaces"])
  {
    // is this the correct workspace
    if (this->Workspace.size() == 0 || this->Workspace == w["name"].asString())
    {
      auto& ds = w["datasets"];
      for (auto& d : ds)
      {
        // is this the correct dataset?
        if (this->Dataset.size() == 0 || this->Dataset == d["name"].asString())
        {
          auto& its = d["imageryTypes"];
          for (auto& it : its)
          {
            std::string itname = it["name"].asString();
            if (std::find(vals.begin(), vals.end(), itname) == vals.end())
            {
              vals.push_back(itname);
            }
          }
        }
      }
    }
  }
}

//-----------------------------------------------------------------------------
void vtkImagoLoader::GetImageTypes(std::vector<std::string>& vals)
{
  vals.clear();
  if (!this->Connect() || !this->AcquireToken() || !this->GetContext())
  {
    return;
  }

  for (auto& w : this->ContextJSON["workspaces"])
  {
    // is this the correct workspace
    if (this->Workspace.size() == 0 || this->Workspace == w["name"].asString())
    {
      auto& ds = w["datasets"];
      for (auto& d : ds)
      {
        // is this the correct dataset?
        if (this->Dataset.size() == 0 || this->Dataset == d["name"].asString())
        {
          auto& its = d["imageryTypes"];
          for (auto& it : its)
          {
            auto& imageTypes = it["imageTypes"];
            for (auto& imageType : imageTypes)
            {
              std::string itname = imageType["name"].asString();
              if (std::find(vals.begin(), vals.end(), itname) == vals.end())
              {
                vals.push_back(itname);
              }
            }
          }
        }
      }
    }
  }
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::IsCellImageDifferent(std::string const& /*oldimg*/, std::string const& newimg)
{
  std::smatch sm;

  // look for http signature
  if (!strncmp(newimg.c_str(), "http", 4))
  {
    std::regex matcher(
      R"=([^ ]*\?((?:de|ds|im|dp)=[^&]*)(&(?:de|ds|im|dp)=[^&]*)?(&(?:de|ds|im|dp)=[^&]*)?(&(?:de|ds|im|dp)=[^&]*)?)=");
    std::regex_search(newimg, sm, matcher);
  }
  // now check holieid signature
  else if (!strncmp(newimg.c_str(), "holeid:", 7))
  {
    std::regex matcher(R"=(holeid:(de=[^&]*)(&dp=[^&]*))=");
    std::regex_search(newimg, sm, matcher);
  }
  else
  {
    return true;
  }

  std::map<std::string, std::string> args;
  for (size_t i = 1; i < sm.size(); ++i)
  {
    std::string first = sm[i];
    if (first[0] == '&')
    {
      first = first.substr(1);
    }
    args[first.substr(0, 2)] = first.substr(3);
  }

  // if we are missing key arguments then warn and return
  if (args.find("dp") == args.end())
  {
    return true;
  }
  double depth = std::stod(args["dp"].c_str());

  // is this depth a new image?
  if (depth >= this->LastStartDepth && depth < this->LastEndDepth)
  {
    return false;
  }
  return true;
}

bool vtkImagoLoader::AcquireToken()
{
  std::vector<char> result;

  if (this->APIToken.size())
  {
    std::vector<std::string> headers;
    headers.push_back("Content-Type: application/json\r\n");
    headers.push_back("imago-api-token: " + this->APIToken + "\r\n");
    if (!sendMessage(this->Connection, "GET", "/integrate/2/session", headers, "", result))
    {
      vtkGenericWarningMacro("Failed to open session");
      return false;
    }

    Json::Value root = getJSON(result);
    if (!root.isObject() || !root.isMember("uid"))
    {
      this->APIToken.clear();
    }
  }

  if (!this->APIToken.size())
  {
    std::vector<std::string> headers;
    headers.push_back("Content-Type: application/json\r\n");
    std::string tokenBody =
      "{ \"username\": \"" + this->UserName + "\", \"password\": \"" + this->Password + "\" }";
    if (!sendMessage(this->Connection, "POST", "/integrate/2/session", headers, tokenBody, result))
    {
      vtkGenericWarningMacro("Failed to open session");
      return false;
    }

    Json::Value root = getJSON(result);
    if (!root.isObject() || !root.isMember("uid") || !root.isMember("apiToken"))
    {
      return false;
    }
    this->UID = root["uid"].asString();
    this->APIToken = root["apiToken"].asString();
  }

  return true;
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetContext()
{
  // return if we already loaded it
  if (!this->ContextJSON.empty())
  {
    return true;
  }

  std::vector<char> result;
  std::vector<std::string> headers;

  // find matching collection to get collectionid etc
  std::string frmdata;
  // get the context and then extract ids from it
  headers.push_back("Content-Type: application/x-www-form-urlencoded\r\n");
  headers.push_back("imago-api-token: " + this->APIToken + "\r\n");
  if (!sendMessage(this->Connection, "GET", "/integrate/2/context", headers, frmdata, result))
  {
    return false;
  }
  this->ContextJSON = getJSON(result);

  if (!this->ContextJSON.isObject() || !this->ContextJSON.isMember("workspaces"))
  {
    this->ContextJSON.clear();
    vtkGenericWarningMacro("Failed to get context from Imago");
    return false;
  }

  return true;
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetImage(std::string const& fname, std::future<vtkImageData*>& future)
{
  std::smatch sm;
  std::regex matcher(R"=(holeid:(de=[^&]*)(&dp=[^&]*))=");
  std::regex_search(fname, sm, matcher);

  std::map<std::string, std::string> args;
  for (size_t i = 1; i < sm.size(); ++i)
  {
    std::string first = sm[i];
    if (first[0] == '&')
    {
      first = first.substr(1);
    }
    args[first.substr(0, 2)] = first.substr(3);
  }

  // if we are missing key arguments then warn and return
  if (args.find("de") == args.end())
  {
    vtkGenericWarningMacro("Lacking collection name for image");
    return false;
  }

  return this->GetImage(this->Workspace, this->Dataset != "Any" ? this->Dataset : "", args["de"],
    this->ImageryType != "Any" ? this->ImageryType : "",
    this->ImageType != "Any" ? this->ImageType : "", args["dp"], future);
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetHttpImage(std::string const& fname, std::future<vtkImageData*>& future)
{
  std::smatch sm;
  std::regex matcher(
    R"=([^ ]*\?((?:de|ds|im|dp)=[^&]*)(&(?:de|ds|im|dp)=[^&]*)?(&(?:de|ds|im|dp)=[^&]*)?(&(?:de|ds|im|dp)=[^&]*)?)=");
  std::regex_search(fname, sm, matcher);

  std::map<std::string, std::string> args;
  for (size_t i = 1; i < sm.size(); ++i)
  {
    std::string first = sm[i];
    if (first[0] == '&')
    {
      first = first.substr(1);
    }
    args[first.substr(0, 2)] = first.substr(3);
  }

  // if we are missing key arguments then warn and return
  if (args.find("de") == args.end())
  {
    vtkGenericWarningMacro("Lacking collection name for image");
    return false;
  }

  return this->GetImage(this->Workspace, this->Dataset != "Any" ? this->Dataset : "", args["de"],
    args["ds"], args["im"], args["dp"], future);
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetCollection(
  std::string const& collection, std::string const& datasetID, Json::Value& resultJSON)
{
  decltype(this->CollectionMap)::key_type key(collection, datasetID);

  // do we already have it in the map?
  auto it = this->CollectionMap.find(key);
  if (it != this->CollectionMap.end())
  {
    resultJSON = it->second;
    return true;
  }

  if (!this->AcquireToken())
  {
    return false;
  }

  std::vector<char> result;

  std::vector<std::string> headers;
  headers.push_back("Content-Type: application/json\r\n");
  headers.push_back("imago-api-token: " + this->APIToken + "\r\n");

  // find matching collection to get collectionid etc
  std::string frmdata;
  std::string urldata = "name=" + collection;
  if (datasetID.size())
  {
    urldata += "&datasetid=" + datasetID;
  }
  // escapeURL(urldata);
  if (!sendMessage(
        this->Connection, "GET", "/integrate/2/collection?" + urldata, headers, frmdata, result))
  {
    return false;
  }
  resultJSON = getJSON(result);

  if (!resultJSON.isObject() || !resultJSON.isMember("collections") ||
    resultJSON["collections"].size() == 0)
  {
    vtkGenericWarningMacro("Failed to find matching collection for: " << collection);
    return false;
  }

  this->CollectionMap[key] = resultJSON;
  return true;
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetImagery(
  std::string const& collectionID, std::string const& imageryTypeID, Json::Value& resultJSON)
{
  decltype(this->ImageryMap)::key_type key(collectionID, imageryTypeID);

  // do we already have it in the map?
  auto it = this->ImageryMap.find(key);
  if (it != this->ImageryMap.end())
  {
    resultJSON = it->second;
    if (!resultJSON.isObject() || !resultJSON.isMember("imageries") ||
      resultJSON["imageries"].size() == 0)
    {
      return false;
    }
    return true;
  }

  if (!this->AcquireToken())
  {
    return false;
  }

  std::vector<char> result;

  std::vector<std::string> headers;
  headers.push_back("Content-Type: application/json\r\n");
  headers.push_back("imago-api-token: " + this->APIToken + "\r\n");

  // find matching collection to get collectionid etc
  std::string frmdata;
  // now search for the imageryID and mimeType for the image
  std::string urldata = "collectionid=" + collectionID;
  if (imageryTypeID.size())
  {
    urldata += "&imagerytypeid=" + imageryTypeID;
  }

  if (!sendMessage(
        this->Connection, "GET", "/integrate/2/imagery?" + urldata, headers, frmdata, result))
  {
    vtkGenericWarningMacro("Failed to request matching imagery for: " << urldata);
    return false;
  }
  resultJSON = getJSON(result);

  this->ImageryMap[key] = resultJSON;

  if (!resultJSON.isObject() || !resultJSON.isMember("imageries") ||
    resultJSON["imageries"].size() == 0)
  {
    return false;
  }

  return true;
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::ImageryHasDepth(Json::Value imageryJSON, double targetDepth)
{
  // find the image we want based on the depth
  for (auto& i : imageryJSON["imageries"])
  {
    double startDepth = i["startDepth"].asDouble();
    double endDepth = i["endDepth"].asDouble();
    if (targetDepth >= startDepth && targetDepth < endDepth)
    {
      std::string imageryID = i["id"].asString();
      // finally get the image
      if (imageryID.size())
      {
        return true;
      }
      break;
    }
  }
  return false;
}

//-----------------------------------------------------------------------------
bool vtkImagoLoader::GetImage(std::string const& workspace, std::string const& dataset,
  std::string const& collection, std::string const& imageryType, std::string const& imageType,
  std::string const& depth, std::future<vtkImageData*>& future)
{
  // make sure we are connected, have a token, and have gotten the context
  if (!this->IsLoggedIn())
  {
    return false;
  }

  // if we have a workspace then get the id
  std::string workspaceID;
  if (workspace.size())
  {
    for (auto& w : this->ContextJSON["workspaces"])
    {
      if (workspace == w["name"].asString())
      {
        workspaceID = w["id"].asString();
      }
    }
  }

  // async call to get the image as this can take a while
  // and we don't want to block the VR session

  future = std::async(std::launch::async,
    [this, collection, workspace, depth, dataset, imageType, imageryType]() -> vtkImageData* {
      // if we have a dataset find the id
      std::string datasetID;
      if (dataset.size())
      {
        for (auto& w : this->ContextJSON["workspaces"])
        {
          // is this the correct workspace
          if (workspace.size() == 0 || this->Workspace == w["name"].asString())
          {
            auto& ds = w["datasets"];
            for (auto& d : ds)
            {
              if (dataset == d["name"].asString())
              {
                datasetID = d["id"].asString();
              }
            }
          }
        }
      }

      // force these async calls to be serial, eg they are asyn to the main thread but serial
      // when multiple invocations
      std::lock_guard<std::mutex> lock(this->Mutex);

      // search for a matching collection
      Json::Value collectionJSON;
      if (!this->GetCollection(collection, datasetID, collectionJSON))
      {
        return nullptr;
      }
      std::string collectionID = collectionJSON["collections"][0]["id"].asString();

      // if we don't have a datasetid yet, then get it now
      if (!datasetID.size())
      {
        datasetID = collectionJSON["collections"][0]["datasetId"].asString();
      }

      // at this point we have the collectionID and datasetID and maybe a workspaceID

      // now find the imagerytype id from the context and datasetid
      std::vector<std::string> imageryTypeIDs;
      std::vector<std::string> imageTypeIDs;
      for (auto& w : this->ContextJSON["workspaces"])
      {
        auto& ds = w["datasets"];
        for (auto& d : ds)
        {
          // is this the correct dataset?
          if (datasetID == d["id"].asString())
          {
            auto& its = d["imageryTypes"];
            for (auto& it : its)
            {
              if (imageryType.size() == 0 || it["name"].asString() == imageryType)
              {
                auto& imageTypes = it["imageTypes"];
                for (auto& iType : imageTypes)
                {
                  if (imageType.size() == 0 || iType["name"].asString() == imageType)
                  {
                    // always set these two together so they match
                    imageryTypeIDs.push_back(it["id"].asString());
                    imageTypeIDs.push_back(iType["id"].asString());
                  }
                }
              }
            }
          }
        }
      }

      // see if we have a match
      std::string imageTypeID;
      std::string imageryTypeID;
      Json::Value imageryJSON;
      double targetDepth = std::stod(depth.c_str());
      for (size_t i = 0; i < imageryTypeIDs.size(); ++i)
      {
        Json::Value tmpJSON;
        if (this->GetImagery(collectionID, imageryTypeIDs[i], tmpJSON))
        {
          // does it have the depth we are looking for?
          if (this->ImageryHasDepth(tmpJSON, targetDepth))
          {
            imageryTypeID = imageryTypeIDs[i];
            imageTypeID = imageTypeIDs[i];
            imageryJSON = tmpJSON;
            break;
          }
        }
      }

      // if nothing found error
      if (imageryTypeID.size() == 0)
      {
        // this can happen when someone has requested a specific image type
        // but the current drillhole doesn't have that image type
        // and that is OK
        return nullptr;
      }

      // get info on the image we want based on the depth
      double startDepth = 0.0;
      double endDepth = 0.0;
      // record last depth requested
      this->LastStartDepth = startDepth;
      this->LastEndDepth = endDepth;
      size_t fileSize = 0;
      std::string imageryID;
      std::string mimeType;
      for (auto& i : imageryJSON["imageries"])
      {
        startDepth = i["startDepth"].asDouble();
        endDepth = i["endDepth"].asDouble();
        if (targetDepth >= startDepth && targetDepth < endDepth)
        {
          std::string sFileSize = i["images"][0]["fileSize"].asString();
          fileSize = std::atol(sFileSize.c_str());
          imageryID = i["id"].asString();
          mimeType = i["images"][0]["mimeType"].asString();
          // finally get the image
          if (!imageryID.size() || !imageTypeID.size())
          {
            vtkGenericWarningMacro("Failed to find matching imagery");
            return nullptr;
          }
          break;
        }
      }

      // is the image cached?
      std::string imageString = imageryID + imageTypeID;
      for (auto& p : this->ImageCache)
      {
        if (p.first == imageString)
        {
          p.second.Get()->Register(nullptr);

          // record last depth requested
          this->LastStartDepth = startDepth;
          this->LastEndDepth = endDepth;
          return p.second;
        }
      }

      if (mimeType != "image/jpeg" && mimeType != "image/tiff")
      {
        vtkGenericWarningMacro("Format not supported for type " << mimeType);
        return nullptr;
      }

      vtkImageData* imageData = nullptr;

      std::vector<char> result;

      // finally get the actual image
      if (!this->AcquireToken())
      {
        return nullptr;
      }

      std::vector<std::string> headers;
      headers.push_back("Content-Type: " + mimeType + "\r\n");
      headers.push_back("imago-api-token: " + this->APIToken + "\r\n");

      std::string frmdata;
      // now search for the imageryID and mimeType for the image
      std::string urldata = "imageryid=" + imageryID + "&imagetypeid=" + imageTypeID;

      int tries = 0;
      bool success = false;
      do
      {
        if (!sendMessage(
              this->Connection, "GET", "/integrate/2/image?" + urldata, headers, frmdata, result))
        {
          vtkGenericWarningMacro("Failed to find matching image for: " << urldata);
          return nullptr;
        }
        if (fileSize == result.size())
        {
          // stick it into a reader
          if (mimeType == "image/jpeg")
          {
            vtkNew<vtkJPEGReader> rdr;
            rdr->SetMemoryBuffer(result.data());
            rdr->SetMemoryBufferLength(result.size());
            rdr->Update();
            if (rdr->GetErrorCode() == 0)
            {
              imageData = rdr->GetOutput();
              imageData->Register(nullptr);
              success = true;
            }
          }
          if (mimeType == "image/tiff")
          {
            vtkNew<vtkTIFFReader> rdr;
            rdr->SetMemoryBuffer(result.data());
            rdr->SetMemoryBufferLength(result.size());
            rdr->Update();
            if (rdr->GetErrorCode() == 0)
            {
              imageData = rdr->GetOutput();
              imageData->Register(nullptr);
              success = true;
            }
          }
        }
        tries++;
      } while (tries < 5 && !success);

      // did we succeed
      if (!success)
      {
        vtkGenericWarningMacro("failed to retrieve file");
        return nullptr;
      }

      if (this->ImageCache.size() > this->CacheLimit)
      {
        this->ImageCache.pop_front();
      }

      this->ImageCache.push_back(
        std::pair<std::string, vtkSmartPointer<vtkImageData>>(imageString, imageData));

      // record last depth requested
      this->LastStartDepth = startDepth;
      this->LastEndDepth = endDepth;
      return imageData;
    });

  return true;
}
