package ezvcard.android;

import static android.text.TextUtils.isEmpty;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.os.Build;
import android.os.RemoteException;
import android.provider.ContactsContract;
import ezvcard.VCard;
import ezvcard.property.Address;
import ezvcard.property.Birthday;
import ezvcard.property.Email;
import ezvcard.property.FormattedName;
import ezvcard.property.Impp;
import ezvcard.property.Nickname;
import ezvcard.property.Note;
import ezvcard.property.Organization;
import ezvcard.property.Photo;
import ezvcard.property.RawProperty;
import ezvcard.property.StructuredName;
import ezvcard.property.Telephone;
import ezvcard.property.Title;
import ezvcard.property.Url;
import ezvcard.property.VCardProperty;
import ezvcard.util.TelUri;

/*
 Copyright (c) 2014-2015, Michael Angstadt
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 1. Redistributions of source code must retain the above copyright notice, this
 list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation
 and/or other materials provided with the distribution.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 The views and conclusions contained in the software and documentation are those
 of the authors and should not be interpreted as representing official policies,
 either expressed or implied, of the FreeBSD Project.
 */

/**
 * Inserts a {@link VCard} into an Android database.
 *
 * @author Pratyush
 * @author Michael Angstadt
 */
public class ContactOperations {
	private static final int rawContactID = 0;

	private final Context context;
	private final NonEmptyContentValues account;

	public ContactOperations(Context context) {
		this(context, null, null);
	}

	public ContactOperations(Context context, String accountName, String accountType) {
		this.context = context;

		account = new NonEmptyContentValues();
		account.put(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType);
		account.put(ContactsContract.RawContacts.ACCOUNT_NAME, accountName);
	}

	public void insertContact(VCard vcard) throws RemoteException, OperationApplicationException {
		// TODO handle Raw properties - Raw properties include various extension which start with "X-" like X-ASSISTANT, X-AIM, X-SPOUSE

		List<NonEmptyContentValues> contentValues = new ArrayList<NonEmptyContentValues>();
		convertName(contentValues, vcard);
		convertNickname(contentValues, vcard);
		convertPhones(contentValues, vcard);
		convertEmails(contentValues, vcard);
		convertAddresses(contentValues, vcard);
		convertIms(contentValues, vcard);

		// handle Android Custom fields..This is only valid for Android generated Vcards. As the Android would
		// generate NickName, ContactEvents other than Birthday and RelationShip with this "X-ANDROID-CUSTOM" name
		convertCustomFields(contentValues, vcard);

		// handle Iphone kinda of group properties. which are grouped together.
		convertGroupedProperties(contentValues, vcard);

		convertBirthdays(contentValues, vcard);

		convertWebsites(contentValues, vcard);
		convertNotes(contentValues, vcard);
		convertPhotos(contentValues, vcard);
		convertOrganization(contentValues, vcard);

		ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(contentValues.size());
		ContentValues cv = account.getContentValues();
		//ContactsContract.RawContact.CONTENT_URI needed to add account, backReference is also not needed
		ContentProviderOperation operation =
				ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) 
						.withValues(cv)
						.build();
		operations.add(operation);
		for (NonEmptyContentValues values : contentValues) {
			cv = values.getContentValues();
			if (cv.size() == 0) {
				continue;
			}

			//@formatter:off
			operation =
				ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
				.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactID)
				.withValues(cv)
				.build();
			//@formatter:on
			operations.add(operation);
		}

		// Executing all the insert operations as a single database transaction
		context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
	}

	private void convertName(List<NonEmptyContentValues> contentValues, VCard vcard) {
		NonEmptyContentValues values = new NonEmptyContentValues(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);

		String firstName = null, lastName = null, namePrefix = null, nameSuffix = null;
		StructuredName n = vcard.getStructuredName();
		if (n != null) {
			firstName = n.getGiven();
			values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, firstName);

			lastName = n.getFamily();
			values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, lastName);

			List<String> prefixes = n.getPrefixes();
			if (!prefixes.isEmpty()) {
				namePrefix = prefixes.get(0);
				values.put(ContactsContract.CommonDataKinds.StructuredName.PREFIX, namePrefix);
			}

			List<String> suffixes = n.getSuffixes();
			if (!suffixes.isEmpty()) {
				nameSuffix = suffixes.get(0);
				values.put(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, nameSuffix);
			}
		}

		FormattedName fn = vcard.getFormattedName();
		String formattedName = (fn == null) ? null : fn.getValue();

		String displayName;
		if (isEmpty(formattedName)) {
			StringBuilder sb = new StringBuilder();
			if (!isEmpty(namePrefix)){
				sb.append(namePrefix).append(' ');
			}
			if (!isEmpty(firstName)){
				sb.append(firstName).append(' ');
			}
			if (!isEmpty(lastName)){
				sb.append(lastName).append(' ');
			}
			if (!isEmpty(nameSuffix)){
				if (sb.length() > 0){
	        		sb.deleteCharAt(sb.length()-1); //delete space character
	        		sb.append(", ");
	        	}
				sb.append(nameSuffix);
			}
			
			displayName = sb.toString().trim();
		} else {
			displayName = formattedName;
		}
		values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName);

		RawProperty xPhoneticFirstName = vcard.getExtendedProperty("X-PHONETIC-FIRST-NAME");
		String firstPhoneticName = (xPhoneticFirstName == null) ? null : xPhoneticFirstName.getValue();
		values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, firstPhoneticName);

		RawProperty xPhoneticLastName = vcard.getExtendedProperty("X-PHONETIC-LAST-NAME");
		String lastPhoneticName = (xPhoneticLastName == null) ? null : xPhoneticLastName.getValue();
		values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, lastPhoneticName);

		contentValues.add(values);
	}

	private void convertNickname(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Nickname nickname : vcard.getNicknames()) {
			List<String> nicknameValues = nickname.getValues();
			if (nicknameValues.isEmpty()) {
				continue;
			}

			for (String nicknameValue : nicknameValues) {
				NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
				cv.put(ContactsContract.CommonDataKinds.Nickname.NAME, nicknameValue);
				contentValues.add(cv);
			}
		}
	}

	private void convertPhones(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Telephone telephone : vcard.getTelephoneNumbers()) {
			String value = telephone.getText();
			TelUri uri = telephone.getUri();
			if (isEmpty(value)) {
				if (uri == null) {
					continue;
				}
				value = uri.toString();
			}

			int phoneKind = DataMappings.getPhoneType(telephone);

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Phone.NUMBER, value);
			cv.put(ContactsContract.CommonDataKinds.Phone.TYPE, phoneKind);
			contentValues.add(cv);
		}
	}

	private void convertEmails(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Email email : vcard.getEmails()) {
			String value = email.getValue();
			if (isEmpty(value)) {
				continue;
			}

			int emailKind = DataMappings.getEmailType(email);

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Email.ADDRESS, value);
			cv.put(ContactsContract.CommonDataKinds.Email.TYPE, emailKind);
			contentValues.add(cv);
		}
	}

	private void convertAddresses(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Address address : vcard.getAddresses()) {
			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);

			String street = address.getStreetAddress();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, street);

			String poBox = address.getPoBox();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.POBOX, poBox);

			String city = address.getLocality();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, city);

			String state = address.getRegion();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, state);

			String zipCode = address.getPostalCode();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, zipCode);

			String country = address.getCountry();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, country);

			String label = address.getLabel();
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, label);

			int addressKind = DataMappings.getAddressType(address);
			cv.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, addressKind);

			contentValues.add(cv);
		}
	}

	private void convertIms(List<NonEmptyContentValues> contentValues, VCard vcard) {
		//handle extended properties
		for (Map.Entry<String, Integer> entry : DataMappings.getImPropertyNameMappings().entrySet()) {
			String propertyName = entry.getKey();
			Integer protocolType = entry.getValue();
			List<RawProperty> rawProperties = vcard.getExtendedProperties(propertyName);
			for (RawProperty rawProperty : rawProperties) {
				NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);

				String value = rawProperty.getValue();
				cv.put(ContactsContract.CommonDataKinds.Im.DATA, value);

				cv.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, protocolType);

				contentValues.add(cv);
			}
		}

		//handle IMPP properties
		for (Impp impp : vcard.getImpps()) {
			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);

			String immpAddress = impp.getHandle();
			cv.put(ContactsContract.CommonDataKinds.Im.DATA, immpAddress);

			int immpProtocolType = DataMappings.getIMTypeFromProtocol(impp.getProtocol());
			cv.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, immpProtocolType);

			contentValues.add(cv);
		}
	}

	private void convertCustomFields(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (AndroidCustomField customField : vcard.getProperties(AndroidCustomField.class)) {
			List<String> values = customField.getValues();
			if (values.isEmpty()) {
				continue;
			}

			NonEmptyContentValues cv;
			if (customField.isNickname()) {
				cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
				cv.put(ContactsContract.CommonDataKinds.Nickname.NAME, values.get(0));
			} else if (customField.isContactEvent()) {
				cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
				cv.put(ContactsContract.CommonDataKinds.Event.START_DATE, values.get(0));
				cv.put(ContactsContract.CommonDataKinds.Event.TYPE, values.get(1));
			} else if (customField.isRelation()) {
				cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE);
				cv.put(ContactsContract.CommonDataKinds.Relation.NAME, values.get(0));
				cv.put(ContactsContract.CommonDataKinds.Relation.TYPE, values.get(1));
			} else {
				continue;
			}

			contentValues.add(cv);
		}
	}

	private void convertGroupedProperties(List<NonEmptyContentValues> contentValues, VCard vcard) {
		List<RawProperty> extendedProperties = vcard.getExtendedProperties();
		Map<String, List<RawProperty>> orderedByGroup = orderPropertiesByGroup(extendedProperties);
		final int ABDATE = 1, ABRELATEDNAMES = 2;

		for (List<RawProperty> properties : orderedByGroup.values()) {
			if (properties.size() == 1) {
				continue;
			}

			String label = null;
			String val = null;
			int mime = 0;
			for (RawProperty property : properties) {
				String name = property.getPropertyName();

				if (name.equalsIgnoreCase("X-ABDATE")) {
					label = property.getValue(); //date
					mime = ABDATE;
					continue;
				}

				if (name.equalsIgnoreCase("X-ABRELATEDNAMES")) {
					label = property.getValue(); //name
					mime = ABRELATEDNAMES;
					continue;
				}

				if (name.equalsIgnoreCase("X-ABLABEL")) {
					val = property.getValue(); // type of value ..Birthday,anniversary
					continue;
				}
			}

			NonEmptyContentValues cv = null;
			switch (mime) {
			case ABDATE:
				cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);

				cv.put(ContactsContract.CommonDataKinds.Event.START_DATE, label);

				int type = DataMappings.getDateType(val);
				cv.put(ContactsContract.CommonDataKinds.Event.TYPE, type);

				break;

			case ABRELATEDNAMES:
				if (val != null) {
					cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
					cv.put(ContactsContract.CommonDataKinds.Nickname.NAME, label);

					if (!val.equals("Nickname")) {
						type = DataMappings.getNameType(val);
						cv.put(ContactsContract.CommonDataKinds.Relation.TYPE, type);
					}
				}

				break;

			default:
				continue;
			}

			contentValues.add(cv);
		}
	}

	private void convertBirthdays(List<NonEmptyContentValues> contentValues, VCard vcard) {
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		for (Birthday birthday : vcard.getBirthdays()) {
			Date date = birthday.getDate();
			if (date == null) {
				continue;
			}

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Event.TYPE, ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY);
			cv.put(ContactsContract.CommonDataKinds.Event.START_DATE, df.format(date));
			contentValues.add(cv);
		}
	}

	private void convertWebsites(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Url url : vcard.getUrls()) {
			String urlValue = url.getValue();
			if (isEmpty(urlValue)) {
				continue;
			}

			int type = DataMappings.getWebSiteType(url.getType());

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Website.URL, urlValue);
			cv.put(ContactsContract.CommonDataKinds.Website.TYPE, type);
			contentValues.add(cv);
		}
	}

	private void convertNotes(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Note note : vcard.getNotes()) {
			String noteValue = note.getValue();

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Note.NOTE, noteValue);
			contentValues.add(cv);
		}
	}

	private void convertPhotos(List<NonEmptyContentValues> contentValues, VCard vcard) {
		for (Photo photo : vcard.getPhotos()) {
			byte[] data = photo.getData();

			NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
			cv.put(ContactsContract.CommonDataKinds.Photo.PHOTO, data);
			contentValues.add(cv);
		}
	}

	private void convertOrganization(List<NonEmptyContentValues> contentValues, VCard vcard) {
		NonEmptyContentValues cv = new NonEmptyContentValues(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);

		Organization organization = vcard.getOrganization();
		if (organization != null) {
			List<String> values = organization.getValues();
			String keys[] = { ContactsContract.CommonDataKinds.Organization.COMPANY, ContactsContract.CommonDataKinds.Organization.DEPARTMENT, ContactsContract.CommonDataKinds.Organization.OFFICE_LOCATION };
			for (int i = 0; i < values.size(); i++) {
				String key = keys[i];
				String value = values.get(i);
				cv.put(key, value);
			}
		}

		List<Title> titleList = vcard.getTitles();
		if (!titleList.isEmpty()) {
			cv.put(ContactsContract.CommonDataKinds.Organization.TITLE, titleList.get(0).getValue());
		}

		contentValues.add(cv);
	}

	/**
	 * Groups properties by their group name.
	 * @param properties the properties to group
	 * @return a map where the key is the group name (null for no group) and the
	 * value is the list of properties that belong to that group
	 */
	private <T extends VCardProperty> Map<String, List<T>> orderPropertiesByGroup(List<T> properties) {
		Map<String, List<T>> groupedProperties = new HashMap<String, List<T>>();

		for (T property : properties) {
			String group = property.getGroup();
			if (isEmpty(group)) {
				continue;
			}

			List<T> groupPropertiesList = groupedProperties.get(group);
			if (groupPropertiesList == null) {
				groupPropertiesList = new ArrayList<T>();
				groupedProperties.put(group, groupPropertiesList);
			}
			groupPropertiesList.add(property);
		}

		return groupedProperties;
	}

	/**
	 * A wrapper for {@link ContentValues} that only adds values which are
	 * non-null and non-empty (in the case of Strings).
	 */
	private static class NonEmptyContentValues {
		private final ContentValues contentValues = new ContentValues();
		private final String contentItemType;

		public NonEmptyContentValues() {
			this(null);
		}

		/**
		 * @param contentItemType the MIME type (value of
		 * {@link ContactsContract.Contacts.Data#MIMETYPE})
		 */
		public NonEmptyContentValues(String contentItemType) {
			this.contentItemType = contentItemType;
		}

		public void put(String key, String value) {
			if (isEmpty(value)) {
				return;
			}
			contentValues.put(key, value);
		}

		public void put(String key, int value) {
			contentValues.put(key, value);
		}

		public void put(String key, byte[] value) {
			if (value == null) {
				return;
			}
			contentValues.put(key, value);
		}

		/**
		 * Gets the wrapped {@link ContentValues} object, adding the MIME type
		 * entry if the values map is not empty.
		 * @return the wrapped {@link ContentValues} object
		 */
		public ContentValues getContentValues() {
			if (contentValues.size() > 0 && contentItemType != null) {
				put(ContactsContract.Contacts.Data.MIMETYPE, contentItemType);
			}
			return contentValues;
		}
	}
}