/******************************************************************************
 * $Id: chartdldr_pi.cpp,v 1.0 2011/02/26 01:54:37 nohal Exp $
 *
 * Project:  OpenCPN
 * Purpose:  Chart downloader Plugin
 * Author:   Pavel Kalian
 *
 ***************************************************************************
 *   Copyright (C) 2011 by Pavel Kalian                                    *
 *   $EMAIL$                                                               *
 *                                                                         *
 *   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; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   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.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************
 */

#include "wx/wxprec.h"

#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif  // precompiled headers

#include "chartdldr_pi.h"
#include "wxWTranslateCatalog.h"
#include <wx/stdpaths.h>
#include <wx/url.h>
#include <wx/progdlg.h>
#include <wx/sstream.h>
#include <wx/wfstream.h>
#include <wx/filename.h>
#include <wx/listctrl.h>
#include <wx/dir.h>
#include <wx/filesys.h>
#include <wx/zipstrm.h>
#include <wx/wfstream.h>
#include <memory>
#include <wx/regex.h>
#include <wx/debug.h>

#ifdef DLDR_USE_LIBARCHIVE
#include <archive.h>
#include <archive_entry.h>
#ifdef CHARTDLDR_RAR_UNARR
#include "unarr.h"
#endif
#else
#include "unarr.h"
#endif

#ifdef __OCPN__ANDROID__
#define _LIBCPP_HAS_NO_OFF_T_FUNCTIONS
#endif

#include <fstream>

#ifdef __OCPN__ANDROID__
#include "androidSupport.h"
#endif

#ifdef __WXMAC__
#define CATALOGS_NAME_WIDTH 300
#define CATALOGS_DATE_WIDTH 120
#define CATALOGS_PATH_WIDTH 100
#define CHARTS_NAME_WIDTH 300
#define CHARTS_STATUS_WIDTH 100
#define CHARTS_DATE_WIDTH 120
#else
#ifdef __OCPN__ANDROID__

#define CATALOGS_NAME_WIDTH 350
#define CATALOGS_DATE_WIDTH 500
#define CATALOGS_PATH_WIDTH 1000
#define CHARTS_NAME_WIDTH 520
#define CHARTS_STATUS_WIDTH 150
#define CHARTS_DATE_WIDTH 200

#else

#define CATALOGS_NAME_WIDTH 200
#define CATALOGS_DATE_WIDTH 130
#define CATALOGS_PATH_WIDTH 250
#define CHARTS_NAME_WIDTH 320
#define CHARTS_STATUS_WIDTH 150
#define CHARTS_DATE_WIDTH 130

#endif
#endif  // __WXMAC__

#ifdef __OCPN__ANDROID__
#include <QtAndroidExtras/QAndroidJniObject>
#include "qdebug.h"

extern JavaVM *java_vm;  // found in androidUtil.cpp, accidentally exported....
JNIEnv *jenv;

#endif

bool getDisplayMetrics();

#define CHART_DIR "Charts"

void write_file(const wxString extract_file, char *data,
                unsigned long datasize) {
  wxFileName fn(extract_file);
  if (wxDirExists(fn.GetPath())) {
    if (!wxFileName::Mkdir(fn.GetPath(), 0755, wxPATH_MKDIR_FULL)) {
      wxLogError(_T("Can not create directory '") + fn.GetPath() + _T("'."));
      return;
    }
  }
  wxFileOutputStream f(extract_file);
  f.Write(data, datasize);
  f.Close();
}

int g_Android_SDK_Version;

bool IsDLDirWritable(wxFileName fn) {
#ifndef __OCPN__ANDROID__
  return fn.IsDirWritable();
#else
  if (g_Android_SDK_Version >= 30) {  // scoped storage?
    // Use a simple test here
    return (fn.GetFullPath().Contains("org.opencpn.opencpn"));  // fast test
  } else
    return fn.IsDirWritable();

#endif
}

// the class factories, used to create and destroy instances of the PlugIn

extern "C" DECL_EXP opencpn_plugin *create_pi(void *ppimgr) {
  return new chartdldr_pi(ppimgr);
}

extern "C" DECL_EXP void destroy_pi(opencpn_plugin *p) { delete p; }

double g_androidDPmm;
chartdldr_pi *g_pi;

//---------------------------------------------------------------------------------------------------------
//
//    ChartDldr PlugIn Implementation
//
//---------------------------------------------------------------------------------------------------------

#include "icons.h"

//---------------------------------------------------------------------------------------------------------
//
//          PlugIn initialization and de-init
//
//---------------------------------------------------------------------------------------------------------

chartdldr_pi::chartdldr_pi(void *ppimgr) : opencpn_plugin_113(ppimgr) {
  // Create the PlugIn icons
  initialize_images();

  m_parent_window = NULL;
  m_pChartSource = NULL;
  m_pconfig = NULL;
  m_preselect_new = false;
  m_preselect_updated = false;
  m_allow_bulk_update = false;
  m_pOptionsPage = NULL;
  m_selected_source = -1;
  m_dldrpanel = NULL;
  m_leftclick_tool_id = -1;
  m_schartdldr_sources = wxEmptyString;

  g_pi = this;
}

int chartdldr_pi::Init(void) {
  AddLocaleCatalog(PLUGIN_CATALOG_NAME);

  //    Get a pointer to the opencpn display canvas, to use as a parent for the
  //    POI Manager dialog
  m_parent_window = GetOCPNCanvasWindow();

  //    Get a pointer to the opencpn configuration object
  m_pconfig = GetOCPNConfigObject();
  m_pOptionsPage = NULL;

  m_pChartSource = NULL;

#ifdef __OCPN__ANDROID__
  androidGetSDKVersion();
#endif

  //    And load the configuration items
  LoadConfig();

  getDisplayMetrics();

  wxStringTokenizer st(m_schartdldr_sources, _T("|"), wxTOKEN_DEFAULT);
  while (st.HasMoreTokens()) {
    wxString s1 = st.GetNextToken();
    wxString s2 = st.GetNextToken();
    wxString s3 = st.GetNextToken();
    if (!s2.IsEmpty())  // scrub empty sources.
      m_ChartSources.push_back(std::make_unique<ChartSource>(s1, s2, s3));
  }
  return (WANTS_PREFERENCES | WANTS_CONFIG | INSTALLS_TOOLBOX_PAGE);
}

bool chartdldr_pi::DeInit(void) {
  wxLogMessage(_T("chartdldr_pi: DeInit"));

  m_ChartSources.clear();
  // wxDELETE(m_pChartSource);
  /* TODO: Seth */
  //      dialog->Close();
  //      dialog->Destroy();
  //      wxDELETE(dialog);
  /* We must delete remaining page if the plugin is disabled while in Options
   * dialog */
  if (m_pOptionsPage) {
    if (DeleteOptionsPage(m_pOptionsPage)) m_pOptionsPage = NULL;
    // TODO: any other memory leak?
  }
  return true;
}

int chartdldr_pi::GetAPIVersionMajor() { return MY_API_VERSION_MAJOR; }

int chartdldr_pi::GetAPIVersionMinor() { return MY_API_VERSION_MINOR; }

int chartdldr_pi::GetPlugInVersionMajor() { return PLUGIN_VERSION_MAJOR; }

int chartdldr_pi::GetPlugInVersionMinor() { return PLUGIN_VERSION_MINOR; }

wxBitmap *chartdldr_pi::GetPlugInBitmap() { return _img_chartdldr_pi; }

wxString chartdldr_pi::GetCommonName() { return _("ChartDownloader"); }

wxString chartdldr_pi::GetShortDescription() {
  return _("Chart Downloader PlugIn for OpenCPN");
}

wxString chartdldr_pi::GetLongDescription() {
  return _(
      "Chart Downloader PlugIn for OpenCPN\n\
Manages chart downloads and updates from sources supporting\n\
NOAA Chart Catalog format");
}

void chartdldr_pi::OnSetupOptions(void) {
  m_pOptionsPage =
      AddOptionsPage(PI_OPTIONS_PARENT_CHARTS, _("Chart Downloader"));
  if (!m_pOptionsPage) {
    wxLogMessage(
        _T("Error: chartdldr_pi::OnSetupOptions AddOptionsPage failed!"));
    return;
  }
  wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
  m_pOptionsPage->SetSizer(sizer);

  m_dldrpanel =
      new ChartDldrPanelImpl(this, m_pOptionsPage, wxID_ANY, wxDefaultPosition,
                             wxDefaultSize, wxDEFAULT_DIALOG_STYLE);

  m_pOptionsPage->InvalidateBestSize();
  sizer->Add(m_dldrpanel, 1, wxALL | wxEXPAND);
  m_dldrpanel->SetBulkUpdate(m_allow_bulk_update);
  m_dldrpanel->FitInside();
}

void chartdldr_pi::OnCloseToolboxPanel(int page_sel, int ok_apply_cancel) {
  /* TODO: Seth */
  m_dldrpanel->CancelDownload();
#ifndef __OCPN__ANDROID__
  OCPN_cancelDownloadFileBackground(
      0);  // Stop the thread, is something like this needed on Android as well?
#endif
  m_selected_source = m_dldrpanel->GetSelectedCatalog();
  SaveConfig();
}

bool chartdldr_pi::LoadConfig(void) {
  wxFileConfig *pConf = (wxFileConfig *)m_pconfig;

  if (pConf) {
    pConf->SetPath(_T ( "/Settings/ChartDnldr" ));
    pConf->Read(_T ( "ChartSources" ), &m_schartdldr_sources, wxEmptyString);
    pConf->Read(_T ( "Source" ), &m_selected_source, -1);

    wxFileName fn(GetWritableDocumentsDir(), wxEmptyString);
    fn.AppendDir(_T(CHART_DIR));

    pConf->Read(_T ( "BaseChartDir" ), &m_base_chart_dir, fn.GetPath());
    wxLogMessage(_T ( "chartdldr_pi:m_base_chart_dir: " ) + m_base_chart_dir);

    // Check to see if the directory is writeable, esp. on App updates.
    wxFileName testFN(m_base_chart_dir);
    if (!IsDLDirWritable(testFN)) {
      wxLogMessage(
          "Cannot write to m_base_chart_dir, override to "
          "GetWritableDocumentsDir()");
      m_base_chart_dir = fn.GetPath();
      wxLogMessage(_T ( "chartdldr_pi: Corrected: " ) + m_base_chart_dir);
    }

    pConf->Read(_T ( "PreselectNew" ), &m_preselect_new, true);
    pConf->Read(_T ( "PreselectUpdated" ), &m_preselect_updated, true);
    pConf->Read(_T ( "AllowBulkUpdate" ), &m_allow_bulk_update, false);
    return true;
  } else
    return false;
}

bool chartdldr_pi::SaveConfig(void) {
  wxFileConfig *pConf = (wxFileConfig *)m_pconfig;

  m_schartdldr_sources.Clear();

  for (size_t i = 0; i < m_ChartSources.size(); i++) {
    std::unique_ptr<ChartSource> &cs = m_ChartSources.at(i);
    m_schartdldr_sources.Append(
        wxString::Format(_T("%s|%s|%s|"), cs->GetName().c_str(),
                         cs->GetUrl().c_str(), cs->GetDir().c_str()));
  }

  if (pConf) {
    pConf->SetPath(_T ( "/Settings/ChartDnldr" ));
    pConf->Write(_T ( "ChartSources" ), m_schartdldr_sources);
    pConf->Write(_T ( "Source" ), m_selected_source);
    pConf->Write(_T ( "BaseChartDir" ), m_base_chart_dir);
    pConf->Write(_T ( "PreselectNew" ), m_preselect_new);
    pConf->Write(_T ( "PreselectUpdated" ), m_preselect_updated);
    pConf->Write(_T ( "AllowBulkUpdate" ), m_allow_bulk_update);

    return true;
  } else
    return false;
}

void SetBackColor(wxWindow *ctrl, wxColour col) {
  static int depth = 0;  // recursion count
  if (depth == 0) {      // only for the window root, not for every child

    ctrl->SetBackgroundColour(col);
  }

  wxWindowList kids = ctrl->GetChildren();
  for (unsigned int i = 0; i < kids.GetCount(); i++) {
    wxWindowListNode *node = kids.Item(i);
    wxWindow *win = node->GetData();

    if (win->IsKindOf(CLASSINFO(wxListBox)))
      ((wxListBox *)win)->SetBackgroundColour(col);

    else if (win->IsKindOf(CLASSINFO(wxTextCtrl)))
      ((wxTextCtrl *)win)->SetBackgroundColour(col);

    //        else if( win->IsKindOf( CLASSINFO(wxStaticText) ) )
    //            ( (wxStaticText*) win )->SetForegroundColour( uitext );

    else if (win->IsKindOf(CLASSINFO(wxChoice)))
      ((wxChoice *)win)->SetBackgroundColour(col);

    else if (win->IsKindOf(CLASSINFO(wxComboBox)))
      ((wxComboBox *)win)->SetBackgroundColour(col);

    else if (win->IsKindOf(CLASSINFO(wxRadioButton)))
      ((wxRadioButton *)win)->SetBackgroundColour(col);

    else if (win->IsKindOf(CLASSINFO(wxScrolledWindow))) {
      ((wxScrolledWindow *)win)->SetBackgroundColour(col);
    }

    else if (win->IsKindOf(CLASSINFO(wxButton))) {
      ((wxButton *)win)->SetBackgroundColour(col);
    }

    else {
      ;
    }

    if (win->GetChildren().GetCount() > 0) {
      depth++;
      wxWindow *w = win;
      SetBackColor(w, col);
      depth--;
    }
  }
}

void chartdldr_pi::ShowPreferencesDialog(wxWindow *parent) {
  ChartDldrPrefsDlgImpl *dialog = new ChartDldrPrefsDlgImpl(parent);

  wxFont fo = GetOCPNGUIScaledFont_PlugIn(_T("Dialog"));
  dialog->SetFont(fo);

#ifdef __OCPN__ANDROID__
  if (m_parent_window) {
    int xmax = m_parent_window->GetSize().GetWidth();
    int ymax = m_parent_window->GetParent()
                   ->GetSize()
                   .GetHeight();  // This would be the Options dialog itself
    dialog->SetSize(xmax, ymax);
    dialog->Layout();

    dialog->Move(0, 0);
  }

  wxColour cl = wxColour(214, 218, 222);
  SetBackColor(dialog, cl);
#endif

  dialog->SetPath(m_base_chart_dir);
  dialog->SetPreferences(m_preselect_new, m_preselect_updated,
                         m_allow_bulk_update);

  dialog->ShowModal();
  dialog->Destroy();
}

void chartdldr_pi::UpdatePrefs(ChartDldrPrefsDlgImpl *dialog) {
  m_base_chart_dir = dialog->GetPath();
  dialog->GetPreferences(m_preselect_new, m_preselect_updated,
                         m_allow_bulk_update);
  SaveConfig();
  if (m_dldrpanel) m_dldrpanel->SetBulkUpdate(m_allow_bulk_update);
}

bool getDisplayMetrics() {
#ifdef __OCPN__ANDROID__

  g_androidDPmm = 4.0;  // nominal default

  //  Get a reference to the running native activity
  QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod(
      "org/qtproject/qt5/android/QtNative", "activity",
      "()Landroid/app/Activity;");

  if (!activity.isValid()) {
    return false;
  }

  //  Call the desired method
  QAndroidJniObject data =
      activity.callObjectMethod("getDisplayMetrics", "()Ljava/lang/String;");

  wxString return_string;
  jstring s = data.object<jstring>();

  //  Need a Java environment to decode the resulting string
  if (java_vm->GetEnv((void **)&jenv, JNI_VERSION_1_6) != JNI_OK) {
    // qDebug() << "GetEnv failed.";
  } else {
    const char *ret_string = (jenv)->GetStringUTFChars(s, NULL);
    return_string = wxString(ret_string, wxConvUTF8);
  }

  //  Return string may have commas instead of periods, if using Euro locale
  //  We just fix it here...
  return_string.Replace(_T(","), _T("."));

  //     wxLogMessage(_T("Metrics:") + return_string);
  //     wxSize screen_size = ::wxGetDisplaySize();
  //     wxString msg;
  //     msg.Printf(_T("wxGetDisplaySize(): %d %d"), screen_size.x,
  //     screen_size.y); wxLogMessage(msg);

  double density = 1.0;
  wxStringTokenizer tk(return_string, _T(";"));
  if (tk.HasMoreTokens()) {
    wxString token = tk.GetNextToken();  // xdpi
    token = tk.GetNextToken();           // density

    long b = ::wxGetDisplaySize().y;
    token.ToDouble(&density);

    token = tk.GetNextToken();  // ldpi

    token = tk.GetNextToken();  // width
    token = tk.GetNextToken();  // height - statusBarHeight
    token = tk.GetNextToken();  // width
    token = tk.GetNextToken();  // height
    token = tk.GetNextToken();  // dm.widthPixels
    token = tk.GetNextToken();  // dm.heightPixels

    token = tk.GetNextToken();  // actionBarHeight
    long abh;
    token.ToLong(&abh);
    //        g_ActionBarHeight = wxMax(abh, 50);

    //        qDebug() << "g_ActionBarHeight" << abh << g_ActionBarHeight;
  }

  double ldpi = 160. * density;

  //    double maxDim = wxMax(::wxGetDisplaySize().x, ::wxGetDisplaySize().y);
  //    ret = (maxDim / ldpi) * 25.4;

  //    msg.Printf(_T("Android Auto Display Size (mm, est.): %g"), ret);
  //    wxLogMessage(msg);

  //  Save some items as global statics for convenience
  g_androidDPmm = ldpi / 25.4;
  //    g_androidDensity = density;

  // qDebug() << "PI Metrics" << g_androidDPmm << density;
  return true;
#else

  return true;
#endif
}

ChartSource::ChartSource(wxString name, wxString url, wxString localdir) {
  m_name = name;
  m_url = url;
  m_dir = localdir;
  m_update_data.clear();
}

ChartSource::~ChartSource() { m_update_data.clear(); }

#define ID_MNU_SELALL 2001
#define ID_MNU_DELALL 2002
#define ID_MNU_INVSEL 2003
#define ID_MNU_SELUPD 2004
#define ID_MNU_SELNEW 2005

enum { ThreadId = wxID_HIGHEST + 1 };

IMPLEMENT_DYNAMIC_CLASS(ChartDldrPanelImpl, ChartDldrPanel)
BEGIN_EVENT_TABLE(ChartDldrPanelImpl, ChartDldrPanel)
END_EVENT_TABLE()

void ChartDldrPanelImpl::OnPopupClick(wxCommandEvent &evt) {
  switch (evt.GetId()) {
    case ID_MNU_SELALL:
      CheckAllCharts(true);
      break;
    case ID_MNU_DELALL:
      CheckAllCharts(false);
      break;
    case ID_MNU_INVSEL:
      InvertCheckAllCharts();
      break;
    case ID_MNU_SELUPD:
      CheckUpdatedCharts(true);
      break;
    case ID_MNU_SELNEW:
      CheckNewCharts(true);
      break;
  }
}

void ChartDldrPanelImpl::OnContextMenu(wxMouseEvent &event) {
  wxMenu menu;

  wxPoint mouseScreen = wxGetMousePosition();
  wxPoint mouseClient = ScreenToClient(mouseScreen);

#ifdef __OCPN__ANDROID__
  wxFont *pf = OCPNGetFont(_T("Menu"), 0);

  // add stuff
  wxMenuItem *item1 = new wxMenuItem(&menu, ID_MNU_SELALL, _("Select all"));
  item1->SetFont(*pf);
  menu.Append(item1);

  wxMenuItem *item2 = new wxMenuItem(&menu, ID_MNU_DELALL, _("Deselect all"));
  item2->SetFont(*pf);
  menu.Append(item2);

  wxMenuItem *item3 =
      new wxMenuItem(&menu, ID_MNU_INVSEL, _("Invert selection"));
  item3->SetFont(*pf);
  menu.Append(item3);

  wxMenuItem *item4 = new wxMenuItem(&menu, ID_MNU_SELUPD, _("Select updated"));
  item4->SetFont(*pf);
  menu.Append(item4);

  wxMenuItem *item5 = new wxMenuItem(&menu, ID_MNU_SELNEW, _("Select new"));
  item5->SetFont(*pf);
  menu.Append(item5);

#else

  menu.Append(ID_MNU_SELALL, _("Select all"), wxT(""));
  menu.Append(ID_MNU_DELALL, _("Deselect all"), wxT(""));
  menu.Append(ID_MNU_INVSEL, _("Invert selection"), wxT(""));
  menu.Append(ID_MNU_SELUPD, _("Select updated"), wxT(""));
  menu.Append(ID_MNU_SELNEW, _("Select new"), wxT(""));

#endif

  menu.Connect(wxEVT_COMMAND_MENU_SELECTED,
               (wxObjectEventFunction)&ChartDldrPanelImpl::OnPopupClick, NULL,
               this);
  // and then display
  PopupMenu(&menu, mouseClient.x, mouseClient.y);
}

void ChartDldrPanelImpl::OnShowLocalDir(wxCommandEvent &event) {
  if (pPlugIn->m_pChartSource == NULL) return;
#ifdef __WXGTK__
  wxExecute(wxString::Format(_T("xdg-open %s"),
                             pPlugIn->m_pChartSource->GetDir().c_str()));
#endif
#ifdef __WXMAC__
  wxExecute(wxString::Format(_T("open %s"),
                             pPlugIn->m_pChartSource->GetDir().c_str()));
#endif
#ifdef __WXMSW__
  wxExecute(wxString::Format(_T("explorer %s"),
                             pPlugIn->m_pChartSource->GetDir().c_str()));
#endif
}

void ChartDldrPanelImpl::SetSource(int id) {
  pPlugIn->SetSourceId(id);

  m_bDeleteSource->Enable(id >= 0);
  m_bUpdateChartList->Enable(id >= 0);
  m_bEditSource->Enable(id >= 0);

  // TODO: DAN - Need to optimze to only update the chart list if needed.
  //             Right now it updates multiple times unnecessarily.
  CleanForm();
  if (id >= 0 && id < (int)pPlugIn->m_ChartSources.size()) {
    ::wxBeginBusyCursor();  // wxSetCursor(wxCURSOR_WAIT);
    //        wxYield();
    std::unique_ptr<ChartSource> &cs = pPlugIn->m_ChartSources.at(id);
    cs->LoadUpdateData();
    cs->UpdateLocalFiles();
    pPlugIn->m_pChartSource = cs.get();
    FillFromFile(cs->GetUrl(), cs->GetDir(), pPlugIn->m_preselect_new,
                 pPlugIn->m_preselect_updated);
    wxURI url(cs->GetUrl());
    m_chartsLabel->SetLabel(wxString::Format(
        _("Charts: %s"), (cs->GetName() + _(" from ") + url.BuildURI() +
                          _T(" -> ") + cs->GetDir())
                             .c_str()));
    if (::wxIsBusy()) ::wxEndBusyCursor();
  } else {
    pPlugIn->m_pChartSource = NULL;
    m_chartsLabel->SetLabel(_("Charts"));
  }
}

void ChartDldrPanelImpl::SelectSource(wxListEvent &event) {
  int i = GetSelectedCatalog();
  if (i >= 0) SetSource(i);
  event.Skip();
}

void ChartDldrPanelImpl::SetBulkUpdate(bool bulk_update) {
  m_bUpdateAllCharts->Enable(bulk_update);
  m_bUpdateAllCharts->Show(bulk_update);
  Layout();
  m_parent->Layout();
}

void ChartDldrPanelImpl::CleanForm() {
#if defined(CHART_LIST)
  clearChartList();
#else
  m_scrollWinChartList->ClearBackground();
#endif /* CHART_LIST */
       // m_stCatalogInfo->Show( false );
}

void ChartDldrPanelImpl::FillFromFile(wxString url, wxString dir, bool selnew,
                                      bool selupd) {
  // load if exists
  wxStringTokenizer tk(url, _T("/"));
  wxString file;
  do {
    file = tk.GetNextToken();
  } while (tk.HasMoreTokens());
  wxFileName fn;
  fn.SetFullName(file);
  fn.SetPath(dir);
  wxString path = fn.GetFullPath();
  if (wxFileExists(path)) {
    pPlugIn->m_pChartCatalog.LoadFromFile(path);
    //            m_tChartSourceInfo->SetValue(pPlugIn->m_pChartCatalog.GetDescription());
    // fill in the rest of the form

    m_updatedCharts = 0;
    m_newCharts = 0;

#if !defined(CHART_LIST)
    // Clear any existing panels
    m_panelArray.clear();
    m_scrollWinChartList->ClearBackground();
#endif /* CHART_LIST */

    for (size_t i = 0; i < pPlugIn->m_pChartCatalog.charts.size(); i++) {
      wxString status;
      wxString latest;
      bool bcheck = false;
      wxString file =
          pPlugIn->m_pChartCatalog.charts.at(i)->GetChartFilename(true);
      if (!pPlugIn->m_pChartSource->ExistsLocaly(
              pPlugIn->m_pChartCatalog.charts.at(i)->number, file)) {
        m_newCharts++;
        status = _("New");
        if (selnew) bcheck = true;
      } else {
        if (pPlugIn->m_pChartSource->IsNewerThanLocal(
                pPlugIn->m_pChartCatalog.charts.at(i)->number, file,
                pPlugIn->m_pChartCatalog.charts.at(i)->GetUpdateDatetime())) {
          m_updatedCharts++;
          status = _("Out of date");
          if (selupd) bcheck = true;
        } else {
          status = _("Up to date");
        }
      }
      latest =
          pPlugIn->m_pChartCatalog.charts.at(i)->GetUpdateDatetime().Format(
              _T("%Y-%m-%d"));

#if defined(CHART_LIST)
      wxVector<wxVariant> data;
      data.push_back(wxVariant(bcheck));
      data.push_back(wxVariant(status));
      data.push_back(wxVariant(latest));
      data.push_back(
          wxVariant(pPlugIn->m_pChartCatalog.charts.at(i)->GetChartTitle()));
      getChartList()->AppendItem(data);
#else
      auto pC = std::make_unique<ChartPanel>(
          m_scrollWinChartList, wxID_ANY, wxDefaultPosition, wxSize(-1, -1),
          pPlugIn->m_pChartCatalog.charts.at(i)->GetChartTitle(), status,
          latest, this, bcheck);
      pC->Connect(wxEVT_RIGHT_DOWN,
                  wxMouseEventHandler(ChartDldrPanel::OnContextMenu), NULL,
                  this);

      m_boxSizerCharts->Add(pC.get(), 0, wxEXPAND | wxLEFT | wxRIGHT, 2);
      m_panelArray.push_back(std::move(pC));
#endif /* CHART_LIST */
    }

#if !defined(CHART_LIST)  // wxDataViewListCtrl handles all of this AFAIK: Dan
    m_scrollWinChartList->ClearBackground();
    m_scrollWinChartList->FitInside();
    m_scrollWinChartList->GetSizer()->Layout();
    Layout();
    m_scrollWinChartList->ClearBackground();
    SetChartInfo(wxString::Format(_("%lu charts total, %lu updated, %lu new"),
                                  pPlugIn->m_pChartCatalog.charts.size(),
                                  m_updatedCharts, m_newCharts));
#else
    SetChartInfo(wxString::Format(
        _("%lu charts total, %lu updated, %lu new, %lu selected"),
        pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
        GetCheckedChartCount()));
#endif /* CHART_LIST */
  }
}

bool ChartSource::ExistsLocaly(wxString chart_number, wxString filename) {
  wxASSERT(this);

  wxStringTokenizer tk(filename, _T("."));
  wxString file = tk.GetNextToken().MakeLower();

  if (!m_update_data.empty()) {
    return m_update_data.find(std::string(chart_number.Lower().mb_str())) !=
               m_update_data.end() ||
           m_update_data.find(std::string(file.mb_str())) !=
               m_update_data.end();
  }
  for (size_t i = 0; i < m_localfiles.Count(); i++) {
    if (m_localfiles.Item(i) == file) return true;
  }
  return false;
}

bool ChartSource::IsNewerThanLocal(wxString chart_number, wxString filename,
                                   wxDateTime validDate) {
  wxStringTokenizer tk(filename, _T("."));
  wxString file = tk.GetNextToken().MakeLower();
  if (!m_update_data.empty()) {
    if (m_update_data[std::string(chart_number.Lower().mbc_str())] <
            validDate.GetTicks() &&
        m_update_data[std::string(file.mbc_str())] < validDate.GetTicks())
      return true;
    else
      return false;
  }
  bool update_candidate = false;

  for (size_t i = 0; i < m_localfiles.Count(); i++) {
    if (m_localfiles.Item(i) == file) {
      if (validDate.IsLaterThan(m_localdt.at(i))) {
        update_candidate = true;
      } else
        return false;
    }
  }
  return update_candidate;
}

int ChartDldrPanelImpl::GetSelectedCatalog() {
  long item =
      m_lbChartSources->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
  return item;
}

void ChartDldrPanelImpl::SelectCatalog(int item) {
  if (item >= 0) {
    m_bDeleteSource->Enable();
    m_bEditSource->Enable();
    m_bUpdateChartList->Enable();
  } else {
    m_bDeleteSource->Disable();
    m_bEditSource->Disable();
    m_bUpdateChartList->Disable();
  }
  m_lbChartSources->SetItemState(item, wxLIST_STATE_SELECTED,
                                 wxLIST_STATE_SELECTED);
}

void ChartDldrPanelImpl::AppendCatalog(std::unique_ptr<ChartSource> &cs) {
  long id = m_lbChartSources->GetItemCount();
  m_lbChartSources->InsertItem(id, cs->GetName());
  m_lbChartSources->SetItem(id, 1, _("(Please update first)"));
  m_lbChartSources->SetItem(id, 2, cs->GetDir());
  wxURI url(cs->GetUrl());
  if (url.IsReference()) {
    OCPNMessageBox_PlugIn(
        this, _("Error, the URL to the chart source data seems wrong."),
        _("Error"));
    return;
  }
  wxFileName fn(url.GetPath());
  fn.SetPath(cs->GetDir());
  wxString path = fn.GetFullPath();
  if (wxFileExists(path)) {
    if (pPlugIn->m_pChartCatalog.LoadFromFile(path, true)) {
      m_lbChartSources->SetItem(id, 0, pPlugIn->m_pChartCatalog.title);
      m_lbChartSources->SetItem(
          id, 1,
          pPlugIn->m_pChartCatalog.GetReleaseDate().Format(
              _T("%Y-%m-%d %H:%M")));
      m_lbChartSources->SetItem(id, 2, path);
#ifdef __OCPN__ANDROID__
      m_lbChartSources->GetHandle()->resizeColumnToContents(0);
      m_lbChartSources->GetHandle()->resizeColumnToContents(1);
      m_lbChartSources->GetHandle()->resizeColumnToContents(2);
#endif
    }
  }
}

void ChartDldrPanelImpl::UpdateAllCharts(wxCommandEvent &event) {
  int failed_to_update = 0;
  int attempted_to_update = 0;
  if ((pPlugIn->m_preselect_new) && (pPlugIn->m_preselect_updated)) {
    wxMessageDialog mess(
        this,
        _("You have chosen to update all chart catalogs.\nThen download all "
          "new and updated charts.\nThis may take a long time."),
        _("Chart Downloader"), wxOK | wxCANCEL);
    if (mess.ShowModal() == wxID_CANCEL) return;
  } else if (pPlugIn->m_preselect_new) {
    wxMessageDialog mess(
        this,
        _("You have chosen to update all chart catalogs.\nThen download only "
          "new (but not updated) charts.\nThis may take a long time."),
        _("Chart Downloader"), wxOK | wxCANCEL);
    if (mess.ShowModal() == wxID_CANCEL) return;
  } else if (pPlugIn->m_preselect_updated) {
    wxMessageDialog mess(
        this,
        _("You have chosen to update all chart catalogs.\nThen download only "
          "updated (but not new) charts.\nThis may take a long time."),
        _("Chart Downloader"), wxOK | wxCANCEL);
    if (mess.ShowModal() == wxID_CANCEL) return;
  }
  updatingAll = true;
  cancelled = false;
  // Flip to the list of charts so user can observe the download progress
  int oldPage = m_DLoadNB->SetSelection(1);
  for (long chartIndex = 0; chartIndex < m_lbChartSources->GetItemCount();
       chartIndex++) {
    m_lbChartSources->SetItemState(chartIndex, wxLIST_STATE_SELECTED,
                                   wxLIST_STATE_SELECTED);
    if (cancelled) break;
    UpdateChartList(event);
    DownloadCharts();
    attempted_to_update += m_downloading;
    failed_to_update += m_failed_downloads;
  }
  wxLogMessage(wxString::Format(
      _T("chartdldr_pi::UpdateAllCharts() downloaded %d out of %d charts."),
      attempted_to_update - failed_to_update, attempted_to_update));
  if (failed_to_update > 0)
    OCPNMessageBox_PlugIn(
        this,
        wxString::Format(_("%d out of %d charts failed to download.\nCheck the "
                           "list, verify there is a working Internet "
                           "connection and repeat the operation if needed."),
                         failed_to_update, attempted_to_update),
        _("Chart Downloader"), wxOK | wxICON_ERROR);
  if (attempted_to_update > failed_to_update) ForceChartDBUpdate();
  updatingAll = false;
  cancelled = false;
  // Flip back to the original page
  m_DLoadNB->SetSelection(oldPage);
}

void ChartDldrPanelImpl::UpdateChartList(wxCommandEvent &event) {
  // TODO: check if everything exists and we can write to the output dir etc.
  if (!m_lbChartSources->GetSelectedItemCount()) return;
  std::unique_ptr<ChartSource> &cs =
      pPlugIn->m_ChartSources.at(GetSelectedCatalog());
  wxURI url(cs->GetUrl());
  if (url.IsReference()) {
    OCPNMessageBox_PlugIn(
        this, _("Error, the URL to the chart source data seems wrong."),
        _("Error"));
    return;
  }

  wxStringTokenizer tk(url.GetPath(), _T("/"));
  wxString file;
  do {
    file = tk.GetNextToken();
  } while (tk.HasMoreTokens());
  wxFileName fn;
  fn.SetFullName(file);
  fn.SetPath(cs->GetDir());
  if (!wxDirExists(cs->GetDir())) {
    if (!wxFileName::Mkdir(cs->GetDir(), 0755, wxPATH_MKDIR_FULL)) {
      OCPNMessageBox_PlugIn(
          this,
          wxString::Format(_("Directory %s can't be created."),
                           cs->GetDir().c_str()),
          _("Chart Downloader"));
      return;
    }
  }

  bool bok = false;

#ifdef __OCPN__ANDROID__
  wxString file_URI = _T("file://") + fn.GetFullPath();

  //     wxFile testFile(tfn.GetFullPath().c_str(), wxFile::write);
  //     if(!testFile.IsOpened()){
  //         wxMessageBox(this, wxString::Format(_("File  %s can't be written.
  //         \nChoose a writable folder for Chart Downloader file storage."),
  //         tfn.GetFullPath().c_str()), _("Chart Downloader")); return;
  //     }
  //     testFile.Close();
  //     ::wxRemoveFile(tfn.GetFullPath());

  _OCPN_DLStatus ret = OCPN_downloadFile(
      cs->GetUrl(), file_URI, _("Downloading file"),
      _("Reading Headers: ") + url.BuildURI(), wxNullBitmap, this,
      OCPN_DLDS_ELAPSED_TIME | OCPN_DLDS_ESTIMATED_TIME |
          OCPN_DLDS_REMAINING_TIME | OCPN_DLDS_SPEED | OCPN_DLDS_SIZE |
          OCPN_DLDS_URL | OCPN_DLDS_CAN_PAUSE | OCPN_DLDS_CAN_ABORT |
          OCPN_DLDS_AUTO_CLOSE,
      10);
  bok = true;

#else
  wxFileName tfn = wxFileName::CreateTempFileName(fn.GetFullPath());
  wxString file_URI = tfn.GetFullPath();

  _OCPN_DLStatus ret = OCPN_downloadFile(
      cs->GetUrl(), file_URI, _("Downloading file"),
      _("Reading Headers: ") + url.BuildURI(), wxNullBitmap, this,
      OCPN_DLDS_ELAPSED_TIME | OCPN_DLDS_ESTIMATED_TIME |
          OCPN_DLDS_REMAINING_TIME | OCPN_DLDS_SPEED | OCPN_DLDS_SIZE |
          OCPN_DLDS_URL | OCPN_DLDS_CAN_PAUSE | OCPN_DLDS_CAN_ABORT |
          OCPN_DLDS_AUTO_CLOSE,
      10);

  bok = wxCopyFile(tfn.GetFullPath(), fn.GetFullPath());
  wxRemoveFile(tfn.GetFullPath());

#endif

  //    wxLogMessage(_T("chartdldr_pi:  OCPN_downloadFile done:"));

  switch (ret) {
    case OCPN_DL_NO_ERROR: {
      if (bok) {
        long id = GetSelectedCatalog();
        SetSource(id);

        m_lbChartSources->SetItem(id, 0, pPlugIn->m_pChartCatalog.title);
        m_lbChartSources->SetItem(
            id, 1,
            pPlugIn->m_pChartCatalog.GetReleaseDate().Format(
                _T("%Y-%m-%d %H:%M")));
        m_lbChartSources->SetItem(id, 2, cs->GetDir());

      } else
        OCPNMessageBox_PlugIn(
            this,
            wxString::Format(_("Failed to Find New Catalog: %s "),
                             url.BuildURI().c_str()),
            _("Chart Downloader"), wxOK | wxICON_ERROR);
      break;
    }
    case OCPN_DL_FAILED: {
      OCPNMessageBox_PlugIn(
          this,
          wxString::Format(_("Failed to Download Catalog: %s \nVerify there is "
                             "a working Internet connection."),
                           url.BuildURI().c_str()),
          _("Chart Downloader"), wxOK | wxICON_ERROR);
      break;
    }

    case OCPN_DL_USER_TIMEOUT:
    case OCPN_DL_ABORTED: {
      cancelled = true;
      break;
    }

    case OCPN_DL_UNKNOWN:
    case OCPN_DL_STARTED: {
      break;
    }

    default:
      wxASSERT(false);  // This should never happen because we handle all
                        // possible cases of ret
  }

  if ((ret == OCPN_DL_NO_ERROR) && bok) m_DLoadNB->SetSelection(1);
}

void ChartSource::GetLocalFiles() {
  if (!UpdateDataExists() || m_update_data.empty()) {
    wxArrayString *allFiles = new wxArrayString;
    if (wxDirExists(GetDir())) wxDir::GetAllFiles(GetDir(), allFiles);
    m_localdt.clear();
    m_localfiles.Clear();
    wxDateTime ct, mt, at;
    wxString name;
    for (size_t i = 0; i < allFiles->Count(); i++) {
      wxFileName fn(allFiles->Item(i));
      name = fn.GetFullName().Lower();
      // Only add unique files names to the local list.
      // This is safe because all chart names within a catalog
      // are necessarily unique.
      if (!ExistsLocaly(wxEmptyString, name)) {
        fn.GetTimes(&at, &mt, &ct);
        m_localdt.push_back(mt);
        m_localfiles.Add(fn.GetName().Lower());

        wxStringTokenizer tk(name, _T("."));
        wxString file = tk.GetNextToken().MakeLower();
        m_update_data[std::string(file.mbc_str())] = mt.GetTicks();
      }
    }
    allFiles->Clear();
    wxDELETE(allFiles);
    SaveUpdateData();
  } else {
    LoadUpdateData();
  }
}

bool ChartSource::UpdateDataExists() {
  return wxFileExists(GetDir() + wxFileName::GetPathSeparator() +
                      _T(UPDATE_DATA_FILENAME));
}

void ChartSource::LoadUpdateData() {
  m_update_data.clear();
  wxString fn =
      GetDir() + wxFileName::GetPathSeparator() + _T(UPDATE_DATA_FILENAME);

  if (!wxFileExists(fn)) return;

  std::ifstream infile(fn.mb_str());

  std::string key;
  long value;

  while (infile >> key >> value) m_update_data[key] = value;

  infile.close();
}

void ChartSource::SaveUpdateData() {
  wxString fn;
  fn = GetDir() + wxFileName::GetPathSeparator() + _T(UPDATE_DATA_FILENAME);

#ifdef __OCPN__ANDROID__
  fn = AndroidGetCacheDir() + wxFileName::GetPathSeparator() +
       _T(UPDATE_DATA_FILENAME);
#endif

  std::ofstream outfile(fn.mb_str());
  if (!outfile.is_open()) return;

  std::map<std::string, time_t>::iterator iter;
  for (iter = m_update_data.begin(); iter != m_update_data.end(); ++iter) {
    if (iter->first.find(" ") == std::string::npos)
      if (!iter->first.empty())
        outfile << iter->first << " " << iter->second << "\n";
  }

  outfile.close();

#ifdef __OCPN__ANDROID__
  AndroidSecureCopyFile(
      fn, GetDir() + wxFileName::GetPathSeparator() + _T(UPDATE_DATA_FILENAME));
#endif
}

void ChartSource::ChartUpdated(wxString chart_number, time_t timestamp) {
  m_update_data[std::string(chart_number.Lower().mb_str())] = timestamp;
  SaveUpdateData();
}

bool ChartDldrPanelImpl::DownloadChart(wxString url, wxString file,
                                       wxString title) {
  return false;
}

void ChartDldrPanelImpl::DisableForDownload(bool enabled) {
  m_bAddSource->Enable(enabled);
  m_bDeleteSource->Enable(enabled);
  m_bEditSource->Enable(enabled);
  m_bUpdateAllCharts->Enable(enabled);
  m_bUpdateChartList->Enable(enabled);
  m_lbChartSources->Enable(enabled);
#if defined(CHART_LIST)
  m_bSelectNew->Enable(enabled);
  m_bSelectUpdated->Enable(enabled);
  m_bSelectAll->Enable(enabled);
#endif /* CHART_LIST */
}

void ChartDldrPanelImpl::OnDownloadCharts(wxCommandEvent &event) {
  if (DownloadIsCancel) {
    cancelled = true;
    return;
  }
  DownloadCharts();
}
#if defined(CHART_LIST)
void ChartDldrPanelImpl::OnSelectChartItem(wxCommandEvent &event) {
  if (!m_bInfoHold)
    SetChartInfo(wxString::Format(
        _("%lu charts total, %lu updated, %lu new, %lu selected"),
        pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
        GetCheckedChartCount()));
  else
    event.Skip();
}
#endif /* CHART_LIST */
#if defined(CHART_LIST)
void ChartDldrPanelImpl::OnSelectNewCharts(wxCommandEvent &event) {
  CheckNewCharts(true);
}
#endif /* CHART_LIST */

#if defined(CHART_LIST)
void ChartDldrPanelImpl::OnSelectUpdatedCharts(wxCommandEvent &event) {
  CheckUpdatedCharts(true);
}
#endif /* CHART_LIST */

#if defined(CHART_LIST)
void ChartDldrPanelImpl::OnSelectAllCharts(wxCommandEvent &event) {
  if (m_bSelectAll->GetLabel() == _("Select All")) {
    CheckAllCharts(true);
    m_bSelectAll->SetLabel(_("Select None"));
    m_bSelectAll->SetToolTip(_("De-select all charts in the list."));
  } else {
    CheckAllCharts(false);
    m_bSelectAll->SetLabel(_("Select All"));
    m_bSelectAll->SetToolTip(_("Select all charts in the list."));
  }
}
#endif /* CHART_LIST */

int ChartDldrPanelImpl::GetChartCount() {
#if defined(CHART_LIST)
  return getChartList()->GetItemCount();
#else
  return m_panelArray.size();
#endif /* CHART_LIST*/
}

int ChartDldrPanelImpl::GetCheckedChartCount() {
#if defined(CHART_LIST)
  int cnt = 0;
  int chartCnt = GetChartCount();
  for (int i = 0; i < chartCnt; i++)
    if (isChartChecked(i)) cnt++;
#else
  int cnt = 0;
  for (int i = 0; i < GetChartCount(); i++) {
    if (m_panelArray.at(i)->GetCB()->IsChecked()) cnt++;
  }
#endif /* CHART_LIST*/
  return cnt;
}

bool ChartDldrPanelImpl::isChartChecked(int i) {
  wxASSERT_MSG(i >= 0,
               wxT("This function should be called with non-negative index."));
  if (i <= GetChartCount())
#if defined(CHART_LIST)
    return getChartList()->GetToggleValue(i, 0);
#else
    return m_panelArray.at(i)->GetCB()->IsChecked();
#endif /* CHART_LIST*/
  else
    return false;
}

void ChartDldrPanelImpl::CheckAllCharts(bool value) {
#if defined(CHART_LIST)
  m_bInfoHold = true;
#endif /* CHART_LIST */

  for (int i = 0; i < GetChartCount(); i++) {
#if defined(CHART_LIST)
    getChartList()->SetToggleValue(value, i, 0);
#else
    m_panelArray.at(i)->GetCB()->SetValue(value);
#endif /* CHART_LIST*/
  }
#if defined(CHART_LIST)
  SetChartInfo(wxString::Format(
      _("%lu charts total, %lu updated, %lu new, %lu selected"),
      pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
      GetCheckedChartCount()));
  m_bInfoHold = false;
#endif /* CHART_LIST */
}

void ChartDldrPanelImpl::CheckNewCharts(bool value) {
  for (int i = 0; i < GetChartCount(); i++) {
#if defined(CHART_LIST)
    if (isNew(i)) getChartList()->SetToggleValue(true, i, 0);
#else
    if (m_panelArray.at(i)->isNew())
      m_panelArray.at(i)->GetCB()->SetValue(value);
#endif /* CHART_LIST*/
  }
#if defined(CHART_LIST)
  SetChartInfo(wxString::Format(
      _("%lu charts total, %lu updated, %lu new, %lu selected"),
      pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
      GetCheckedChartCount()));
#endif /* CHART_LIST */
}

void ChartDldrPanelImpl::CheckUpdatedCharts(bool value) {
  for (int i = 0; i < GetChartCount(); i++) {
#if defined(CHART_LIST)
    if (isUpdated(i)) getChartList()->SetToggleValue(value, i, 0);
#else
    if (m_panelArray.at(i)->isUpdated())
      m_panelArray.at(i)->GetCB()->SetValue(value);
#endif /* CHART_LIST */
  }
#if defined(CHART_LIST)
  SetChartInfo(wxString::Format(
      _("%lu charts total, %lu updated, %lu new, %lu selected"),
      pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
      GetCheckedChartCount()));
#endif /* CHART_LIST */
}

void ChartDldrPanelImpl::InvertCheckAllCharts() {
#if defined(CHART_LIST)
  m_bInfoHold = true;
#endif /* CHART_LIST */
  for (int i = 0; i < GetChartCount(); i++)
#if defined(CHART_LIST)
    getChartList()->SetToggleValue(!isChartChecked(i), i, 0);
#else
    m_panelArray.at(i)->GetCB()->SetValue(!isChartChecked(i));
#endif /* CHART_LIST */
#if defined(CHART_LIST)
  m_bInfoHold = false;
  SetChartInfo(wxString::Format(
      _("%lu charts total, %lu updated, %lu new, %lu selected"),
      pPlugIn->m_pChartCatalog.charts.size(), m_updatedCharts, m_newCharts,
      GetCheckedChartCount()));
#endif /* CHART_LIST */
}

void ChartDldrPanelImpl::DownloadCharts() {
  if (!m_bconnected) {
    Connect(
        wxEVT_DOWNLOAD_EVENT,
        (wxObjectEventFunction)(wxEventFunction)&ChartDldrPanelImpl::onDLEvent);
    m_bconnected = true;
  }

  if (!GetCheckedChartCount() && !updatingAll) {
    OCPNMessageBox_PlugIn(this, _("No charts selected for download."));
    return;
  }
  std::unique_ptr<ChartSource> &cs =
      pPlugIn->m_ChartSources.at(GetSelectedCatalog());

  cancelled = false;
  to_download = GetCheckedChartCount();
  m_downloading = 0;
  m_failed_downloads = 0;
  DisableForDownload(false);
  // wxString old_label = m_bDnldCharts->GetLabel();     // Broken on Android??
  m_bDnldCharts->SetLabel(_("Abort download"));
  DownloadIsCancel = true;

  wxFileName downloaded_p;
  int idx = -1;

  for (int i = 0; i < GetChartCount() && to_download; i++) {
    int index = i;
    if (cancelled) break;
    // Prepare download queues
    if (!isChartChecked(i)) continue;
    m_bTransferComplete = false;
    m_bTransferSuccess = true;
    m_totalsize = _("Unknown");
    m_transferredsize = _T("0");
    m_downloading++;
    if (pPlugIn->m_pChartCatalog.charts.at(index)->NeedsManualDownload()) {
      if (wxID_YES ==
          OCPNMessageBox_PlugIn(
              this,
              wxString::Format(
                  _("The selected chart '%s' can't be downloaded automatically, do you want me to open a browser window and download them manually?\n\n \
After downloading the charts, please extract them to %s"),
                  pPlugIn->m_pChartCatalog.charts.at(index)->title.c_str(),
                  pPlugIn->m_pChartSource->GetDir().c_str()),
              _("Chart Downloader"), wxYES_NO | wxCENTRE | wxICON_QUESTION)) {
        wxLaunchDefaultBrowser(
            pPlugIn->m_pChartCatalog.charts.at(index)->GetManualDownloadUrl());
      }
      continue;
    }

    // download queue
    wxURI url(pPlugIn->m_pChartCatalog.charts.at(index)->GetDownloadLocation());
    if (url.IsReference()) {
      OCPNMessageBox_PlugIn(
          this,
          wxString::Format(
              _("Error, the URL to the chart (%s) data seems wrong."),
              url.BuildURI().c_str()),
          _("Error"));
      this->Enable();
      /// XXX undo anything? return or break?
      return;
    }
    // construct local file path
    wxString file =
        pPlugIn->m_pChartCatalog.charts.at(index)->GetChartFilename();
    wxFileName fn;
    fn.SetFullName(file);
    fn.SetPath(cs->GetDir());
    wxString path = fn.GetFullPath();
    if (wxFileExists(path)) wxRemoveFile(path);
    wxString title = pPlugIn->m_pChartCatalog.charts.at(index)->GetChartTitle();

    //  Ready to start download
#ifdef __OCPN__ANDROID__
    wxString file_path = _T("file://") + fn.GetFullPath();
#else
    wxString file_path = fn.GetFullPath();
#endif

    long handle;
    OCPN_downloadFileBackground(url.BuildURI(), file_path, this, &handle);

    if (idx >= 0) {
      if (pPlugIn->ProcessFile(
              downloaded_p.GetFullPath(), downloaded_p.GetPath(), true,
              pPlugIn->m_pChartCatalog.charts.at(idx)->GetUpdateDatetime())) {
        cs->ChartUpdated(pPlugIn->m_pChartCatalog.charts.at(idx)->number,
                         pPlugIn->m_pChartCatalog.charts.at(idx)
                             ->GetUpdateDatetime()
                             .GetTicks());
      } else {
        m_failed_downloads++;
      }
      idx = -1;
    }

    while (!m_bTransferComplete && m_bTransferSuccess && !cancelled) {
      if (m_failed_downloads)
        SetChartInfo(wxString::Format(
            _("Downloading chart %u of %u, %u downloads failed (%s / %s)"),
            m_downloading, to_download, m_failed_downloads,
            m_transferredsize.c_str(), m_totalsize.c_str()));
      else
        SetChartInfo(wxString::Format(
            _("Downloading chart %u of %u (%s / %s)"), m_downloading,
            to_download, m_transferredsize.c_str(), m_totalsize.c_str()));

      // if(g_pi && g_pi->m_dldrpanel)
      // g_pi->m_dldrpanel->Raise();
      wxYield();
      wxMilliSleep(30);
    }

    if (cancelled) {
      idx = -1;
      OCPN_cancelDownloadFileBackground(handle);
    }

    if (m_bTransferSuccess && !cancelled) {
      idx = index;
      downloaded_p = path;
    } else {
      idx = -1;
      if (wxFileExists(path)) wxRemoveFile(path);
      m_failed_downloads++;
    }
  }
  if (idx >= 0) {
    if (pPlugIn->ProcessFile(
            downloaded_p.GetFullPath(), downloaded_p.GetPath(), true,
            pPlugIn->m_pChartCatalog.charts.at(idx)->GetUpdateDatetime())) {
      cs->ChartUpdated(pPlugIn->m_pChartCatalog.charts.at(idx)->number,
                       pPlugIn->m_pChartCatalog.charts.at(idx)
                           ->GetUpdateDatetime()
                           .GetTicks());
    } else {
      m_failed_downloads++;
    }
  }
  DisableForDownload(true);
  m_bDnldCharts->SetLabel(_("Download selected charts"));
  DownloadIsCancel = false;
  SetSource(GetSelectedCatalog());
  if (m_failed_downloads > 0 && !updatingAll && !cancelled)
    OCPNMessageBox_PlugIn(
        this,
        wxString::Format(_("%d out of %d charts failed to download.\nCheck the "
                           "list, verify there is a working Internet "
                           "connection and repeat the operation if needed."),
                         m_failed_downloads, m_downloading),
        _("Chart Downloader"), wxOK | wxICON_ERROR);

  if (cancelled)
    OCPNMessageBox_PlugIn(this, _("Chart download cancelled."),
                          _("Chart Downloader"), wxOK | wxICON_INFORMATION);

  if ((m_downloading - m_failed_downloads > 0) && !updatingAll)
    ForceChartDBUpdate();
}

ChartDldrPanelImpl::~ChartDldrPanelImpl() {
  Disconnect(
      wxEVT_DOWNLOAD_EVENT,
      (wxObjectEventFunction)(wxEventFunction)&ChartDldrPanelImpl::onDLEvent);
  m_bconnected = false;

#ifndef __OCPN__ANDROID__
  OCPN_cancelDownloadFileBackground(
      0);  // Stop the thread, is something like this needed on Android as well?
#endif
#if defined(CHART_LIST)
  clearChartList();
#endif /* CHART_LIST */
}

ChartDldrPanelImpl::ChartDldrPanelImpl(chartdldr_pi *plugin, wxWindow *parent,
                                       wxWindowID id, const wxPoint &pos,
                                       const wxSize &size, long style)
    : ChartDldrPanel(parent, id, pos, size, style) {
  m_bDeleteSource->Disable();
  m_bUpdateChartList->Disable();
  m_bEditSource->Disable();
  m_lbChartSources->InsertColumn(0, _("Catalog"), wxLIST_FORMAT_LEFT,
                                 CATALOGS_NAME_WIDTH);
  m_lbChartSources->InsertColumn(1, _("Released"), wxLIST_FORMAT_LEFT,
                                 CATALOGS_DATE_WIDTH);
  m_lbChartSources->InsertColumn(2, _("Local path"), wxLIST_FORMAT_LEFT,
                                 CATALOGS_PATH_WIDTH);
  m_lbChartSources->Enable();
  m_bInfoHold = false;
  downloadInProgress = false;
  cancelled = true;
  to_download = -1;
  m_downloading = -1;
  updatingAll = false;
  pPlugIn = plugin;
  m_populated = false;
  DownloadIsCancel = false;
  m_failed_downloads = 0;
  SetChartInfo(wxEmptyString);
  m_bTransferComplete = true;
  m_bTransferSuccess = true;

  Connect(
      wxEVT_DOWNLOAD_EVENT,
      (wxObjectEventFunction)(wxEventFunction)&ChartDldrPanelImpl::onDLEvent);
  m_bconnected = true;

  for (size_t i = 0; i < pPlugIn->m_ChartSources.size(); i++) {
    AppendCatalog(pPlugIn->m_ChartSources.at(i));
  }
  m_populated = true;
}

void ChartDldrPanelImpl::OnPaint(wxPaintEvent &event) {
  if (!m_populated) {
    m_populated = true;
    for (size_t i = 0; i < pPlugIn->m_ChartSources.size(); i++) {
      AppendCatalog(pPlugIn->m_ChartSources.at(i));
    }
  }
#ifdef __WXMAC__
  // Mojave does not paint the controls correctly without this.
  m_lbChartSources->Refresh(true);
#endif
  event.Skip();
}

void ChartDldrPanelImpl::DeleteSource(wxCommandEvent &event) {
  if (!m_lbChartSources->GetSelectedItemCount()) return;
  if (wxID_YES != OCPNMessageBox_PlugIn(
                      this,
                      _("Do you really want to remove the chart source?\nThe "
                        "local chart files will not be removed,\nbut you will "
                        "not be able to update the charts anymore."),
                      _("Chart Downloader"), wxYES_NO | wxCENTRE))
    return;
  int ToBeRemoved = GetSelectedCatalog();
  m_lbChartSources->SetItemState(ToBeRemoved, 0,
                                 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
  pPlugIn->m_ChartSources.erase(pPlugIn->m_ChartSources.begin() + ToBeRemoved);
  m_lbChartSources->DeleteItem(ToBeRemoved);
  CleanForm();
  pPlugIn->SetSourceId(-1);
  SelectCatalog(-1);
  pPlugIn->SaveConfig();
  event.Skip();
}

void ChartDldrPanelImpl::AddSource(wxCommandEvent &event) {
  ChartDldrGuiAddSourceDlg *dialog = new ChartDldrGuiAddSourceDlg(this);
  dialog->SetBasePath(pPlugIn->GetBaseChartDir());

  wxSize sz = GetParent()
                  ->GetGrandParent()
                  ->GetSize();  // This is the options panel true size
  dialog->SetSize(sz.GetWidth(), sz.GetHeight());
  dialog->Center();

#ifdef __OCPN__ANDROID__
  androidDisableRotation();
#endif

  if (dialog->ShowModal() == wxID_OK) {
    std::unique_ptr<ChartSource> cs =
        std::make_unique<ChartSource>(dialog->m_tSourceName->GetValue(),
                                      dialog->m_tChartSourceUrl->GetValue(),
                                      dialog->m_tcChartDirectory->GetValue());
    dialog->Destroy();
    AppendCatalog(cs);
    bool covered = false;
    for (size_t i = 0; i < GetChartDBDirArrayString().GetCount(); i++) {
      if (cs->GetDir().StartsWith((GetChartDBDirArrayString().Item(i)))) {
        covered = true;
        break;
      }
    }
    if (!covered) {
      wxString dir = cs->GetDir();
      AddChartDirectory(dir);
    }

    long itemSelectedNow = GetSelectedCatalog();
    m_lbChartSources->SetItemState(itemSelectedNow, 0, wxLIST_STATE_SELECTED);

    SelectCatalog(m_lbChartSources->GetItemCount() - 1);
    pPlugIn->m_ChartSources.push_back(std::move(cs));
    pPlugIn->SaveConfig();
  }
#ifdef __OCPN__ANDROID__
  androidEnableRotation();
#endif

  event.Skip();
}

void ChartDldrPanelImpl::DoEditSource() {
  if (!m_lbChartSources->GetSelectedItemCount()) return;
  int cat = GetSelectedCatalog();
  ChartDldrGuiAddSourceDlg *dialog = new ChartDldrGuiAddSourceDlg(this);
  dialog->SetBasePath(pPlugIn->GetBaseChartDir());
  dialog->SetSourceEdit(pPlugIn->m_ChartSources.at(cat));
  dialog->SetTitle(_("Edit Chart Source"));

  dialog->ShowModal();
  int retcode = dialog->GetReturnCode();
  {
    if (retcode == wxID_OK) {
      pPlugIn->m_ChartSources.at(cat)->SetName(
          dialog->m_tSourceName->GetValue());
      pPlugIn->m_ChartSources.at(cat)->SetUrl(
          dialog->m_tChartSourceUrl->GetValue());
      pPlugIn->m_ChartSources.at(cat)->SetDir(
          dialog->m_tcChartDirectory->GetValue());

      m_lbChartSources->SetItem(cat, 0,
                                pPlugIn->m_ChartSources.at(cat)->GetName());
      m_lbChartSources->SetItem(cat, 1, _("(Please update first)"));
      m_lbChartSources->SetItem(cat, 2,
                                pPlugIn->m_ChartSources.at(cat)->GetDir());
      wxURI url(pPlugIn->m_ChartSources.at(cat)->GetUrl());
      wxFileName fn(url.GetPath());
      fn.SetPath(pPlugIn->m_ChartSources.at(cat)->GetDir());
      wxString path = fn.GetFullPath();
      if (wxFileExists(path)) {
        if (pPlugIn->m_pChartCatalog.LoadFromFile(path, true)) {
          m_lbChartSources->SetItem(cat, 0, pPlugIn->m_pChartCatalog.title);
          m_lbChartSources->SetItem(
              cat, 1,
              pPlugIn->m_pChartCatalog.GetReleaseDate().Format(
                  _T("%Y-%m-%d %H:%M")));
          m_lbChartSources->SetItem(cat, 2, path);
        }
      }
      bool covered = false;
      for (size_t i = 0; i < GetChartDBDirArrayString().GetCount(); i++) {
        if (pPlugIn->m_ChartSources.at(cat)->GetDir().StartsWith(
                (GetChartDBDirArrayString().Item(i)))) {
          covered = true;
          break;
        }
      }
      if (!covered)
        OCPNMessageBox_PlugIn(
            this,
            wxString::Format(
                _("Path %s seems not to be covered by your configured Chart "
                  "Directories.\nTo see the charts you have to adjust the "
                  "configuration on the 'Chart Files' tab."),
                pPlugIn->m_ChartSources.at(cat)->GetDir().c_str()),
            _("Chart Downloader"));

      pPlugIn->SaveConfig();
      SetSource(cat);
    }
  }
}

void ChartDldrPanelImpl::EditSource(wxCommandEvent &event) {
  DoEditSource();
  event.Skip();
}

void ChartDldrPanelImpl::OnLeftDClick(wxMouseEvent &event) {
  DoEditSource();
  event.Skip();
}

bool chartdldr_pi::ProcessFile(const wxString &aFile,
                               const wxString &aTargetDir, bool aStripPath,
                               wxDateTime aMTime) {
  if (aFile.Lower().EndsWith(_T("zip")))  // Zip compressed
  {
    bool ret = ExtractZipFiles(aFile, aTargetDir, aStripPath, aMTime, false);
    if (ret)
      wxRemoveFile(aFile);
    else
      wxLogError(_T("chartdldr_pi: Unable to extract: ") + aFile);
    return ret;
  }
#ifdef DLDR_USE_LIBARCHIVE
  else if (aFile.Lower().EndsWith(_T("rar"))) {
#ifdef CHARTDLDR_RAR_UNARR
    bool ret = ExtractUnarrFiles(aFile, aTargetDir, aStripPath, aMTime, false);
#else
    bool ret =
        ExtractLibArchiveFiles(aFile, aTargetDir, aStripPath, aMTime, false);
#endif
    if (ret)
      wxRemoveFile(aFile);
    else
      wxLogError(_T("chartdldr_pi: Unable to extract: ") + aFile);
    return ret;
  } else if (aFile.Lower().EndsWith(_T("tar")) ||
             aFile.Lower().EndsWith(_T("gz")) ||
             aFile.Lower().EndsWith(_T("bz2")) ||
             aFile.Lower().EndsWith(_T("lzma")) ||
             aFile.Lower().EndsWith(_T("7z")) ||
             aFile.Lower().EndsWith(_T("xz"))) {
    bool ret =
        ExtractLibArchiveFiles(aFile, aTargetDir, aStripPath, aMTime, false);
    if (ret)
      wxRemoveFile(aFile);
    else
      wxLogError(_T("chartdldr_pi: Unable to extract: ") + aFile);
    return ret;
  }
#else
  else if (aFile.Lower().EndsWith(_T("rar")) ||
           aFile.Lower().EndsWith(_T("tar"))
#ifdef HAVE_BZIP2
           || aFile.Lower().EndsWith(_T("bz2"))
#endif
#ifdef HAVE_ZLIB
           || aFile.Lower().EndsWith(_T("gz"))
#endif
#ifdef HAVE_7Z
           || aFile.Lower().EndsWith(
                  _T("7z"))  // TODO: Could it actually extract more formats the
                             // LZMA SDK supports?
#endif
  ) {
    bool ret = ExtractUnarrFiles(aFile, aTargetDir, aStripPath, aMTime, false);
    if (ret)
      wxRemoveFile(aFile);
    else
      wxLogError(_T("chartdldr_pi: Unable to extract: ") + aFile);
    return ret;
  }
#endif

#ifdef __OCPN__ANDROID__
  else if (aFile.Lower().EndsWith(_T("tar")) ||
           aFile.Lower().EndsWith(_T("gz")) ||
           aFile.Lower().EndsWith(_T("bz2")) ||
           aFile.Lower().EndsWith(_T("lzma")) ||
           aFile.Lower().EndsWith(_T("7z")) ||
           aFile.Lower().EndsWith(_T("xz"))) {
    int nStrip = 0;
    if (aStripPath) nStrip = 1;

    if (m_dldrpanel) m_dldrpanel->SetChartInfo(_("Installing charts."));

    androidShowBusyIcon();
    bool ret = AndroidUnzip(aFile, aTargetDir, nStrip, true);
    androidHideBusyIcon();

    return ret;
  }
#endif

  else  // Uncompressed
  {
    wxFileName fn(aFile);
    if (fn.GetPath() != aTargetDir)  // We have to move the file somewhere
    {
      if (!wxDirExists(aTargetDir)) {
        if (wxFileName::Mkdir(aTargetDir, 0755, wxPATH_MKDIR_FULL)) {
          if (!wxRenameFile(aFile, aTargetDir)) return false;
        } else
          return false;
      }
    }
    wxString name = fn.GetFullName();
    fn.Clear();
    fn.Assign(aTargetDir, name);
    fn.SetTimes(&aMTime, &aMTime, &aMTime);
  }
  return true;
}

#ifdef DLDR_USE_LIBARCHIVE
#ifndef __OCPN__ANDROID__
static int copy_data(struct archive *ar, struct archive *aw) {
  int r;
  const void *buff;
  size_t size;
  __LA_INT64_T offset;

  for (;;) {
    r = archive_read_data_block(ar, &buff, &size, &offset);
    if (r == ARCHIVE_EOF) return (ARCHIVE_OK);
    if (r < ARCHIVE_OK) return (r);
    r = archive_write_data_block(aw, buff, size, offset);
    if (r < ARCHIVE_OK) {
      // fprintf(stderr, "%s\n", archive_error_string(aw));
      wxLogError(wxString::Format("Chartdldr_pi: LibArchive error: %s",
                                  archive_error_string(aw)));
      return (r);
    }
  }
}
#endif

bool chartdldr_pi::ExtractLibArchiveFiles(const wxString &aArchiveFile,
                                          const wxString &aTargetDir,
                                          bool aStripPath, wxDateTime aMTime,
                                          bool aRemoveArchive) {
#ifndef __OCPN__ANDROID__
  struct archive *a;
  struct archive *ext;
  struct archive_entry *entry;
  int flags;
  int r;

  /* Select which attributes we want to restore. */
  flags = ARCHIVE_EXTRACT_TIME;
  /*
  flags |= ARCHIVE_EXTRACT_PERM;
  flags |= ARCHIVE_EXTRACT_ACL;
  flags |= ARCHIVE_EXTRACT_FFLAGS;
  */

  a = archive_read_new();
  archive_read_support_format_all(a);
  archive_read_support_filter_all(a);
  archive_read_support_compression_all(a);
  ext = archive_write_disk_new();
  archive_write_disk_set_options(ext, flags);
  archive_write_disk_set_standard_lookup(ext);
  if ((r = archive_read_open_filename(a, aArchiveFile.c_str(), 10240))) {
    return false;
  }
  for (;;) {
    r = archive_read_next_header(a, &entry);
    if (r == ARCHIVE_EOF) break;
    if (r < ARCHIVE_OK)
      // fprintf(stderr, "%s\n", archive_error_string(a));
      wxLogError(wxString::Format("Chartdldr_pi: LibArchive error: %s",
                                  archive_error_string(a)));
    if (r < ARCHIVE_WARN) return false;
    if (aStripPath) {
      const char *currentFile = archive_entry_pathname(entry);
      std::string fullOutputPath = currentFile;
      size_t sep = fullOutputPath.find_last_of("\\/");
      if (sep != std::string::npos)
        fullOutputPath =
            fullOutputPath.substr(sep + 1, fullOutputPath.size() - sep - 1);
      archive_entry_set_pathname(entry, fullOutputPath.c_str());
    }
    if (aTargetDir != wxEmptyString) {
      const char *currentFile = archive_entry_pathname(entry);
      const std::string fullOutputPath =
          aTargetDir.ToStdString() +
          wxString(wxFileName::GetPathSeparator()).ToStdString() + currentFile;
      archive_entry_set_pathname(entry, fullOutputPath.c_str());
    }
    r = archive_write_header(ext, entry);
    if (r < ARCHIVE_OK)
      // fprintf(stderr, "%s\n", archive_error_string(ext));
      wxLogError(wxString::Format("Chartdldr_pi: LibArchive error: %s",
                                  archive_error_string(ext)));
    else if (archive_entry_size(entry) > 0) {
      r = copy_data(a, ext);
      if (r < ARCHIVE_OK)
        // fprintf(stderr, "%s\n", archive_error_string(ext));
        wxLogError(wxString::Format("Chartdldr_pi: LibArchive error: %s",
                                    archive_error_string(ext)));
      if (r < ARCHIVE_WARN) return false;
    }
    r = archive_write_finish_entry(ext);

    if (r < ARCHIVE_OK)
      // fprintf(stderr, "%s\n", archive_error_string(ext));
      wxLogError(wxString::Format("Chartdldr_pi: LibArchive error: %s",
                                  archive_error_string(ext)));
    if (r < ARCHIVE_WARN) return false;
  }
  archive_read_close(a);
  // archive_read_free(a);
  archive_write_close(ext);
  // archive_write_free(ext);

  if (aRemoveArchive) wxRemoveFile(aArchiveFile);
  return true;

#else

  return rv;

#endif  // Android
}
#endif

#if defined(CHARTDLDR_RAR_UNARR) || !defined(DLDR_USE_LIBARCHIVE)
ar_archive *ar_open_any_archive(ar_stream *stream, const char *fileext) {
  ar_archive *ar = ar_open_rar_archive(stream);
  if (!ar)
    ar =
        ar_open_zip_archive(stream, fileext && (strcmp(fileext, ".xps") == 0 ||
                                                strcmp(fileext, ".epub") == 0));
  if (!ar) ar = ar_open_7z_archive(stream);
  if (!ar) ar = ar_open_tar_archive(stream);
  return ar;
}

bool chartdldr_pi::ExtractUnarrFiles(const wxString &aRarFile,
                                     const wxString &aTargetDir,
                                     bool aStripPath, wxDateTime aMTime,
                                     bool aRemoveRar) {
  ar_stream *stream = NULL;
  ar_archive *ar = NULL;
  int entry_count = 1;
  int entry_skips = 0;
  int error_step = 1;
  bool ret = true;

  stream = ar_open_file(aRarFile.c_str());
  if (!stream) {
    wxLogError(_T("Can not open file '") + aRarFile + _T("'."));
    ar_close_archive(ar);
    ar_close(stream);
    return false;
  }
  ar = ar_open_any_archive(stream, strrchr(aRarFile.c_str(), '.'));
  if (!ar) {
    wxLogError(_T("Can not open archive '") + aRarFile + _T("'."));
    ar_close_archive(ar);
    ar_close(stream);
    return false;
  }
  while (ar_parse_entry(ar)) {
    size_t size = ar_entry_get_size(ar);
    wxString name = ar_entry_get_name(ar);
    if (aStripPath) {
      wxFileName fn(name);
      /* We can completly replace the entry path */
      // fn.SetPath(aTargetDir);
      // name = fn.GetFullPath();
      /* Or only remove the first dir (eg. ENC_ROOT) */
      if (fn.GetDirCount() > 0) {
        fn.RemoveDir(0);
        name = aTargetDir + wxFileName::GetPathSeparator() + fn.GetFullPath();
      } else {
        name = aTargetDir + wxFileName::GetPathSeparator() + name;
      }
    }
    wxFileName fn(name);
    if (!fn.DirExists()) {
      if (!wxFileName::Mkdir(fn.GetPath())) {
        wxLogError(_T("Can not create directory '") + fn.GetPath() + _T("'."));
        ret = false;
        break;
      }
    }
    wxFileOutputStream file(name);
    if (!file) {
      wxLogError(_T("Can not create file '") + name + _T("'."));
      ret = false;
      break;
    }
    while (size > 0) {
      unsigned char buffer[1024];
      size_t count = size < sizeof(buffer) ? size : sizeof(buffer);
      if (!ar_entry_uncompress(ar, buffer, count)) break;
      file.Write(buffer, count);
      size -= count;
    }
    file.Close();
    fn.SetTimes(&aMTime, &aMTime, &aMTime);
    if (size > 0) {
      wxLogError("Warning: Failed to uncompress... skipping");
      entry_skips++;
      ret = false;
    }
  }
  if (!ar_at_eof(ar)) {
    wxLogError("Error: Failed to parse entry %d!", entry_count);
    ret = false;
  }
  ar_close_archive(ar);
  ar_close(stream);

  if (aRemoveRar) wxRemoveFile(aRarFile);

#ifdef _UNIX
  // reset LC_NUMERIC locale, some locales use a comma for decimal point
  // and it corrupts navobj.xml file
  setlocale(LC_NUMERIC, "C");
#endif

  return ret;
}
#endif

bool chartdldr_pi::ExtractZipFiles(const wxString &aZipFile,
                                   const wxString &aTargetDir, bool aStripPath,
                                   wxDateTime aMTime, bool aRemoveZip) {
  bool ret = true;

#ifdef __OCPN__ANDROID__
  int nStrip = 0;
  if (aStripPath) nStrip = 1;

  ret = AndroidUnzip(aZipFile, aTargetDir, nStrip, true);
#else
  std::unique_ptr<wxZipEntry> entry(new wxZipEntry());

  do {
    wxLogMessage(_T("chartdldr_pi: Going to extract '") + aZipFile + _T("'."));
    wxFileInputStream in(aZipFile);

    if (!in) {
      wxLogMessage(_T("Can not open file '") + aZipFile + _T("'."));
      ret = false;
      break;
    }
    wxZipInputStream zip(in);
    ret = false;

    while (entry.reset(zip.GetNextEntry()), entry.get() != NULL) {
      // access meta-data
      wxString name = entry->GetName();
      if (aStripPath) {
        wxFileName fn(name);
        /* We can completly replace the entry path */
        // fn.SetPath(aTargetDir);
        // name = fn.GetFullPath();
        /* Or only remove the first dir (eg. ENC_ROOT) */
        if (fn.GetDirCount() > 0) fn.RemoveDir(0);
        name = aTargetDir + wxFileName::GetPathSeparator() + fn.GetFullPath();
      } else {
        name = aTargetDir + wxFileName::GetPathSeparator() + name;
      }

      // read 'zip' to access the entry's data
      if (entry->IsDir()) {
        int perm = entry->GetMode();
        if (!wxFileName::Mkdir(name, perm, wxPATH_MKDIR_FULL)) {
          wxLogMessage(_T("Can not create directory '") + name + _T("'."));
          ret = false;
          break;
        }
      } else {
        if (!zip.OpenEntry(*entry.get())) {
          wxLogMessage(_T("Can not open zip entry '") + entry->GetName() +
                       _T("'."));
          ret = false;
          break;
        }
        if (!zip.CanRead()) {
          wxLogMessage(_T("Can not read zip entry '") + entry->GetName() +
                       _T("'."));
          ret = false;
          break;
        }

        wxFileName fn(name);
        if (!fn.DirExists()) {
          if (!wxFileName::Mkdir(fn.GetPath())) {
            wxLogMessage(_T("Can not create directory '") + fn.GetPath() +
                         _T("'."));
            ret = false;
            break;
          }
        }

        wxFileOutputStream file(name);

        if (!file) {
          wxLogMessage(_T("Can not create file '") + name + _T("'."));
          ret = false;
          break;
        }
        zip.Read(file);
        fn.SetTimes(&aMTime, &aMTime, &aMTime);
        ret = true;
      }
    }

  } while (false);

  if (aRemoveZip) wxRemoveFile(aZipFile);
#endif  // Android

  return ret;
}

ChartDldrGuiAddSourceDlg::ChartDldrGuiAddSourceDlg(wxWindow *parent)
    : AddSourceDlg(parent) {
  wxFileName fn;
  fn.SetPath(*GetpSharedDataLocation());
  fn.AppendDir(_T("plugins"));
  fn.AppendDir(_T("chartdldr_pi"));
  fn.AppendDir(_T("data"));

  int w = 16;  // default for desktop
  int h = 16;

#ifdef __OCPN__ANDROID__
  w = 6 * g_androidDPmm;  // mm nominal size
  h = w;

  p_buttonIconList = new wxImageList(w, h);

  fn.SetFullName(_T("button_right.png"));
  wxImage im1(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  im1.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_buttonIconList->Add(im1);

  fn.SetFullName(_T("button_right.png"));
  wxImage im2(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  im2.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_buttonIconList->Add(im2);

  fn.SetFullName(_T("button_down.png"));
  wxImage im3(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  im3.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_buttonIconList->Add(im3);

  fn.SetFullName(_T("button_down.png"));
  wxImage im4(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  im4.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_buttonIconList->Add(im4);

  m_treeCtrlPredefSrcs->AssignButtonsImageList(p_buttonIconList);
#else
  p_iconList = new wxImageList(w, h);

  fn.SetFullName(_T("folder.png"));
  wxImage ima(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  ima.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_iconList->Add(ima);

  fn.SetFullName(_T("file.png"));
  wxImage imb(fn.GetFullPath(), wxBITMAP_TYPE_PNG);
  imb.Rescale(w, h, wxIMAGE_QUALITY_HIGH);
  p_iconList->Add(imb);

  m_treeCtrlPredefSrcs->AssignImageList(p_iconList);
#endif /* __OCPN__ANDROID__ */

  m_treeCtrlPredefSrcs->SetIndent(w);

  m_base_path = wxEmptyString;
  m_last_path = wxEmptyString;
  LoadSources();
  m_nbChoice->SetSelection(0);
  // m_treeCtrlPredefSrcs->ExpandAll();

  Fit();

  applyStyle();
}

bool ChartDldrGuiAddSourceDlg::LoadSources() {
  wxTreeItemId tree = m_treeCtrlPredefSrcs->AddRoot(_T("root"));

  wxFileName fn;
  fn.SetPath(*GetpPrivateApplicationDataLocation());
  fn.SetFullName(_T("chartdldr_pi-chart_sources.xml"));
  if (!fn.FileExists()) {
    fn.SetPath(*GetpSharedDataLocation());
    fn.AppendDir(_T("plugins"));
    fn.AppendDir(_T("chartdldr_pi"));
    fn.AppendDir(_T("data"));
    fn.SetFullName(_T("chart_sources.xml"));
    if (!fn.FileExists()) {
      wxLogMessage(wxString::Format(
          _T("Error: chartdldr_pi::LoadSources() %s not found!"),
          fn.GetFullPath().c_str()));
      return false;
    }
  }
  wxString path = fn.GetFullPath();

  pugi::xml_document *doc = new pugi::xml_document;
  bool ret = doc->load_file(path.mb_str());
  if (ret) {
    pugi::xml_node root = doc->first_child();

    for (pugi::xml_node element = root.first_child(); element;
         element = element.next_sibling()) {
      if (!strcmp(element.name(), "sections")) {
        LoadSections(tree, element);
      }
    }
  }
  wxDELETE(doc);
  return true;
}

bool ChartDldrGuiAddSourceDlg::LoadSections(const wxTreeItemId &root,
                                            pugi::xml_node &node) {
  for (pugi::xml_node element = node.first_child(); element;
       element = element.next_sibling()) {
    if (!strcmp(element.name(), "section")) {
      LoadSection(root, element);
    }
  }
  return true;
}

bool ChartDldrGuiAddSourceDlg::LoadSection(const wxTreeItemId &root,
                                           pugi::xml_node &node) {
  wxTreeItemId item;
  for (pugi::xml_node element = node.first_child(); element;
       element = element.next_sibling()) {
    if (!strcmp(element.name(), "name")) {
      item = m_treeCtrlPredefSrcs->AppendItem(
          root, wxString::FromUTF8(element.first_child().value()), 0, 0);

      wxFont *pFont = OCPNGetFont(_T("Dialog"), 0);
      if (pFont) m_treeCtrlPredefSrcs->SetItemFont(item, *pFont);
    }
    if (!strcmp(element.name(), "sections")) LoadSections(item, element);
    if (!strcmp(element.name(), "catalogs")) LoadCatalogs(item, element);
  }

  return true;
}

bool ChartDldrGuiAddSourceDlg::LoadCatalogs(const wxTreeItemId &root,
                                            pugi::xml_node &node) {
  for (pugi::xml_node element = node.first_child(); element;
       element = element.next_sibling()) {
    if (!strcmp(element.name(), "catalog")) LoadCatalog(root, element);
  }

  return true;
}

bool ChartDldrGuiAddSourceDlg::LoadCatalog(const wxTreeItemId &root,
                                           pugi::xml_node &node) {
  wxString name, type, location, dir;
  for (pugi::xml_node element = node.first_child(); element;
       element = element.next_sibling()) {
    if (!strcmp(element.name(), "name"))
      name = wxString::FromUTF8(element.first_child().value());
    else if (!strcmp(element.name(), "type"))
      type = wxString::FromUTF8(element.first_child().value());
    else if (!strcmp(element.name(), "location"))
      location = wxString::FromUTF8(element.first_child().value());
    else if (!strcmp(element.name(), "dir"))
      dir = wxString::FromUTF8(element.first_child().value());
  }
  ChartSource *cs = new ChartSource(name, location, dir);
  wxTreeItemId id = m_treeCtrlPredefSrcs->AppendItem(root, name, 1, 1, cs);

  wxFont *pFont = OCPNGetFont(_T("Dialog"), 0);
  if (pFont) m_treeCtrlPredefSrcs->SetItemFont(id, *pFont);

  return true;
}

ChartDldrGuiAddSourceDlg::~ChartDldrGuiAddSourceDlg() {}

wxString ChartDldrGuiAddSourceDlg::FixPath(wxString path) {
  wxString sep(wxFileName::GetPathSeparator());
  wxString s = path;
  s.Replace(_T("/"), sep, true);
  s.Replace(_T(USERDATA), m_base_path);
  s.Replace(sep + sep, sep);
  return s;
}

void ChartDldrGuiAddSourceDlg::OnChangeType(wxCommandEvent &event) {
  m_treeCtrlPredefSrcs->Enable(m_nbChoice->GetSelection() == 0);
  m_tSourceName->Enable(m_nbChoice->GetSelection() == 1);
  m_tChartSourceUrl->Enable(m_nbChoice->GetSelection() == 1);
}

void ChartDldrGuiAddSourceDlg::OnSourceSelected(wxTreeEvent &event) {
  wxTreeItemId item = m_treeCtrlPredefSrcs->GetSelection();
  ChartSource *cs = (ChartSource *)(m_treeCtrlPredefSrcs->GetItemData(item));
  if (cs) {
    m_dirExpanded = FixPath(cs->GetDir());

    m_tSourceName->SetValue(cs->GetName());
    m_tChartSourceUrl->SetValue(cs->GetUrl());
    if (m_tcChartDirectory->GetValue() == m_last_path) {
      m_tcChartDirectory->SetValue(FixPath(cs->GetDir()));
      m_panelChartDirectory->SetText(FixPath(cs->GetDir()));

      m_buttonChartDirectory->Enable();
      m_last_path = m_tcChartDirectory->GetValue();
    }
  }
  event.Skip();
}

void ChartDldrGuiAddSourceDlg::SetSourceEdit(std::unique_ptr<ChartSource> &cs) {
  m_nbChoice->SetSelection(1);
  m_tChartSourceUrl->Enable();
  m_treeCtrlPredefSrcs->Disable();
  m_tSourceName->SetValue(cs->GetName());
  m_tChartSourceUrl->SetValue(cs->GetUrl());
  m_tcChartDirectory->SetValue(FixPath(cs->GetDir()));
  m_panelChartDirectory->SetText(FixPath(cs->GetDir()));

  m_buttonChartDirectory->Enable();
}

ChartDldrPrefsDlgImpl::ChartDldrPrefsDlgImpl(wxWindow *parent)
    : ChartDldrPrefsDlg(parent) {}

ChartDldrPrefsDlgImpl::~ChartDldrPrefsDlgImpl() {}

void ChartDldrPrefsDlgImpl::SetPath(const wxString path) {
  // if( !wxDirExists(path) )
  // if( !wxFileName::Mkdir(path, 0755, wxPATH_MKDIR_FULL) )
  //{
  //    OCPNMessageBox_PlugIn(this, wxString::Format(_("Directory %s can't be
  //    created."), m_dpDefaultDir->GetTextCtrlValue().c_str()), _("Chart
  //    Downloader")); return;
  //}
  m_tcDefaultDir->SetValue(path);
}

void ChartDldrPrefsDlgImpl::GetPreferences(bool &preselect_new,
                                           bool &preselect_updated,
                                           bool &bulk_update) {
  preselect_new = m_cbSelectNew->GetValue();
  preselect_updated = m_cbSelectUpdated->GetValue();
  bulk_update = m_cbBulkUpdate->GetValue();
}
void ChartDldrPrefsDlgImpl::SetPreferences(bool preselect_new,
                                           bool preselect_updated,
                                           bool bulk_update) {
  m_cbSelectNew->SetValue(preselect_new);
  m_cbSelectUpdated->SetValue(preselect_updated);
  m_cbBulkUpdate->SetValue(bulk_update);
}

void ChartDldrGuiAddSourceDlg::OnOkClick(wxCommandEvent &event) {
  wxString msg = wxEmptyString;

  if (m_nbChoice->GetSelection() == 0) {
    wxTreeItemId item = m_treeCtrlPredefSrcs->GetSelection();
    if (m_treeCtrlPredefSrcs->GetSelection().IsOk()) {
      ChartSource *cs =
          (ChartSource *)(m_treeCtrlPredefSrcs->GetItemData(item));
      if (!cs)
        msg +=
            _("You must select one of the predefined chart sources or create "
              "one of your own.\n");
    } else
      msg +=
          _("You must select one of the predefined chart sources or create one "
            "of your own.\n");
  }
  if (m_nbChoice->GetSelection() == 1 &&
      m_tSourceName->GetValue() == wxEmptyString)
    msg += _("The chart source must have a name.\n");
  wxURI url(m_tChartSourceUrl->GetValue());
  if (m_nbChoice->GetSelection() == 1 &&
      (m_tChartSourceUrl->GetValue() == wxEmptyString ||
       !ValidateUrl(m_tChartSourceUrl->GetValue())))
    msg += _("The chart source must have a valid URL.\n");
  if (m_tcChartDirectory->GetValue() == wxEmptyString)
    msg += _("You must select a local folder to store the charts.\n");
  else if (!wxDirExists(m_tcChartDirectory->GetValue()))
    if (!wxFileName::Mkdir(m_tcChartDirectory->GetValue(), 0755,
                           wxPATH_MKDIR_FULL))
      msg += wxString::Format(_("Directory %s can't be created."),
                              m_tcChartDirectory->GetValue().c_str()) +
             _T("\n");

  if (msg != wxEmptyString)
    OCPNMessageBox_PlugIn(this, msg, _("Chart source definition problem"),
                          wxOK | wxCENTRE | wxICON_ERROR);
  else {
    event.Skip();
    SetReturnCode(wxID_OK);
    EndModal(wxID_OK);
  }
}

void ChartDldrGuiAddSourceDlg::OnCancelClick(wxCommandEvent &event) {
  SetReturnCode(wxID_CANCEL);
  EndModal(wxID_CANCEL);
}

void ChartDldrPrefsDlgImpl::OnOkClick(wxCommandEvent &event) {
  if (!wxDirExists(m_tcDefaultDir->GetValue())) {
    if (!wxFileName::Mkdir(m_tcDefaultDir->GetValue(), 0755,
                           wxPATH_MKDIR_FULL)) {
      OCPNMessageBox_PlugIn(
          this,
          wxString::Format(_("Directory %s can't be created."),
                           m_tcDefaultDir->GetValue().c_str()),
          _("Chart Downloader"));
      return;
    }
  }

  if (g_pi) {
    g_pi->UpdatePrefs(this);
  }

  event.Skip();
  EndModal(wxID_OK);

  // Hide();
  // Close();
}

void ChartDldrPrefsDlg::OnCancelClick(wxCommandEvent &event) {
  event.Skip();
  EndModal(wxID_CANCEL);
  // Close();
}

void ChartDldrPrefsDlg::OnOkClick(wxCommandEvent &event) {
  event.Skip();
  // Close();
}

bool ChartDldrGuiAddSourceDlg::ValidateUrl(const wxString Url,
                                           bool catalog_xml) {
  wxRegEx re;
  if (catalog_xml)
    re.Compile(
        _T("^https?\\://[a-zA-Z0-9\\./_-]*\\.[xX][mM][lL]$"));  // TODO: wxRegEx
                                                                // sucks a bit,
                                                                // this RE is
                                                                // way too naive
  else
    re.Compile(
        _T("^https?\\://[a-zA-Z0-9\\./_-]*$"));  // TODO: wxRegEx sucks a bit,
                                                 // this RE is way too naive
  return re.Matches(Url);
}

static wxString FormatBytes(double bytes) {
  return wxString::Format(_T("%.1fMB"), bytes / 1024 / 1024);
}

void ChartDldrPanelImpl::onDLEvent(OCPN_downloadEvent &ev) {
  //    wxString msg;
  //    msg.Printf(_T("onDLEvent  %d %d"),ev.getDLEventCondition(),
  //    ev.getDLEventStatus()); wxLogMessage(msg);

  switch (ev.getDLEventCondition()) {
    case OCPN_DL_EVENT_TYPE_END:
      m_bTransferComplete = true;
      m_bTransferSuccess =
          (ev.getDLEventStatus() == OCPN_DL_NO_ERROR) ? true : false;
      break;

    case OCPN_DL_EVENT_TYPE_PROGRESS:
      m_totalsize = FormatBytes(ev.getTotal());
      m_transferredsize = FormatBytes(ev.getTransferred());

      break;
    default:
      break;
  }
}
