/*
 * Copyright (C) 2007 Apple Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer. 
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution. 
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "EditingDelegate.h"

#include "DumpRenderTree.h"
#include "LayoutTestController.h"
#include <WebCore/COMPtr.h>
#include <wtf/Platform.h>
#include <JavaScriptCore/Assertions.h>
#include <string>
#include <tchar.h>

using std::wstring;

EditingDelegate::EditingDelegate()
    : m_refCount(1)
    , m_acceptsEditing(true)
{
}

// IUnknown
HRESULT STDMETHODCALLTYPE EditingDelegate::QueryInterface(REFIID riid, void** ppvObject)
{
    *ppvObject = 0;
    if (IsEqualGUID(riid, IID_IUnknown))
        *ppvObject = static_cast<IWebEditingDelegate*>(this);
    else if (IsEqualGUID(riid, IID_IWebEditingDelegate))
        *ppvObject = static_cast<IWebEditingDelegate*>(this);
    else
        return E_NOINTERFACE;

    AddRef();
    return S_OK;
}

ULONG STDMETHODCALLTYPE EditingDelegate::AddRef(void)
{
    return ++m_refCount;
}

ULONG STDMETHODCALLTYPE EditingDelegate::Release(void)
{
    ULONG newRef = --m_refCount;
    if (!newRef)
        delete this;

    return newRef;
}

static wstring dumpPath(IDOMNode* node)
{
    ASSERT(node);

    wstring result;

    BSTR name;
    if (FAILED(node->nodeName(&name)))
        return result;
    result.assign(name, SysStringLen(name));
    SysFreeString(name);

    COMPtr<IDOMNode> parent;
    if (SUCCEEDED(node->parentNode(&parent)))
        result += TEXT(" > ") + dumpPath(parent.get());

    return result;
}

static wstring dump(IDOMRange* range)
{
    ASSERT(range);

    int startOffset;
    if (FAILED(range->startOffset(&startOffset)))
        return 0;

    int endOffset;
    if (FAILED(range->endOffset(&endOffset)))
        return 0;

    COMPtr<IDOMNode> startContainer;
    if (FAILED(range->startContainer(&startContainer)))
        return 0;

    COMPtr<IDOMNode> endContainer;
    if (FAILED(range->endContainer(&endContainer)))
        return 0;

    wchar_t buffer[1024];
    _snwprintf(buffer, ARRAYSIZE(buffer), L"range from %ld of %s to %ld of %s", startOffset, dumpPath(startContainer.get()), endOffset, dumpPath(endContainer.get()));
    return buffer;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldBeginEditingInDOMRange( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMRange* range,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldBeginEditingInDOMRange:%s\n"), dump(range));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldEndEditingInDOMRange( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMRange* range,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldEndEditingInDOMRange:%s\n"), dump(range));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldInsertNode( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMNode* node,
    /* [in] */ IDOMRange* range,
    /* [in] */ WebViewInsertAction action)
{
    static LPCTSTR insertactionstring[] = {
        TEXT("WebViewInsertActionTyped"),
        TEXT("WebViewInsertActionPasted"),
        TEXT("WebViewInsertActionDropped"),
    };

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldInsertNode:%s replacingDOMRange:%s givenAction:%s\n"), dumpPath(node), dump(range), insertactionstring[action]);

    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldInsertText( 
    /* [in] */ IWebView* webView,
    /* [in] */ BSTR text,
    /* [in] */ IDOMRange* range,
    /* [in] */ WebViewInsertAction action,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    static LPCTSTR insertactionstring[] = {
        TEXT("WebViewInsertActionTyped"),
        TEXT("WebViewInsertActionPasted"),
        TEXT("WebViewInsertActionDropped"),
    };

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldInsertText:%s replacingDOMRange:%s givenAction:%s\n"), text ? text : TEXT(""), dump(range), insertactionstring[action]);

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldDeleteDOMRange( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMRange* range,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldDeleteDOMRange:%s\n"), dump(range));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldChangeSelectedDOMRange( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMRange* currentRange,
    /* [in] */ IDOMRange* proposedRange,
    /* [in] */ WebSelectionAffinity selectionAffinity,
    /* [in] */ BOOL stillSelecting,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    static LPCTSTR affinitystring[] = {
        TEXT("NSSelectionAffinityUpstream"),
        TEXT("NSSelectionAffinityDownstream")
    };
    static LPCTSTR boolstring[] = {
        TEXT("FALSE"),
        TEXT("TRUE")
    };

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldChangeSelectedDOMRange:%s toDOMRange:%s affinity:%s stillSelecting:%s\n"), dump(currentRange), dump(proposedRange), affinitystring[selectionAffinity], boolstring[stillSelecting]);

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldApplyStyle( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMCSSStyleDeclaration* style,
    /* [in] */ IDOMRange* range,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldApplyStyle:%s toElementsInDOMRange:%s\n"), TEXT("'style description'")/*[[style description] UTF8String]*/, dump(range));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::shouldChangeTypingStyle( 
    /* [in] */ IWebView* webView,
    /* [in] */ IDOMCSSStyleDeclaration* currentStyle,
    /* [in] */ IDOMCSSStyleDeclaration* proposedStyle,
    /* [retval][out] */ BOOL* result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: shouldChangeTypingStyle:%s toStyle:%s\n"), TEXT("'currentStyle description'"), TEXT("'proposedStyle description'"));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::doPlatformCommand( 
    /* [in] */ IWebView *webView,
    /* [in] */ BSTR command,
    /* [retval][out] */ BOOL *result)
{
    if (!result) {
        ASSERT_NOT_REACHED();
        return E_POINTER;
    }

    if (::gLayoutTestController->dumpEditingCallbacks() && !done)
        _tprintf(TEXT("EDITING DELEGATE: doPlatformCommand:%s\n"), command ? command : TEXT(""));

    *result = m_acceptsEditing;
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::webViewDidBeginEditing( 
    /* [in] */ IWebNotification* notification)
{
    if (::gLayoutTestController->dumpEditingCallbacks() && !done) {
        BSTR name;
        notification->name(&name);
        _tprintf(TEXT("EDITING DELEGATE: webViewDidBeginEditing:%s\n"), name ? name : TEXT(""));
        SysFreeString(name);
    }
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::webViewDidChange( 
    /* [in] */ IWebNotification *notification)
{
    if (::gLayoutTestController->dumpEditingCallbacks() && !done) {
        BSTR name;
        notification->name(&name);
        _tprintf(TEXT("EDITING DELEGATE: webViewDidBeginEditing:%s\n"), name ? name : TEXT(""));
        SysFreeString(name);
    }
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::webViewDidEndEditing( 
    /* [in] */ IWebNotification *notification)
{
    if (::gLayoutTestController->dumpEditingCallbacks() && !done) {
        BSTR name;
        notification->name(&name);
        _tprintf(TEXT("EDITING DELEGATE: webViewDidEndEditing:%s\n"), name ? name : TEXT(""));
        SysFreeString(name);
    }
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::webViewDidChangeTypingStyle( 
    /* [in] */ IWebNotification *notification)
{
    if (::gLayoutTestController->dumpEditingCallbacks() && !done) {
        BSTR name;
        notification->name(&name);
        _tprintf(TEXT("EDITING DELEGATE: webViewDidChangeTypingStyle:%s\n"), name ? name : TEXT(""));
        SysFreeString(name);
    }
    return S_OK;
}

HRESULT STDMETHODCALLTYPE EditingDelegate::webViewDidChangeSelection( 
    /* [in] */ IWebNotification *notification)
{
    if (::gLayoutTestController->dumpEditingCallbacks() && !done) {
        BSTR name;
        notification->name(&name);
        _tprintf(TEXT("EDITING DELEGATE: webViewDidChangeSelection:%s\n"), name ? name : TEXT(""));
        SysFreeString(name);
    }
    return S_OK;
}

static int indexOfFirstWordCharacter(const TCHAR* text)
{
    const TCHAR* cursor = text;
    while (*cursor && !iswalpha(*cursor))
        ++cursor;
    return *cursor ? (cursor - text) : -1;
};

static int wordLength(const TCHAR* text)
{
    const TCHAR* cursor = text;
    while (*cursor && iswalpha(*cursor))
        ++cursor;
    return cursor - text;
};

HRESULT STDMETHODCALLTYPE EditingDelegate::checkSpellingOfString(
            /* [in] */ IWebView* view,
            /* [in] */ LPCTSTR text,
            /* [in] */ int length,
            /* [out] */ int* misspellingLocation,
            /* [out] */ int* misspellingLength)
{
    static const TCHAR* misspelledWords[] = {
        // These words are known misspelled words in webkit tests.
        // If there are other misspelled words in webkit tests, please add them in
        // this array.
        TEXT("foo"),
        TEXT("Foo"),
        TEXT("baz"),
        TEXT("fo"),
        TEXT("LibertyF"),
        TEXT("chello"),
        TEXT("xxxtestxxx"),
        TEXT("XXxxx"),
        TEXT("Textx"),
        TEXT("blockquoted"),
        TEXT("asd"),
        TEXT("Lorem"),
        TEXT("Nunc"),
        TEXT("Curabitur"),
        TEXT("eu"),
        TEXT("adlj"),
        TEXT("adaasj"),
        TEXT("sdklj"),
        TEXT("jlkds"),
        TEXT("jsaada"),
        TEXT("jlda"),
        TEXT("zz"),
        TEXT("contentEditable"),
        0,
    };

    wstring textString(text, length);
    int wordStart = indexOfFirstWordCharacter(textString.c_str());
    if (-1 == wordStart)
        return S_OK;
    wstring word = textString.substr(wordStart, wordLength(textString.c_str() + wordStart));
    for (size_t i = 0; misspelledWords[i]; ++i) {
        if (word == misspelledWords[i]) {
            *misspellingLocation = wordStart;
            *misspellingLength = word.size();
            break;
        }
    }

    return S_OK;
}