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

  Copyright (c) Kitware, Inc.
  All rights reserved.

     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.

=========================================================================*/
#include "mvLobby.h"
#include "mvAudioHandler.h"
#include "mvLobbyClient.h"
#include "mvView.h"


#include <algorithm>
#include <queue>
#include <sstream>
#include <codecvt>


#include "vtksys/CommandLineArguments.hxx"
#include "vtksys/SystemTools.hxx"


#include "vtkActor.h"
#include "vtkCallbackCommand.h"
#include "vtkCamera.h"
#include "vtkCullerCollection.h"
#include "vtkEventData.h"
#include "vtkGlobFileNames.h"
#include "vtkImageData.h"
#include "vtkInformation.h"
#include "vtkJPEGReader.h"
#include "vtkLight.h"
#include "vtkMultiThreader.h"
#include "vtkMutexLock.h"
#include "vtkNew.h"
#include "vtkOpenGLPolyDataMapper.h"
#include "vtkOpenGLRenderer.h"
#include "vtkOpenGLTexture.h"
#include "vtkOutputWindow.h"
#include "vtkPlaneSource.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"
#include "vtkSelection.h"
#include "vtkSelectionNode.h"

#include "vtkOpenVRCamera.h"
#include "vtkOpenVRInteractorStyle.h"
#include "vtkOpenVRModel.h"
#include "vtkOpenVROverlay.h"
#include "vtkOpenVRRenderWindow.h"
#include "vtkOpenVRRenderWindowInteractor.h"
#include "vtkOpenVRRenderer.h"


#include "vtkWindows.h"
#include <mmsystem.h>
#include <shobjidl_core.h>

#ifndef M_PI
#define M_PI vtkMath::Pi()
#endif

namespace
{
VTK_THREAD_RETURN_TYPE AsynchronousLoadViewFunction(void* data)
{
  vtkMultiThreader::ThreadInfo* tinfo = static_cast<vtkMultiThreader::ThreadInfo*>(data);

  mvLobby* self = static_cast<mvLobby*>(tinfo->UserData);
  self->AsynchronousLoadView(data);

  return VTK_THREAD_RETURN_VALUE;
}

} // end namespace

mvLobby::mvLobby(int argc, char* argv[])
{
  this->State = Unknown;
  this->ArcContentSize = 38;
  this->ArcGap = 8;
  this->PosterHeight = 2.5;
  this->EventCommand = nullptr;
  this->CurrentView = -1;
  this->PosterCenter[0] = 0.0;
  this->PosterCenter[1] = 0.0;
  this->PosterRadius = 3.5;
  this->Volume = -1.0;
  this->AudioHandler = nullptr;
  this->Client = new mvLobbyClient();

  bool displayHelp = false;

  typedef vtksys::CommandLineArguments argT;
  argT arguments;
  arguments.Initialize(argc, argv);
  arguments.StoreUnusedArguments(true);

  arguments.AddBooleanArgument(
    "--help", &displayHelp, "Provide a listing of command line options.");
  arguments.AddBooleanArgument("/?", &displayHelp, "Provide a listing of command line options.");

  bool hideMap = false;
  arguments.AddBooleanArgument("--hide-map", &hideMap, "Hide the map on the floor.");

  arguments.AddArgument("--background-color", argT::MULTI_ARGUMENT, &this->BackgroundColor,
    "R G B 0 to 255 values for the background color");

  arguments.AddArgument("--views", argT::EQUAL_ARGUMENT, &(this->ViewsDirectory),
    "(optional) Where to find the views.");

  bool generateLOD = false;
  arguments.AddBooleanArgument("--generate-lod", &generateLOD,
    "Process the data and generate a LOD version for any large files.");

  std::string lodView;
  arguments.AddArgument("--generate-lod-for-view", argT::EQUAL_ARGUMENT, &lodView,
    "Generate LOD files for just the named view.");

  this->Client->AddArguments(arguments);

  arguments.AddArgument("--volume", argT::EQUAL_ARGUMENT, &(this->Volume),
    "(default 1.0) Volume of narration and video playback.");

  if (!arguments.Parse())
  {
    cerr << "Problem parsing arguments" << endl;
    return;
  }

  if (displayHelp)
  {
    cerr << "Usage" << endl << endl << "  mapview [options]" << endl << endl << "Options" << endl;
    cerr << arguments.GetHelp();
    return;
  }

  // generate LODs if requested
  if (generateLOD || lodView.length())
  {
    this->State = GeneratingLOD;
    if (!this->Initialize(argc, argv))
    {
      return;
    }
    this->InitializeViews();
    this->GenerateLODs(lodView);
    return;
  }

  this->AudioHandler = new mvAudioHandler();

  // set audio volume
  if (this->Volume >= 0.0)
  {
    mvAudioHandler::SetVolumeLevel(this->Volume);
  }

  this->DisplayMap = !hideMap;

  if (!this->Initialize(argc, argv))
  {
    return;
  }
  if (this->DisplayMap)
  {
    this->LoadMap();
  }
  this->InitializeViews();
  this->Start();
}

mvLobby::~mvLobby()
{
  if (this->EventCommand)
  {
    this->EventCommand->Delete();
  }
  for (auto view : this->Views)
  {
    delete view;
  }
  delete this->AudioHandler;
  delete this->Client;
}

vtkRenderer* mvLobby::GetRenderer()
{
  return this->Renderer;
}

void mvLobby::AsynchronousLoadView(void* data)
{
  // load a view's data
  this->Views[this->CurrentView]->LoadMinimumData(data);
  this->State = CompletedAsynchronousLoad;
}

void mvLobby::RenderLock()
{
  this->RenderMutex->Lock();
}

void mvLobby::RenderUnlock()
{
  this->RenderMutex->Unlock();
}

void mvLobby::CancelLoadView()
{
  if (this->State.load() != LoadingView)
  {
    return;
  }

  this->Threader->TerminateThread(this->AsynchronousLoadViewThreadId);
  this->ReturnFromView();
}

void mvLobby::Start()
{
  if (this->LobbyAudio.size())
  {
    PlaySound(
      this->LobbyAudio.c_str(), nullptr, SND_ASYNC | SND_FILENAME | SND_NODEFAULT | SND_LOOP);
  }

  this->Interactor->Start();
}

vtkOpenVROverlay* mvLobby::GetDashboardOverlay()
{
  return this->RenderWindow->GetDashboardOverlay();
}

void mvLobby::NewPose(int poseNum)
{
  this->RenderWindow->UpdateHMDMatrixPose();
  this->RenderWindow->GetDashboardOverlay()->LoadCameraPose(poseNum);
  this->RenderWindow->UpdateHMDMatrixPose();
}

int mvLobby::GetViewNumberFromEvent(vtkEventDataDevice3D* edd)
{
  // find intersection, from
  // http://mathworld.wolfram.com/Circle-LineIntersection.html
  const double* worldPos = edd->GetWorldPosition();
  const double* worldDir = edd->GetWorldDirection();
  double x1 = worldPos[0];
  double x2 = worldPos[0] + worldDir[0];
  double y1 = worldPos[1];
  double y2 = worldPos[1] + worldDir[1];
  double dx = x2 - x1;
  double dy = y2 - y1;
  double D = x1 * y2 - x2 * y1;
  double sdy = dy < 0 ? -1.0 : 1.0;
  double drdr = dx * dx + dy * dy;
  double rr = this->PosterRadius * this->PosterRadius;
  double delta = rr * drdr - D;
  // do we not have an intersection?
  if (delta < 0)
  {
    return -1;
  }

  // find the intersection points
  double rx1 = (D * dy + sdy * dx * sqrt(rr * drdr - D * D)) / drdr;
  double ry1 = (-D * dx + fabs(dy) * sqrt(rr * drdr - D * D)) / drdr;
  double rx2 = (D * dy - sdy * dx * sqrt(rr * drdr - D * D)) / drdr;
  double ry2 = (-D * dx - fabs(dy) * sqrt(rr * drdr - D * D)) / drdr;
  // now solve for 3D points
  double r1[3];
  r1[0] = rx1;
  r1[1] = ry1;
  double t1 = 0.0;
  if (worldDir[0])
  {
    t1 = (r1[0] - worldPos[0]) / worldDir[0];
  }
  else
  {
    if (worldDir[1])
    {
      t1 = (r1[1] - worldPos[1]) / worldDir[1];
    }
    else
    {
      return -1;
    }
  }
  r1[2] = worldPos[2] + t1 * worldDir[2];
  double r2[3];
  double t2 = 0.0;
  r2[0] = rx2;
  r2[1] = ry2;
  if (worldDir[0])
  {
    t2 = (r2[0] - worldPos[0]) / worldDir[0];
  }
  else
  {
    t2 = (r2[1] - worldPos[1]) / worldDir[1];
  }
  r2[2] = worldPos[2] + t2 * worldDir[2];

  // now find the view number
  int numViews = static_cast<int>(this->Views.size());
  double arcstep = this->ArcContentSize + this->ArcGap;
  int viewNum = -1;

  if (t1 > 0 && (t1 < t2 || t2 < 0))
  {
    if (r1[2] > 0.5 && r1[2] < (0.5 + this->PosterHeight))
    {
      double angle = vtkMath::DegreesFromRadians(
        atan2(r1[1] - this->PosterCenter[1], r1[0] - this->PosterCenter[0]));
      if (angle < -90)
      {
        angle = 360 + angle;
      }
      double count = (this->ArcStart - angle) / arcstep;
      if (count - floor(count) < this->ArcContentSize / arcstep)
      {
        viewNum = static_cast<int>(floor(count));
      }
    }
  }
  if (viewNum == -1 && t2 > 0 && (t2 < t1 || t1 < 0))
  {
    if (r2[2] > 0.5 && r2[2] < (0.5 + this->PosterHeight))
    {
      double angle = vtkMath::DegreesFromRadians(
        atan2(r2[1] - this->PosterCenter[1], r2[0] - this->PosterCenter[0]));
      if (angle < -90)
      {
        angle = 360 + angle;
      }
      double count = (this->ArcStart - angle) / arcstep;
      if (count - floor(count) < this->ArcContentSize / arcstep)
      {
        viewNum = static_cast<int>(floor(count));
      }
    }
  }

  if (viewNum >= this->Views.size())
  {
    return -1;
  }
  return viewNum;
}

void mvLobby::ServerTourStopChanged(
  int viewIndex, double updateTranslation[3], double updateDirection[3])
{
  if (this->CurrentView >= 0)
  {
    this->Views[this->CurrentView]->GoToTourStop(
      viewIndex, false, true, updateTranslation, updateDirection);
  }
}

void mvLobby::ServerViewChanged(std::string const& viewName)
{
  if (viewName == "Lobby")
  {
    // View "Lobby" is the lobby for collaborators.
    if (this->State.load() != InLobby)
    {
      this->ReturnFromView(true);
    }
  }
  else
  {
    int viewIndex = 0;
    for (auto view : this->Views)
    {
      if (view->GetName() == viewName)
      {
        // if we are already in that view reset to the first position
        if (this->State.load() == InView && this->CurrentView == viewIndex)
        {
          this->Views[this->CurrentView]->GoToTourStop(0, false, false);
        }
        else
        {
          // if we are in another view, exit that one first
          if (this->State.load() != InLobby)
          {
            this->ReturnFromView(true);
          }
          this->LoadView(viewIndex, true);
        }
        break;
      }
      viewIndex++;
    }
  }
}

void mvLobby::EventCallback(
  vtkObject* caller, unsigned long eventID, void* clientdata, void* calldata)
{
  mvLobby* self = static_cast<mvLobby*>(clientdata);

  if (eventID == vtkCommand::StartEvent)
  {
    self->RenderMutex->Lock();
  }

  if (eventID == vtkCommand::EndEvent)
  {
    self->RenderMutex->Unlock();
  }

  if (eventID == vtkCommand::RenderEvent)
  {
    if (self->State.load() == CompletedAsynchronousLoad)
    {
      self->FinishLoadView();
    }
    self->Client->Render();
  }

  // we consume all button events
  if (eventID == vtkCommand::Button3DEvent)
  {
    self->EventCommand->AbortFlagOn();
    self->Style->FindPokedRenderer(0, 0);
  }

  // let the view process the button/other event if loaded
  // Lobby still processes MoveEvent, below.
  if (self->State.load() == InView)
  {
    self->Views[self->CurrentView]->EventCallback(eventID, calldata);
    // return;
  }
  else if (eventID == vtkCommand::RenderEvent && self->State.load() == LoadingView)
  {
    self->Views[self->CurrentView]->EventCallback(eventID, calldata);
  }
  else if (eventID == vtkCommand::Button3DEvent)
  {
    // self->EventCommand->AbortFlagOn();
    // self->Style->FindPokedRenderer(0,0);

    vtkEventData* edata = static_cast<vtkEventData*>(calldata);
    vtkEventDataDevice3D* edd = edata->GetAsEventDataDevice3D();
    if (!edd)
    {
      return;
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::ApplicationMenu &&
      edd->GetAction() == vtkEventDataAction::Release)
    {
      if (self->State.load() == LoadingView || self->State.load() == CompletedAsynchronousLoad)
      {
        self->CancelLoadView();
        return;
      }
      if (self->State.load() == InLobby)
      {
        self->Interactor->TerminateApp();
      }
      return;
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::Trigger &&
      edd->GetAction() == vtkEventDataAction::Press)
    {
      if (self->State.load() == InLobby)
      {
        self->Style->ShowRay(edd->GetDevice());
        self->Client->SendMessage("SR", static_cast<int>(edd->GetDevice()));
        vtkOpenVRModel* mod = self->RenderWindow->GetTrackedDeviceModel(edd->GetDevice());
        if (mod)
        {
          // Set length to its max if interactive picking is off
          mod->SetRayLength(self->Camera->GetClippingRange()[1]);
        }
      }
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::Trigger &&
      edd->GetAction() == vtkEventDataAction::Release)
    {
      self->Style->HideRay(edd->GetDevice());
      self->Client->SendMessage("HR", static_cast<int>(edd->GetDevice()));
      if (self->State.load() == InLobby)
      {
        int viewNum = self->GetViewNumberFromEvent(edd);
        if (viewNum >= 0 && self->CurrentView == -1)
        {
          if (self->Views[viewNum]->GetActive())
          {
            self->LoadView(viewNum);
          }
        }
        return;
      }
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::TrackPad &&
      edd->GetAction() == vtkEventDataAction::Release)
    {
      self->Style->EndDolly3D(edd);
    }
    return;
  }
}

void mvLobby::GenerateLODs(std::string const& viewName)
{
  for (auto view : this->Views)
  {
    if (viewName.length() == 0 ||
      viewName == vtksys::SystemTools::GetFilenameName(view->GetRootDirectory()))
    {
      cerr << "Generating LODs for "
           << vtksys::SystemTools::GetFilenameName(view->GetRootDirectory()) << "\n";
      view->GenerateLODs();
    }
  }
}

std::string ws2s(const std::wstring& wstr)
{
    using convert_typeX = std::codecvt_utf8<wchar_t>;
    std::wstring_convert<convert_typeX, wchar_t> converterX;

    return converterX.to_bytes(wstr);
}

std::string GetViewsDirectoryDialog()
{
  // CoCreate the File Open Dialog object.
  std::string name;
  IFileDialog* pfd = NULL;
  HRESULT hr =
    CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd));
  if (SUCCEEDED(hr))
  {
    // Create an event handling object, and hook it up to the dialog.
    // IFileDialogEvents* pfde = NULL;
    // hr = CDialogEventHandler_CreateInstance(IID_PPV_ARGS(&pfde));
    // if (SUCCEEDED(hr))
    {
      // Hook up the event handler.
      // DWORD dwCookie;
      // hr = pfd->Advise(pfde, &dwCookie);
      // if (SUCCEEDED(hr))
      {
        // Set the options on the dialog.
        DWORD dwFlags;

        // Before setting, always get the options first in order
        // not to override existing options.
        hr = pfd->GetOptions(&dwFlags);
        if (SUCCEEDED(hr))
        {
          // In this case, get shell items only for file system items.
          hr = pfd->SetOptions(dwFlags | FOS_PICKFOLDERS);
          if (SUCCEEDED(hr))
          {
            pfd->SetTitle(L"Select Views Directory");
            // Show the dialog
            hr = pfd->Show(NULL);
            if (SUCCEEDED(hr))
            {
              // Obtain the result once the user clicks
              // the 'Open' button.
              // The result is an IShellItem object.
              IShellItem* psiResult;
              hr = pfd->GetResult(&psiResult);
              if (SUCCEEDED(hr))
              {
                // We are just going to print out the
                // name of the file for sample sake.
                PWSTR pszFilePath = NULL;
                hr = psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
                if (SUCCEEDED(hr))
                {
                  std::wstring wname = pszFilePath;
                  name = ws2s(wname);
                  CoTaskMemFree(pszFilePath);
                }
                psiResult->Release();
              }
            }
          }
        }
        // Unhook the event handler.
        // pfd->Unadvise(dwCookie);
      }
      // pfde->Release();
    }
    pfd->Release();
  }
  return name;
}

bool mvLobby::FindViewsDirectory(char* argv0)
{
  // if specified on the command line, and it exists use that
  if (this->ViewsDirectory.length() &&
    vtksys::SystemTools::FileExists(this->ViewsDirectory.c_str(), false))
  {
    return true;
  }

  // if found relative to the program, use that
  {
    // handle command line args, get the exe dir
    std::string errorMsg;
    bool found = vtksys::SystemTools::FindProgramPath(argv0, this->ViewsDirectory, errorMsg);
    if (found)
    {
      this->ViewsDirectory = vtksys::SystemTools::CollapseFullPath(this->ViewsDirectory);
      this->ViewsDirectory = vtksys::SystemTools::GetProgramPath(this->ViewsDirectory);
      std::string testDir = this->ViewsDirectory + "/Views";
      if (vtksys::SystemTools::FileExists(testDir.c_str(), false))
      {
        this->ViewsDirectory = testDir;
        return true;
      }

      // try ../
      this->ViewsDirectory += "/..";
      this->ViewsDirectory = vtksys::SystemTools::CollapseFullPath(this->ViewsDirectory);
      testDir = this->ViewsDirectory + "/Views";
      if (vtksys::SystemTools::FileExists(testDir.c_str(), false))
      {
        this->ViewsDirectory = testDir;
        return true;
      }
    }
  }

  // look in Documents
  {
    char const* uprofile = std::getenv("USERPROFILE");
    if (uprofile)
    {
      std::string testDir = vtksys::SystemTools::CollapseFullPath(uprofile);
      testDir += "/Documents/Views";
      if (vtksys::SystemTools::FileExists(testDir.c_str(), false))
      {
        this->ViewsDirectory = testDir;
        return true;
      }
    }
  }

  {
    std::string testDir = GetViewsDirectoryDialog();
    if (testDir.length() && vtksys::SystemTools::FileExists(testDir.c_str(), false))
    {
      this->ViewsDirectory = testDir;
      return true;
    }
  }

  vtkOutputWindow::GetInstance()->PromptUserOn();
  vtkGenericWarningMacro("Unable to find Views subdirectory");
  return false;
}

bool mvLobby::Initialize(int argc, char* argv[])
{
  if (!this->FindViewsDirectory(argv[0]))
  {
    return false;
  }

  // look for lobby audio file
  this->LobbyAudio = this->ViewsDirectory + "/Lobby.wav";
  if (!vtksys::SystemTools::FileExists(this->LobbyAudio.c_str(), true))
  {
    this->LobbyAudio = "";
  }

  this->Renderer->SetActiveCamera(this->Camera);
  this->RenderWindow->SetSize(800, 400);
  this->RenderWindow->AddRenderer(this->Renderer.Get());
  this->Renderer->SetClippingRangeExpansion(0.01);
  if (this->BackgroundColor.size() == 3)
  {
    this->Renderer->SetBackground(this->BackgroundColor[0] / 255.0,
      this->BackgroundColor[1] / 255.0, this->BackgroundColor[2] / 255.0);
  }
  this->Interactor->SetRenderWindow(this->RenderWindow.Get());
  this->Interactor->SetInteractorStyle(this->Style);
  this->Renderer->RemoveCuller(this->Renderer->GetCullers()->GetLastItem());

  // initialize the camera
  this->Camera->SetViewUp(0, 0, 1);
  this->Camera->SetPosition(0, -5, 1);

  {
    vtkLight* light = vtkLight::New();
    light->SetPosition(0.0, 1.0, 0.0);
    light->SetIntensity(1.0);
    light->SetLightTypeToSceneLight();
    this->Renderer->AddLight(light);
    light->Delete();
  }
  {
    vtkLight* light = vtkLight::New();
    light->SetPosition(0.8, -0.2, 0.0);
    light->SetIntensity(0.8);
    light->SetLightTypeToSceneLight();
    this->Renderer->AddLight(light);
    light->Delete();
  }
  {
    vtkLight* light = vtkLight::New();
    light->SetPosition(-0.3, -0.2, 0.7);
    light->SetIntensity(0.6);
    light->SetLightTypeToSceneLight();
    this->Renderer->AddLight(light);
    light->Delete();
  }
  {
    vtkLight* light = vtkLight::New();
    light->SetPosition(-0.3, -0.2, -0.7);
    light->SetIntensity(0.4);
    light->SetLightTypeToSceneLight();
    this->Renderer->AddLight(light);
    light->Delete();
  }

  this->RenderWindow->SetMultiSamples(8);

  this->Client->SetLobby(this);
  this->Client->Initialize(this->Renderer);

  this->EventCommand = vtkCallbackCommand::New();
  this->EventCommand->SetClientData(this);
  this->EventCommand->SetCallback(mvLobby::EventCallback);

  this->Renderer->AddObserver(vtkCommand::StartEvent, this->EventCommand, 2.0);
  this->Renderer->AddObserver(vtkCommand::EndEvent, this->EventCommand, 2.0);

  this->Interactor->AddObserver(vtkCommand::RenderEvent, this->EventCommand, 2.0);

  this->RenderWindow->SetPhysicalViewUp(0, 0, 1);
  this->RenderWindow->SetPhysicalViewDirection(0, 1, 0);
  this->Interactor->AddObserver(vtkCommand::Button3DEvent, this->EventCommand, 2.0);
  this->Style->AddTooltipForInput(vtkEventDataDevice::RightController,
    vtkEventDataDeviceInput::Trigger, "Load a mine, or move to the\nnext location within a mine");
  this->Style->AddTooltipForInput(vtkEventDataDevice::LeftController,
    vtkEventDataDeviceInput::Trigger, "Load a mine, or move to the\nnext location within a mine");
  this->Style->AddTooltipForInput(vtkEventDataDevice::RightController,
    vtkEventDataDeviceInput::ApplicationMenu, "Return to the lobby or\nexit from the lobby");
  this->Style->AddTooltipForInput(vtkEventDataDevice::LeftController,
    vtkEventDataDeviceInput::ApplicationMenu, "Return to the lobby or\nexit from the lobby");
  this->Style->AddTooltipForInput(vtkEventDataDevice::RightController,
    vtkEventDataDeviceInput::TrackPad,
    "Within a mine, hold down to fly\nin the direction the controller is pointing");
  this->Style->AddTooltipForInput(vtkEventDataDevice::LeftController,
    vtkEventDataDeviceInput::TrackPad,
    "Within a mine, hold down to fly\nin the direction the controller is pointing");
  this->Style->SetCurrentRenderer(this->Renderer);
  // this->Style->ToggleDrawControls();

  this->State = InLobby;

  return true;
}

void mvLobby::LoadMap()
{
  // create flat earth
  std::string fileName = this->ViewsDirectory;
  fileName += "\\World4096.jpg";

  // check if the map file is present, if not take the first jpeg found
  if (!vtksys::SystemTools::FileExists(fileName.c_str(), true))
  {
    vtkNew<vtkGlobFileNames> glob;
    glob->SetDirectory(this->ViewsDirectory.c_str());
    glob->RecurseOff();
    glob->AddFileNames("*.jpg");
    int numjpg = glob->GetNumberOfFileNames();
    if (numjpg > 0)
    {
      fileName = glob->GetNthFileName(0);
    }
  }

  // if no file then sip the map
  if (!vtksys::SystemTools::FileExists(fileName.c_str(), true))
  {
    this->DisplayMap = false;
    return;
  }

  vtkNew<vtkJPEGReader> mapReader;
  mapReader->SetFileName(fileName.c_str());
  vtkNew<vtkTexture> mapTexture;
  mapTexture->SetInputConnection(mapReader->GetOutputPort());
  mapTexture->InterpolateOn();
  mapTexture->MipmapOn();

  vtkNew<vtkPlaneSource> mapSource;
  mapSource->SetOrigin(-2.0, -1.0, 0.0);
  mapSource->SetPoint1(2.0, -1.0, 0.0);
  mapSource->SetPoint2(-2.0, 1.0, 0.0);
  vtkNew<vtkPolyDataMapper> mapMapper;
  mapMapper->SetInputConnection(mapSource->GetOutputPort());
  this->MapActor->GetProperty()->SetAmbient(1.0);
  this->MapActor->GetProperty()->SetDiffuse(0.0);
  this->MapActor->SetMapper(mapMapper);
  this->MapActor->SetTexture(mapTexture);
  this->Renderer->AddActor(this->MapActor);
}

namespace
{
bool sortViews(mvView* a, mvView* b)
{
  double angleA = atan2(a->GetLatitude(), a->GetLongitude());
  double angleB = atan2(b->GetLatitude(), b->GetLongitude());
  if (angleA < -M_PI / 2.0)
  {
    angleA = 2.0 * M_PI + angleA;
  }
  if (angleB < -M_PI / 2.0)
  {
    angleB = 2.0 * M_PI + angleB;
  }
  return (angleB < angleA);
}
}

void mvLobby::InitializeViews()
{
  // look in subdirs for Views and map them
  vtkNew<vtkGlobFileNames> glob;
  glob->SetDirectory(this->ViewsDirectory.c_str());
  glob->RecurseOn();
  glob->AddFileNames("*.mvx");

  // each view subtends ArcContentSize degree arc with a ArcGap degree gap
  int numViews = glob->GetNumberOfFileNames();
  for (int i = 0; i < numViews; ++i)
  {
    // extract the data
    mvView* view = new mvView();
    view->Initialize(glob->GetNthFileName(i), this);
    this->Views.push_back(view);
  }
  std::sort(this->Views.begin(), this->Views.end(), sortViews);

  // use the first view's poster aspect ratio as the standard
  this->Views[0]->GetPosterActor()->GetTexture()->Update();
  int* dims = this->Views[0]->GetPosterActor()->GetTexture()->GetInput()->GetDimensions();

  double targetWidth = this->PosterHeight * dims[0] / dims[1];
  this->ArcContentSize = 360.0 * targetWidth / (2.0 * M_PI * this->PosterRadius);
  double totalArc = this->ArcContentSize * numViews + this->ArcGap * (numViews - 1);

  // shrink poster height if needed
  while (totalArc + this->ArcGap > 360.0)
  {
    this->PosterHeight *= 0.9;
    targetWidth = this->PosterHeight * dims[0] / dims[1];
    this->ArcContentSize = 360.0 * targetWidth / (2.0 * M_PI * this->PosterRadius);
    totalArc = this->ArcContentSize * numViews + this->ArcGap * (numViews - 1);
  }

  this->ArcStart = totalArc / 2.0 + 90.0;
  double arcstep = this->ArcContentSize + this->ArcGap;
  double acsRad = vtkMath::RadiansFromDegrees(this->ArcContentSize);
  int i = 0;
  for (auto view : this->Views)
  {
    double pos = vtkMath::RadiansFromDegrees(this->ArcStart - i * arcstep);
    view->SetPosition(this->PosterRadius * cos(pos) + this->PosterCenter[0],
      this->PosterRadius * sin(pos) + this->PosterCenter[1],
      this->PosterRadius * cos(pos - acsRad) + this->PosterCenter[0],
      this->PosterRadius * sin(pos - acsRad) + this->PosterCenter[1], this->PosterHeight);
    ++i;
  }

  this->Renderer->ResetCameraClippingRange();
}

void mvLobby::LoadView(size_t viewNum, bool fromCollab)
{
  // stop any audio
  PlaySound(nullptr, nullptr, SND_ASYNC);

  this->CurrentView = static_cast<int>(viewNum);
  if (this->State.load() == InLobby && !fromCollab)
  {
    this->Client->SendMessage("V", this->Views[this->CurrentView]->GetName());
  }

  this->State = LoadingView;
  this->Views[this->CurrentView]->LoadMainThread();
  this->AsynchronousLoadViewThreadId =
    this->Threader->SpawnThread(AsynchronousLoadViewFunction, this);
}

void mvLobby::FinishLoadView()
{
  this->State = InView;

  // turn off lobby actors
  this->MapActor->VisibilityOff();
  for (auto v : this->Views)
  {
    v->LobbyOff();
  }

  this->Views[this->CurrentView]->FinishLoadView();
  this->Views[this->CurrentView]->GoToFirstTourStop();
}

void mvLobby::ReturnFromView(bool fromCollab)
{
  if (this->State.load() == InView && !fromCollab)
  {
    this->Client->SendMessage("V", std::string("Lobby"));
  }

  this->Views[this->CurrentView]->ReturnFromView();

  // turn back on lobby actors
  if (this->DisplayMap)
  {
    this->MapActor->VisibilityOn();
  }
  for (auto v : this->Views)
  {
    v->LobbyOn();
  }

  this->Camera->SetViewUp(0, 0, 1);
  this->Camera->SetPosition(0, -5, 1);
  this->Camera->SetFocalPoint(0, 0, 0);

  this->RenderWindow->SetPhysicalViewUp(0, 0, 1);
  this->RenderWindow->SetPhysicalViewDirection(0, 1, 0);
  this->RenderWindow->SetPhysicalScale(1.0);
  this->RenderWindow->SetPhysicalTranslation(0, 0, 0);

  this->Renderer->ResetCameraClippingRange();

  if (this->LobbyAudio.size())
  {
    PlaySound(
      this->LobbyAudio.c_str(), nullptr, SND_ASYNC | SND_FILENAME | SND_NODEFAULT | SND_LOOP);
  }

  this->State = InLobby;
  this->CurrentView = -1;
}
