/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * 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 net.java.sip.communicator.service.contactsource;

import java.util.*;
import java.util.regex.*;

/**
 * Provides an abstract implementation of a <tt>ContactQuery</tt> which runs in
 * a separate <tt>Thread</tt>.
 *
 * @author Lyubomir Marinov
 * @param <T> the very type of <tt>ContactSourceService</tt> which performs the
 * <tt>ContactQuery</tt>
 */
public abstract class AsyncContactQuery<T extends ContactSourceService>
    extends AbstractContactQuery<T>
{
    /**
     * The {@link #query} in the form of a <tt>String</tt> telephone number if
     * such parsing, formatting and validation is possible; otherwise,
     * <tt>null</tt>.
     */
    private String phoneNumberQuery;

    /**
     * The <tt>Pattern</tt> for which the associated
     * <tt>ContactSourceService</tt> is being queried.
     */
    protected final Pattern query;

    /**
     * The indicator which determines whether there has been an attempt to
     * convert {@link #query} to {@link #phoneNumberQuery}. If the conversion has
     * been successful, <tt>phoneNumberQuery</tt> will be non-<tt>null</tt>.
     */
    private boolean queryIsConvertedToPhoneNumber;

    /**
     * The <tt>SourceContact</tt>s which match {@link #query}.
     */
    private Collection<SourceContact> queryResults
        = new LinkedList<SourceContact>();

    /**
     * The <tt>Thread</tt> in which this <tt>AsyncContactQuery</tt> is
     * performing {@link #query}.
     */
    private Thread thread;

    /**
     * Initializes a new <tt>AsyncContactQuery</tt> instance which is to perform
     * a specific <tt>query</tt> on behalf of a specific <tt>contactSource</tt>.
     *
     * @param contactSource the <tt>ContactSourceService</tt> which is to
     * perform the new <tt>ContactQuery</tt> instance
     * @param query the <tt>Pattern</tt> for which <tt>contactSource</tt> is
     * being queried
     * @param isSorted indicates if the results of this query should be sorted
     */
    protected AsyncContactQuery(T contactSource,
                                Pattern query,
                                boolean isSorted)
    {
        super(contactSource);

        this.query = query;

        if (isSorted)
            queryResults = new TreeSet<SourceContact>();
    }

    /**
     * Initializes a new <tt>AsyncContactQuery</tt> instance which is to perform
     * a specific <tt>query</tt> on behalf of a specific <tt>contactSource</tt>.
     *
     * @param contactSource the <tt>ContactSourceService</tt> which is to
     * perform the new <tt>ContactQuery</tt> instance
     * @param query the <tt>Pattern</tt> for which <tt>contactSource</tt> is
     * being queried
     */
    protected AsyncContactQuery(T contactSource, Pattern query)
    {
        super(contactSource);

        this.query = query;
    }

    /**
     * Adds a specific <tt>SourceContact</tt> to the list of
     * <tt>SourceContact</tt>s to be returned by this <tt>ContactQuery</tt> in
     * response to {@link #getQueryResults()}.
     *
     * @param sourceContact the <tt>SourceContact</tt> to be added to the
     * <tt>queryResults</tt> of this <tt>ContactQuery</tt>
     * @param showMoreEnabled indicates whether show more label should be shown 
     * or not.
     * @return <tt>true</tt> if the <tt>queryResults</tt> of this
     * <tt>ContactQuery</tt> has changed in response to the call
     */
    protected boolean addQueryResult(SourceContact sourceContact, 
        boolean showMoreEnabled)
    {
        boolean changed;

        synchronized (queryResults)
        {
            changed = queryResults.add(sourceContact);
        }
        if (changed)
            fireContactReceived(sourceContact, showMoreEnabled);

        return changed;
    }

    /**
     * Adds a specific <tt>SourceContact</tt> to the list of
     * <tt>SourceContact</tt>s to be returned by this <tt>ContactQuery</tt> in
     * response to {@link #getQueryResults()}.
     *
     * @param sourceContact the <tt>SourceContact</tt> to be added to the
     * <tt>queryResults</tt> of this <tt>ContactQuery</tt>
     * @return <tt>true</tt> if the <tt>queryResults</tt> of this
     * <tt>ContactQuery</tt> has changed in response to the call
     */
    protected boolean addQueryResult(SourceContact sourceContact)
    {
        boolean changed;

        synchronized (queryResults)
        {
            changed = queryResults.add(sourceContact);
        }
        if (changed)
            fireContactReceived(sourceContact);

        return changed;
    }

    /**
     * Removes a specific <tt>SourceContact</tt> from the list of
     * <tt>SourceContact</tt>s.
     *
     * @param sourceContact the <tt>SourceContact</tt> to be removed from the
     * <tt>queryResults</tt> of this <tt>ContactQuery</tt>
     * @return <tt>true</tt> if the <tt>queryResults</tt> of this
     * <tt>ContactQuery</tt> has changed in response to the call
     */
    protected boolean removeQueryResult(SourceContact sourceContact)
    {
        boolean changed;

        synchronized (queryResults)
        {
            changed = queryResults.remove(sourceContact);
        }
        if (changed)
            fireContactRemoved(sourceContact);

        return changed;
    }

    /**
     * Adds a set of <tt>SourceContact</tt> instances to the list of
     * <tt>SourceContact</tt>s to be returned by this <tt>ContactQuery</tt> in
     * response to {@link #getQueryResults()}.
     *
     * @param sourceContacts the set of <tt>SourceContact</tt> to be added to
     * the <tt>queryResults</tt> of this <tt>ContactQuery</tt>
     * @return <tt>true</tt> if the <tt>queryResults</tt> of this
     * <tt>ContactQuery</tt> has changed in response to the call
     */
    protected boolean addQueryResults(
        final Set<? extends SourceContact> sourceContacts)
    {
        final boolean changed;

        synchronized (queryResults)
        {
            changed = queryResults.addAll(sourceContacts);
        }

        if (changed)
        {
            // TODO Need something to fire one event for multiple contacts.
            for (SourceContact contact : sourceContacts)
            {
                fireContactReceived(contact, false);
            }
        }

        return changed;
    }


    /**
     * Gets the {@link #query} of this <tt>AsyncContactQuery</tt> as a
     * <tt>String</tt> which represents a phone number (if possible).
     *
     * @return a <tt>String</tt> which represents the <tt>query</tt> of this
     * <tt>AsyncContactQuery</tt> as a phone number if such parsing, formatting
     * and validation is possible; otherwise, <tt>null</tt>
     */
    protected String getPhoneNumberQuery()
    {
        if ((phoneNumberQuery == null) && !queryIsConvertedToPhoneNumber)
        {
            try
            {
                String pattern = query.pattern();

                if (pattern != null)
                {
                    int patternLength = pattern.length();

                    if ((patternLength > 2)
                            && (pattern.charAt(0) == '^')
                            && (pattern.charAt(patternLength - 1) == '$'))
                    {
                        phoneNumberQuery
                            = pattern.substring(1, patternLength - 1);
                    }
                    else if ((patternLength > 4)
                        && (pattern.charAt(0) == '\\')
                        && (pattern.charAt(1) == 'Q')
                        && (pattern.charAt(patternLength - 2) == '\\')
                        && (pattern.charAt(patternLength - 1) == 'E'))
                    {
                        phoneNumberQuery
                            = pattern.substring(2, patternLength - 2);
                    }

                }
            }
            finally
            {
                queryIsConvertedToPhoneNumber = true;
            }
        }
        return phoneNumberQuery;
    }

    /**
     * Gets the number of <tt>SourceContact</tt>s which match this
     * <tt>ContactQuery</tt>.
     *
     * @return the number of <tt>SourceContact</tt> which match this
     * <tt>ContactQuery</tt>
     */
    public int getQueryResultCount()
    {
        synchronized (queryResults)
        {
            return queryResults.size();
        }
    }

    /**
     * Gets the <tt>List</tt> of <tt>SourceContact</tt>s which match this
     * <tt>ContactQuery</tt>.
     *
     * @return the <tt>List</tt> of <tt>SourceContact</tt>s which match this
     * <tt>ContactQuery</tt>
     * @see ContactQuery#getQueryResults()
     */
    public List<SourceContact> getQueryResults()
    {
        List<SourceContact> qr;

        synchronized (queryResults)
        {
            qr = new ArrayList<SourceContact>(queryResults.size());
            qr.addAll(queryResults);
        }
        return qr;
    }

    /**
     * Returns the query string, this query was created for.
     *
     * @return the query string, this query was created for
     */
    public String getQueryString()
    {
        return query.toString();
    }

    /**
     * Performs this <tt>ContactQuery</tt> in a background <tt>Thread</tt>.
     */
    protected abstract void run();

    /**
     * Starts this <tt>AsyncContactQuery</tt>.
     */
    public synchronized void start()
    {
        if (thread == null)
        {
            thread
                = new Thread()
                {
                    @Override
                    public void run()
                    {
                        boolean completed = false;

                        try
                        {
                            AsyncContactQuery.this.run();
                            completed = true;
                        }
                        finally
                        {
                            synchronized (AsyncContactQuery.this)
                            {
                                if (thread == Thread.currentThread())
                                    stopped(completed);
                            }
                        }
                    }
                };
            thread.setDaemon(true);
            thread.start();
        }
        else
            throw new IllegalStateException("thread");
    }

    /**
     * Notifies this <tt>AsyncContactQuery</tt> that it has stopped performing
     * in the associated background <tt>Thread</tt>.
     *
     * @param completed <tt>true</tt> if this <tt>ContactQuery</tt> has
     * successfully completed, <tt>false</tt> if an error has been encountered
     * during its execution
     */
    protected void stopped(boolean completed)
    {
        if (getStatus() == QUERY_IN_PROGRESS)
            setStatus(completed ? QUERY_COMPLETED : QUERY_ERROR);
    }

    /**
     * Determines whether a specific <tt>String</tt> phone number matches the
     * {@link #query} of this <tt>AsyncContactQuery</tt>.
     *
     * @param phoneNumber the <tt>String</tt> which represents the phone number
     * to match to the <tt>query</tt> of this <tt>AsyncContactQuery</tt>
     * @return <tt>true</tt> if the specified <tt>phoneNumber</tt> matches the
     * <tt>query</tt> of this <tt>AsyncContactQuery</tt>; otherwise,
     * <tt>false</tt>
     */
    protected boolean phoneNumberMatches(String phoneNumber)
    {
        /*
         * PhoneNumberI18nService implements functionality to aid the parsing,
         * formatting and validation of international phone numbers so attempt
         * to use it to determine whether the specified phoneNumber matches the
         * query. For example, check whether the normalized phoneNumber matches
         * the query.
         */

        boolean phoneNumberMatches = false;

        if (query
                .matcher(ContactSourceActivator.getPhoneNumberI18nService()
                    .normalize(phoneNumber)).find())
        {
            phoneNumberMatches = true;
        }
        else
        {
            /*
             * The fact that the normalized form of the phoneNumber doesn't
             * match the query doesn't mean that, for example, it doesn't
             * match the normalized form of the query. The latter, though,
             * requires the query to look like a phone number as well. In
             * order to not accidentally start matching all queries to phone
             * numbers, it seems justified to normalize the query only when
             * it is a phone number, not whenever it looks like a piece of a
             * phone number.
             */

            String phoneNumberQuery = getPhoneNumberQuery();

            if ((phoneNumberQuery != null)
                    && (phoneNumberQuery.length() != 0))
            {
                try
                {
                    phoneNumberMatches
                        = ContactSourceActivator.getPhoneNumberI18nService()
                            .phoneNumbersMatch(
                                phoneNumberQuery,
                                phoneNumber);
                }
                catch (IllegalArgumentException iaex)
                {
                    /*
                     * Ignore it, phoneNumberMatches will remain equal to
                     * false.
                     */
                }
            }
        }
        return phoneNumberMatches;
    }
}
