/*
 * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define STRICT
#ifndef _WIN32_WINNT
/* REMIND : 0x500 means Windows 2000 .. seems like we can update
 * for Windows XP when we move the SDK and build platform
 */
#define _WIN32_WINNT 0x0500
#endif
#define _ATL_APARTMENT_THREADED


#include <atlbase.h>
//You may derive a class from CComModule and use it if you want to override
//something, but do not change the name of _Module
extern CComModule _Module;
#include <atlcom.h>
#include <atlwin.h>

#include <atlhost.h>
#include <commdlg.h>
#include <commctrl.h>
#include <windowsx.h>
#include <urlmon.h>
#include <wininet.h>
#include <shellapi.h>
#include <time.h>
#include <math.h>
#include <stdio.h>

#include <jni.h>

#include "resource.h"       // main symbols
#include "DownloadHelper.h"

DownloadHelper::DownloadHelper() {

    m_showProgressDialog = TRUE;
    m_pszURL = NULL;
    m_pszFileName = NULL;
    m_pszNameText = NULL;
}

DownloadHelper::~DownloadHelper() {

}

HRESULT DownloadHelper::doDownload() {
    return DownloadFile(m_pszURL, m_pszFileName, FALSE, m_showProgressDialog);
}

HRESULT DownloadHelper::DownloadFile(const TCHAR* szURL,
        const TCHAR* szLocalFile, BOOL bResumable, BOOL bUIFeedback) {
    HINTERNET hOpen = NULL;
    HINTERNET hConnect = NULL;
    HINTERNET hRequest = NULL;
    HANDLE hFile = INVALID_HANDLE_VALUE;
    DWORD dwDownloadError = 0;
    DWORD nContentLength = 0;

    /* Some of error messages use drive letter.
       Result is something like "(C:)".
       NB: Parentheses are added here because in some other places
           we same message but can not provide disk label info */
    TCHAR drivePath[5];
    /* assuming szLocalFile is not NULL */
    _sntprintf(drivePath, 5, "(%c:)", szLocalFile[0]);
    WCHAR* wName = CT2CW(drivePath);

    __try {
        m_csDownload.Lock();

        time(&m_startTime);

    }
    __finally {
        m_csDownload.Unlock();
    }

    __try {
        // block potential security hole
        if (strstr(szURL, TEXT("file://")) != NULL) {
            dwDownloadError = 1;
            __leave;
        }

        HWND hProgressInfo = NULL;
        TCHAR szStatus[BUFFER_SIZE];

        if (bUIFeedback) {
            // init download dialg text
            m_dlg->initDialogText(m_pszURL, m_pszNameText);
        }

        // Open Internet Call
        hOpen = ::InternetOpen("deployHelper", INTERNET_OPEN_TYPE_PRECONFIG,
                NULL, NULL, NULL);

        if (hOpen == NULL) {
            dwDownloadError = 1;
            __leave;
        }

        // URL components
        URL_COMPONENTS url_components;
        ::ZeroMemory(&url_components, sizeof(URL_COMPONENTS));

        TCHAR szHostName[BUFFER_SIZE], szUrlPath[BUFFER_SIZE],
                szExtraInfo[BUFFER_SIZE];
        url_components.dwStructSize = sizeof(URL_COMPONENTS);
        url_components.lpszHostName = szHostName;
        url_components.dwHostNameLength = BUFFER_SIZE;
        url_components.nPort = NULL;
        url_components.lpszUrlPath = szUrlPath;
        url_components.dwUrlPathLength = BUFFER_SIZE;
        url_components.lpszExtraInfo = szExtraInfo;
        url_components.dwExtraInfoLength = BUFFER_SIZE;

        // Crack the URL into pieces
        ::InternetCrackUrl(szURL, lstrlen(szURL), NULL, &url_components);

        // Open Internet Connection
        hConnect = ::InternetConnect(hOpen, url_components.lpszHostName,
                url_components.nPort, "", "", INTERNET_SERVICE_HTTP, NULL,
                NULL);

        if (hConnect == NULL) {
            dwDownloadError = 1;
            __leave;
        }

        // Determine the relative URL path by combining
        // Path and ExtraInfo
        char szURL[4096];

        if (url_components.dwUrlPathLength !=  0)
            lstrcpy(szURL, url_components.lpszUrlPath);
        else
            lstrcpy(szURL, "/");

        if (url_components.dwExtraInfoLength != 0)
            lstrcat(szURL, url_components.lpszExtraInfo);

        BOOL bRetryHttpRequest = FALSE;
        int numberOfRetry = 0;
        long secondsToWait = 60;

        do {
            bRetryHttpRequest = FALSE;

            // Make a HTTP GET request
            hRequest = ::HttpOpenRequest(hConnect, "GET", szURL, "HTTP/1.1",
                    "", NULL,
                    INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_DONT_CACHE,
                    0);

            if (hRequest == NULL) {
                dwDownloadError = 1;
                __leave;
            }

            // Create or open existing destination file
            hFile = ::CreateFile(szLocalFile, GENERIC_WRITE, 0, NULL,
                    OPEN_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, NULL);

            if (hFile == INVALID_HANDLE_VALUE) {
                if (bUIFeedback) {
                    if (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_DISK_WRITE_ERROR,
                                            IDS_DISK_WRITE_ERROR_CAPTION,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            wName)) {
                         bRetryHttpRequest = TRUE;
                         continue;
                    }
                }
                dwDownloadError = 1;
                __leave;
            }
            DWORD fileSize = GetFileSize(hFile, NULL);

            // Check if resumable download is enabled
            if (bResumable == FALSE) {
                // Start from scratch
                fileSize = 0;
            }

            FILETIME tWrite;
            BOOL rangereq = FALSE;
            if ((fileSize != 0) && (fileSize != 0xFFFFFFFF) &&
                    GetFileTime(hFile, NULL, NULL, &tWrite)) {
                char szHead[100];
                SYSTEMTIME tLocal;
                char buf[INTERNET_RFC1123_BUFSIZE];

                FileTimeToSystemTime(&tWrite, &tLocal);
                InternetTimeFromSystemTime(&tLocal, INTERNET_RFC1123_FORMAT,
                        buf, INTERNET_RFC1123_BUFSIZE);
                sprintf(szHead, "Range: bytes=%d-\r\nIf-Range: %s\r\n",
                        fileSize, buf);
                HttpAddRequestHeaders(hRequest, szHead, lstrlen(szHead),
                        HTTP_ADDREQ_FLAG_ADD|HTTP_ADDREQ_FLAG_REPLACE);
                rangereq = TRUE;
            }

            // This is a loop to handle various potential error when the
            // connection is made
            BOOL bCont = TRUE;

            while ((FALSE == ::HttpSendRequest(hRequest, NULL, NULL, NULL, NULL))
            && bCont ) {
                // We might have an invalid CA.
                DWORD dwErrorCode = GetLastError();

                switch(dwErrorCode) {
                    case E_JDHELPER_TIMEOUT:
                    case E_JDHELPER_NAME_NOT_RESOLVED:
                    case E_JDHELPER_CANNOT_CONNECT: {
                        bCont = FALSE;
                        // Display the information dialog
                        if (bUIFeedback) {
                            // decrement download counter to prevent progress
                            // dialog from popping up while the message box is
                            // up
                            m_dlg->bundleInstallComplete();
                            if (dwErrorCode == E_JDHELPER_TIMEOUT) {
                                bRetryHttpRequest =
                                    (IDRETRY == m_dlg->SafeMessageBox(
                                       IDS_HTTP_STATUS_REQUEST_TIMEOUT,
                                       IDS_HTTP_INSTRUCTION_REQUEST_TIMEOUT,
                                       IDS_ERROR_CAPTION,
                                       DIALOG_ERROR_RETRYCANCEL));
                            } else {
                                bRetryHttpRequest =
                                    (IDRETRY == m_dlg->SafeMessageBox(
                                       IDS_HTTP_STATUS_SERVER_NOT_REACHABLE,
                                       IDS_HTTP_INSTRUCTION_SERVER_NOT_REACHABLE,
                                       IDS_ERROR_CAPTION,
                                       DIALOG_ERROR_RETRYCANCEL));
                            }
                            // re-increment counter because it will be decremented
                            // again upon return
                            m_dlg->bundleInstallStart();
                            bCont = bRetryHttpRequest;
                        }
                        break;
                    }
                    case ERROR_INTERNET_INVALID_CA:
                    case ERROR_INTERNET_SEC_CERT_CN_INVALID:
                    case ERROR_INTERNET_SEC_CERT_DATE_INVALID:
                    case ERROR_INTERNET_HTTP_TO_HTTPS_ON_REDIR:
                    case ERROR_INTERNET_INCORRECT_PASSWORD:
                    case ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED:
                    default: {
                        // Unless the user agrees to continue, we just
                        // abandon now !
                        bCont = FALSE;

                        // Make sure to test the return code from
                        // InternetErrorDlg user may click OK or Cancel. In
                        // case of Cancel, request should not be resubmitted
                        if (bUIFeedback) {
                            if (ERROR_SUCCESS == ::InternetErrorDlg(
                                    NULL, hRequest,
                                    dwErrorCode,
                                    FLAGS_ERROR_UI_FILTER_FOR_ERRORS |
                                    FLAGS_ERROR_UI_FLAGS_GENERATE_DATA |
                                    FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS,
                                    NULL))
                                bCont = TRUE;
                        }
                    }
                }
            }

            if (bCont == FALSE) {
                // User has denied the request
                dwDownloadError = 1;
                __leave;
            }

            //
            // Read HTTP status code
            //
            DWORD dwErrorCode = GetLastError();
            DWORD dwStatus=0;
            DWORD dwStatusSize = sizeof(DWORD);

            if (FALSE == ::HttpQueryInfo(hRequest, HTTP_QUERY_FLAG_NUMBER |
                    HTTP_QUERY_STATUS_CODE, &dwStatus, &dwStatusSize, NULL)) {
                dwErrorCode = GetLastError();
            }

            bCont = TRUE;
            while ((dwStatus == HTTP_STATUS_PROXY_AUTH_REQ ||
                    dwStatus == HTTP_STATUS_DENIED) &&
                    bCont) {
                int result = ::InternetErrorDlg(GetDesktopWindow(), hRequest, ERROR_INTERNET_INCORRECT_PASSWORD,
                        FLAGS_ERROR_UI_FILTER_FOR_ERRORS |
                        FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS |
                        FLAGS_ERROR_UI_FLAGS_GENERATE_DATA,
                        NULL);
                if (ERROR_CANCELLED == result) {
                    bCont = FALSE;
                }
                else {
                    ::HttpSendRequest(hRequest, NULL, 0, NULL, 0);

                    // Reset buffer length
                    dwStatusSize = sizeof(DWORD);

                    ::HttpQueryInfo(hRequest, HTTP_QUERY_FLAG_NUMBER |
                            HTTP_QUERY_STATUS_CODE, &dwStatus, &dwStatusSize,
                            NULL);
                }
            }

            if (dwStatus == HTTP_STATUS_OK ||
                    dwStatus == HTTP_STATUS_PARTIAL_CONTENT) {
                // Determine content length, so we may show the progress bar
                // meaningfully
                //
                nContentLength = 0;
                DWORD nLengthSize = sizeof(DWORD);
                ::HttpQueryInfo(hRequest,
                        HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER,
                        &nContentLength, &nLengthSize, NULL);

                if (nContentLength <= 0) {
                    // If can't estimate content length, estimate it
                    // to be 6MB
                    nContentLength = 15000000;
                }
                else if (rangereq && (fileSize != 0) &&
                        (nContentLength == fileSize)) {
                    // If the file is already downloaded completely and then
                    // we send a range request, the whole file is sent instead
                    // of nothing. So avoid downloading again.
                    // Some times return value is 206, even when whole file
                    // is sent. So check if "Content-range:" is present in the
                    // reply
                    char buffer[256];
                    DWORD length = sizeof(buffer);
                    if(!HttpQueryInfo(hRequest, HTTP_QUERY_CONTENT_RANGE,
                            buffer, &length, NULL)) {
                        if(HttpQueryInfo(hRequest, HTTP_QUERY_LAST_MODIFIED,
                                buffer, &length, NULL)) {
                            SYSTEMTIME systime;
                            FILETIME filtime;
                            InternetTimeToSystemTime(buffer, &systime, NULL);
                            SystemTimeToFileTime(&systime, &filtime);
                            if ((CompareFileTime(&tWrite, &filtime)) == 1) {
                                // no need to download
                                dwDownloadError = 0;
                                __leave;
                            }
                        }
                        else {
                            ::SetFilePointer(hFile, 0, 0, FILE_BEGIN);
                            ::SetEndOfFile(hFile); // truncate the file
                        }
                    }

                }

                TCHAR szBuffer[8096];
                DWORD dwBufferSize = 8096;

                // Read from HTTP connection and write into
                // destination file
                //
                DWORD nRead = 0;
                DWORD dwTotalRead = 0;
                BOOL bCancel = FALSE;

                if (dwStatus == HTTP_STATUS_PARTIAL_CONTENT) {
                    // If we are using resumable download, fake
                    // start time so it looks like we have begun
                    // the download several minutes again.
                    //
                    m_startTime = m_startTime - 100;

                    ::SetFilePointer(hFile, 0, 0, FILE_END); // seek to end
                }
                else {
                    ::SetFilePointer(hFile, 0, 0, FILE_BEGIN);
                    ::SetEndOfFile(hFile); // truncate the file
                }

                do {
                    nRead=0;

                    if (::InternetReadFile(hRequest, szBuffer, dwBufferSize,
                            &nRead)) {
                        if (nRead) {
                            DWORD dwNumberOfBytesWritten = NULL;

                            BOOL ret = WriteFile(hFile, szBuffer, nRead,
                                    &dwNumberOfBytesWritten, NULL);

                            if (!ret) {
                                // WriteFile failed
                                if (bUIFeedback) {
                                    if (GetLastError() == ERROR_DISK_FULL) {
                                       bRetryHttpRequest =
                                            (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_DISK_FULL_ERROR,
                                            IDS_DISK_FULL_ERROR_CAPTION,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            wName));
                                    } else {
                                        bRetryHttpRequest =
                                            (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_DISK_WRITE_ERROR,
                                            IDS_DISK_WRITE_ERROR_CAPTION,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            wName));
                                    }
                                    if (!bRetryHttpRequest) {
                                        dwDownloadError = 1;
                                        break;
                                    }
                                }
                                continue;
                            }
                        }

                        dwTotalRead += nRead;

                        // update download progress dialog
                        m_dlg->OnProgress(nRead);
                        // Check if download has been cancelled
                        if (m_dlg->isDownloadCancelled()) {
                            m_dlg->decrementProgressMax(nContentLength,
                                    dwTotalRead);
                            bCancel = TRUE;
                            break;
                        }

                    }
                    else {
                        bCancel = TRUE;
                        break;
                    }
                }
                while (nRead);


                if (bCancel) {
                    // User has cancelled the operation or InternetRead failed
                    // don't do return here, we need to cleanup
                    dwDownloadError = 1;
                    __leave;
                }
            }
            else if (dwStatus == 416 && (fileSize != 0) &&
                    (fileSize != 0xFFFFFFFF)) {
                // This error could be returned, When the full file exists
                // and a range request is sent with range beyond filessize.
                // The best way to fix this is in future is, to send HEAD
                // request and get filelength before sending range request.
                dwDownloadError = 0;
                __leave;
            }
            else if (dwStatus == 403) { // Forbidden from Akamai means we need to get a new download token
                JNIEnv *env = m_dlg->getJNIEnv();
                jclass exceptionClass = env->FindClass("java/net/HttpRetryException");
                if (exceptionClass == NULL) {
                    /* Unable to find the exception class, give up. */
                    __leave;
                }
                jmethodID constructor;
                constructor = env->GetMethodID(exceptionClass,
                               "<init>", "(Ljava/lang/String;I)V");
                if (constructor != NULL) {
                    jobject exception = env->NewObject(exceptionClass,
                            constructor, env->NewStringUTF("Forbidden"),
                            403);
                    env->Throw((jthrowable) exception);
                }
                __leave;
            }
            else if(dwStatus >= 400 && dwStatus < 600) {
                /* NB: Following case seems to be never used!

                   HTTP_STATUS_FORBIDDEN is the same as 403 and
                   403 was specially handled few lines above! */
                if (dwStatus == HTTP_STATUS_FORBIDDEN) {
                    if (bUIFeedback) {
                        bRetryHttpRequest = (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_HTTP_STATUS_FORBIDDEN,
                                            IDS_HTTP_INSTRUCTION_FORBIDDEN,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            L"403"));
                    }
                }
                else if (dwStatus == HTTP_STATUS_SERVER_ERROR) {
                    if (bUIFeedback) {
                       bRetryHttpRequest = (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_HTTP_STATUS_SERVER_ERROR,
                                            IDS_HTTP_INSTRUCTION_UNKNOWN_ERROR,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            L"500"));
                    }
                }
                else if (dwStatus == HTTP_STATUS_SERVICE_UNAVAIL) {
                    if (numberOfRetry < 5) {
                        // If the server is busy, automatically retry

                        // We wait couple seconds before retry to avoid
                        // congestion
                        for (long i = (long) secondsToWait; i >= 0; i--) {
                            // Update status
                            if (bUIFeedback) {
                                char szBuffer[BUFFER_SIZE];
                                ::LoadString(_Module.GetResourceInstance(),
                                        IDS_DOWNLOAD_STATUS_RETRY, szStatus,
                                        BUFFER_SIZE);
                                wsprintf(szBuffer, szStatus, i);

                                ::SetWindowText(hProgressInfo, szBuffer);
                            }

                            // Sleep 1 second
                            ::Sleep(1000);
                        }

                        // We use a semi-binary backoff algorithm to
                        // determine seconds to wait
                        numberOfRetry += 1;
                        secondsToWait = secondsToWait + 30;
                        bRetryHttpRequest = TRUE;

                        continue;
                    }
                    else {
                        if (bUIFeedback) {
                            bRetryHttpRequest = (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_HTTP_STATUS_SERVICE_UNAVAIL,
                                            IDS_HTTP_INSTRUCTION_SERVICE_UNAVAIL,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            L"503"));

                            if (bRetryHttpRequest) {
                                numberOfRetry = 0;
                                secondsToWait = 60;
                                continue;
                            }
                        }
                    }
                }
                else {
                    if (bUIFeedback) {
                        WCHAR szBuffer[10];
                        _snwprintf(szBuffer, 10, L"%d", dwStatus);
                        bRetryHttpRequest = (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_HTTP_STATUS_OTHER,
                                            IDS_HTTP_INSTRUCTION_UNKNOWN_ERROR,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            szBuffer));
                    }
                }
                if (!bRetryHttpRequest) {
                    dwDownloadError = 1;
                }
            }
            else {
                if (bUIFeedback) {
                    WCHAR szBuffer[10];
                    _snwprintf(szBuffer, 10, L"%d", dwStatus);
                    bRetryHttpRequest = (IDRETRY == m_dlg->SafeMessageBox(
                                            IDS_HTTP_STATUS_OTHER,
                                            IDS_HTTP_INSTRUCTION_UNKNOWN_ERROR,
                                            IDS_ERROR_CAPTION,
                                            DIALOG_ERROR_RETRYCANCEL,
                                            szBuffer));
                }
                if (!bRetryHttpRequest) {
                    dwDownloadError = 1;
                }
            }



            // Close HTTP request
            //
            // This is necessary if the HTTP request
            // is retried
            if (hRequest)
                ::InternetCloseHandle(hRequest);
            if (hFile != INVALID_HANDLE_VALUE) {
                ::CloseHandle(hFile);
                hFile = INVALID_HANDLE_VALUE;
            }
        }
        while (bRetryHttpRequest);
    }
    __finally {
        if (hRequest)
            ::InternetCloseHandle(hRequest);

        if (hConnect)
            ::InternetCloseHandle(hConnect);

        if (hOpen)
            ::InternetCloseHandle(hOpen);

        if (hFile != INVALID_HANDLE_VALUE)
            ::CloseHandle(hFile);
    }



    // Exit dialog
    if (dwDownloadError == 0) {
        return S_OK;
    } else {
        DeleteFile(szLocalFile);
        return E_FAIL;
    }
}
