/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * 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 com.android.server.appsearch.contactsindexer;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Data;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.server.appsearch.contactsindexer.appsearchtypes.Person;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
 * Helper Class to handle data for different MIME types from CP2, and build {@link Person} from
 * them.
 *
 * 
This class is not thread safe.
 *
 * @hide
 */
public final class ContactDataHandler {
    private final Map mHandlers;
    private final Set mNeededColumns;
    /** Constructor. */
    public ContactDataHandler(Resources resources) {
        // Create handlers for different MIME types
        mHandlers = new ArrayMap<>();
        mHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataHandler(resources));
        mHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataHandler());
        mHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneHandler(resources));
        mHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, new StructuredPostalHandler(resources));
        mHandlers.put(StructuredName.CONTENT_ITEM_TYPE, new StructuredNameHandler());
        mHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataHandler());
        mHandlers.put(Relation.CONTENT_ITEM_TYPE, new RelationDataHandler(resources));
        mHandlers.put(Note.CONTENT_ITEM_TYPE, new NoteDataHandler());
        // Retrieve all the needed columns from different data handlers.
        Set neededColumns = new ArraySet<>();
        neededColumns.add(ContactsContract.Data.MIMETYPE);
        for (DataHandler handler : mHandlers.values()) {
            handler.addNeededColumns(neededColumns);
        }
        // We need to make sure this is unmodifiable since the reference is returned in
        // getNeededColumns().
        mNeededColumns = Collections.unmodifiableSet(neededColumns);
    }
    /** Returns an unmodifiable set of columns this {@link ContactDataHandler} is asking for. */
    public Set getNeededColumns() {
        return mNeededColumns;
    }
    /**
     * Adds the information of the current row from {@link ContactsContract.Data} table
     * into the {@link PersonBuilderHelper}.
     *
     * By reading each row in the table, we will get the detailed information about a
     * Person(contact).
     *
     * @param builderHelper a helper to build the {@link Person}.
     */
    public void convertCursorToPerson(@NonNull Cursor cursor,
            @NonNull PersonBuilderHelper builderHelper) {
        Objects.requireNonNull(cursor);
        Objects.requireNonNull(builderHelper);
        int mimetypeIndex = cursor.getColumnIndex(Data.MIMETYPE);
        String mimeType = cursor.getString(mimetypeIndex);
        DataHandler handler = mHandlers.get(mimeType);
        if (handler != null) {
            handler.addData(builderHelper, cursor);
        }
    }
    abstract static class DataHandler {
        /** Gets the column as a string. */
        @Nullable
        protected final String getColumnString(@NonNull Cursor cursor, @NonNull String column) {
            Objects.requireNonNull(cursor);
            Objects.requireNonNull(column);
            int columnIndex = cursor.getColumnIndex(column);
            if (columnIndex == -1) {
                return null;
            }
            return cursor.getString(columnIndex);
        }
        /** Gets the column as an int. */
        protected final int getColumnInt(@NonNull Cursor cursor, @NonNull String column) {
            Objects.requireNonNull(cursor);
            Objects.requireNonNull(column);
            int columnIndex = cursor.getColumnIndex(column);
            if (columnIndex == -1) {
                return 0;
            }
            return cursor.getInt(columnIndex);
        }
        /** Adds the columns needed for the {@code DataHandler}. */
        public abstract void addNeededColumns(Collection columns);
        /** Adds the data into {@link PersonBuilderHelper}. */
        public abstract void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor);
    }
    private abstract static class SingleColumnDataHandler extends DataHandler {
        private final String mColumn;
        protected SingleColumnDataHandler(@NonNull String column) {
            Objects.requireNonNull(column);
            mColumn = column;
        }
        /** Adds the columns needed for the {@code DataHandler}. */
        @Override
        public final void addNeededColumns(@NonNull Collection columns) {
            Objects.requireNonNull(columns);
            columns.add(mColumn);
        }
        /** Adds the data into {@link PersonBuilderHelper}. */
        @Override
        public final void addData(@NonNull PersonBuilderHelper builderHelper,
                @NonNull Cursor cursor) {
            Objects.requireNonNull(builderHelper);
            Objects.requireNonNull(cursor);
            String data = getColumnString(cursor, mColumn);
            if (!TextUtils.isEmpty(data)) {
                addSingleColumnStringData(builderHelper, data);
            }
        }
        protected abstract void addSingleColumnStringData(PersonBuilderHelper builderHelper,
                String data);
    }
    private abstract static class ContactPointDataHandler extends DataHandler {
        private final Resources mResources;
        private final String[] mDataColumns;
        private final String mTypeColumn;
        private final String mLabelColumn;
        public ContactPointDataHandler(
                @NonNull Resources resources, @NonNull String[] dataColumns,
                @NonNull String typeColumn, @NonNull String labelColumn) {
            mResources = Objects.requireNonNull(resources);
            mDataColumns = Objects.requireNonNull(dataColumns);
            mTypeColumn = Objects.requireNonNull(typeColumn);
            mLabelColumn = Objects.requireNonNull(labelColumn);
        }
        /** Adds the columns needed for the {@code DataHandler}. */
        @Override
        public final void addNeededColumns(@NonNull Collection columns) {
            Objects.requireNonNull(columns);
            columns.add(Data._ID);
            columns.add(Data.IS_PRIMARY);
            columns.add(Data.IS_SUPER_PRIMARY);
            for (int i = 0; i < mDataColumns.length; ++i) {
                columns.add(mDataColumns[i]);
            }
            columns.add(mTypeColumn);
            columns.add(mLabelColumn);
        }
        /**
         * Adds the data for ContactsPoint(email, telephone, postal addresses) into
         * {@link Person.Builder}.
         */
        @Override
        public final void addData(@NonNull PersonBuilderHelper builderHelper,
                @NonNull Cursor cursor) {
            Objects.requireNonNull(builderHelper);
            Objects.requireNonNull(cursor);
            Map data = new ArrayMap<>(mDataColumns.length);
            for (int i = 0; i < mDataColumns.length; ++i) {
                String col = getColumnString(cursor, mDataColumns[i]);
                if (!TextUtils.isEmpty(col)) {
                    data.put(mDataColumns[i], col);
                }
            }
            if (!data.isEmpty()) {
                // get the corresponding label to the type.
                int type = getColumnInt(cursor, mTypeColumn);
                String label = getTypeLabel(mResources, type,
                        getColumnString(cursor, mLabelColumn));
                addContactPointData(builderHelper, label, data);
            }
        }
        @NonNull
        protected abstract String getTypeLabel(Resources resources, int type, String label);
        /**
         * Adds the information in the {@link Person.Builder}.
         *
         * @param builderHelper a helper to build the {@link Person}.
         * @param label         the corresponding label to the {@code type} for the data.
         * @param data          data read from the designed columns in the row.
         */
        protected abstract void addContactPointData(
                PersonBuilderHelper builderHelper, String label, Map data);
    }
    private static final class EmailDataHandler extends ContactPointDataHandler {
        private static final String[] COLUMNS = {
                Email.ADDRESS,
        };
        public EmailDataHandler(@NonNull Resources resources) {
            super(resources, COLUMNS, Email.TYPE, Email.LABEL);
        }
        /**
         * Adds the Email information in the {@link Person.Builder}.
         *
         * @param builderHelper a builder to build the {@link Person}.
         * @param label         The corresponding label to the {@code type}. E.g. {@link
         *                      com.android.internal.R.string#emailTypeHome} to {@link
         *                      Email#TYPE_HOME} or custom label for the data if {@code type} is
         *                      {@link
         *                      Email#TYPE_CUSTOM}.
         * @param data          data read from the designed column {@code Email.ADDRESS} in the row.
         */
        @Override
        protected void addContactPointData(
                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
                @NonNull Map data) {
            Objects.requireNonNull(builderHelper);
            Objects.requireNonNull(data);
            Objects.requireNonNull(label);
            String email = data.get(Email.ADDRESS);
            if (!TextUtils.isEmpty(email)) {
                builderHelper.addEmailToPerson(label, email);
            }
        }
        @NonNull
        @Override
        protected String getTypeLabel(@NonNull Resources resources, int type,
                @Nullable String label) {
            Objects.requireNonNull(resources);
            return Email.getTypeLabel(resources, type, label).toString();
        }
    }
    private static final class PhoneHandler extends ContactPointDataHandler {
        private static final String[] COLUMNS = {
                Phone.NUMBER,
                Phone.NORMALIZED_NUMBER,
        };
        private final Resources mResources;
        public PhoneHandler(@NonNull Resources resources) {
            super(resources, COLUMNS, Phone.TYPE, Phone.LABEL);
            mResources = Objects.requireNonNull(resources);
        }
        /**
         * Adds the phone number information in the {@link Person.Builder}.
         *
         * @param builderHelper helper to build the {@link Person}.
         * @param label         corresponding label to {@code type}. E.g. {@link
         *                      com.android.internal.R.string#phoneTypeHome} to {@link
         *                      Phone#TYPE_HOME}, or custom label for the data if {@code type} is
         *                      {@link Phone#TYPE_CUSTOM}.
         * @param data          data read from the designed columns {@link Phone#NUMBER} in the row.
         */
        @Override
        protected void addContactPointData(
                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
                @NonNull Map data) {
            Objects.requireNonNull(builderHelper);
            Objects.requireNonNull(data);
            Objects.requireNonNull(label);
            // Add original phone number directly to the final phone number
            // list. E.g. (202) 555-0111
            String phoneNumberOriginal = data.get(Phone.NUMBER);
            if (TextUtils.isEmpty(phoneNumberOriginal)) {
                return;
            }
            builderHelper.addPhoneToPerson(label, phoneNumberOriginal);
            // Try to get phone number in e164 from CP2.
            String phoneNumberE164FromCP2 = data.get(Phone.NORMALIZED_NUMBER);
            // Try to include different variants based on the national (e.g. (202) 555-0111), and
            // the e164 format of the original number. The variants are generated with the best
            // efforts, depending on the locales available in the current configuration on the
            // system.
            Set phoneNumberVariants =
                    ContactsIndexerPhoneNumberUtils.createPhoneNumberVariants(mResources,
                            phoneNumberOriginal, phoneNumberE164FromCP2);
            phoneNumberVariants.remove(phoneNumberOriginal);
            for (String variant : phoneNumberVariants) {
                // Append phone variants to a different list, which will be appended into
                // the final one during buildPerson.
                builderHelper.addPhoneVariantToPerson(label, variant);
            }
        }
        @NonNull
        @Override
        protected String getTypeLabel(@NonNull Resources resources, int type,
                @Nullable String label) {
            Objects.requireNonNull(resources);
            return Phone.getTypeLabel(resources, type, label).toString();
        }
    }
    private static final class StructuredPostalHandler extends ContactPointDataHandler {
        private static final String[] COLUMNS = {
                StructuredPostal.FORMATTED_ADDRESS,
        };
        public StructuredPostalHandler(@NonNull Resources resources) {
            super(
                    resources,
                    COLUMNS,
                    StructuredPostal.TYPE,
                    StructuredPostal.LABEL);
        }
        /**
         * Adds the postal address information in the {@link Person.Builder}.
         *
         * @param builderHelper helper to build the {@link Person}.
         * @param label         corresponding label to {@code type}. E.g. {@link
         *                      com.android.internal.R.string#postalTypeHome} to {@link
         *                      StructuredPostal#TYPE_HOME}, or custom label for the data if {@code
         *                      type} is {@link StructuredPostal#TYPE_CUSTOM}.
         * @param data          data read from the designed column
         *                      {@link StructuredPostal#FORMATTED_ADDRESS} in the row.
         */
        @Override
        protected void addContactPointData(
                @NonNull PersonBuilderHelper builderHelper, @NonNull String label,
                @NonNull Map data) {
            Objects.requireNonNull(builderHelper);
            Objects.requireNonNull(data);
            Objects.requireNonNull(label);
            String address = data.get(StructuredPostal.FORMATTED_ADDRESS);
            if (!TextUtils.isEmpty(address)) {
                builderHelper.addAddressToPerson(label, address);
            }
        }
        @NonNull
        @Override
        protected String getTypeLabel(@NonNull Resources resources, int type,
                @Nullable String label) {
            Objects.requireNonNull(resources);
            return StructuredPostal.getTypeLabel(resources, type, label).toString();
        }
    }
    private static final class NicknameDataHandler extends SingleColumnDataHandler {
        public NicknameDataHandler() {
            super(Nickname.NAME);
        }
        @Override
        protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
                @NonNull String data) {
            Objects.requireNonNull(builder);
            Objects.requireNonNull(data);
            builder.getPersonBuilder().addAdditionalName(Person.TYPE_NICKNAME, data);
        }
    }
    private static final class StructuredNameHandler extends DataHandler {
        private static final String[] COLUMNS = {
                Data.RAW_CONTACT_ID,
                Data.NAME_RAW_CONTACT_ID,
                // Only those three fields we need to set in the builder.
                StructuredName.GIVEN_NAME,
                StructuredName.MIDDLE_NAME,
                StructuredName.FAMILY_NAME,
        };
        /** Adds the columns needed for the {@code DataHandler}. */
        @Override
        public final void addNeededColumns(Collection columns) {
            Collections.addAll(columns, COLUMNS);
        }
        /** Adds the data into {@link Person.Builder}. */
        @Override
        public final void addData(@NonNull PersonBuilderHelper builderHelper, Cursor cursor) {
            Objects.requireNonNull(builderHelper);
            String rawContactId = getColumnString(cursor, Data.RAW_CONTACT_ID);
            String nameRawContactId = getColumnString(cursor, Data.NAME_RAW_CONTACT_ID);
            String givenName = getColumnString(cursor, StructuredName.GIVEN_NAME);
            String familyName = getColumnString(cursor, StructuredName.FAMILY_NAME);
            String middleName = getColumnString(cursor, StructuredName.MIDDLE_NAME);
            Person.Builder builder = builderHelper.getPersonBuilder();
            // only set given, middle and family name iff rawContactId is same as
            // nameRawContactId. In this case those three match the value for displayName in CP2.
            if (!TextUtils.isEmpty(rawContactId)
                    && !TextUtils.isEmpty(nameRawContactId)
                    && rawContactId.equals(nameRawContactId)) {
                if (givenName != null) {
                    builder.setGivenName(givenName);
                }
                if (familyName != null) {
                    builder.setFamilyName(familyName);
                }
                if (middleName != null) {
                    builder.setMiddleName(middleName);
                }
            }
        }
    }
    private static final class OrganizationDataHandler extends DataHandler {
        private static final String[] COLUMNS = {
                Organization.TITLE,
                Organization.DEPARTMENT,
                Organization.COMPANY,
        };
        private final StringBuilder mStringBuilder = new StringBuilder();
        @Override
        public void addNeededColumns(Collection columns) {
            for (String column : COLUMNS) {
                columns.add(column);
            }
        }
        @Override
        public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
            mStringBuilder.setLength(0);
            for (String column : COLUMNS) {
                String value = getColumnString(cursor, column);
                if (!TextUtils.isEmpty(value)) {
                    if (mStringBuilder.length() != 0) {
                        mStringBuilder.append(", ");
                    }
                    mStringBuilder.append(value);
                }
            }
            if (mStringBuilder.length() > 0) {
                builder.getPersonBuilder().addAffiliation(mStringBuilder.toString());
            }
        }
    }
    private static final class RelationDataHandler extends DataHandler {
        private static final String[] COLUMNS = {
                Relation.NAME,
                Relation.TYPE,
                Relation.LABEL,
        };
        private final Resources mResources;
        public RelationDataHandler(@NonNull Resources resources) {
            mResources = resources;
        }
        @Override
        public void addNeededColumns(Collection columns) {
            for (String column : COLUMNS) {
                columns.add(column);
            }
        }
        @Override
        public void addData(@NonNull PersonBuilderHelper builder, Cursor cursor) {
            String relationName = getColumnString(cursor, Relation.NAME);
            if (TextUtils.isEmpty(relationName)) {
                // Get the relation name from type. If it is a custom type, get it from
                // label.
                int type = getColumnInt(cursor, Relation.TYPE);
                String label = getColumnString(cursor, Relation.LABEL);
                relationName = Relation.getTypeLabel(mResources, type, label).toString();
                if (TextUtils.isEmpty(relationName)) {
                    return;
                }
            }
            builder.getPersonBuilder().addRelation(relationName);
        }
    }
    private static final class NoteDataHandler extends SingleColumnDataHandler {
        public NoteDataHandler() {
            super(Note.NOTE);
        }
        @Override
        protected void addSingleColumnStringData(@NonNull PersonBuilderHelper builder,
                @NonNull String data) {
            Objects.requireNonNull(builder);
            Objects.requireNonNull(data);
            builder.getPersonBuilder().addNote(data);
        }
    }
}