/*
 * Web Services on Devices
 *
 * Copyright 2017 Owen Rudge for CodeWeavers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include <stdarg.h>

#define COBJMACROS

#include "wsdapi_internal.h"
#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(wsdapi);

LPWSTR duplicate_string(void *parentMemoryBlock, LPCWSTR value)
{
    int valueLen;
    LPWSTR dup;

    valueLen = lstrlenW(value) + 1;

    dup = WSDAllocateLinkedMemory(parentMemoryBlock, valueLen * sizeof(WCHAR));

    if (dup) memcpy(dup, value, valueLen * sizeof(WCHAR));
    return dup;
}

static WSDXML_NAMESPACE *duplicate_namespace(void *parentMemoryBlock, WSDXML_NAMESPACE *ns)
{
    WSDXML_NAMESPACE *newNs;

    newNs = WSDAllocateLinkedMemory(parentMemoryBlock, sizeof(WSDXML_NAMESPACE));

    if (newNs == NULL)
    {
        return NULL;
    }

    newNs->Encoding = ns->Encoding;

    /* On Windows, both Names and NamesCount are set to null even if there are names present */
    newNs->NamesCount = 0;
    newNs->Names = NULL;

    newNs->PreferredPrefix = duplicate_string(newNs, ns->PreferredPrefix);
    newNs->Uri = duplicate_string(newNs, ns->Uri);

    return newNs;
}

WSDXML_NAME *duplicate_name(void *parentMemoryBlock, WSDXML_NAME *name)
{
    WSDXML_NAME *dup;

    dup = WSDAllocateLinkedMemory(parentMemoryBlock, sizeof(WSDXML_NAME));

    if (dup == NULL)
    {
        return NULL;
    }

    dup->Space = duplicate_namespace(dup, name->Space);
    dup->LocalName = duplicate_string(dup, name->LocalName);

    if (dup->LocalName == NULL)
    {
        WSDFreeLinkedMemory(dup);
        return NULL;
    }

    return dup;
}

HRESULT WINAPI WSDXMLAddChild(WSDXML_ELEMENT *pParent, WSDXML_ELEMENT *pChild)
{
    WSDXML_NODE *currentNode;

    TRACE("(%p, %p)\n", pParent, pChild);

    if ((pParent == NULL) || (pChild == NULL) || (pChild->Node.Parent != NULL))
    {
        return E_INVALIDARG;
    }

    /* See if the parent already has a child */
    currentNode = pParent->FirstChild;

    if (currentNode == NULL)
    {
        pParent->FirstChild = (WSDXML_NODE *)pChild;
    }
    else
    {
        /* Find the last sibling node and make this child the next sibling */
        WSDXMLAddSibling((WSDXML_ELEMENT *)currentNode, pChild);
    }

    pChild->Node.Parent = pParent;

    /* Link the memory allocations */
    WSDAttachLinkedMemory(pParent, pChild);

    return S_OK;
}

HRESULT WINAPI WSDXMLAddSibling(WSDXML_ELEMENT *pFirst, WSDXML_ELEMENT *pSecond)
{
    WSDXML_NODE *currentNode;

    TRACE("(%p, %p)\n", pFirst, pSecond);

    if ((pFirst == NULL) || (pSecond == NULL))
    {
        return E_INVALIDARG;
    }

    /* See if the first node already has a sibling */
    currentNode = pFirst->Node.Next;

    if (currentNode == NULL)
    {
        pFirst->Node.Next = (WSDXML_NODE *)pSecond;
    }
    else
    {
        /* Find the last sibling node and make the second element the next sibling */
        while (1)
        {
            if (currentNode->Next == NULL)
            {
                currentNode->Next = (WSDXML_NODE *)pSecond;
                break;
            }

            currentNode = currentNode->Next;
        }
    }

    /* Reparent the second node under the first */
    pSecond->Node.Parent = pFirst->Node.Parent;

    /* Link the memory allocations */
    WSDAttachLinkedMemory(pFirst->Node.Parent, pSecond);

    return S_OK;
}

HRESULT WINAPI WSDXMLBuildAnyForSingleElement(WSDXML_NAME *pElementName, LPCWSTR pszText, WSDXML_ELEMENT **ppAny)
{
    WSDXML_TEXT *child;

    TRACE("(%p, %s, %p)\n", pElementName, debugstr_w(pszText), ppAny);

    if ((pElementName == NULL) || ((pszText != NULL) && (lstrlenW(pszText) > WSD_MAX_TEXT_LENGTH)))
    {
        return E_INVALIDARG;
    }

    if (ppAny == NULL)
    {
        return E_POINTER;
    }

    *ppAny = WSDAllocateLinkedMemory(NULL, sizeof(WSDXML_ELEMENT));

    if (*ppAny == NULL)
    {
        return E_OUTOFMEMORY;
    }

    ZeroMemory(*ppAny, sizeof(WSDXML_ELEMENT));

    (*ppAny)->Name = duplicate_name(*ppAny, pElementName);

    if ((*ppAny)->Name == NULL)
    {
        WSDFreeLinkedMemory(*ppAny);
        return E_OUTOFMEMORY;
    }

    if (pszText != NULL)
    {
        child = WSDAllocateLinkedMemory(*ppAny, sizeof(WSDXML_TEXT));

        if (child == NULL)
        {
            WSDFreeLinkedMemory(*ppAny);
            return E_OUTOFMEMORY;
        }

        child->Node.Parent = *ppAny;
        child->Node.Next = NULL;
        child->Node.Type = TextType;
        child->Text = duplicate_string(child, pszText);

        if (child->Text == NULL)
        {
            WSDFreeLinkedMemory(*ppAny);
            return E_OUTOFMEMORY;
        }

        (*ppAny)->FirstChild = (WSDXML_NODE *)child;
    }

    return S_OK;
}

HRESULT WINAPI WSDXMLCleanupElement(WSDXML_ELEMENT *pAny)
{
    TRACE("(%p)\n", pAny);

    if (pAny == NULL)
    {
        return E_INVALIDARG;
    }

    WSDFreeLinkedMemory(pAny);
    return S_OK;
}

HRESULT WINAPI WSDXMLGetValueFromAny(const WCHAR *pszNamespace, const WCHAR *pszName, WSDXML_ELEMENT *pAny, LPCWSTR *ppszValue)
{
    WSDXML_ELEMENT *element;
    WSDXML_TEXT *text;

    if (pAny == NULL)
        return E_INVALIDARG;

    if (ppszValue == NULL)
        return E_POINTER;

    if ((pszNamespace == NULL) || (pszName == NULL) || (lstrlenW(pszNamespace) > WSD_MAX_TEXT_LENGTH) || (lstrlenW(pszName) > WSD_MAX_TEXT_LENGTH))
        return E_INVALIDARG;

    element = pAny;

    while (element != NULL)
    {
        if (element->Node.Type == ElementType)
        {
            if ((lstrcmpW(element->Name->LocalName, pszName) == 0) && (lstrcmpW(element->Name->Space->Uri, pszNamespace) == 0))
            {
                if ((element->FirstChild == NULL) || (element->FirstChild->Type != TextType))
                {
                    return E_FAIL;
                }

                text = (WSDXML_TEXT *) element->FirstChild;
                *ppszValue = (LPCWSTR) text->Text;

                return S_OK;
            }
        }

        element = (WSDXML_ELEMENT *) element->Node.Next;
    }

    return E_FAIL;
}

/* IWSDXMLContext implementation */

struct xmlNamespace
{
    struct list entry;
    WSDXML_NAMESPACE *namespace;
};

typedef struct IWSDXMLContextImpl
{
    IWSDXMLContext IWSDXMLContext_iface;
    LONG ref;

    struct list *namespaces;
    int nextUnknownPrefix;
} IWSDXMLContextImpl;

static WSDXML_NAMESPACE *find_namespace(struct list *namespaces, LPCWSTR uri)
{
    struct xmlNamespace *ns;

    LIST_FOR_EACH_ENTRY(ns, namespaces, struct xmlNamespace, entry)
    {
        if (lstrcmpW(ns->namespace->Uri, uri) == 0)
        {
            return ns->namespace;
        }
    }

    return NULL;
}

static WSDXML_NAME *find_name(WSDXML_NAMESPACE *ns, LPCWSTR name)
{
    int i;

    for (i = 0; i < ns->NamesCount; i++)
    {
        if (lstrcmpW(ns->Names[i].LocalName, name) == 0)
        {
            return &ns->Names[i];
        }
    }

    return NULL;
}

static WSDXML_NAME *add_name(WSDXML_NAMESPACE *ns, LPCWSTR name)
{
    WSDXML_NAME *names;
    WSDXML_NAME *newName;
    int i;

    names = WSDAllocateLinkedMemory(ns, sizeof(WSDXML_NAME) * (ns->NamesCount + 1));

    if (names == NULL)
    {
        return NULL;
    }

    if (ns->NamesCount > 0)
    {
        /* Copy the existing names array over to the new allocation */
        memcpy(names, ns->Names, sizeof(WSDXML_NAME) * ns->NamesCount);

        for (i = 0; i < ns->NamesCount; i++)
        {
            /* Attach the local name memory to the new names allocation */
            WSDAttachLinkedMemory(names, names[i].LocalName);
        }

        WSDFreeLinkedMemory(ns->Names);
    }

    ns->Names = names;

    newName = &names[ns->NamesCount];

    newName->LocalName = duplicate_string(names, name);
    newName->Space = ns;

    if (newName->LocalName == NULL)
    {
        return NULL;
    }

    ns->NamesCount++;
    return newName;
}

static BOOL is_prefix_unique(struct list *namespaces, LPCWSTR prefix)
{
    struct xmlNamespace *ns;

    LIST_FOR_EACH_ENTRY(ns, namespaces, struct xmlNamespace, entry)
    {
        if (lstrcmpW(ns->namespace->PreferredPrefix, prefix) == 0)
        {
            return FALSE;
        }
    }

    return TRUE;
}

static LPWSTR generate_namespace_prefix(IWSDXMLContextImpl *impl, void *parentMemoryBlock, LPCWSTR uri)
{
    WCHAR formatString[] = { 'u','n','%','d', 0 };
    WCHAR suggestedPrefix[7];

    /* Find a unique prefix */
    while (impl->nextUnknownPrefix < 1000)
    {
        wsprintfW(suggestedPrefix, formatString, impl->nextUnknownPrefix++);

        /* For the unlikely event where somebody has explicitly created a prefix called 'unX', check it is unique */
        if (is_prefix_unique(impl->namespaces, suggestedPrefix))
        {
            return duplicate_string(parentMemoryBlock, suggestedPrefix);
        }
    }

    return NULL;
}

static WSDXML_NAMESPACE *add_namespace(struct list *namespaces, LPCWSTR uri)
{
    struct xmlNamespace *ns = WSDAllocateLinkedMemory(namespaces, sizeof(struct xmlNamespace));

    if (ns == NULL)
    {
        return NULL;
    }

    ns->namespace = WSDAllocateLinkedMemory(ns, sizeof(WSDXML_NAMESPACE));

    if (ns->namespace == NULL)
    {
        WSDFreeLinkedMemory(ns);
        return NULL;
    }

    ZeroMemory(ns->namespace, sizeof(WSDXML_NAMESPACE));
    ns->namespace->Uri = duplicate_string(ns->namespace, uri);

    if (ns->namespace->Uri == NULL)
    {
        WSDFreeLinkedMemory(ns);
        return NULL;
    }

    list_add_tail(namespaces, &ns->entry);
    return ns->namespace;
}

static inline IWSDXMLContextImpl *impl_from_IWSDXMLContext(IWSDXMLContext *iface)
{
    return CONTAINING_RECORD(iface, IWSDXMLContextImpl, IWSDXMLContext_iface);
}

static HRESULT WINAPI IWSDXMLContextImpl_QueryInterface(IWSDXMLContext *iface, REFIID riid, void **ppv)
{
    IWSDXMLContextImpl *This = impl_from_IWSDXMLContext(iface);

    TRACE("(%p, %s, %p)\n", This, debugstr_guid(riid), ppv);

    if (!ppv)
    {
        WARN("Invalid parameter\n");
        return E_INVALIDARG;
    }

    *ppv = NULL;

    if (IsEqualIID(riid, &IID_IUnknown) ||
        IsEqualIID(riid, &IID_IWSDXMLContext))
    {
        *ppv = &This->IWSDXMLContext_iface;
    }
    else
    {
        WARN("Unknown IID %s\n", debugstr_guid(riid));
        return E_NOINTERFACE;
    }

    IUnknown_AddRef((IUnknown*)*ppv);
    return S_OK;
}

static ULONG WINAPI IWSDXMLContextImpl_AddRef(IWSDXMLContext *iface)
{
    IWSDXMLContextImpl *This = impl_from_IWSDXMLContext(iface);
    ULONG ref = InterlockedIncrement(&This->ref);

    TRACE("(%p) ref=%d\n", This, ref);
    return ref;
}

static ULONG WINAPI IWSDXMLContextImpl_Release(IWSDXMLContext *iface)
{
    IWSDXMLContextImpl *This = impl_from_IWSDXMLContext(iface);
    ULONG ref = InterlockedDecrement(&This->ref);

    TRACE("(%p) ref=%d\n", This, ref);

    if (ref == 0)
    {
        WSDFreeLinkedMemory(This);
    }

    return ref;
}

static HRESULT WINAPI IWSDXMLContextImpl_AddNamespace(IWSDXMLContext *iface, LPCWSTR pszUri, LPCWSTR pszSuggestedPrefix, WSDXML_NAMESPACE **ppNamespace)
{
    IWSDXMLContextImpl *This = impl_from_IWSDXMLContext(iface);
    WSDXML_NAMESPACE *ns;
    LPCWSTR newPrefix = NULL;
    BOOL setNewPrefix;

    TRACE("(%p, %s, %s, %p)\n", This, debugstr_w(pszUri), debugstr_w(pszSuggestedPrefix), ppNamespace);

    if ((pszUri == NULL) || (pszSuggestedPrefix == NULL) || (lstrlenW(pszUri) > WSD_MAX_TEXT_LENGTH) ||
        (lstrlenW(pszSuggestedPrefix) > WSD_MAX_TEXT_LENGTH))
    {
        return E_INVALIDARG;
    }

    ns = find_namespace(This->namespaces, pszUri);

    if (ns == NULL)
    {
        ns = add_namespace(This->namespaces, pszUri);

        if (ns == NULL)
        {
            return E_OUTOFMEMORY;
        }
    }

    setNewPrefix = (ns->PreferredPrefix == NULL);

    if ((ns->PreferredPrefix == NULL) || (lstrcmpW(ns->PreferredPrefix, pszSuggestedPrefix) != 0))
    {
        newPrefix = pszSuggestedPrefix;
        setNewPrefix = TRUE;
    }

    if (setNewPrefix)
    {
        WSDFreeLinkedMemory((void *)ns->PreferredPrefix);
        ns->PreferredPrefix = NULL;

        if ((newPrefix != NULL) && (is_prefix_unique(This->namespaces, newPrefix)))
        {
            ns->PreferredPrefix = duplicate_string(ns, newPrefix);
        }
        else
        {
            ns->PreferredPrefix = generate_namespace_prefix(This, ns, pszUri);
            if (ns->PreferredPrefix == NULL)
            {
                return E_FAIL;
            }
        }
    }

    if (ppNamespace != NULL)
    {
        *ppNamespace = duplicate_namespace(NULL, ns);

        if (*ppNamespace == NULL)
        {
            return E_OUTOFMEMORY;
        }
    }

    return S_OK;
}

static HRESULT WINAPI IWSDXMLContextImpl_AddNameToNamespace(IWSDXMLContext *iface, LPCWSTR pszUri, LPCWSTR pszName, WSDXML_NAME **ppName)
{
    IWSDXMLContextImpl *This = impl_from_IWSDXMLContext(iface);
    WSDXML_NAMESPACE *ns;
    WSDXML_NAME *name;

    TRACE("(%p, %s, %s, %p)\n", This, debugstr_w(pszUri), debugstr_w(pszName), ppName);

    if ((pszUri == NULL) || (pszName == NULL) || (lstrlenW(pszUri) > WSD_MAX_TEXT_LENGTH) || (lstrlenW(pszName) > WSD_MAX_TEXT_LENGTH))
    {
        return E_INVALIDARG;
    }

    ns = find_namespace(This->namespaces, pszUri);

    if (ns == NULL)
    {
        /* The namespace doesn't exist, add it */
        ns = add_namespace(This->namespaces, pszUri);

        if (ns == NULL)
        {
            return E_OUTOFMEMORY;
        }

        ns->PreferredPrefix = generate_namespace_prefix(This, ns, pszUri);
        if (ns->PreferredPrefix == NULL)
        {
            return E_FAIL;
        }
    }

    name = find_name(ns, pszName);

    if (name == NULL)
    {
        name = add_name(ns, pszName);

        if (name == NULL)
        {
            return E_OUTOFMEMORY;
        }
    }

    if (ppName != NULL)
    {
        *ppName = duplicate_name(NULL, name);

        if (*ppName == NULL)
        {
            return E_OUTOFMEMORY;
        }
    }

    return S_OK;
}

static HRESULT WINAPI IWSDXMLContextImpl_SetNamespaces(IWSDXMLContext *iface, const PCWSDXML_NAMESPACE *pNamespaces, WORD wNamespacesCount, BYTE bLayerNumber)
{
    FIXME("(%p, %p, %d, %d)\n", iface, pNamespaces, wNamespacesCount, bLayerNumber);
    return E_NOTIMPL;
}

static HRESULT WINAPI IWSDXMLContextImpl_SetTypes(IWSDXMLContext *iface, const PCWSDXML_TYPE *pTypes, DWORD dwTypesCount, BYTE bLayerNumber)
{
    FIXME("(%p, %p, %d, %d)\n", iface, pTypes, dwTypesCount, bLayerNumber);
    return E_NOTIMPL;
}

static const IWSDXMLContextVtbl xmlcontext_vtbl =
{
    IWSDXMLContextImpl_QueryInterface,
    IWSDXMLContextImpl_AddRef,
    IWSDXMLContextImpl_Release,
    IWSDXMLContextImpl_AddNamespace,
    IWSDXMLContextImpl_AddNameToNamespace,
    IWSDXMLContextImpl_SetNamespaces,
    IWSDXMLContextImpl_SetTypes
};

HRESULT WINAPI WSDXMLCreateContext(IWSDXMLContext **ppContext)
{
    IWSDXMLContextImpl *obj;

    TRACE("(%p)\n", ppContext);

    if (ppContext == NULL)
    {
        WARN("Invalid parameter: ppContext == NULL\n");
        return E_POINTER;
    }

    *ppContext = NULL;

    obj = WSDAllocateLinkedMemory(NULL, sizeof(*obj));

    if (!obj)
    {
        return E_OUTOFMEMORY;
    }

    obj->IWSDXMLContext_iface.lpVtbl = &xmlcontext_vtbl;
    obj->ref = 1;
    obj->namespaces = WSDAllocateLinkedMemory(obj, sizeof(struct list));
    obj->nextUnknownPrefix = 0;

    if (obj->namespaces == NULL)
    {
        WSDFreeLinkedMemory(obj);
        return E_OUTOFMEMORY;
    }

    list_init(obj->namespaces);

    *ppContext = &obj->IWSDXMLContext_iface;
    TRACE("Returning iface %p\n", *ppContext);

    return S_OK;
}