/*
 * Copyright (C) 2013 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.print;

import android.annotation.DrawableRes;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.print.PrinterInfoProto;
import android.text.TextUtils;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * This class represents the description of a printer. Instances of
 * this class are created by print services to report to the system
 * the printers they manage. The information of this class has two
 * major components, printer properties such as name, id, status,
 * description and printer capabilities which describe the various
 * print modes a printer supports such as media sizes, margins, etc.
 * <p>
 * Once {@link PrinterInfo.Builder#build() built} the objects are immutable.
 * </p>
 */
public final class PrinterInfo implements Parcelable {

    /** @hide */
    @IntDef(prefix = { "STATUS_" }, value = {
            STATUS_IDLE,
            STATUS_BUSY,
            STATUS_UNAVAILABLE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Status {
    }

    /** Printer status: the printer is idle and ready to print. */
    public static final int STATUS_IDLE = PrinterInfoProto.STATUS_IDLE;

    /** Printer status: the printer is busy printing. */
    public static final int STATUS_BUSY = PrinterInfoProto.STATUS_BUSY;

    /** Printer status: the printer is not available. */
    public static final int STATUS_UNAVAILABLE = PrinterInfoProto.STATUS_UNAVAILABLE;

    private final @NonNull PrinterId mId;

    /** Resource inside the printer's services's package to be used as an icon */
    private final int mIconResourceId;

    /** If a custom icon can be loaded for the printer */
    private final boolean mHasCustomPrinterIcon;

    /** The generation of the icon in the cache. */
    private final int mCustomPrinterIconGen;

    /** Intent that launches the activity showing more information about the printer. */
    private final @Nullable PendingIntent mInfoIntent;

    private final @NonNull String mName;

    private final @Status int mStatus;

    private final @Nullable String mDescription;

    private final @Nullable PrinterCapabilitiesInfo mCapabilities;

    private PrinterInfo(@NonNull PrinterId printerId, @NonNull String name, @Status int status,
            int iconResourceId, boolean hasCustomPrinterIcon, String description,
            PendingIntent infoIntent, PrinterCapabilitiesInfo capabilities,
            int customPrinterIconGen) {
        mId = printerId;
        mName = name;
        mStatus = status;
        mIconResourceId = iconResourceId;
        mHasCustomPrinterIcon = hasCustomPrinterIcon;
        mDescription = description;
        mInfoIntent = infoIntent;
        mCapabilities = capabilities;
        mCustomPrinterIconGen = customPrinterIconGen;
    }

    /**
     * Get the globally unique printer id.
     *
     * @return The printer id.
     */
    public @NonNull PrinterId getId() {
        return mId;
    }

    /**
     * Get the icon to be used for this printer. If no per printer icon is available, the printer's
     * service's icon is returned. If the printer has a custom icon this icon might get requested
     * asynchronously. Once the icon is loaded the discovery sessions will be notified that the
     * printer changed.
     *
     * @param context The context that will be using the icons
     * @return The icon to be used for the printer or null if no icon could be found.
     * @hide
     */
    @TestApi
    public @Nullable Drawable loadIcon(@NonNull Context context) {
        Drawable drawable = null;
        PackageManager packageManager = context.getPackageManager();

        if (mHasCustomPrinterIcon) {
            PrintManager printManager = (PrintManager) context
                    .getSystemService(Context.PRINT_SERVICE);

            Icon icon = printManager.getCustomPrinterIcon(mId);

            if (icon != null) {
                drawable = icon.loadDrawable(context);
            }
        }

        if (drawable == null) {
            try {
                String packageName = mId.getServiceName().getPackageName();
                PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
                ApplicationInfo appInfo = packageInfo.applicationInfo;

                // If no custom icon is available, try the icon from the resources
                if (mIconResourceId != 0) {
                    drawable = packageManager.getDrawable(packageName, mIconResourceId, appInfo);
                }

                // Fall back to the printer's service's icon if no per printer icon could be found
                if (drawable == null) {
                    drawable = appInfo.loadIcon(packageManager);
                }
            } catch (NameNotFoundException e) {
            }
        }

        return drawable;
    }

    /**
     * Check if the printer has a custom printer icon.
     *
     * @return {@code true} iff the printer has a custom printer icon.
     *
     * @hide
     */
    public boolean getHasCustomPrinterIcon() {
        return mHasCustomPrinterIcon;
    }

    /**
     * Get the printer name.
     *
     * @return The printer name.
     */
    public @NonNull String getName() {
        return mName;
    }

    /**
     * Gets the printer status.
     *
     * @return The status.
     *
     * @see #STATUS_BUSY
     * @see #STATUS_IDLE
     * @see #STATUS_UNAVAILABLE
     */
    public @Status int getStatus() {
        return mStatus;
    }

    /**
     * Gets the  printer description.
     *
     * @return The description.
     */
    public @Nullable String getDescription() {
        return mDescription;
    }

    /**
     * Get the {@link PendingIntent} that launches the activity showing more information about the
     * printer.
     *
     * @return the {@link PendingIntent} that launches the activity showing more information about
     *         the printer or null if it is not configured
     * @hide
     */
    public @Nullable PendingIntent getInfoIntent() {
        return mInfoIntent;
    }

    /**
     * Gets the printer capabilities.
     *
     * @return The capabilities.
     */
    public @Nullable PrinterCapabilitiesInfo getCapabilities() {
        return mCapabilities;
    }

    /**
     * Check if printerId is valid.
     *
     * @param printerId The printerId that might be valid
     * @return The valid printerId
     * @throws IllegalArgumentException if printerId is not valid.
     */
    private static @NonNull PrinterId checkPrinterId(PrinterId printerId) {
        return Preconditions.checkNotNull(printerId, "printerId cannot be null.");
    }

    /**
     * Check if status is valid.
     *
     * @param status The status that might be valid
     * @return The valid status
     * @throws IllegalArgumentException if status is not valid.
     */
    private static @Status int checkStatus(int status) {
        if (!(status == STATUS_IDLE
                || status == STATUS_BUSY
                || status == STATUS_UNAVAILABLE)) {
            throw new IllegalArgumentException("status is invalid.");
        }

        return status;
    }

    /**
     * Check if name is valid.
     *
     * @param name The name that might be valid
     * @return The valid name
     * @throws IllegalArgumentException if name is not valid.
     */
    private static @NonNull String checkName(String name) {
        return Preconditions.checkStringNotEmpty(name, "name cannot be empty.");
    }

    private PrinterInfo(Parcel parcel) {
        // mName can be null due to unchecked set in Builder.setName and status can be invalid
        // due to unchecked set in Builder.setStatus, hence we can only check mId for a valid state
        mId = checkPrinterId((PrinterId) parcel.readParcelable(null, android.print.PrinterId.class));
        mName = checkName(parcel.readString());
        mStatus = checkStatus(parcel.readInt());
        mDescription = parcel.readString();
        mCapabilities = parcel.readParcelable(null, android.print.PrinterCapabilitiesInfo.class);
        mIconResourceId = parcel.readInt();
        mHasCustomPrinterIcon = parcel.readByte() != 0;
        mCustomPrinterIconGen = parcel.readInt();
        mInfoIntent = parcel.readParcelable(null, android.app.PendingIntent.class);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int flags) {
        parcel.writeParcelable(mId, flags);
        parcel.writeString(mName);
        parcel.writeInt(mStatus);
        parcel.writeString(mDescription);
        parcel.writeParcelable(mCapabilities, flags);
        parcel.writeInt(mIconResourceId);
        parcel.writeByte((byte) (mHasCustomPrinterIcon ? 1 : 0));
        parcel.writeInt(mCustomPrinterIconGen);
        parcel.writeParcelable(mInfoIntent, flags);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + mId.hashCode();
        result = prime * result + mName.hashCode();
        result = prime * result + mStatus;
        result = prime * result + ((mDescription != null) ? mDescription.hashCode() : 0);
        result = prime * result + ((mCapabilities != null) ? mCapabilities.hashCode() : 0);
        result = prime * result + mIconResourceId;
        result = prime * result + (mHasCustomPrinterIcon ? 1 : 0);
        result = prime * result + mCustomPrinterIconGen;
        result = prime * result + ((mInfoIntent != null) ? mInfoIntent.hashCode() : 0);
        return result;
    }

    /**
     * Compare two {@link PrinterInfo printerInfos} in all aspects beside being null and the
     * {@link #mStatus}.
     *
     * @param other the other {@link PrinterInfo}
     * @return true iff the infos are equivalent
     * @hide
     */
    public boolean equalsIgnoringStatus(PrinterInfo other) {
        if (!mId.equals(other.mId)) {
            return false;
        }
        if (!mName.equals(other.mName)) {
           return false;
        }
        if (!TextUtils.equals(mDescription, other.mDescription)) {
            return false;
        }
        if (mCapabilities == null) {
            if (other.mCapabilities != null) {
                return false;
            }
        } else if (!mCapabilities.equals(other.mCapabilities)) {
            return false;
        }
        if (mIconResourceId != other.mIconResourceId) {
            return false;
        }
        if (mHasCustomPrinterIcon != other.mHasCustomPrinterIcon) {
            return false;
        }
        if (mCustomPrinterIconGen != other.mCustomPrinterIconGen) {
            return false;
        }
        if (mInfoIntent == null) {
            if (other.mInfoIntent != null) {
                return false;
            }
        } else if (!mInfoIntent.equals(other.mInfoIntent)) {
            return false;
        }
        return true;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        PrinterInfo other = (PrinterInfo) obj;
        if (!equalsIgnoringStatus(other)) {
            return false;
        }
        if (mStatus != other.mStatus) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("PrinterInfo{");
        builder.append("id=").append(mId);
        builder.append(", name=").append(mName);
        builder.append(", status=").append(mStatus);
        builder.append(", description=").append(mDescription);
        builder.append(", capabilities=").append(mCapabilities);
        builder.append(", iconResId=").append(mIconResourceId);
        builder.append(", hasCustomPrinterIcon=").append(mHasCustomPrinterIcon);
        builder.append(", customPrinterIconGen=").append(mCustomPrinterIconGen);
        builder.append(", infoIntent=").append(mInfoIntent);
        builder.append("\"}");
        return builder.toString();
    }

    /**
     * Builder for creating of a {@link PrinterInfo}.
     */
    public static final class Builder {
        private @NonNull PrinterId mPrinterId;
        private @NonNull String mName;
        private @Status int mStatus;
        private int mIconResourceId;
        private boolean mHasCustomPrinterIcon;
        private String mDescription;
        private PendingIntent mInfoIntent;
        private PrinterCapabilitiesInfo mCapabilities;
        private int mCustomPrinterIconGen;

        /**
         * Constructor.
         *
         * @param printerId The printer id. Cannot be null.
         * @param name The printer name. Cannot be empty.
         * @param status The printer status. Must be a valid status.
         * @throws IllegalArgumentException If the printer id is null, or the
         * printer name is empty or the status is not a valid one.
         */
        public Builder(@NonNull PrinterId printerId, @NonNull String name, @Status int status) {
            mPrinterId = checkPrinterId(printerId);
            mName = checkName(name);
            mStatus = checkStatus(status);
        }

        /**
         * Constructor.
         *
         * @param other Other info from which to start building.
         */
        public Builder(@NonNull PrinterInfo other) {
            mPrinterId = other.mId;
            mName = other.mName;
            mStatus = other.mStatus;
            mIconResourceId = other.mIconResourceId;
            mHasCustomPrinterIcon = other.mHasCustomPrinterIcon;
            mDescription = other.mDescription;
            mInfoIntent = other.mInfoIntent;
            mCapabilities = other.mCapabilities;
            mCustomPrinterIconGen = other.mCustomPrinterIconGen;
        }

        /**
         * Sets the printer status.
         *
         * @param status The status.
         * @return This builder.
         * @see PrinterInfo#STATUS_IDLE
         * @see PrinterInfo#STATUS_BUSY
         * @see PrinterInfo#STATUS_UNAVAILABLE
         */
        public @NonNull Builder setStatus(@Status int status) {
            mStatus = checkStatus(status);
            return this;
        }

        /**
         * Set a drawable resource as icon for this printer. If no icon is set the printer's
         * service's icon is used for the printer.
         *
         * @param iconResourceId The resource ID of the icon.
         * @return This builder.
         * @see PrinterInfo.Builder#setHasCustomPrinterIcon
         */
        public @NonNull Builder setIconResourceId(@DrawableRes int iconResourceId) {
            mIconResourceId = Preconditions.checkArgumentNonnegative(iconResourceId,
                    "iconResourceId can't be negative");
            return this;
        }

        /**
         * Declares that the print service can load a custom per printer's icon. If both
         * {@link PrinterInfo.Builder#setIconResourceId} and a custom icon are set the resource icon
         * is shown while the custom icon loads but then the custom icon is used. If
         * {@link PrinterInfo.Builder#setIconResourceId} is not set the printer's service's icon is
         * shown while loading.
         * <p>
         * The icon is requested asynchronously and only when needed via
         * {@link android.printservice.PrinterDiscoverySession#onRequestCustomPrinterIcon}.
         * </p>
         *
         * @param hasCustomPrinterIcon If the printer has a custom icon or not.
         *
         * @return This builder.
         */
        public @NonNull Builder setHasCustomPrinterIcon(boolean hasCustomPrinterIcon) {
            mHasCustomPrinterIcon = hasCustomPrinterIcon;
            return this;
        }

        /**
         * Sets the <strong>localized</strong> printer name which
         * is shown to the user
         *
         * @param name The name.
         * @return This builder.
         */
        public @NonNull Builder setName(@NonNull String name) {
            mName = checkName(name);
            return this;
        }

        /**
         * Sets the <strong>localized</strong> printer description
         * which is shown to the user
         *
         * @param description The description.
         * @return This builder.
         */
        public @NonNull Builder setDescription(@NonNull String description) {
            mDescription = description;
            return this;
        }

        /**
         * Sets the {@link PendingIntent} that launches an activity showing more information about
         * the printer.
         *
         * @param infoIntent The {@link PendingIntent intent}.
         * @return This builder.
         */
        public @NonNull Builder setInfoIntent(@NonNull PendingIntent infoIntent) {
            mInfoIntent = infoIntent;
            return this;
        }

        /**
         * Sets the printer capabilities.
         *
         * @param capabilities The capabilities.
         * @return This builder.
         */
        public @NonNull Builder setCapabilities(@NonNull PrinterCapabilitiesInfo capabilities) {
            mCapabilities = capabilities;
            return this;
        }

        /**
         * Creates a new {@link PrinterInfo}.
         *
         * @return A new {@link PrinterInfo}.
         */
        public @NonNull PrinterInfo build() {
            return new PrinterInfo(mPrinterId, mName, mStatus, mIconResourceId,
                    mHasCustomPrinterIcon, mDescription, mInfoIntent, mCapabilities,
                    mCustomPrinterIconGen);
        }

        /**
         * Increments the generation number of the custom printer icon. As the {@link PrinterInfo}
         * does not match the previous one anymore, users of the {@link PrinterInfo} will reload the
         * icon if needed.
         *
         * @return This builder.
         * @hide
         */
        public @NonNull Builder incCustomPrinterIconGen() {
            mCustomPrinterIconGen++;
            return this;
        }
    }

    public static final @android.annotation.NonNull Parcelable.Creator<PrinterInfo> CREATOR =
            new Parcelable.Creator<PrinterInfo>() {
        @Override
        public PrinterInfo createFromParcel(Parcel parcel) {
            return new PrinterInfo(parcel);
        }

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