/*
 * Copyright (C) 2017 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 android.service.autofill;

import static android.view.autofill.Helper.sDebug;

import android.annotation.DrawableRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.widget.ImageView;
import android.widget.RemoteViews;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * Replaces the content of a child {@link ImageView} of a
 * {@link RemoteViews presentation template} with the first image that matches a regular expression
 * (regex).
 *
 * <p>Typically used to display credit card logos. Example:
 *
 * <pre class="prettyprint">
 *   new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"),
 *                                   R.drawable.ic_credit_card_logo1, "Brand 1")
 *     .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2")
 *     .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3")
 *     .build();
 * </pre>
 *
 * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
 * expensive to evaluate, so use the minimum number of regexs and add the most common first
 * (for example, if this is a tranformation for a credit card logo and the most common credit card
 * issuers are banks X and Y, add the regexes that resolves these 2 banks first).
 */
public final class ImageTransformation extends InternalTransformation implements Transformation,
        Parcelable {
    private static final String TAG = "ImageTransformation";

    private final AutofillId mId;
    private final ArrayList<Option> mOptions;

    private ImageTransformation(Builder builder) {
        mId = builder.mId;
        mOptions = builder.mOptions;
    }

    /** @hide */
    @TestApi
    @Override
    public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
            int childViewId) throws Exception {
        final String value = finder.findByAutofillId(mId);
        if (value == null) {
            Log.w(TAG, "No view for id " + mId);
            return;
        }
        final int size = mOptions.size();
        if (sDebug) {
            Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against");
        }

        for (int i = 0; i < size; i++) {
            final Option option = mOptions.get(i);
            try {
                if (option.pattern.matcher(value).matches()) {
                    Log.d(TAG, "Found match at " + i + ": " + option);
                    parentTemplate.setImageViewResource(childViewId, option.resId);
                    if (option.contentDescription != null) {
                        parentTemplate.setContentDescription(childViewId,
                                option.contentDescription);
                    }
                    return;
                }
            } catch (Exception e) {
                // Do not log full exception to avoid PII leaking
                Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id "
                        + option.resId + ": " + e.getClass());
                throw e;

            }
        }
        if (sDebug) Log.d(TAG, "No match for " + value);
    }

    /**
     * Builder for {@link ImageTransformation} objects.
     */
    public static class Builder {
        private final AutofillId mId;
        private final ArrayList<Option> mOptions = new ArrayList<>();
        private boolean mDestroyed;

        /**
         * Creates a new builder for a autofill id and add a first option.
         *
         * @param id id of the screen field that will be used to evaluate whether the image should
         * be used.
         * @param regex regular expression defining what should be matched to use this image.
         * @param resId resource id of the image (in the autofill service's package). The
         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
         *
         * @deprecated use
         * {@link #Builder(AutofillId, Pattern, int, CharSequence)} instead.
         */
        @Deprecated
        public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) {
            mId = Objects.requireNonNull(id);
            addOption(regex, resId);
        }

        /**
         * Creates a new builder for a autofill id and add a first option.
         *
         * @param id id of the screen field that will be used to evaluate whether the image should
         * be used.
         * @param regex regular expression defining what should be matched to use this image.
         * @param resId resource id of the image (in the autofill service's package). The
         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
         * @param contentDescription content description to be applied in the child view.
         */
        public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId,
                @NonNull CharSequence contentDescription) {
            mId = Objects.requireNonNull(id);
            addOption(regex, resId, contentDescription);
        }

        /**
         * Adds an option to replace the child view with a different image when the regex matches.
         *
         * @param regex regular expression defining what should be matched to use this image.
         * @param resId resource id of the image (in the autofill service's package). The
         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
         *
         * @return this build
         *
         * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead.
         */
        @Deprecated
        public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) {
            addOptionInternal(regex, resId, null);
            return this;
        }

        /**
         * Adds an option to replace the child view with a different image and content description
         * when the regex matches.
         *
         * @param regex regular expression defining what should be matched to use this image.
         * @param resId resource id of the image (in the autofill service's package). The
         * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
         * @param contentDescription content description to be applied in the child view.
         *
         * @return this build
         */
        public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId,
                @NonNull CharSequence contentDescription) {
            addOptionInternal(regex, resId, Objects.requireNonNull(contentDescription));
            return this;
        }

        private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId,
                @Nullable CharSequence contentDescription) {
            throwIfDestroyed();

            Objects.requireNonNull(regex);
            Preconditions.checkArgument(resId != 0);

            mOptions.add(new Option(regex, resId, contentDescription));
        }


        /**
         * Creates a new {@link ImageTransformation} instance.
         */
        public ImageTransformation build() {
            throwIfDestroyed();
            mDestroyed = true;
            return new ImageTransformation(this);
        }

        private void throwIfDestroyed() {
            Preconditions.checkState(!mDestroyed, "Already called build()");
        }
    }

    /////////////////////////////////////
    // Object "contract" methods. //
    /////////////////////////////////////
    @Override
    public String toString() {
        if (!sDebug) return super.toString();

        return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]";
    }

    /////////////////////////////////////
    // Parcelable "contract" methods. //
    /////////////////////////////////////
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel parcel, int flags) {
        parcel.writeParcelable(mId, flags);

        final int size = mOptions.size();
        final Pattern[] patterns = new Pattern[size];
        final int[] resIds = new int[size];
        final CharSequence[] contentDescriptions = new String[size];
        for (int i = 0; i < size; i++) {
            final Option option = mOptions.get(i);
            patterns[i] = option.pattern;
            resIds[i] = option.resId;
            contentDescriptions[i] = option.contentDescription;
        }
        parcel.writeSerializable(patterns);
        parcel.writeIntArray(resIds);
        parcel.writeCharSequenceArray(contentDescriptions);
    }

    public static final @android.annotation.NonNull Parcelable.Creator<ImageTransformation> CREATOR =
            new Parcelable.Creator<ImageTransformation>() {
        @Override
        public ImageTransformation createFromParcel(Parcel parcel) {
            final AutofillId id = parcel.readParcelable(null, android.view.autofill.AutofillId.class);

            final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
            final int[] resIds = parcel.createIntArray();
            final CharSequence[] contentDescriptions = parcel.readCharSequenceArray();

            // Always go through the builder to ensure the data ingested by the system obeys the
            // contract of the builder to avoid attacks using specially crafted parcels.
            final CharSequence contentDescription = contentDescriptions[0];
            final ImageTransformation.Builder builder = (contentDescription != null)
                    ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription)
                    : new ImageTransformation.Builder(id, regexs[0], resIds[0]);

            final int size = regexs.length;
            for (int i = 1; i < size; i++) {
                if (contentDescriptions[i] != null) {
                    builder.addOption(regexs[i], resIds[i], contentDescriptions[i]);
                } else {
                    builder.addOption(regexs[i], resIds[i]);
                }
            }

            return builder.build();
        }

        @Override
        public ImageTransformation[] newArray(int size) {
            return new ImageTransformation[size];
        }
    };

    private static final class Option {
        public final Pattern pattern;
        public final int resId;
        public final CharSequence contentDescription;

        Option(Pattern pattern, int resId, CharSequence contentDescription) {
            this.pattern = pattern;
            this.resId = resId;
            this.contentDescription = TextUtils.trimNoCopySpans(contentDescription);
        }
    }
}
