/*
 * ContactsCache.java
 *
 * Copyright (C) 2011 Tim Marston <tim@ed.am>
 *
 * This file is part of the Import Contacts program (hereafter referred
 * to as "this program"). For more information, see
 * http://ed.am/dev/android/import-contacts
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package am.ed.importcontacts;

import java.util.HashMap;
import java.util.HashSet;

import am.ed.importcontacts.Importer.AbortImportException;

import android.app.Activity;
import android.database.Cursor;
import android.provider.Contacts;


public class ContactsCache
{
	/**
	 * Information that can be used to identify a contact within the cache
	 */
	static public class CacheIdentifier
	{
		public enum Type {
			NONE, NAME, ORGANISATION, PRIMARY_NUMBER, PRIMARY_EMAIL }

		private Type _type;
		private String _detail;

		protected CacheIdentifier()
		{
			_type = Type.NONE;
		}

		protected CacheIdentifier( Type type, String detail )
		{
			_type = type;
			_detail = detail;
		}

		public Type getType()
		{
			return _type;
		}

		public String getDetail()
		{
			return _detail;
		}
	}

	// mappings of contact names, organisations and primary numbers to ids
	private HashMap< String, Long > _contactsByName;
	private HashMap< String, Long > _contactsByOrg;
	private HashMap< String, Long > _contactsByNumber;
	private HashMap< String, Long > _contactsByEmail;

	// mapping of contact ids to sets of associated data
	private HashMap< Long, HashSet< String > > _contactNumbers;
	private HashMap< Long, HashSet< String > > _contactEmails;
	private HashMap< Long, HashSet< String > > _contactAddresses;
	private HashMap< Long, HashSet< String > > _contactOrganisations;

	public static CacheIdentifier createIdentifier(
		Importer.ContactData contact )
	{
		if( contact.hasName() ) {
			String name = normaliseName( contact.getName() );
			if( name != null )
				return new CacheIdentifier(
					CacheIdentifier.Type.NAME, name );
		}

		if( contact.hasPrimaryOrganisation() ) {
			String organisation = normaliseOrganisation(
				contact.getPrimaryOrganisation() );
			if( organisation != null )
				return new CacheIdentifier(
					CacheIdentifier.Type.ORGANISATION, organisation );
		}

		if( contact.hasPrimaryNumber() ) {
			String number = normalisePhoneNumber( contact.getPrimaryNumber() );
			if( number != null )
			return new CacheIdentifier(
				CacheIdentifier.Type.PRIMARY_NUMBER, number );
		}

		if( contact.hasPrimaryEmail() ) {
			String email = normaliseEmailAddress( contact.getPrimaryEmail() );
			if( email != null )
			return new CacheIdentifier(
				CacheIdentifier.Type.PRIMARY_EMAIL, email );
		}

		return null;
	}

	public boolean canLookup( CacheIdentifier identifier )
	{
		return lookup( identifier ) != null;
	}

	public Long lookup( CacheIdentifier identifier )
	{
		switch( identifier.getType() )
		{
		case NAME:
			return _contactsByName.get( identifier.getDetail() );
		case ORGANISATION:
			return _contactsByOrg.get( identifier.getDetail() );
		case PRIMARY_NUMBER:
			return _contactsByNumber.get( identifier.getDetail() );
		case PRIMARY_EMAIL:
			return _contactsByEmail.get( identifier.getDetail() );
		}
		return null;
	}

	public Long removeLookup( CacheIdentifier identifier )
	{
		switch( identifier.getType() )
		{
		case NAME:
			return _contactsByName.remove( identifier.getDetail() );
		case ORGANISATION:
			return _contactsByOrg.remove( identifier.getDetail() );
		case PRIMARY_NUMBER:
			return _contactsByNumber.remove( identifier.getDetail() );
		case PRIMARY_EMAIL:
			return _contactsByEmail.remove( identifier.getDetail() );
		}
		return null;
	}

	public void addLookup( CacheIdentifier identifier, Long id )
	{
		switch( identifier.getType() )
		{
		case NAME:
			_contactsByName.put( identifier.getDetail(), id );
			break;
		case ORGANISATION:
			_contactsByOrg.put( identifier.getDetail(), id );
			break;
		case PRIMARY_NUMBER:
			_contactsByNumber.put( identifier.getDetail(), id );
			break;
		case PRIMARY_EMAIL:
			_contactsByEmail.put( identifier.getDetail(), id );
			break;
		}
	}

	public void removeAssociatedData( Long id )
	{
		_contactNumbers.remove( id );
		_contactEmails.remove( id );
		_contactAddresses.remove( id );
		_contactOrganisations.remove( id );
	}

	public boolean hasAssociatedNumber( Long id, String number )
	{
		number = normalisePhoneNumber( number );
		if( number == null ) return false;

		HashSet< String > set = _contactNumbers.get( id );
		return set != null && set.contains( number );
	}

	public void addAssociatedNumber( Long id, String number )
	{
		number = normalisePhoneNumber( number );
		if( number == null ) return;

		HashSet< String > set = _contactNumbers.get( id );
		if( set == null ) {
			set = new HashSet< String >();
			_contactNumbers.put( id, set );
		}
		set.add( normalisePhoneNumber( number ) );
	}

	public boolean hasAssociatedEmail( Long id, String email )
	{
		email = normaliseEmailAddress( email );
		if( email == null ) return false;

		HashSet< String > set = _contactEmails.get( id );
		return set != null && set.contains( normaliseEmailAddress( email ) );
	}

	public void addAssociatedEmail( Long id, String email )
	{
		email = normaliseEmailAddress( email );
		if( email == null ) return;

		HashSet< String > set = _contactEmails.get( id );
		if( set == null ) {
			set = new HashSet< String >();
			_contactEmails.put( id, set );
		}
		set.add( normaliseEmailAddress( email ) );
	}

	public boolean hasAssociatedAddress( Long id, String address )
	{
		address = normaliseAddress( address );
		if( address == null ) return false;

		HashSet< String > set = _contactAddresses.get( id );
		return set != null && set.contains( normaliseAddress( address ) );
	}

	public void addAssociatedAddress( Long id, String address )
	{
		address = normaliseAddress( address );
		if( address == null ) return;

		HashSet< String > set = _contactAddresses.get( id );
		if( set == null ) {
			set = new HashSet< String >();
			_contactAddresses.put( id, set );
		}
		set.add( normaliseAddress( address ) );
	}

	public boolean hasAssociatedOrganisation( Long id, String organisation )
	{
		organisation = normaliseOrganisation( organisation );
		if( organisation == null ) return false;

		HashSet< String > set = _contactOrganisations.get( id );
		return set != null && set.contains(
			normaliseOrganisation( organisation ) );
	}

	public void addAssociatedOrganisation( Long id, String organisation )
	{
		organisation = normaliseOrganisation( organisation );
		if( organisation == null ) return;

		HashSet< String > set = _contactOrganisations.get( id );
		if( set == null ) {
			set = new HashSet< String >();
			_contactOrganisations.put( id, set );
		}
		set.add( normaliseOrganisation( organisation ) );
	}

	public void buildCache( Activity activity )
		throws AbortImportException
	{
		Cursor cur;

		// init id lookups
		_contactsByName = new HashMap< String, Long >();
		_contactsByOrg = new HashMap< String, Long >();
		_contactsByNumber = new HashMap< String, Long >();
		_contactsByEmail = new HashMap< String, Long >();

		// init associated data cache
		_contactNumbers = new HashMap< Long, HashSet< String > >();
		_contactEmails = new HashMap< Long, HashSet< String > >();
		_contactAddresses = new HashMap< Long, HashSet< String > >();
		_contactOrganisations = new HashMap< Long, HashSet< String > >();

		// set of contact ids that we have not yet added
		HashSet< Long > unadded = new HashSet< Long >();

		// get all contacts
		cur = activity.managedQuery( Contacts.People.CONTENT_URI,
			new String[] {
				Contacts.People._ID,
				Contacts.People.NAME,
			}, null, null, null );
		while( cur.moveToNext() ) {
			Long id = cur.getLong(
				cur.getColumnIndex( Contacts.People._ID ) );
			String name = normaliseName( cur.getString(
				cur.getColumnIndex( Contacts.People.NAME ) ) );
			if( name != null )
			{
				// if we can, add a lookup for the contact id by name
				if( name.length() > 0 ) {
					addLookup( new CacheIdentifier(
						CacheIdentifier.Type.NAME, name ), id );
					continue;
				}
			}

			// record that a lookup for this contact's id still needs to be
			// added by some other means
			unadded.add( id );
		}

		// get contact organisations, primary ones first
		cur = activity.managedQuery( Contacts.Organizations.CONTENT_URI,
			new String[] {
				Contacts.Phones.PERSON_ID,
				Contacts.Organizations.COMPANY,
			}, null, null, Contacts.Organizations.ISPRIMARY + " DESC" );
		while( cur.moveToNext() ) {
			Long id = cur.getLong( cur.getColumnIndex(
				Contacts.Organizations.PERSON_ID ) );
			String organisation = normaliseOrganisation( cur.getString(
				cur.getColumnIndex( Contacts.Organizations.COMPANY ) ) );
			if( organisation != null )
			{
				// if this is an organisation name for a contact for whom we
				// have not added a lookup, add a lookup for the contact id
				// by organisation
				if( unadded.contains( id ) ) {
					addLookup( new CacheIdentifier(
						CacheIdentifier.Type.ORGANISATION, organisation ), id );
					unadded.remove( id );
				}

				// add associated data
				addAssociatedOrganisation( id, organisation );
			}
		}

		// get all phone numbers, primary ones first
		cur = activity.managedQuery( Contacts.Phones.CONTENT_URI,
			new String[] {
				Contacts.Phones.PERSON_ID,
				Contacts.Phones.NUMBER,
			}, null, null, Contacts.Phones.ISPRIMARY + " DESC" );
		while( cur.moveToNext() ) {
			Long id = cur.getLong(
				cur.getColumnIndex( Contacts.Phones.PERSON_ID ) );
			String number = normalisePhoneNumber( cur.getString(
				cur.getColumnIndex( Contacts.Phones.NUMBER ) ) );
			if( number != null )
			{
				// if this is a number for a contact for whom we have not
				// added a lookup, add a lookup for the contact id by phone
				// number
				if( unadded.contains( id ) ) {
					addLookup( new CacheIdentifier(
						CacheIdentifier.Type.PRIMARY_NUMBER, number ), id );
					unadded.remove( id );
				}

				// add associated data
				addAssociatedNumber( id, number );
			}
		}

		// now get all email addresses, primary ones first, and postal addresses
		cur = activity.managedQuery( Contacts.ContactMethods.CONTENT_URI,
			new String[] {
				Contacts.ContactMethods.PERSON_ID,
				Contacts.ContactMethods.DATA,
				Contacts.ContactMethods.KIND,
			}, Contacts.ContactMethods.KIND + " IN( ?, ? )", new String[] {
				"" + Contacts.KIND_EMAIL,
				"" + Contacts.KIND_POSTAL,
			}, Contacts.ContactMethods.ISPRIMARY + " DESC" );
		while( cur.moveToNext() ) {
			Long id = cur.getLong(
				cur.getColumnIndex( Contacts.ContactMethods.PERSON_ID ) );
			int kind = cur.getInt(
				cur.getColumnIndex( Contacts.ContactMethods.KIND ) );
			if( kind == Contacts.KIND_EMAIL )
			{
				String email = normaliseEmailAddress( cur.getString(
					cur.getColumnIndex( Contacts.ContactMethods.DATA ) ) );
				if( email != null )
				{
					// if this is an email address for a contact for whom we
					// have not added a lookup, add a lookup for the contact
					// id by email address
					if( unadded.contains( id ) ) {
						addLookup( new CacheIdentifier(
							CacheIdentifier.Type.PRIMARY_EMAIL, email ), id );
						unadded.remove( id );
					}

					// add associated data
					addAssociatedEmail( id, email );
				}
			}
			else if( kind == Contacts.KIND_POSTAL )
			{
				String address = normaliseAddress( cur.getString(
					cur.getColumnIndex( Contacts.ContactMethods.DATA ) ) );
				if( address != null )
				{
					// add associated data
					addAssociatedAddress( id, address );
				}
			}
		}
	}

	static private String normaliseName( String name )
	{
		if( name == null ) return null;
		name = name.trim();
		return name.length() > 0? name : null;
	}

	static private String normalisePhoneNumber( String number )
	{
		if( number == null ) return null;
		number = number.trim().replaceAll( "[-\\(\\) ]", "" );
		return number.length() > 0? number : null;
	}

	static private String normaliseEmailAddress( String email )
	{
		if( email == null ) return null;
		email = email.trim().toLowerCase();
		return email.length() > 0? email : null;
	}

	static private String normaliseOrganisation( String organisation )
	{
		if( organisation == null ) return null;
		organisation = organisation.trim();
		return organisation.length() > 0? organisation : null;
	}

	static private String normaliseAddress( String address )
	{
		if( address == null ) return null;
		address = address.trim();
		return address.length() > 0? address : null;
	}
}
