/*
 * 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.plugin.thunderbird;

import java.io.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.*;

import org.jitsi.util.StringUtils;

import mork.*;
import net.java.sip.communicator.service.contactsource.*;
import net.java.sip.communicator.service.contactsource.ContactDetail.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;

/**
 * Queries a Thunderbird address book for contacts matching the given pattern.
 *
 * @author Ingo Bauersachs
 */
public class ThunderbirdContactQuery
    extends AsyncContactQuery<ThunderbirdContactSourceService>
{
    /** Class logger */
    private final static Logger logger = Logger
        .getLogger(ThunderbirdContactQuery.class);

    /**
     * Creates a new instance of this class.
     *
     * @param owner The contact source that created this query.
     * @param query The pattern to match against the contacts database.
     */
    public ThunderbirdContactQuery(ThunderbirdContactSourceService owner,
        Pattern query)
    {
        super(owner, query);
    }

    /**
     * Starts the query against the address book database.
     */
    @Override
    protected void run()
    {
        String filename = super.getContactSource().getFilename();
        File file = new File(filename);
        try
        {
            if (file.lastModified() > getContactSource().lastDatabaseFileChange)
            {
                // parse the Thunderbird Mork database
                InputStreamReader sr =
                    new InputStreamReader(new FileInputStream(filename));
                MorkDocument md = new MorkDocument(sr);
                sr.close();

                // We now have rows in their tables and additional rows at
                // transaction level. Put the to a better format:
                // DB -> Tables -> Rows
                Map<String, Map<String, Row>> db =
                    new HashMap<String, Map<String, Row>>();
                for (Table t : md.getTables())
                {
                    String tableId = t.getTableId() + "/" + t.getScopeName();
                    Map<String, Row> table = db.get(tableId);
                    if (table == null)
                    {
                        table = new HashMap<String, Row>();
                        db.put(tableId, table);
                    }

                    for (Row r : t.getRows())
                    {
                        String scope = r.getScopeName();
                        if (scope == null)
                        {
                            scope = t.getScopeName();
                        }

                        table.put(r.getRowId() + "/" + scope, r);
                    }
                }

                // The additional rows at the root-level update/replace the ones
                // in the tables. There's usually neither a table nor a scope
                // defined, so lets just use the default.
                String defaultScope = md.getDicts().get(0).dereference("^80");
                for (Row r : md.getRows())
                {
                    String scope = r.getScopeName();
                    if (scope == null)
                    {
                        scope = defaultScope;
                    }

                    String tableId = "1/" + scope;
                    Map<String, Row> table = db.get(tableId);
                    if (table == null)
                    {
                        table = new HashMap<String, Row>();
                        db.put(tableId, table);
                    }

                    String rowId = r.getRowId() + "/" + scope;
                    if (rowId.startsWith("-"))
                    {
                        rowId = rowId.substring(1);
                    }

                    table.put(rowId, r);
                }

                super.getContactSource().database = db;
                super.getContactSource().defaultScope = defaultScope;
                super.getContactSource().lastDatabaseFileChange =
                    file.lastModified();
            }

            // okay, "transactions" are applied, now perform the search
            for (Entry<String, Map<String, Row>> table
                : super.getContactSource().database.entrySet())
            {
                for (Map.Entry<String, Row> e : table.getValue().entrySet())
                {
                    if (e.getKey().endsWith(getContactSource().defaultScope))
                    {
                        readEntry(e.getValue());
                    }
                }
            }

            super.stopped(true);
        }
        catch (FileNotFoundException e)
        {
            logger.warn("Could not open address book", e);
        }
        catch (Exception e)
        {
            logger.warn("Could not parse " + file, e);
        }
    }

    /**
     * Processes a database row by matching it against the query and adding it
     * to the result set if it matched.
     *
     * @param r The database row representing a contact.
     */
    private void readEntry(Row r)
    {
        // match the pattern against this contact
        boolean hadMatch = false;
        for (Alias value : r.getAliases().values())
        {
            if (value != null
                && (super.query.matcher(value.getValue()).find() || super
                    .phoneNumberMatches(value.getValue())))
            {
                hadMatch = true;
                break;
            }
        }

        // nope, didn't match, ignore
        if (!hadMatch)
        {
            return;
        }

        List<ContactDetail> details = new LinkedList<ContactDetail>();

        // e-mail(s)
        for (String email : getPropertySet(r, "PrimaryEmail", "SecondEmail",
            "DefaultEmail"))
        {
            ContactDetail detail = new ContactDetail(email, Category.Email);
            detail.addSupportedOpSet(OperationSetPersistentPresence.class);
            details.add(detail);
        }

        // phone number(s)
        this.addPhoneDetail(details, r, "HomePhone", SubCategory.Home);
        this.addPhoneDetail(details, r, "WorkPhone", SubCategory.Work);
        this.addPhoneDetail(details, r, "CellularNumber", SubCategory.Mobile);

        // and the dispaly name
        String displayName = r.getValue("DisplayName");
        if (StringUtils.isNullOrEmpty(displayName, true))
        {
            displayName = r.getValue("LastName");
            if (displayName != null)
            {
                displayName = displayName.trim();
            }

            String firstName = r.getValue("FirstName");
            if (!StringUtils.isNullOrEmpty(firstName, true))
            {
                displayName = firstName + " " + displayName;
            }
        }

        // create the contact and add it to the results
        GenericSourceContact sc =
            new GenericSourceContact(super.getContactSource(), displayName,
                details);
        addQueryResult(sc);
    }

    /**
     * Adds a "Phone" {@link ContactDetail} to a query contact.
     *
     * @param details The {@link List} of {@link ContactDetail}s to which the
     *            details is added.
     * @param r The source database row of the contact.
     * @param property The source database property name to add as a detail.
     * @param category The Phone-{@link SubCategory} for the phone number to
     *            add.
     */
    private void addPhoneDetail(List<ContactDetail> details, Row r,
        String property, SubCategory category)
    {
        String phone = r.getValue(property);
        if (StringUtils.isNullOrEmpty(phone, true))
        {
            return;
        }

        phone
            = ThunderbirdActivator.getPhoneNumberI18nService().normalize(phone);
        ContactDetail detail =
            new ContactDetail(phone, ContactDetail.Category.Phone,
                new ContactDetail.SubCategory[]
                { category });

        detail.addSupportedOpSet(OperationSetBasicTelephony.class);
        detail.addSupportedOpSet(OperationSetPersistentPresence.class);
        details.add(detail);
    }

    /**
     * Gets a set of non-empty properties from the source database row.
     *
     * @param r The source database row to process.
     * @param properties The property-names to extract.
     * @return A set of non-empty properties from the source database row.
     */
    private Set<String> getPropertySet(Row r, String... properties)
    {
        Set<String> validValues = new HashSet<String>(properties.length);
        for (String prop : properties)
        {
            String value = r.getValue(prop);
            if (!StringUtils.isNullOrEmpty(value, true))
            {
                validValues.add(value);
            }
        }

        return validValues;
    }
}
