/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.emailcommon.mail;

import android.os.Parcel;
import android.os.Parcelable;
import android.text.Html;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;

import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;

import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.decoder.DecoderUtil;

import java.util.ArrayList;
import java.util.regex.Pattern;

/**
 * This class represent email address.
 *
 * RFC822 email address may have following format.
 *   "name" <address> (comment)
 *   "name" <address>
 *   name <address>
 *   address
 * Name and comment part should be MIME/base64 encoded in header if necessary.
 *
 */
public class Address implements Parcelable {
    public static final String ADDRESS_DELIMETER = ",";
    /**
     *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
     */
    private String mAddress;

    /**
     * Name part. No surrounding double quote, and no MIME/base64 encoding.
     * This must be null if Address has no name part.
     */
    private String mPersonal;

    /**
     * When personal is set, it will return the first token of the personal
     * string. Otherwise, it will return the e-mail address up to the '@' sign.
     */
    private String mSimplifiedName;

    // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
    private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
    // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
    private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
    // Regex that matches escaped character '\\([\\"])'
    private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");

    // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
    // TODO: Fix this to better constrain comments.
    /** Regex for the local part of an email address. */
    private static final String LOCAL_PART = "[^@]+";
    /** Regex for each part of the domain part, i.e. the thing between the dots. */
    private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
    /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
    private static final String DOMAIN_PART =
            "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;

    /** Pattern to check if an email address is valid. */
    private static final Pattern EMAIL_ADDRESS =
            Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");

    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];

    // delimiters are chars that do not appear in an email address, used by fromHeader
    private static final char LIST_DELIMITER_EMAIL = '\1';
    private static final char LIST_DELIMITER_PERSONAL = '\2';

    private static final String LOG_TAG = LogTag.getLogTag();

    @VisibleForTesting
    public Address(String address) {
        setAddress(address);
    }

    public Address(String address, String personal) {
        setPersonal(personal);
        setAddress(address);
    }

    /**
     * Returns a simplified string for this e-mail address.
     * When a name is known, it will return the first token of that name. Otherwise, it will
     * return the e-mail address up to the '@' sign.
     */
    public String getSimplifiedName() {
        if (mSimplifiedName == null) {
            if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
                int atSign = mAddress.indexOf('@');
                mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
            } else if (!TextUtils.isEmpty(mPersonal)) {

                // TODO: use Contacts' NameSplitter for more reliable first-name extraction

                int end = mPersonal.indexOf(' ');
                while (end > 0 && mPersonal.charAt(end - 1) == ',') {
                    end--;
                }
                mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);

            } else {
                LogUtils.w(LOG_TAG, "Unable to get a simplified name");
                mSimplifiedName = "";
            }
        }
        return mSimplifiedName;
    }

    public static synchronized Address getEmailAddress(String rawAddress) {
        if (TextUtils.isEmpty(rawAddress)) {
            return null;
        }
        String name, address;
        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
        if (tokens.length > 0) {
            final String tokenizedName = tokens[0].getName();
            name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
                    : "";
            address = Html.fromHtml(tokens[0].getAddress()).toString();
        } else {
            name = "";
            address = rawAddress == null ?
                    "" : Html.fromHtml(rawAddress).toString();
        }
        return new Address(address, name);
    }

    public String getAddress() {
        return mAddress;
    }

    public void setAddress(String address) {
        mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
    }

    /**
     * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
     *
     * @return Name part of email address. Returns null if it is omitted.
     */
    public String getPersonal() {
        return mPersonal;
    }

    /**
     * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
     * It will be also unquoted and MIME/base64 decoded.
     *
     * @param personal name part of email address as UTF-16 string. Null is acceptable.
     */
    public void setPersonal(String personal) {
        mPersonal = decodeAddressPersonal(personal);
    }

    /**
     * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
     * It will be also unquoted and MIME/base64 decoded.
     *
     * @param personal name part of email address as UTF-16 string. Null is acceptable.
     */
    public static String decodeAddressPersonal(String personal) {
        if (personal != null) {
            personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
            personal = UNQUOTE.matcher(personal).replaceAll("$1");
            personal = DecoderUtil.decodeEncodedWords(personal);
            if (personal.length() == 0) {
                personal = null;
            }
        }
        return personal;
    }

    /**
     * This method is used to check that all the addresses that the user
     * entered in a list (e.g. To:) are valid, so that none is dropped.
     */
    @VisibleForTesting
    public static boolean isAllValid(String addressList) {
        // This code mimics the parse() method below.
        // I don't know how to better avoid the code-duplication.
        if (addressList != null && addressList.length() > 0) {
            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
            for (int i = 0, length = tokens.length; i < length; ++i) {
                Rfc822Token token = tokens[i];
                String address = token.getAddress();
                if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Parse a comma-delimited list of addresses in RFC822 format and return an
     * array of Address objects.
     *
     * @param addressList Address list in comma-delimited string.
     * @return An array of 0 or more Addresses.
     */
    public static Address[] parse(String addressList) {
        if (addressList == null || addressList.length() == 0) {
            return EMPTY_ADDRESS_ARRAY;
        }
        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
        ArrayList<Address> addresses = new ArrayList<Address>();
        for (int i = 0, length = tokens.length; i < length; ++i) {
            Rfc822Token token = tokens[i];
            String address = token.getAddress();
            if (!TextUtils.isEmpty(address)) {
                if (isValidAddress(address)) {
                    String name = token.getName();
                    if (TextUtils.isEmpty(name)) {
                        name = null;
                    }
                    addresses.add(new Address(address, name));
                }
            }
        }
        return addresses.toArray(new Address[addresses.size()]);
    }

    /**
     * Checks whether a string email address is valid.
     * E.g. name@domain.com is valid.
     */
    @VisibleForTesting
    static boolean isValidAddress(final String address) {
        return EMAIL_ADDRESS.matcher(address).find();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Address) {
            // It seems that the spec says that the "user" part is case-sensitive,
            // while the domain part in case-insesitive.
            // So foo@yahoo.com and Foo@yahoo.com are different.
            // This may seem non-intuitive from the user POV, so we
            // may re-consider it if it creates UI trouble.
            // A problem case is "replyAll" sending to both
            // a@b.c and to A@b.c, which turn out to be the same on the server.
            // Leave unchanged for now (i.e. case-sensitive).
            return getAddress().equals(((Address) o).getAddress());
        }
        return super.equals(o);
    }

    @Override
    public int hashCode() {
        return getAddress().hashCode();
    }

    /**
     * Get human readable address string.
     * Do not use this for email header.
     *
     * @return Human readable address string.  Not quoted and not encoded.
     */
    @Override
    public String toString() {
        if (mPersonal != null && !mPersonal.equals(mAddress)) {
            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
                return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
            } else {
                return mPersonal + " <" + mAddress + ">";
            }
        } else {
            return mAddress;
        }
    }

    /**
     * Ensures that the given string starts and ends with the double quote character. The string is
     * not modified in any way except to add the double quote character to start and end if it's not
     * already there.
     *
     * sample -> "sample"
     * "sample" -> "sample"
     * ""sample"" -> "sample"
     * "sample"" -> "sample"
     * sa"mp"le -> "sa"mp"le"
     * "sa"mp"le" -> "sa"mp"le"
     * (empty string) -> ""
     * " -> ""
     */
    private static String ensureQuotedString(String s) {
        if (s == null) {
            return null;
        }
        if (!s.matches("^\".*\"$")) {
            return "\"" + s + "\"";
        } else {
            return s;
        }
    }

    /**
     * Get human readable comma-delimited address string.
     *
     * @param addresses Address array
     * @return Human readable comma-delimited address string.
     */
    @VisibleForTesting
    public static String toString(Address[] addresses) {
        return toString(addresses, ADDRESS_DELIMETER);
    }

    /**
     * Get human readable address strings joined with the specified separator.
     *
     * @param addresses Address array
     * @param separator Separator
     * @return Human readable comma-delimited address string.
     */
    public static String toString(Address[] addresses, String separator) {
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        if (addresses.length == 1) {
            return addresses[0].toString();
        }
        StringBuilder sb = new StringBuilder(addresses[0].toString());
        for (int i = 1; i < addresses.length; i++) {
            sb.append(separator);
            // TODO: investigate why this .trim() is needed.
            sb.append(addresses[i].toString().trim());
        }
        return sb.toString();
    }

    /**
     * Get RFC822/MIME compatible address string.
     *
     * @return RFC822/MIME compatible address string.
     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
     */
    public String toHeader() {
        if (mPersonal != null) {
            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
        } else {
            return mAddress;
        }
    }

    /**
     * Get RFC822/MIME compatible comma-delimited address string.
     *
     * @param addresses Address array
     * @return RFC822/MIME compatible comma-delimited address string.
     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
     */
    public static String toHeader(Address[] addresses) {
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        if (addresses.length == 1) {
            return addresses[0].toHeader();
        }
        StringBuilder sb = new StringBuilder(addresses[0].toHeader());
        for (int i = 1; i < addresses.length; i++) {
            // We need space character to be able to fold line.
            sb.append(", ");
            sb.append(addresses[i].toHeader());
        }
        return sb.toString();
    }

    /**
     * Get Human friendly address string.
     *
     * @return the personal part of this Address, or the address part if the
     * personal part is not available
     */
    @VisibleForTesting
    public String toFriendly() {
        if (mPersonal != null && mPersonal.length() > 0) {
            return mPersonal;
        } else {
            return mAddress;
        }
    }

    /**
     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
     * details on the per-address conversion).
     *
     * @param addresses Array of Address[] values
     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
     * was null or empty.
     */
    @VisibleForTesting
    public static String toFriendly(Address[] addresses) {
        if (addresses == null || addresses.length == 0) {
            return null;
        }
        if (addresses.length == 1) {
            return addresses[0].toFriendly();
        }
        StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
        for (int i = 1; i < addresses.length; i++) {
            sb.append(", ");
            sb.append(addresses[i].toFriendly());
        }
        return sb.toString();
    }

    /**
     * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)).
     */
    @VisibleForTesting
    public static String fromHeaderToString(String addressList) {
        return toString(fromHeader(addressList));
    }

    /**
     * Returns exactly the same result as Address.toHeader(Address.parse(addressList)).
     */
    @VisibleForTesting
    public static String parseToHeader(String addressList) {
        return Address.toHeader(Address.parse(addressList));
    }

    /**
     * Returns null if the addressList has 0 addresses, otherwise returns the first address.
     * The same as Address.fromHeader(addressList)[0] for non-empty list.
     * This is an utility method that offers some performance optimization opportunities.
     */
    @VisibleForTesting
    public static Address firstAddress(String addressList) {
        Address[] array = fromHeader(addressList);
        return array.length > 0 ? array[0] : null;
    }

    /**
     * This method exists to convert an address list formatted in a deprecated legacy format to the
     * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
     * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
     *
     * This implementation is brute-force, and could be replaced with a more efficient version
     * if desired.
     */
    public static String reformatToHeader(String addressList) {
        return toHeader(fromHeader(addressList));
    }

    /**
     * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
     * @return array of addresses parsed from <code>addressList</code>
     */
    @VisibleForTesting
    public static Address[] fromHeader(String addressList) {
        if (addressList == null || addressList.length() == 0) {
            return EMPTY_ADDRESS_ARRAY;
        }
        // IF we're CSV, just parse
        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
            return Address.parse(addressList);
        }
        // Otherwise, do backward-compatible unpack
        ArrayList<Address> addresses = new ArrayList<Address>();
        int length = addressList.length();
        int pairStartIndex = 0;
        int pairEndIndex;

        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
           is used, not for every email address; i.e. not for every iteration of the while().
           This reduces the theoretical complexity from quadratic to linear,
           and provides some speed-up in practice by removing redundant scans of the string.
        */
        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);

        while (pairStartIndex < length) {
            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
            if (pairEndIndex == -1) {
                pairEndIndex = length;
            }
            Address address;
            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
                // in this case the DELIMITER_PERSONAL is in a future pair,
                // so don't use personal, and don't update addressEndIndex
                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
            } else {
                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
                        addressList.substring(addressEndIndex + 1, pairEndIndex));
                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
            }
            addresses.add(address);
            pairStartIndex = pairEndIndex + 1;
        }
        return addresses.toArray(new Address[addresses.size()]);
    }

    public static final Creator<Address> CREATOR = new Creator<Address>() {
        @Override
        public Address createFromParcel(Parcel parcel) {
            return new Address(parcel);
        }

        @Override
        public Address[] newArray(int size) {
            return new Address[size];
        }
    };

    public Address(Parcel in) {
        setPersonal(in.readString());
        setAddress(in.readString());
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeString(mPersonal);
        out.writeString(mAddress);
    }
}
