/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2015 CERN
 * Code derived from "wizard_add_fplib.cpp" ( author Maciej Suminski <maciej.suminski@cern.ch> )
 * Copyright (C) 2014-2015 Jean-Pierre Charras, jp.charras at wanadoo.fr
 * Copyright (C) 1992-2015 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * 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, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

/**
 * @brief Wizard for selecting and dowloading 3D shapes libraries of footprints
 * consisting of 3 steps:
 * - select source and destination (Github URL and local folder)
 * - pick and select libraries
 * - download files
 */

#include <wx/wx.h>
#include <wx/uri.h>
#include <wx/dir.h>
#include <widgets/app_progress_dialog.h>

#include <pgm_base.h>
#include <project.h>
#include <wizard_3DShape_Libs_downloader.h>
#include <confirm.h>
#include <3d_viewer/eda_3d_viewer.h>
#include <bitmaps.h>
#include <settings/common_settings.h>

#include <../github/github_getliblist.h>

// a key to store the default Kicad Github 3D libs URL
#define KICAD_3DLIBS_URL_KEY wxT( "kicad_3Dlib_url" )
#define KICAD_3DLIBS_LAST_DOWNLOAD_DIR wxT( "kicad_3Dlib_last_download_dir" )

#define DEFAULT_GITHUB_3DSHAPES_LIBS_URL \
    "https://github.com/KiCad/kicad-packages3d"
//    wxT( "https://github.com/KiCad/kicad-library/tree/master/modules/packages3d" )

void Invoke3DShapeLibsDownloaderWizard( wxWindow* aCaller )
{
    WIZARD_3DSHAPE_LIBS_DOWNLOADER wizard( aCaller );
    wizard.RunWizard( wizard.GetFirstPage() );
}


WIZARD_3DSHAPE_LIBS_DOWNLOADER::WIZARD_3DSHAPE_LIBS_DOWNLOADER( wxWindow* aParent ) :
    WIZARD_3DSHAPE_LIBS_DOWNLOADER_BASE( aParent )
{
    m_welcomeDlg = m_pages[0];
    m_githubListDlg = m_pages[1];
    m_reviewDlg = m_pages[2];

    // Initialize default download dir (local target folder of 3D shapes libs)
    wxString default_path;
    wxGetEnv( KISYS3DMOD, &default_path );

    auto cfg = Pgm().GetCommonSettings();

    setDownloadDir( cfg->m_3DLibsDownloadPath.empty() ? default_path : cfg->m_3DLibsDownloadPath );

    // Restore the Github 3D shapes libs url
    wxString githubUrl = cfg->m_3DLibsUrl;

    if( githubUrl.IsEmpty() )
        githubUrl = DEFAULT_GITHUB_3DSHAPES_LIBS_URL;

    SetGithubURL( githubUrl );


    // Give the minimal size to the dialog, which allows displaying any page
    wxSize minsize;

    for( unsigned ii = 0; ii < m_pages.size(); ii++ )
    {
        wxSize size = m_pages[ii]->GetSizer()->CalcMin();
        minsize.x = std::max( minsize.x, size.x );
        minsize.y = std::max( minsize.y, size.y );
    }

    SetMinSize( minsize );
    SetPageSize( minsize );
    GetSizer()->SetSizeHints( this );
    Center();

    setupDialogOrder();
    updateGithubControls();

    // When starting m_textCtrlGithubURL has the focus, and the text is selected,
    // and not fully visible.
    // Forcing deselection does not work, at least on W7 with wxWidgets 3.0.2
    // So (and also because m_textCtrlGithubURL and m_downloadDir are rarely modified
    // the focus is given to another widget.
    m_hyperlinkGithubKicad->SetFocus();

    Connect( wxEVT_RADIOBUTTON, wxCommandEventHandler( WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnSourceCheck ), NULL, this );
    Connect( wxEVT_CHECKLISTBOX, wxCommandEventHandler( WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnCheckGithubList ), NULL, this );
}


WIZARD_3DSHAPE_LIBS_DOWNLOADER::~WIZARD_3DSHAPE_LIBS_DOWNLOADER()
{
    auto cfg = Pgm().GetCommonSettings();

    cfg->m_3DLibsUrl          = GetGithubURL().ToStdString();
    cfg->m_3DLibsDownloadPath = getDownloadDir().ToStdString();
}



void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnPageChanged( wxWizardEvent& aEvent )
{
    SetBitmap( KiBitmap( wizard_add_fplib_icon_xpm ) );
    enableNext( true );

    if( GetCurrentPage() == m_githubListDlg )
        setupGithubList();
    else if( GetCurrentPage() == m_reviewDlg )
        setupReview();
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnCheckGithubList( wxCommandEvent& aEvent )
{
    wxArrayInt dummy;

    enableNext( m_checkList3Dlibnames->GetCheckedItems( dummy ) > 0 );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnSourceCheck( wxCommandEvent& aEvent )
{
    updateGithubControls();
    setupDialogOrder();
}

void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnGridLibReviewSize( wxSizeEvent& event )
{
    // Adjust the width of the column 1 of m_gridLibReview (library names) to the
    // max available width.
    int gridwidth = m_gridLibReview->GetClientSize().x;
    gridwidth -= m_gridLibReview->GetColSize( 0 ) + m_gridLibReview->GetColLabelSize();

    if( gridwidth < 200 )
        gridwidth = 200;

    m_gridLibReview->SetColSize( 1, gridwidth );

    event.Skip();
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::setupReview()
{
    // Prepare the last page of the wizard.

    m_LocalFolderInfo->SetLabel( getDownloadDir() );

    wxArrayInt checkedIndices;
    m_checkList3Dlibnames->GetCheckedItems( checkedIndices );

    m_libraries.Clear();

    // populate m_libraries with the name of libraries, without the github path:
    for( unsigned int ii = 0; ii < checkedIndices.GetCount(); ++ii )
    {
        m_libraries.Add( m_checkList3Dlibnames->GetString( checkedIndices[ii] ).AfterLast( '/' ) );
    }

    // Adjust number of rows in m_gridLibReview:
    int delta = m_libraries.GetCount() - m_gridLibReview->GetNumberRows();

    if( delta < 0 )
        m_gridLibReview->DeleteRows( -delta );
    else if( delta > 0 )
        m_gridLibReview->AppendRows( delta );

    // For user info, verify the existence of these libs in local folder
    wxArrayString liblist;
    wxFileName fn;
    fn.AssignDir( getDownloadDir() );

    for( unsigned int ii = 0; ii < m_libraries.GetCount(); ++ii )
    {
        fn.SetName( m_libraries[ii] );

        wxDir dirs;
        bool isNew = ! dirs.Exists( fn.GetFullPath() );
        wxString info = isNew ? _( "New" ) : _( "Update" );

        liblist.Add( info + wxT("  ") + m_libraries[ii] );

        m_gridLibReview->SetCellValue( ii, 0, info );
        m_gridLibReview->SetCellValue( ii, 1, m_libraries[ii] );
    }

    m_gridLibReview->AutoSizeColumn( 0 );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnSelectAll3Dlibs( wxCommandEvent& aEvent )
{
    for( unsigned int i = 0; i < m_checkList3Dlibnames->GetCount(); ++i )
        m_checkList3Dlibnames->Check( i, true );

    // The list might be empty, e.g. in case of download error
    wxArrayInt dummy;
    enableNext( m_checkList3Dlibnames->GetCheckedItems( dummy ) > 0 );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnUnselectAll3Dlibs( wxCommandEvent& aEvent )
{
    for( unsigned int i = 0; i < m_checkList3Dlibnames->GetCount(); ++i )
        m_checkList3Dlibnames->Check( i, false );

    enableNext( false );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnChangeSearch( wxCommandEvent& aEvent )
{
    wxString searchPhrase = m_searchCtrl3Dlibs->GetValue().Lower();

    // Store the current selection
    wxArrayInt checkedIndices;
    m_checkList3Dlibnames->GetCheckedItems( checkedIndices );
    wxArrayString checkedStrings;

    for( unsigned int i = 0; i < checkedIndices.GetCount(); ++i )
        checkedStrings.Add( m_checkList3Dlibnames->GetString( checkedIndices[i] ).AfterLast( '/' ) );

    m_checkList3Dlibnames->Clear();

    // Rebuild the list, putting the matching entries on the top
    int matching = 0;   // number of entries matching the search phrase
    for( unsigned int i = 0; i < m_githubLibs.GetCount(); ++i )
    {
        const wxString& lib = m_githubLibs[i].AfterLast( '/' );
        bool wasChecked = ( checkedStrings.Index( lib ) != wxNOT_FOUND );
        int insertedIdx = -1;

        if( !searchPhrase.IsEmpty() && lib.Lower().BeforeLast( '.' ).Contains( searchPhrase ) )
        {
            insertedIdx = m_checkList3Dlibnames->Insert( lib, matching++ );
            m_checkList3Dlibnames->SetSelection( insertedIdx );
        }
        else
            insertedIdx = m_checkList3Dlibnames->Append( lib );

        if( wasChecked )
            m_checkList3Dlibnames->Check( insertedIdx );
    }

    if( !m_checkList3Dlibnames->IsEmpty() )
        m_checkList3Dlibnames->EnsureVisible( 0 );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnWizardFinished( wxWizardEvent& aEvent )
{
    // we download a localy copy of the libraries
    wxString error;

    if( !downloadGithubLibsFromList( m_libraries, &error ) )
    {
        DisplayError( this, error );
    }
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnBrowseButtonClick( wxCommandEvent& aEvent )
{
    wxString path = getDownloadDir();

    path = wxDirSelector( _("Choose a folder to save the downloaded libraries" ),
                             path, 0, wxDefaultPosition, this );

    if( !path.IsEmpty() && wxDirExists( path ) )
    {
        setDownloadDir( path );
        updateGithubControls();
    }
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnDefault3DPathButtonClick( wxCommandEvent& event )
{
    wxString default_path;
    wxGetEnv( KISYS3DMOD, &default_path );

    if( !default_path.IsEmpty() && wxDirExists( default_path ) )
    {
        setDownloadDir( default_path );
        updateGithubControls();
    }
    else
        wxMessageBox( _( "KISYS3DMOD path not defined , or not existing" ) );
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnCheckSaveCopy( wxCommandEvent& aEvent )
{
    updateGithubControls();
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::getLibsListGithub( wxArrayString& aList )
{
    wxBeginBusyCursor();

    // Be sure there is no trailing '/' at the end of the repo name
    wxString git_url = m_textCtrlGithubURL->GetValue();

    if( git_url.EndsWith( wxT( "/" ) ) )
    {
        git_url.RemoveLast();
        m_textCtrlGithubURL->SetValue( git_url );
    }

    GITHUB_GETLIBLIST getter( git_url );
    getter.Get3DshapesLibsList( &aList, filter3dshapeslibraries );

    wxEndBusyCursor();
}


// Download the .3Dshapes libraries folders found in aUrlList and store them on disk
// in a master folder
bool WIZARD_3DSHAPE_LIBS_DOWNLOADER::downloadGithubLibsFromList( wxArrayString& aUrlList,
                                                     wxString* aErrorMessage )
{
    // Display a progress bar to show the download state
    // The title is updated for each downloaded library.
    // the state will be updated by downloadOneLib() for each file.
    // for OSX do not enable wPD_APP_MODAL, keep wxPD_AUTO_HIDE
    APP_PROGRESS_DIALOG pdlg( _( "Downloading 3D libraries" ), wxEmptyString,
                              aUrlList.GetCount(), this, true,
#ifndef __WXMAC__
                           wxPD_APP_MODAL |
#endif
                           wxPD_CAN_ABORT | wxPD_AUTO_HIDE );

    // Built the full server name string:
    wxURI repo( GetGithubURL() );
    wxString server = repo.GetScheme() + "://" + repo.GetServer();

    // Download libs:
    for( size_t ii = 0; ii < aUrlList.GetCount(); ii++ )
    {
        wxString& libsrc_name = aUrlList[ii];

        // Recover the full URL lib from short name:
        // (note: m_githubLibs stores the URL relative to the server name)
        wxString url;

        for( unsigned jj = 0; jj < m_githubLibs.GetCount(); jj++ )
        {
            if( m_githubLibs[jj].EndsWith( libsrc_name ) )
            {
                url = server + m_githubLibs[jj];
                break;
            }
        }

        wxFileName fn( libsrc_name );
        // Set our local path
        fn.SetPath( getDownloadDir() );
        wxString libdst_name = fn.GetFullPath();

        // Display the name of the library to download in the wxProgressDialog
        pdlg.SetTitle( wxString::Format( wxT("%s [%lu/%lu]" ),
                       libsrc_name.AfterLast( '/' ).GetData(),
                       ii + 1, aUrlList.GetCount() ) );

        if( !wxDirExists( libdst_name ) )
            wxMkdir( libdst_name );

        if( !downloadOneLib( url, libdst_name, &pdlg, aErrorMessage ) )
            return false;
    }

    return true;
}


bool WIZARD_3DSHAPE_LIBS_DOWNLOADER::downloadOneLib( const wxString& aLibURL,
                const wxString& aLocalLibName, wxProgressDialog* aIndicator,
                wxString* aErrorMessage )
{
    wxArrayString fileslist;

    bool success;

    // Get the list of candidate files: with ext .wrl .stp .step .STEP .STP or .wings
    do
    {
        GITHUB_GETLIBLIST getter( aLibURL );
        success = getter.Get3DshapesLibsList( &fileslist, filter3dshapesfiles );
    } while( 0 );

    if( !success )
        return false;

    // Load each file in list:
    wxURI repo( aLibURL );

    wxString server = repo.GetServer();

    // Github gives the current url of files inside .3dshapes folders like:
    //  "https://github.com/KiCad/kicad-library/blob/master/modules/packages3d/Capacitors_SMD.3dshapes/C_0402.wrl"
    // which displays a html page showing the file in html form.
    //
    // the URL of the corresponding raw file is
    // "https://github.com/KiCad/kicad-library/raw/master/modules/packages3d/Capacitors_SMD.3dshapes/C_0402.wrl"
    //
    // However Github redirects this current url to raw.githubusercontent.com/fullfilename
    // when trying to download raw files.
    //  "https://github.com/KiCad/kicad-library/raw/master/modules/packages3d/Capacitors_SMD.3dshapes/C_0402.wrl"
    // would be redirected to:
    //  "https://raw.githubusercontent.com/KiCad/kicad-library/master/modules/packages3d/Capacitors_SMD.3dshapes/C_0402.wrl"
    // So use raw.githubusercontent.com instead of github.com
    // (and removes the "/raw" in path)  speed up the downloads (x2 faster).
    //
    // wxURI has no way to change the server name, so we need to use tricks to make the URL.
    //
    // Comment this next line to use the github.com URL
#define FORCE_GITHUB_RAW_URL

#ifdef FORCE_GITHUB_RAW_URL
    if( server.Cmp( wxT( "github.com" ) ) == 0 )
        server = wxT( "raw.githubusercontent.com" );
#endif

    wxString full_url_base = repo.GetScheme() + wxT( "://" ) + server;
    wxString target_full_url;

    for( unsigned ii = 0; ii < fileslist.GetCount(); ii++ )
    {
        target_full_url = full_url_base + fileslist[ii];

#ifdef FORCE_GITHUB_RAW_URL
        // Remove "blob/" in URL string to build the URL on "raw.githubusercontent.com"
        // server from "github.com" URL string:
        target_full_url.Replace( wxT( "blob/" ), wxT( "" ) );
#else
        // Replace "blob" by "raw" in URL to access the raw file itself, not the html page
        // on "github.com" server
        target_full_url.Replace( wxT( "blob" ), wxT( "raw" ) );
#endif
        aIndicator->SetRange( fileslist.GetCount() );
        bool abort = !aIndicator->Update( ii, target_full_url.AfterLast( '/' ) );

        if( abort )
        {
            if( aErrorMessage )
                *aErrorMessage << _( "Aborted by user" );
            return false;
        }

        // Download the current file.
        // Get3DshapesLibsList actually downloads and stores the target_full_url content.
        GITHUB_GETLIBLIST getter( target_full_url );
        success = getter.Get3DshapesLibsList( NULL, NULL );

        if( !success )
            break;

        wxFileName fn;
        fn.AssignDir( aLocalLibName );
        fn.SetFullName( fileslist[ii].AfterLast( '/' ) );

        // The entire downloaded file is stored in getter buffer
        const std::string& buffer = getter.GetBuffer();

        // Write is "as this". It can be a binary file.
        wxFile file(fn.GetFullPath(), wxFile::write);
        file.Write( &buffer[0], buffer.size() );
    }

    return success;
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::setupGithubList()
{
    // Enable 'Next' only if there is at least one library selected
    wxArrayInt checkedIndices;
    m_checkList3Dlibnames->GetCheckedItems( checkedIndices );
    enableNext( checkedIndices.GetCount() > 0 );

    // Update only if the text has changed or the list is empty
    if( m_githubLibs.GetCount() == 0 || m_textCtrlGithubURL->IsModified() )
    {
        m_githubLibs.Clear();
        getLibsListGithub( m_githubLibs );

        // Populate the list
        m_checkList3Dlibnames->Clear();
        for( unsigned int i = 0; i < m_githubLibs.GetCount(); ++i )
        {
            const wxString& lib = m_githubLibs[i].AfterLast( '/' );
            m_checkList3Dlibnames->Append( lib );
        }

        m_textCtrlGithubURL->SetModified( 0 );
    }

    if( !m_checkList3Dlibnames->IsEmpty() )
        m_checkList3Dlibnames->EnsureVisible( 0 );

    // Clear the search box
    m_searchCtrl3Dlibs->Clear();

    // Clear the review list so it will be reloaded
    m_libraries.clear();
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::updateGithubControls()
{
    bool valid = wxFileName::IsDirWritable( getDownloadDir() );

    // Shows or not the warning text if the target 3d folder does not exist, or is not
    // writable.
    m_invalidDirWarningText->Show( !valid );
    m_bitmapDirWarn->Show( !valid );

    // If the dialog starts with m_invalidDirWarningText and m_bitmapDirWarn not shown
    // the size and position of the sizer containing these widgets can be incorrect,
    // until a wxSizeEvent is fired, and the widgets are not shown, or truncated,
    // at least on Windows. So fire a dummy wxSizeEvent if the size looks bad
    if( m_invalidDirWarningText->IsShown() && m_invalidDirWarningText->GetSize().x < 2 )
    {
        wxSizeEvent event( GetSize() );
        wxPostEvent( this, event );
    }

    // Allow to go further only if there is a valid target directory selected
    enableNext( valid );
}

// Called when the local folder name is edited.
void WIZARD_3DSHAPE_LIBS_DOWNLOADER::OnLocalFolderChange( wxCommandEvent& event )
{
    updateGithubControls();
}


void WIZARD_3DSHAPE_LIBS_DOWNLOADER::setupDialogOrder()
{
    m_welcomeDlg->SetNext( m_githubListDlg );
    m_githubListDlg->SetPrev( m_welcomeDlg );
    m_githubListDlg->SetNext( m_reviewDlg );
    m_reviewDlg->SetPrev( m_githubListDlg );
}