/*
 * 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.impl.protocol.jabber;

import java.lang.reflect.*;
import java.net.*;
import java.text.*;
import java.util.*;

import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.ServerStoredDetails.*;
import net.java.sip.communicator.util.*;

import org.apache.commons.lang3.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.SmackException.*;
import org.jivesoftware.smack.XMPPException.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smackx.vcardtemp.*;
import org.jivesoftware.smackx.vcardtemp.packet.*;
import org.jxmpp.jid.*;

/**
 * Handles and retrieves all info of our contacts or our account info
 *
 * @author Damian Minkov
 */
public class InfoRetreiver
{
    private static final Logger logger =
        Logger.getLogger(InfoRetreiver.class);

    /**
     * A callback to the Jabber provider that created us.
     */
    private ProtocolProviderServiceJabberImpl jabberProvider = null;

    // here is kept all the details retrieved so far
    private final Map<EntityBareJid, List<GenericDetail>> retreivedDetails
        = new Hashtable<>();

    private static final String TAG_FN_OPEN = "<FN>";
    private static final String TAG_FN_CLOSE = "</FN>";

    /**
     * The timeout to wait before considering vcard has time outed.
     */
    private final long vcardTimeoutReply;

    protected InfoRetreiver(ProtocolProviderServiceJabberImpl jabberProvider)
    {
        this.jabberProvider = jabberProvider;

        vcardTimeoutReply
            = JabberActivator.getConfigurationService().getLong(
                    ProtocolProviderServiceJabberImpl
                        .VCARD_REPLY_TIMEOUT_PROPERTY,
                    -1);
    }

    /**
     * returns the user details from the specified class or its descendants
     * the class is one from the
     * net.java.sip.communicator.service.protocol.ServerStoredDetails
     * or implemented one in the operation set for the user info
     *
     * @param uin String
     * @param detailClass Class
     * @return Iterator
     */
    <T extends GenericDetail> Iterator<T> getDetailsAndDescendants(
        EntityBareJid uin,
        Class<T> detailClass)
    {
        List<GenericDetail> details = getContactDetails(uin);
        List<T> result = new LinkedList<>();

        for (GenericDetail item : details)
            if(detailClass.isInstance(item))
            {
                @SuppressWarnings("unchecked")
                T t = (T) item;

                result.add(t);
            }

        return result.iterator();
    }

    /**
     * returns the user details from the specified class
     * exactly that class not its descendants
     *
     * @param uin String
     * @param detailClass Class
     * @return Iterator
     */
    Iterator<GenericDetail> getDetails(
        EntityBareJid uin,
        Class<? extends GenericDetail> detailClass)
    {
        List<GenericDetail> details = getContactDetails(uin);
        List<GenericDetail> result = new LinkedList<>();

        for (GenericDetail item : details)
            if(detailClass.equals(item.getClass()))
                result.add(item);

        return result.iterator();
    }

    /**
     * request the full info for the given contactAddress
     * waits and return this details
     *
     * @param contactAddress String
     * @return Vector the details
     */
    List<GenericDetail> getContactDetails(EntityBareJid contactAddress)
    {
        List<GenericDetail> result = getCachedContactDetails(contactAddress);

        if(result == null)
        {
            return retrieveDetails(contactAddress);
        }

        return result;
    }

    /**
     * Retrieve details and return them or if missing return an empty list.
     * @param contactAddress the address to search for.
     * @return the details or empty list.
     */
    protected List<GenericDetail> retrieveDetails(EntityBareJid contactAddress)
    {
        List<GenericDetail> result = new LinkedList<>();
        try
        {
            XMPPConnection connection = jabberProvider.getConnection();

            if(connection == null || !connection.isAuthenticated())
                return null;

            VCard card;

            // if there is no value or is equals to the default one
            // load vcard using smack load method
            if(vcardTimeoutReply == -1
               || vcardTimeoutReply == SmackConfiguration.getDefaultReplyTimeout())
            {
                card = VCardManager.getInstanceFor(connection).loadVCard(contactAddress);
            }
            else
            {
                card = new VCard();
                load(card, connection, contactAddress, vcardTimeoutReply);
            }

            String tmp;

            tmp = checkForFullName(card);
            if(tmp != null)
                result.add(new DisplayNameDetail(
                    StringEscapeUtils.unescapeXml(tmp)));

            tmp = card.getFirstName();
            if(tmp != null)
                result.add(new FirstNameDetail(
                    StringEscapeUtils.unescapeXml(tmp)));

            tmp = card.getMiddleName();
            if(tmp != null)
                result.add(new MiddleNameDetail(
                    StringEscapeUtils.unescapeXml(tmp)));

            tmp = card.getLastName();
            if(tmp != null)
                result.add(new LastNameDetail(
                    StringEscapeUtils.unescapeXml(tmp)));

            tmp = card.getNickName();
            if(tmp != null)
                result.add(new NicknameDetail(
                    StringEscapeUtils.unescapeXml(tmp)));

            tmp = card.getField("BDAY");
            if (tmp != null)
            {
                try
                {
                    Calendar birthDateCalendar = Calendar.getInstance();
                    DateFormat dateFormat =
                        new SimpleDateFormat(
                            JabberActivator.getResources().getI18NString(
                                "plugin.accountinfo.BDAY_FORMAT"));
                    Date birthDate =
                        dateFormat.parse(tmp);
                    birthDateCalendar.setTime(birthDate);
                    BirthDateDetail bd = new BirthDateDetail(birthDateCalendar);
                    result.add(bd);
                }
                catch (ParseException e) {}
            }
            // Home Details
            // addrField one of
            // POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
            // LOCALITY, REGION, PCODE, CTRY
            tmp = card.getAddressFieldHome("STREET");
            if(tmp != null)
                result.add(new AddressDetail(tmp));

            tmp = card.getAddressFieldHome("LOCALITY");
            if(tmp != null)
                result.add(new CityDetail(tmp));

            tmp = card.getAddressFieldHome("REGION");
            if(tmp != null)
                result.add(new ProvinceDetail(tmp));

            tmp = card.getAddressFieldHome("PCODE");
            if(tmp != null)
                result.add(new PostalCodeDetail(tmp));

                tmp = card.getAddressFieldHome("CTRY");
                if(tmp != null)
                    result.add(new CountryDetail(tmp));

            // phoneType one of
            //VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

            tmp = card.getPhoneHome("VOICE");
            if(tmp != null)
                result.add(new PhoneNumberDetail(tmp));

            tmp = card.getPhoneHome("VIDEO");
            if(tmp != null)
                result.add(new VideoDetail(tmp));

            tmp = card.getPhoneHome("FAX");
            if(tmp != null)
                result.add(new FaxDetail(tmp));

            tmp = card.getPhoneHome("PAGER");
            if(tmp != null)
                result.add(new PagerDetail(tmp));

            tmp = card.getPhoneHome("CELL");
            if(tmp != null)
                result.add(new MobilePhoneDetail(tmp));

            tmp = card.getPhoneHome("TEXT");
            if(tmp != null)
                result.add(new MobilePhoneDetail(tmp));

            tmp = card.getEmailHome();
            if(tmp != null)
                result.add(new EmailAddressDetail(tmp));

            // Work Details
            // addrField one of
            // POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
            // LOCALITY, REGION, PCODE, CTRY
            tmp = card.getAddressFieldWork("STREET");
            if(tmp != null)
                result.add(new WorkAddressDetail(tmp));

            tmp = card.getAddressFieldWork("LOCALITY");
            if(tmp != null)
                result.add(new WorkCityDetail(tmp));

            tmp = card.getAddressFieldWork("REGION");
            if(tmp != null)
                result.add(new WorkProvinceDetail(tmp));

            tmp = card.getAddressFieldWork("PCODE");
            if(tmp != null)
                result.add(new WorkPostalCodeDetail(tmp));

//                tmp = card.getAddressFieldWork("CTRY");
//                if(tmp != null)
//                    result.add(new WorkCountryDetail(tmp);

            // phoneType one of
            //VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF

            tmp = card.getPhoneWork("VOICE");
            if(tmp != null)
                result.add(new WorkPhoneDetail(tmp));

            tmp = card.getPhoneWork("VIDEO");
            if(tmp != null)
                result.add(new WorkVideoDetail(tmp));

            tmp = card.getPhoneWork("FAX");
            if(tmp != null)
                result.add(new WorkFaxDetail(tmp));

            tmp = card.getPhoneWork("PAGER");
            if(tmp != null)
                result.add(new WorkPagerDetail(tmp));

            tmp = card.getPhoneWork("CELL");
            if(tmp != null)
                result.add(new WorkMobilePhoneDetail(tmp));

            tmp = card.getPhoneWork("TEXT");
            if(tmp != null)
                result.add(new WorkMobilePhoneDetail(tmp));

            tmp = card.getEmailWork();
            if(tmp != null)
                result.add(new WorkEmailAddressDetail(tmp));

            tmp = card.getOrganization();
            if(tmp != null)
                result.add(new WorkOrganizationNameDetail(tmp));

            tmp = card.getOrganizationUnit();
            if(tmp != null)
                result.add(new WorkDepartmentNameDetail(tmp));

            tmp = card.getField("TITLE");
            if(tmp != null)
                result.add(new JobTitleDetail(tmp));

            tmp = card.getField("ABOUTME");
            if (tmp != null)
                result.add(new AboutMeDetail(tmp));

            byte[] imageBytes = card.getAvatar();
            if(imageBytes != null && imageBytes.length > 0)
            {
                result.add(new ImageDetail("Image", imageBytes));
            }

            try
            {
                tmp = card.getField("URL");
                if(tmp != null)
                    result.add(new URLDetail("URL", new URL(tmp)));
            }
            catch(MalformedURLException e){}
        }
        catch (Exception exc)
        {
            String msg = "Cannot load details for contact "
                + contactAddress + " : " + exc.getMessage();
            if(logger.isTraceEnabled())
                logger.trace(msg, exc);
            else
                logger.error(msg);
        }

        retreivedDetails.put(contactAddress, result);

        return result;
    }

    /**
     * request the full info for the given contactAddress if available
     * in cache.
     *
     * @param contactAddress to search for
     * @return list of the details if any.
     */
    List<GenericDetail> getCachedContactDetails(EntityBareJid contactAddress)
    {
        return retreivedDetails.get(contactAddress);
    }

    /**
     * Adds a cached contact details.
     * @param contactAddress the contact address
     * @param details the details to add
     */
    void addCachedContactDetails(
        EntityBareJid contactAddress, List<GenericDetail> details)
    {
        retreivedDetails.put(contactAddress, details);
    }

    /**
     * Checks for full name tag in the <tt>card</tt>.
     * @param card the card to check.
     * @return the Full name if existing, null otherwise.
     */
    String checkForFullName(VCard card)
    {
        String vcardXml = card.toXML().toString();

        int indexOpen = vcardXml.indexOf(TAG_FN_OPEN);

        if(indexOpen == -1)
            return null;

        int indexClose = vcardXml.indexOf(TAG_FN_CLOSE, indexOpen);

        // something is wrong!
        if(indexClose == -1)
            return null;

        return vcardXml.substring(indexOpen + TAG_FN_OPEN.length(), indexClose);
    }

    /**
     * Load VCard for the given user.
     * Using the specified timeout.
     *
     * @param vcard VCard
     * @param connection XMPP connection
     * @param user the user
     * @param timeout timeout in second
     * @throws XMPPException if something went wrong during VCard loading
     */
    public void load(VCard vcard,
                     XMPPConnection connection,
                     EntityBareJid user,
                     long timeout)
        throws XMPPException, NotConnectedException, InterruptedException,
               OperationFailedException
    {
        vcard.setTo(user);
        vcard.setType(IQ.Type.get);


        VCard result = null;
        try
        {
            StanzaCollector collector
                = connection.createStanzaCollectorAndSend(vcard);
            try
            {
                result = collector.nextResultOrThrow(timeout);
            }
            finally
            {
                collector.cancel();
            }
        }
        catch (ClassCastException e)
        {
            logger.error("No vcard for " + user);
        }
        catch (NoResponseException e)
        {
            throw new OperationFailedException(
                "Timeout getting VCard information",
                OperationFailedException.GENERAL_ERROR
            );
        }

        // copy loaded vcard fields in the supplied one.
        Field[] fields = VCard.class.getDeclaredFields();
        for (Field field : fields)
        {
            if (field.getDeclaringClass() == VCard.class &&
                    !Modifier.isFinal(field.getModifiers()))
            {
                try
                {
                    field.setAccessible(true);
                    field.set(vcard, field.get(result));
                }
                catch (IllegalAccessException e)
                {
                    throw new RuntimeException("Cannot set field:" + field, e);
                }
            }
        }
    }

    /**
     * Work department
     */
    public static class WorkDepartmentNameDetail
        extends NameDetail
    {
        /**
         * Constructor.
         *
         * @param workDepartmentName name of the work department
         */
        public WorkDepartmentNameDetail(String workDepartmentName)
        {
            super("Work Department Name", workDepartmentName);
        }
    }

    /**
     * Fax at work
     */
    public static class WorkFaxDetail
        extends FaxDetail
    {
        /**
         * Constructor.
         *
         * @param number work fax number
         */
        public WorkFaxDetail(String number)
        {
            super(number);
            super.detailDisplayName = "WorkFax";
        }
    }

    /**
     * Pager at work
     */
    public static class WorkPagerDetail
        extends PhoneNumberDetail
    {
        /**
         * Constructor.
         *
         * @param number work pager number
         */
        public WorkPagerDetail(String number)
        {
            super(number);
            super.detailDisplayName = "WorkPager";
        }
    }
}
