/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * Module: rbutil
 * File: rbutil.cpp
 *
 * Copyright (C) 2005 Christi Alice Scarborough
 *
 * All files in this archive are subject to the GNU General Public License.
 * See the file COPYING in the source tree root for full license agreement.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "rbutil.h"
#include "installlog.h"

// This class allows us to return directories as well as files to
// wxDir::Traverse
class wxDirTraverserIncludeDirs : public wxDirTraverser
    {
    public:
        wxDirTraverserIncludeDirs(wxArrayString& files) : m_files(files) { }

        virtual wxDirTraverseResult OnFile(const wxString& filename)
        {
            m_files.Add(filename);
            return wxDIR_CONTINUE;
        }

        virtual wxDirTraverseResult OnDir(const wxString& dirname)
        {
            m_files.Add(dirname);
            return wxDIR_CONTINUE;
        }

    private:
        wxArrayString& m_files;
    };

wxDEFINE_SCOPED_PTR_TYPE(wxZipEntry);

const wxChar* _rootmatch[] = {
    wxT("rockbox.*"),
    wxT("ajbrec.ajz"),
    wxT("archos.mod"),
    wxT(".scrobbler.*"),
    wxT("battery_bench.txt"),
    wxT("battery.dummy"),
};
const wxArrayString* rootmatch = new wxArrayString(
    (size_t) (sizeof(_rootmatch) / sizeof(wxChar*)), _rootmatch);

int DownloadURL(wxString src, wxString dest)
{
    int input, errnum = 0, success = false;
    wxString buf, errstr;
    wxLogVerbose(_("=== begin DownloadURL(%s,%s)"), src.c_str(),
        dest.c_str());

    buf.Printf(_("Fetching %s"), src.c_str());
    wxProgressDialog* progress = new wxProgressDialog(_("Downloading"),
                buf, 100, NULL, wxPD_APP_MODAL |
                wxPD_AUTO_HIDE | wxPD_SMOOTH | wxPD_ELAPSED_TIME |
                wxPD_REMAINING_TIME | wxPD_CAN_ABORT);
    progress->Update(0);

    input = true;
    wxURL* in_http = new wxURL(src);
    if (in_http->GetError() == wxURL_NOERR)
    {

    wxFFileOutputStream* os = new wxFFileOutputStream(dest);
    input = false;
    if (os->IsOk())
    {
        wxInputStream* is = in_http->GetInputStream();
        input = true;
        if (is)
        {
            size_t filesize = is->GetSize();
            input = true;
            if (is->IsOk())
            {
                char buffer[FILE_BUFFER_SIZE + 1];
                size_t current = 0;

                while (! is->Eof())
                {
                    is->Read(buffer, FILE_BUFFER_SIZE);
                    input = true;
                    if (is->LastRead() )
                    {
                        os->Write(buffer, is->LastRead());
                        input = false;
                        if (os->IsOk())
                        {
                            current += os->LastWrite();
                            if (!progress->Update(current * 100 / filesize))
                            {
                                errstr = _("Download aborted by user");
                                errnum = 1000;
                                break;
                            }

                        } else
                        {
                            errnum = os->GetLastError();
                            errstr.Printf(_("Can't write to output stream (%s)"),
                                        stream_err_str(errnum).c_str() );

                            break;
                        }

                    } else
                    {
                        errnum = is->GetLastError();
                        if (errnum == wxSTREAM_EOF)
                        {
                            errnum = 0;
                            break;
                        }
                        errstr.Printf(_("Can't read from input stream (%s)"),
                                      stream_err_str(errnum).c_str() );
                    }
                }

                os->Close();
                if (! errnum)
                {
                    errnum = os->GetLastError();
                    errstr.Printf(_("Can't close output file (%s)"),
                            stream_err_str(errnum).c_str() );

                    input = false;
                }

                if (! errnum) success = true;

                } else
                {
                    errnum = is->GetLastError();
                    errstr.Printf(_("Can't get input stream size (%s)"),
                            stream_err_str(errnum).c_str() );

                }
            } else
            {
                errnum = in_http->GetError();
                errstr.Printf(_("Can't get input stream (%d)"), errnum);
            }
            delete is;
        } else
        {
            errnum = os->GetLastError();
            errstr.Printf(_("Can't create output stream (%s)"),
                    stream_err_str(errnum).c_str() );
        }
        delete os;
    } else
    {
        errstr.Printf(_("Can't open URL %s (%d)"), src.c_str(),
            in_http->GetError() );
        errnum = 100;
    }

    delete in_http;
    delete progress;

    if (!success)
    {
        if (errnum == 0) errnum = 999;
        if (input)
        {
            buf.Printf(_("%s reading\n%s"),
                errstr.c_str(), src.c_str());
            ERR_DIALOG(buf, _("Download URL"));
        } else
        {
            buf.Printf(_("%s writing to download\n/%s"),
                errstr.c_str(), dest.c_str());
            ERR_DIALOG(buf, _("Download URL"));
        }

    }

    wxLogVerbose(_("=== end DownloadURL"));
    return errnum;
}

int UnzipFile(wxString src, wxString destdir, bool isInstall)
{
    wxZipEntryPtr       entry;
    wxString            in_str, progress_msg, buf;
    int                 errnum = 0, curfile = 0, totalfiles = 0;
    InstallLog*         log = NULL;

    wxLogVerbose(_("===begin UnzipFile(%s,%s,%i)"),
               src.c_str(), destdir.c_str(), isInstall);

    wxFFileInputStream* in_file = new wxFFileInputStream(src);
    wxZipInputStream* in_zip = new wxZipInputStream(*in_file);
    if (in_file->Ok() )
    {
        if (! in_zip->IsOk() )
        {
            errnum = in_zip->GetLastError();
            buf.Printf(_("Can't open ZIP stream %s for reading (%s)"),
                src.c_str(), stream_err_str(errnum).c_str() );
            ERR_DIALOG(buf, _("Unzip File") );
            delete in_zip;
            delete in_file;
            return true;
        }

        totalfiles = in_zip->GetTotalEntries();
        if (! in_zip->IsOk() )
        {
            errnum = in_zip->GetLastError();
            buf.Printf(_("Error Getting total ZIP entries for %s (%s)"),
                src.c_str(), stream_err_str(errnum).c_str() );
            ERR_DIALOG(buf, _("Unzip File") );
            delete in_zip;
            delete in_file;
            return true;
        }
    } else
    {
        errnum = in_file->GetLastError();
        buf.Printf(_("Can't open %s (%s)"), src.c_str(),
                   stream_err_str(errnum).c_str() );
        ERR_DIALOG(buf, _("Unzip File") );
        delete in_zip;
        delete in_file;
        return true;
    }

    wxProgressDialog* progress = new wxProgressDialog(_("Unpacking archive"),
                  _("Preparing to unpack the downloaded files to your audio"
                  "device"), totalfiles, NULL, wxPD_APP_MODAL |
                  wxPD_AUTO_HIDE | wxPD_SMOOTH | wxPD_ELAPSED_TIME |
                  wxPD_REMAINING_TIME | wxPD_CAN_ABORT);
    progress->Update(0);

    // We're not overly worried if the logging fails
    if (isInstall)
    {
        buf.Printf(wxT("%s" PATH_SEP UNINSTALL_FILE), destdir.c_str());
        log = new InstallLog(buf);
    }

    while (! errnum &&
           (entry.reset(in_zip->GetNextEntry()), entry.get() != NULL) )
    {

        curfile++;
        wxString name = entry->GetName();
        progress_msg.Printf(_("Unpacking %s"), name.c_str());
        if (! progress->Update(curfile, progress_msg) ) {
            buf.Printf(_("Unpacking cancelled by user"));
            MESG_DIALOG(buf);
            errnum = 1000;
            break;
        }

        in_str.Printf(wxT("%s" PATH_SEP "%s"), destdir.c_str(), name.c_str());

        if (entry->IsDir() ) {
            wxDir* dirname = new wxDir(in_str);
            if (! dirname->Exists(in_str) ) {
                if (! wxMkdir(in_str, 0777) ) {
                    buf.Printf(_("Unable to create directory %s"),
                               in_str.c_str() );
                    errnum = 100;
                    delete dirname;
                    break;
                }
            }
            log->WriteFile(name, true); // Directory
            delete dirname;
            continue;
        }

        wxFFileOutputStream* out = new wxFFileOutputStream(in_str);
        if (! out->IsOk() )
        {
            buf.Printf(_("Can't open file %s for writing"), in_str.c_str() );
            delete out;
            return 100;
        } else if (isInstall)
        {
            log->WriteFile(name);
        }

        in_zip->Read(*out);
        if (! out->IsOk()) {
            buf.Printf(_("Can't write to %s (%d)"), in_str.c_str(),
                        errnum = out->GetLastError() );
        }

        if (!in_zip->IsOk() && ! in_file->GetLastError() == wxSTREAM_EOF)
        {
            buf.Printf(_("Can't read from %s (%d)"), src.c_str(),
            errnum = in_file->GetLastError() );
        }

        if (! out->Close() && errnum == 0)
        {
            buf.Printf(_("Unable to close %s (%d)"), in_str.c_str(),
                errnum = out->GetLastError() );

        }

        delete out;

    }

    delete in_zip; delete in_file; delete progress;

    if (errnum)
    {
        ERR_DIALOG(buf, _("Unzip File"));
    }

    if (log) delete log;
    wxLogVerbose(_("=== end UnzipFile"));
    return(errnum);
}

int Uninstall(const wxString dir, bool isFullUninstall) {
    wxString buf, uninst;
    bool gooddata = false;
    unsigned int i;
    bool errflag = false;
    InstallLog *log = NULL;
    wxArrayString* FilesToRemove = NULL;

    wxLogVerbose(_("=== begin Uninstall(%s,%i)"), dir.c_str(), isFullUninstall);

    wxProgressDialog* progress = new wxProgressDialog(_("Uninstalling"),
                _("Reading uninstall data from jukebox"), 100, NULL,
                wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH |
                wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME | wxPD_CAN_ABORT);
    progress->Update(0);

    if (! isFullUninstall)
    {

        buf.Printf(wxT("%s" PATH_SEP UNINSTALL_FILE), dir.c_str());
        log = new InstallLog(buf, false); // Don't create the log
        FilesToRemove = log->GetInstalledFiles();
        if (log) delete log;

        if (FilesToRemove == NULL || FilesToRemove->GetCount() < 1) {
            wxLogNull lognull;
            if ( wxMessageDialog(NULL,
                _("Rockbox Utility can't find any uninstall data on this "
                "jukebox.\n"
                "Would you like to attempt a full uninstall?\n"
                "(WARNING: A full uninstall removes all files in your Rockbox "
                "folder)"),
                _("Standard uninstall not possible"),
                wxICON_EXCLAMATION | wxYES_NO | wxNO_DEFAULT).ShowModal()
                == wxID_YES)
            {
                isFullUninstall = true;
            }
            else {
                MESG_DIALOG(_("Uninstall cancelled by user"));
                delete progress;
                return 1000;
            }
        }
    }

    if (isFullUninstall )
    {
        buf.Printf(wxT("%s" PATH_SEP ".rockbox"), dir.c_str());
        if (rm_rf(buf) )
        {
            WARN_DIALOG(_("Unable to completely remove Rockbox directory"),
            _("Full uninstall") );
            errflag = true;
        }

        wxDir* root = new wxDir(dir);
        wxArrayString* special = new wxArrayString();
        // Search for files for deletion in the jukebox root
        for (i = 0; i < rootmatch->GetCount(); i++)
        {
            const wxString match = (*rootmatch)[i];
            root->GetAllFiles(dir, special, match, wxDIR_FILES);
        }
        delete root;

        // Sort in reverse order so we get directories last
        special->Sort(true);

        for (i = 0; i < special->GetCount(); i++)
        {

            if (wxDirExists((*special)[i]) )
            {
                // We don't check the return code since we don't want non
                // empty dirs disappearing.
                wxRmdir((*special)[i]);

            } else if (wxFileExists((*special)[i]) )
            {
                if (! wxRemoveFile((*special)[i]) )
                {
                    buf.Printf(_("Can't delete %s"), (*special)[i].c_str());
                    WARN_DIALOG(buf.c_str(), _("Full uninstall"));
                    errflag = true;
                }
            }
            // Otherwise there isn't anything there, so we don't have to worry.
        }
        delete special;
    } else
    {
        wxString instplat, this_path_sep;
        unsigned int totalfiles, rc;
        totalfiles = FilesToRemove->GetCount();
        FilesToRemove->Sort(true); // Reverse alphabetical ie dirs after files

        for (i = 0; i < totalfiles; i++)
        {
            // If we're running on the device, let's not delete our own
            // installation, eh?
            if (gv->portable &&
                FilesToRemove->Item(i).StartsWith(wxT(PATH_SEP
                    "RockboxUtility")) )
            {
                continue;
            }

            wxString* buf2 = new wxString;
            buf.Printf("%s%s", dir.c_str() , FilesToRemove->Item(i).c_str() );
            buf2->Format(_("Deleting %s"), buf.c_str());

            if (! progress->Update((i + 1) * 100 / totalfiles, *buf2) )
            {
                WARN_DIALOG(_("Cancelled by user"), _("Normal Uninstall"));
                delete progress;
                return true;
            }

            if (wxDirExists(buf) )
            {
                // If we're about to attempt to remove .rockbox. delete
                // install data first
                buf2->Printf(wxT("%s" PATH_SEP ".rockbox"), dir.c_str() );
                if ( buf.IsSameAs(buf2->c_str()) )
                {
                    buf2->Printf(wxT("%s" PATH_SEP UNINSTALL_FILE), dir.c_str());
                    wxRemoveFile(*buf2);
                }

                if ( rc = ! wxRmdir(buf) )
                {
                    buf = buf.Format(_("Can't remove directory %s"),
                        buf.c_str());
                    errflag = true;
                    WARN_DIALOG(buf.c_str(), _("Standard uninstall"));
                }
            } else if (wxFileExists(buf) )
            {
                if ( rc = ! wxRemoveFile(buf) )
                {
                    buf = buf.Format(_("Can't delete file %s"),
                        buf.c_str());
                    errflag = true;
                    WARN_DIALOG(buf.c_str(), _("Standard uninstall"));
                }
            } else
            {
                errflag = true;
                buf = buf.Format(_("Can't find file or directory %s"),
                    buf.c_str() );
                WARN_DIALOG(buf.c_str(), _("Standard uninstall") );
            }

            uninst = uninst.AfterFirst('\n');
        }
        if (errflag)
        {
            ERR_DIALOG(_("Unable to remove some files"),
                _("Standard uninstall"))    ;
        }

        if (FilesToRemove != NULL) delete FilesToRemove;
    }

    delete progress;
    wxLogVerbose(_("=== end Uninstall"));
    return errflag;
}


wxString stream_err_str(int errnum)
{
    wxString out;

    switch (errnum) {
    case wxSTREAM_NO_ERROR:
        out = wxT("wxSTREAM_NO_ERROR");
        break;
    case wxSTREAM_EOF:
        out = wxT("wxSTREAM_EOF");
        break;
    case wxSTREAM_WRITE_ERROR:
        out = wxT("wxSTREAM_WRITE_ERROR");
        break;
    case wxSTREAM_READ_ERROR:
        out = wxT("wxSTREAM_READ_ERROR");
        break;
    default:
        out = wxT("UNKNOWN");
        break;
    }
    return out;
}

bool InstallRbutil(wxString dest)
{
    wxArrayString   filestocopy;
    wxString        str, buf, dstr, localpath, destdir;
    unsigned int    i;
    wxDir           dir;
    bool            copied_exe = false, made_rbdir = false;
    InstallLog*     log;

    buf.Printf(wxT("%s" PATH_SEP ".rockbox"), dest.c_str() );

    if (! wxDirExists(buf) )
    {
        wxMkdir(buf);
        made_rbdir = true;
    }

    buf.Printf(wxT("%s" PATH_SEP UNINSTALL_FILE), dest.c_str() );
    log = new InstallLog(buf);
    if (made_rbdir) log->WriteFile(wxT(".rockbox"), true);

    destdir.Printf(wxT("%s" PATH_SEP "RockboxUtility"), dest.c_str());
    if (! wxDirExists(destdir) )
    {
        if (! wxMkdir(destdir, 0777) )
        {
            buf.Printf(wxT("%s (%s)"),
                _("Unable to create directory for installer"), destdir.c_str());
            WARN_DIALOG(buf , _("Portable install") );
            return false;
        }
        log->WriteFile(wxT("RockboxUtility"), true);
    }

    dir.GetAllFiles(gv->ResourceDir, &filestocopy, wxT("*"),
        wxDIR_FILES);
    if (filestocopy.GetCount() < 1)
    {
        WARN_DIALOG(_("No files to copy"), _("Portable install") );
        return false;
    }

    // Copy the contents of the program directory
    for (i = 0; i < filestocopy.GetCount(); i++)
    {
        if (filestocopy[i].AfterLast(PATH_SEP_CHR) == EXE_NAME)
        {
            copied_exe = true;
        }

        dstr.Printf(wxT("%s" PATH_SEP "%s"), destdir.c_str(),
            filestocopy[i].AfterLast(PATH_SEP_CHR).c_str());
        if (! wxCopyFile(filestocopy[i], dstr) )
        {
            buf.Printf(wxT("%s (%s -> %s)"),
                _("Error copying file"), filestocopy[i].c_str(), dstr.c_str());
            WARN_DIALOG(buf, _("Portable Install") );
            return false;
        }
        buf = dstr;
        buf.Replace(dest, wxEmptyString, false);
        log->WriteFile(buf);
    }

    if (! copied_exe)
    {
        str.Printf(wxT("%s" PATH_SEP EXE_NAME), gv->AppDir.c_str());
        dstr.Printf(wxT("%s" PATH_SEP EXE_NAME), destdir.c_str(),
            filestocopy[i].AfterLast(PATH_SEP_CHR).c_str());
        if (! wxCopyFile(str, dstr) )
        {
            buf.Printf(wxT("Can't copy program binary %s -> %s"),
                str.c_str(), dstr.c_str() );
            WARN_DIALOG(buf, _("Portable Install") );
            return false;
        }
        buf = dstr;
        buf.Replace(dest, wxEmptyString, false);
        log->WriteFile(buf);
    }

    // Copy the local ini file so that it knows that it's a portable copy
    gv->UserConfig->Flush();
    dstr.Printf(wxT("%s" PATH_SEP "RockboxUtility.cfg"), destdir.c_str());
    if (! wxCopyFile(gv->UserConfigFile, dstr) )
    {
        buf.Printf(wxT("%s (%s -> %s)"),
            _("Unable to install user config file"), gv->UserConfigFile.c_str(),
            dstr.c_str() );
        WARN_DIALOG(buf, _("Portable Install") );
        return false;
    }
    buf = dstr;
    buf.Replace(dest, wxEmptyString, false);
    log->WriteFile(buf);

    delete log;
    return true;
}

bool rm_rf(wxString file)
{
    wxLogVerbose(_("=== begin rm-rf(%s)"), file.c_str() );

    wxString buf;
    wxArrayString selected;
    wxDirTraverserIncludeDirs wxdtid(selected);
    unsigned int rc = 0, i;
    bool errflag = false;

    if (wxFileExists(file) )
    {
        rc = ! wxRemoveFile(file);
    } else if (wxDirExists(file) )
    {
        wxDir* dir = new wxDir(file);;
        dir->Traverse(wxdtid);
        delete dir;
        // Sort into reverse alphabetical order for deletion in correct order
        // (directories after files)
        selected.Sort(true);
        selected.Add(file);

        wxProgressDialog* progress = new wxProgressDialog(_("Removing files"),
                _("Deleting files"), selected.GetCount(), NULL,
                wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH |
                wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME | wxPD_CAN_ABORT);

        for (i = 0; i < selected.GetCount(); i++)
        {
            wxLogVerbose(selected[i]);
            if (progress != NULL)
            {
                buf.Printf(_("Deleting %s"), selected[i].c_str() );
                if (! progress->Update(i, buf))
                {
                    WARN_DIALOG(_("Cancelled by user"), _("Erase Files"));
                    delete progress;
                    return true;
                }
            }

            if (wxDirExists(selected[i]) )
            {
                if (rc = ! wxRmdir(selected[i]) )
                {
                    buf.Printf(_("Can't remove directory %s"),
                        selected[i].c_str());
                    errflag = true;
                    WARN_DIALOG(buf.c_str(), _("Erase files"));
                }
            } else if (rc = ! wxRemoveFile(selected[i]) )
            {
                buf.Printf(_("Error deleting file %s"), selected[i].c_str() );
                errflag = true;
                WARN_DIALOG(buf.c_str(),_("Erase files"));
            }
        }
        delete progress;
    } else
    {
        buf.Printf(_("Can't find expected file %s"), file.c_str());
        WARN_DIALOG(buf.c_str(), _("Erase files"));
        return true;
    }

    wxLogVerbose(_("=== end rm-rf"));
    return rc ? true : false;
}

