/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * 
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsAbLDAPAutoCompFormatter.h"
#include "nsIAutoCompleteResults.h"
#include "nsServiceManagerUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsIMsgHeaderParser.h"
#include "nsILDAPMessage.h"
#include "nsIStringBundle.h"
#include "nsXPCOM.h"
#include "nsISupportsPrimitives.h"
#include "nsError.h"
#include "nsMemory.h"
#include "nsILDAPErrors.h"
#include "nsMsgUtils.h"
#include "mozilla/Services.h"

#define LDAP_ERROR_BUNDLE "chrome://mozldap/locale/ldap.properties"
#define LDAP_AUTOCOMPLETE_ERROR_BUNDLE "chrome://messenger/locale/addressbook/ldapAutoCompErrs.properties"

NS_IMPL_ISUPPORTS2(nsAbLDAPAutoCompFormatter, 
		   nsILDAPAutoCompFormatter, 
		   nsIAbLDAPAutoCompFormatter)

nsAbLDAPAutoCompFormatter::nsAbLDAPAutoCompFormatter() :
    mNameFormat(NS_LITERAL_STRING("[cn]")),
    mAddressFormat(NS_LITERAL_STRING("{mail}"))
{
}

nsAbLDAPAutoCompFormatter::~nsAbLDAPAutoCompFormatter()
{
}

NS_IMETHODIMP
nsAbLDAPAutoCompFormatter::Format(nsILDAPMessage *aMsg, 
                                  nsIAutoCompleteItem **aItem) 
{
    nsresult rv;

    nsCOMPtr<nsIMsgHeaderParser> msgHdrParser = 
        do_GetService("@mozilla.org/messenger/headerparser;1", &rv);
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::Format(): do_GetService()"
                 " failed trying to obtain"
                 " '@mozilla.org/messenger/headerparser;1'");
        return NS_ERROR_NOT_AVAILABLE;
    }

    // generate the appropriate string
    //
    nsAutoCString name;
    rv = ProcessFormat(mNameFormat, aMsg, &name, 0);
    if (NS_FAILED(rv)) {
        // Something went wrong lower down the stack; a message should
        // have already been logged there.  Return an error, rather
        // than trying to generate a bogus nsIAutoCompleteItem
        //
        return rv;
    }

    nsAutoCString address;
    rv = ProcessFormat(mAddressFormat, aMsg, &address, 0);
    if (NS_FAILED(rv)) {
        // Something went wrong lower down the stack; a message should have 
        // already been logged there.  Return an error, rather than trying to 
        // generate a bogus nsIAutoCompleteItem.
        //
        return rv;
    }

    nsCString value;
    rv = msgHdrParser->MakeFullAddressString(name.get(), address.get(), 
                                             getter_Copies(value));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::Format(): call to"
                 " MakeFullAddressString() failed");
        return rv;
    }

    // create an nsIAutoCompleteItem to hold the returned value
    //
    nsCOMPtr<nsIAutoCompleteItem> item = do_CreateInstance(
        NS_AUTOCOMPLETEITEM_CONTRACTID, &rv);

    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::Format(): couldn't"
                 " create " NS_AUTOCOMPLETEITEM_CONTRACTID "\n");
        return NS_ERROR_NOT_AVAILABLE;
    }

    // this is that part that actually gets autocompleted to
    //
    rv = item->SetValue(NS_ConvertUTF8toUTF16(value));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::Format(): "
                 "item->SetValue failed");
        return rv;
    }

    // generate the appropriate string to appear as a comment off to the side
    //
    nsAutoCString comment;
    rv = ProcessFormat(mCommentFormat, aMsg, &comment, 0);
    if (NS_SUCCEEDED(rv)) {
        rv = item->SetComment(NS_ConvertUTF8toUTF16(comment).get());
        if (NS_FAILED(rv)) {
            NS_WARNING("nsAbLDAPAutoCompFormatter::Format():"
                       " item->SetComment() failed");
        }
    }

    rv = item->SetClassName("remote-abook");
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompleteFormatter::Format():"
                   " item->SetClassName() failed");
    }

    // all done; return the item
    NS_IF_ADDREF(*aItem = item);
    return NS_OK;
}

NS_IMETHODIMP
nsAbLDAPAutoCompFormatter::FormatException(int32_t aState, 
                                           nsresult aErrorCode, 
                                           nsIAutoCompleteItem **aItem) 
{
    int32_t errorKey;
    nsresult rv;

    // create an nsIAutoCompleteItem to hold the returned value
    //
    nsCOMPtr<nsIAutoCompleteItem> item = do_CreateInstance(
        NS_AUTOCOMPLETEITEM_CONTRACTID, &rv);

    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::FormatException(): couldn't"
                 " create " NS_AUTOCOMPLETEITEM_CONTRACTID "\n");
        return NS_ERROR_NOT_AVAILABLE;
    }

    // get the string bundle service
    //
    nsString errMsg, ldapErrMsg, alertMsg, ldapHint;
    nsString errCodeNum;

    nsCOMPtr<nsIStringBundleService> stringBundleSvc =
        mozilla::services::GetStringBundleService();
    if (stringBundleSvc) {
        NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException():"
                 " error getting string bundle service");
        return NS_ERROR_UNEXPECTED;
    }

    // get the string bundles relevant here: the main LDAP bundle,
    // and the ldap AutoCompletion-specific bundle
    //
    nsCOMPtr<nsIStringBundle> ldapBundle, ldapACBundle;

    rv = stringBundleSvc->CreateBundle(
        LDAP_ERROR_BUNDLE,
        getter_AddRefs(ldapBundle));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException():"
                 " error creating string bundle "
                 LDAP_ERROR_BUNDLE);
        return rv;
    } 

    rv = stringBundleSvc->CreateBundle(
        LDAP_AUTOCOMPLETE_ERROR_BUNDLE,
        getter_AddRefs(ldapACBundle));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException():"
                 " error creating string bundle "
                 LDAP_AUTOCOMPLETE_ERROR_BUNDLE);
        return rv;
    }

    // get the general error that goes in the dropdown and the window
    // title
    //
    rv = ldapACBundle->GetStringFromID(aState, getter_Copies(errMsg));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException():"
                 " error getting general error from bundle "
                 LDAP_AUTOCOMPLETE_ERROR_BUNDLE);
        return rv;
    }

    // for LDAP errors
    //
    if (NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_LDAP) {

        errorKey = NS_ERROR_GET_CODE(aErrorCode);

        // put the number itself in string form
        //
        errCodeNum.AppendInt(errorKey);

        // get the LDAP error message itself
        //
        rv = ldapBundle->GetStringFromID(NS_ERROR_GET_CODE(aErrorCode), 
                                                getter_Copies(ldapErrMsg));
        if (NS_FAILED(rv)) {
            NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException"
                     "(): error getting string 2 from bundle "
                     LDAP_ERROR_BUNDLE);
            return rv;
        }
  
    } else {

        // put the entire nsresult in string form
        //
        errCodeNum.AppendLiteral("0x");
        errCodeNum.AppendInt(static_cast<uint32_t>(aErrorCode), 16);

        // figure out the key to index into the string bundle
        //
        const int32_t HOST_NOT_FOUND_ERROR=5000;
        const int32_t GENERIC_ERROR=9999;
        errorKey = ( (aErrorCode == NS_ERROR_UNKNOWN_HOST) ? 
                     HOST_NOT_FOUND_ERROR : GENERIC_ERROR );

        // get the specific error message itself
        rv = ldapACBundle->GetStringFromID(errorKey,
                                           getter_Copies(ldapErrMsg));
        if (NS_FAILED(rv)) {
            NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException"
                     "(): error getting specific non LDAP error-string "
                     "from bundle "
                     LDAP_AUTOCOMPLETE_ERROR_BUNDLE);
            return rv;
        }
    }

    // and try to find a hint about what the user should do next
    //
    const int32_t HINT_BASE=10000;
    const int32_t GENERIC_HINT_CODE = 9999;
    rv = ldapACBundle->GetStringFromID(HINT_BASE + errorKey, 
                                       getter_Copies(ldapHint));
    if (NS_FAILED(rv)) {
        rv = ldapACBundle->GetStringFromID(HINT_BASE + GENERIC_HINT_CODE,
                                           getter_Copies(ldapHint));
        if (NS_FAILED(rv)) {
            NS_ERROR("nsAbLDAPAutoCompleteFormatter::FormatException()"
                     "(): error getting hint string from bundle "
                     LDAP_AUTOCOMPLETE_ERROR_BUNDLE);
            return rv;
        }
    }
        
    const PRUnichar *stringParams[3] = { errCodeNum.get(),
                                         ldapErrMsg.get(), ldapHint.get() };

    rv = ldapACBundle->FormatStringFromName(
        NS_LITERAL_STRING("errorAlertFormat").get(), stringParams, 3,
        getter_Copies(alertMsg));
    if (NS_FAILED(rv)) {
        NS_WARNING("Failed to format warning string\n");
    }

    // put the error message, between angle brackets, into the XPIDL |value|
    // attribute.  Note that the hardcoded string is only used since 
    // stringbundles have already failed us.
    //
    if (errMsg.Length()) {
        nsString tErrMsg(NS_LITERAL_STRING("<"));
        tErrMsg.Append(errMsg);
        tErrMsg.AppendLiteral(">");
        rv = item->SetValue(tErrMsg);
    } else {
        rv = item->SetValue(
            NS_LITERAL_STRING("<Unknown LDAP autocompletion error>"));
    }

    if (NS_FAILED(rv)) {
        NS_ERROR("nsAbLDAPAutoCompFormatter::FormatException(): "
                 "item->SetValue failed");
        return rv;
    }
    
    // pass the alert message in as the param; if that fails, proceed anyway
    //
    nsCOMPtr<nsISupportsString> alert(do_CreateInstance(
                                           NS_SUPPORTS_STRING_CONTRACTID,
                                           &rv));
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompFormatter::FormatException(): "
                   "could not create nsISupportsString");
    } else {
        rv = alert->SetData(alertMsg);
        if (NS_FAILED(rv)) {
            NS_WARNING("nsAbLDAPAutoCompFormatter::FormatException(): "
                     "alert.Set() failed");
        } else {
            rv = item->SetParam(alert);
            if (NS_FAILED(rv)) {
                NS_WARNING("nsAbLDAPAutoCompFormatter::FormatException(): "
                           "item->SetParam failed");
            }
        }
    }

    // this is a remote addressbook, set the class name so the autocomplete 
    // item can be styled to show this
    //
    rv = item->SetClassName("remote-err");
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompleteFormatter::FormatException():"
                   " item->SetClassName() failed");
    }

    // all done; return the item
    //
    NS_IF_ADDREF(*aItem = item);
    return NS_OK;
}

NS_IMETHODIMP
nsAbLDAPAutoCompFormatter::GetAttributes(nsACString &aResult)
{
    nsAutoCString searchAttrs;
    nsresult rv = ProcessFormat(mNameFormat, 0, 0, &searchAttrs);
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompFormatter::SetNameFormat(): "
                   "ProcessFormat() failed");
        return rv;
    }
    rv = ProcessFormat(mAddressFormat, 0, 0, &searchAttrs);
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompFormatter::SetNameFormat(): "
                   "ProcessFormat() failed");
        return rv;
    }
    rv = ProcessFormat(mCommentFormat, 0, 0, &searchAttrs);
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompFormatter::SetNameFormat(): "
                   "ProcessFormat() failed");
        return rv;
    }

    aResult = searchAttrs;
    return NS_OK;
}

// parse and process a formatting attribute.
// Generate an autocomplete value from the
// information in aMessage and append it to aValue.  Any errors
// (including failure to find a required attribute while building up aValue) 
// return an NS_ERROR_* up the stack so that the caller doesn't try and
// generate an nsIAutoCompleteItem from this.
//
nsresult
nsAbLDAPAutoCompFormatter::ProcessFormat(const nsAString & aFormat,
                                         nsILDAPMessage *aMessage, 
                                         nsACString *aValue,
                                         nsCString *aAttrs)
{
    nsresult rv;    // temp for return values

    // get some iterators to parse aFormat
    //
    const PRUnichar *iter = aFormat.BeginReading();
    const PRUnichar *iterEnd = aFormat.EndReading();

    // get the console service for error logging
    //
    nsCOMPtr<nsIConsoleService> consoleSvc = 
        do_GetService("@mozilla.org/consoleservice;1", &rv);
    if (NS_FAILED(rv)) {
        NS_WARNING("nsAbLDAPAutoCompFormatter::ProcessFormat(): "
                   "couldn't get console service");
    }

    bool attrRequired = false;     // is this attr required or optional?
    nsAutoCString attrName;             // current attr to get

    // parse until we hit the end of the string
    //
    while (iter != iterEnd) {

        switch (*iter) {            // process the next char

        case PRUnichar('{'):

            attrRequired = true;  // this attribute is required

            /*FALLTHROUGH*/

        case PRUnichar('['):

            rv = ParseAttrName(&iter, iterEnd, attrRequired,
                               consoleSvc, attrName);

            if ( NS_FAILED(rv) ) {

                // something unrecoverable happened; stop parsing and 
                // propagate the error up the stack
                //
                return rv;
            }

            // If we're building a string of attributes...
            if (aAttrs) { 
                // ...and it doesn't already contain this string
                if (aAttrs->Find(attrName, CaseInsensitiveCompare) == kNotFound)
                {
                    // add it
                    if (!aAttrs->IsEmpty())
                        aAttrs->Append(',');
                    aAttrs->Append(attrName);
                }
            } else {

                // otherwise, append the first value of this attr to aValue
                // XXXdmose should do better than this; bug 76595

                rv = AppendFirstAttrValue(attrName, aMessage, attrRequired, 
                                          *aValue);
                if ( NS_FAILED(rv) ) {

                    // something unrecoverable happened; stop parsing and 
                    // propagate the error up the stack
                    //
                    return rv;
                }
            }

            attrName.Truncate();     // clear out for next pass
            attrRequired = false; // reset to the default for the next pass

            break;

        case PRUnichar('\\'):

            // advance the iterator and be sure we haven't run off the end
            //
            ++iter;
            if (iter == iterEnd) {

                // abort; missing escaped char
                //
                if (consoleSvc) {
                    consoleSvc->LogStringMessage(
                        NS_LITERAL_STRING(
                            "LDAP addressbook autocomplete formatter: error parsing format string: premature end of string after \\ escape").get());

                    NS_ERROR("LDAP addressbook autocomplete formatter: error "
                             "parsing format string: premature end of string "
                             "after \\ escape");
                }

                return NS_ERROR_ILLEGAL_VALUE;
            }

            /*FALLTHROUGH*/

        default:
            
            // if we're not just building an array of attribute names, append
            // this character to the item we're generating.
            //
            if (!aAttrs) {

                // this character gets treated as a literal
                //
                aValue->Append(NS_ConvertUTF16toUTF8(iter, 1));
            }
        }

        ++iter; // advance the iterator
    }

    return NS_OK;
}

nsresult 
nsAbLDAPAutoCompFormatter::ParseAttrName(
    const PRUnichar **aIter,                     // iterators for mOutputString
    const PRUnichar *aIterEnd, 
    bool aAttrRequired,                       // required?  or just optional?
    nsCOMPtr<nsIConsoleService> &aConsoleSvc,   // no need to reacquire this
    nsACString &aAttrName)                      // attribute token
{
    // reset attrname, and move past the opening brace
    //
    ++(*aIter);

    // get the rest of the attribute name
    //
    do {

        // be sure we haven't run off the end
        //
        if (*aIter == aIterEnd) {

            // abort; missing closing delimiter
            //
            if (aConsoleSvc) {
                aConsoleSvc->LogStringMessage(
                    NS_LITERAL_STRING(
                        "LDAP address book autocomplete formatter: error parsing format string: missing } or ]").get());

                NS_ERROR("LDAP address book autocomplete formatter: error "
                         "parsing format string: missing } or ]");
            }

            return NS_ERROR_ILLEGAL_VALUE;

        } else if ( (aAttrRequired && **aIter == PRUnichar('}')) ||
                    (!aAttrRequired && **aIter == PRUnichar(']')) ) {

            // done with this attribute
            //
            break;

        } else {

            // this must be part of the attribute name
            //
            aAttrName.Append(static_cast<char>(**aIter));
        }

        ++(*aIter);

    } while (1);

    return NS_OK;
}

nsresult
nsAbLDAPAutoCompFormatter::AppendFirstAttrValue(
    const nsACString &aAttrName, // attr to get
    nsILDAPMessage *aMessage, // msg to get values from
    bool aAttrRequired, // is this a required value?
    nsACString &aValue)
{
    // get the attribute values for the field which will be used 
    // to fill in nsIAutoCompleteItem::value
    //
    uint32_t numVals;
    PRUnichar **values;

    nsresult rv;
    rv = aMessage->GetValues(nsCString(aAttrName).get(), &numVals, 
                             &values);
    if (NS_FAILED(rv)) {

        switch (rv) {
        case NS_ERROR_LDAP_DECODING_ERROR:
            // this may not be an error, per se; it could just be that the 
            // requested attribute does not exist in this particular message,
            // either because we didn't request it with the search operation,
            // or because it doesn't exist on the server.
            //
            break;

        case NS_ERROR_OUT_OF_MEMORY:
        case NS_ERROR_UNEXPECTED:
            break;

        default:
            NS_ERROR("nsLDAPAutoCompleteSession::OnLDAPSearchEntry(): "
                     "unexpected return code from aMessage->getValues()");
            rv = NS_ERROR_UNEXPECTED;
            break;
        }

        // if this was a required attribute, don't append anything to aValue
        // and return the error code
        //
        if (aAttrRequired) {
            return rv;
        } else {
            // otherwise forget about this attribute, but return NS_OK, which
            // will cause our caller to continue processing nameFormat in 
            // order to generate an nsIAutoCompleteItem.
            //
            return NS_OK;
        }
    }

    // append the value to our string; then free the array of results
    //
    aValue.Append(NS_ConvertUTF16toUTF8(values[0]));
    NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(numVals, values);

    // if this attribute wasn't required, we fall through to here, and return 
    // ok
    //
    return NS_OK;
}

// attribute AString nameFormat;
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::GetNameFormat(nsAString & aNameFormat)
{
    aNameFormat  = mNameFormat;
    return NS_OK;
}
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::SetNameFormat(const nsAString & aNameFormat)
{
    mNameFormat = aNameFormat; 
    return NS_OK;
}

// attribute AString addressFormat;
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::GetAddressFormat(nsAString & aAddressFormat)
{
    aAddressFormat  = mAddressFormat;
    return NS_OK;
}
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::SetAddressFormat(const nsAString & 
                                            aAddressFormat)
{
    mAddressFormat = aAddressFormat; 
    return NS_OK;
}

// attribute AString commentFormat;
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::GetCommentFormat(nsAString & aCommentFormat)
{
    aCommentFormat  = mCommentFormat;
    return NS_OK;
}
NS_IMETHODIMP 
nsAbLDAPAutoCompFormatter::SetCommentFormat(const nsAString & 
                                            aCommentFormat)
{
    mCommentFormat = aCommentFormat; 

    return NS_OK;
}
