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

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.annotation.UserIdInt;
import android.net.MacAddress;
import android.os.Parcel;
import android.os.Parcelable;

import java.util.Date;
import java.util.Objects;

/**
 * Details for a specific "association" that has been established between an app and companion
 * device.
 * <p>
 * An association gives an app the ability to interact with a companion device without needing to
 * acquire broader runtime permissions. An association only exists after the user has confirmed that
 * an app should have access to a companion device.
 */
public final class AssociationInfo implements Parcelable {
    /**
     * A String indicates the selfManaged device is not connected.
     */
    private static final String LAST_TIME_CONNECTED_NONE = "None";
    /**
     * A unique ID of this Association record.
     * Disclosed to the clients (i.e. companion applications) for referring to this record (e.g. in
     * {@code disassociate()} API call).
     */
    private final int mId;
    @UserIdInt
    private final int mUserId;
    @NonNull
    private final String mPackageName;
    @Nullable
    private final String mTag;
    @Nullable
    private final MacAddress mDeviceMacAddress;
    @Nullable
    private final CharSequence mDisplayName;
    @Nullable
    private final String mDeviceProfile;
    @Nullable
    private final AssociatedDevice mAssociatedDevice;
    private final boolean mSelfManaged;
    private final boolean mNotifyOnDeviceNearby;
    /**
     * Indicates that the association has been revoked (removed), but we keep the association
     * record for final clean up (e.g. removing the app from the list of the role holders).
     *
     * @see CompanionDeviceManager#disassociate(int)
     */
    private final boolean mRevoked;
    /**
     * Indicates that the association is waiting for its corresponding companion app to be installed
     * before it can be added to CDM. This is likely because it was restored onto the device from a
     * backup.
     */
    private final boolean mPending;
    private final long mTimeApprovedMs;
    /**
     * A long value indicates the last time connected reported by selfManaged devices
     * Default value is Long.MAX_VALUE.
     */
    private final long mLastTimeConnectedMs;
    private final int mSystemDataSyncFlags;

    /**
     * Creates a new Association.
     *
     * @hide
     */
    public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName,
            @Nullable String tag, @Nullable MacAddress macAddress,
            @Nullable CharSequence displayName, @Nullable String deviceProfile,
            @Nullable AssociatedDevice associatedDevice, boolean selfManaged,
            boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs,
            long lastTimeConnectedMs, int systemDataSyncFlags) {
        if (id <= 0) {
            throw new IllegalArgumentException("Association ID should be greater than 0");
        }
        if (macAddress == null && displayName == null) {
            throw new IllegalArgumentException("MAC address and the Display Name must NOT be null "
                    + "at the same time");
        }

        mId = id;
        mUserId = userId;
        mPackageName = packageName;
        mDeviceMacAddress = macAddress;
        mDisplayName = displayName;
        mTag = tag;
        mDeviceProfile = deviceProfile;
        mAssociatedDevice = associatedDevice;
        mSelfManaged = selfManaged;
        mNotifyOnDeviceNearby = notifyOnDeviceNearby;
        mRevoked = revoked;
        mPending = pending;
        mTimeApprovedMs = timeApprovedMs;
        mLastTimeConnectedMs = lastTimeConnectedMs;
        mSystemDataSyncFlags = systemDataSyncFlags;
    }

    /**
     * @return the unique ID of this association record.
     */
    public int getId() {
        return mId;
    }

    /**
     * @return the ID of the user who "owns" this association.
     * @hide
     */
    @UserIdInt
    public int getUserId() {
        return mUserId;
    }

    /**
     * @return the package name of the app which this association refers to.
     * @hide
     */
    @SystemApi
    @NonNull
    public String getPackageName() {
        return mPackageName;
    }

    /**
     * @return the tag of this association.
     * @see CompanionDeviceManager#setAssociationTag(int, String)
     */
    @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG)
    @Nullable
    public String getTag() {
        return mTag;
    }

    /**
     * @return the MAC address of the device.
     */
    @Nullable
    public MacAddress getDeviceMacAddress() {
        return mDeviceMacAddress;
    }

    /** @hide */
    @Nullable
    public String getDeviceMacAddressAsString() {
        return mDeviceMacAddress != null ? mDeviceMacAddress.toString().toUpperCase() : null;
    }

    /**
     * @return the display name of the companion device (optionally) provided by the companion
     * application.
     *
     * @see AssociationRequest.Builder#setDisplayName(CharSequence)
     */
    @Nullable
    public CharSequence getDisplayName() {
        return mDisplayName;
    }

    /**
     * @return the companion device profile used when establishing this
     *         association, or {@code null} if no specific profile was used.
     * @see AssociationRequest.Builder#setDeviceProfile(String)
     */
    @Nullable
    public String getDeviceProfile() {
        return mDeviceProfile;
    }

    /**
     * Companion device that was associated. Note that this field is not persisted across sessions.
     * Device can be one of the following types:
     *
     * <ul>
     *     <li>for classic Bluetooth - {@link AssociatedDevice#getBluetoothDevice()}</li>
     *     <li>for Bluetooth LE - {@link AssociatedDevice#getBleDevice()}</li>
     *     <li>for WiFi - {@link AssociatedDevice#getWifiDevice()}</li>
     * </ul>
     *
     * @return the companion device that was associated, or {@code null} if the device is
     *         self-managed or this association info was retrieved from persistent storage.
     */
    @Nullable
    public AssociatedDevice getAssociatedDevice() {
        return mAssociatedDevice;
    }

    /**
     * @return whether the association is managed by the companion application it belongs to.
     * @see AssociationRequest.Builder#setSelfManaged(boolean)
     */
    @SuppressLint("UnflaggedApi") // promoting from @SystemApi
    public boolean isSelfManaged() {
        return mSelfManaged;
    }

    /** @hide */
    public boolean isNotifyOnDeviceNearby() {
        return mNotifyOnDeviceNearby;
    }

    /** @hide */
    public long getTimeApprovedMs() {
        return mTimeApprovedMs;
    }

    /** @hide */
    public boolean belongsToPackage(@UserIdInt int userId, String packageName) {
        return mUserId == userId && Objects.equals(mPackageName, packageName);
    }

    /**
     * @return if the association has been revoked (removed).
     * @hide
     */
    public boolean isRevoked() {
        return mRevoked;
    }

    /**
     * @return true if the association is waiting for its corresponding app to be installed
     * before it can be added to CDM.
     * @hide
     */
    public boolean isPending() {
        return mPending;
    }

    /**
     * @return true if the association is not revoked nor pending
     * @hide
     */
    public boolean isActive() {
        return !mRevoked && !mPending;
    }

    /**
     * @return the last time self reported disconnected for selfManaged only.
     * @hide
     */
    public long getLastTimeConnectedMs() {
        return mLastTimeConnectedMs;
    }

    /**
     * @return Enabled system data sync flags set via
     * {@link CompanionDeviceManager#enableSystemDataSyncForTypes(int, int)} (int, int)} and
     * {@link CompanionDeviceManager#disableSystemDataSyncForTypes(int, int)} (int, int)}.
     * Or by default all flags are 1 (enabled).
     */
    public int getSystemDataSyncFlags() {
        return mSystemDataSyncFlags;
    }

    /**
     * Utility method for checking if the association represents a device with the given MAC
     * address.
     *
     * @return {@code false} if the association is "self-managed".
     *         {@code false} if the {@code addr} is {@code null} or is not a valid MAC address.
     *         Otherwise - the result of {@link MacAddress#equals(Object)}
     *
     * @hide
     */
    public boolean isLinkedTo(@Nullable String addr) {
        if (mSelfManaged) return false;

        if (addr == null) return false;

        final MacAddress macAddress;
        try {
            macAddress = MacAddress.fromString(addr);
        } catch (IllegalArgumentException e) {
            return false;
        }
        return macAddress.equals(mDeviceMacAddress);
    }

    /**
     * Utility method to be used by CdmService only.
     *
     * @return whether CdmService should bind the companion application that "owns" this association
     *         when the device is present.
     *
     * @hide
     */
    public boolean shouldBindWhenPresent() {
        return mNotifyOnDeviceNearby || mSelfManaged;
    }

    /** @hide */
    @NonNull
    public String toShortString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("id=").append(mId);
        if (mDeviceMacAddress != null) {
            sb.append(", addr=").append(getDeviceMacAddressAsString());
        }
        if (mSelfManaged) {
            sb.append(", self-managed");
        }
        sb.append(", pkg=u").append(mUserId).append('/').append(mPackageName);
        return sb.toString();
    }

    @Override
    public String toString() {
        return "Association{"
                + "mId=" + mId
                + ", mUserId=" + mUserId
                + ", mPackageName='" + mPackageName + '\''
                + ", mTag='" + mTag + '\''
                + ", mDeviceMacAddress=" + mDeviceMacAddress
                + ", mDisplayName='" + mDisplayName + '\''
                + ", mDeviceProfile='" + mDeviceProfile + '\''
                + ", mSelfManaged=" + mSelfManaged
                + ", mAssociatedDevice=" + mAssociatedDevice
                + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby
                + ", mRevoked=" + mRevoked
                + ", mPending=" + mPending
                + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs)
                + ", mLastTimeConnectedMs=" + (
                    mLastTimeConnectedMs == Long.MAX_VALUE
                        ? LAST_TIME_CONNECTED_NONE : new Date(mLastTimeConnectedMs))
                + ", mSystemDataSyncFlags=" + mSystemDataSyncFlags
                + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AssociationInfo)) return false;
        final AssociationInfo that = (AssociationInfo) o;
        return mId == that.mId
                && mUserId == that.mUserId
                && mSelfManaged == that.mSelfManaged
                && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby
                && mRevoked == that.mRevoked
                && mPending == that.mPending
                && mTimeApprovedMs == that.mTimeApprovedMs
                && mLastTimeConnectedMs == that.mLastTimeConnectedMs
                && Objects.equals(mPackageName, that.mPackageName)
                && Objects.equals(mTag, that.mTag)
                && Objects.equals(mDeviceMacAddress, that.mDeviceMacAddress)
                && Objects.equals(mDisplayName, that.mDisplayName)
                && Objects.equals(mDeviceProfile, that.mDeviceProfile)
                && Objects.equals(mAssociatedDevice, that.mAssociatedDevice)
                && mSystemDataSyncFlags == that.mSystemDataSyncFlags;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName,
                mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked,
                mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags);
    }

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

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mId);
        dest.writeInt(mUserId);
        dest.writeString(mPackageName);
        dest.writeString(mTag);
        dest.writeTypedObject(mDeviceMacAddress, 0);
        dest.writeCharSequence(mDisplayName);
        dest.writeString(mDeviceProfile);
        dest.writeTypedObject(mAssociatedDevice, 0);
        dest.writeBoolean(mSelfManaged);
        dest.writeBoolean(mNotifyOnDeviceNearby);
        dest.writeBoolean(mRevoked);
        dest.writeBoolean(mPending);
        dest.writeLong(mTimeApprovedMs);
        dest.writeLong(mLastTimeConnectedMs);
        dest.writeInt(mSystemDataSyncFlags);
    }

    private AssociationInfo(@NonNull Parcel in) {
        mId = in.readInt();
        mUserId = in.readInt();
        mPackageName = in.readString();
        mTag = in.readString();
        mDeviceMacAddress = in.readTypedObject(MacAddress.CREATOR);
        mDisplayName = in.readCharSequence();
        mDeviceProfile = in.readString();
        mAssociatedDevice = in.readTypedObject(AssociatedDevice.CREATOR);
        mSelfManaged = in.readBoolean();
        mNotifyOnDeviceNearby = in.readBoolean();
        mRevoked = in.readBoolean();
        mPending = in.readBoolean();
        mTimeApprovedMs = in.readLong();
        mLastTimeConnectedMs = in.readLong();
        mSystemDataSyncFlags = in.readInt();
    }

    @NonNull
    public static final Parcelable.Creator<AssociationInfo> CREATOR =
            new Parcelable.Creator<AssociationInfo>() {
        @Override
        public AssociationInfo[] newArray(int size) {
            return new AssociationInfo[size];
        }

        @Override
        public AssociationInfo createFromParcel(@NonNull Parcel in) {
            return new AssociationInfo(in);
        }
    };

    /**
     * Builder for {@link AssociationInfo}
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_NEW_ASSOCIATION_BUILDER)
    @TestApi
    public static final class Builder {
        private final int mId;
        private final int mUserId;
        private final String mPackageName;
        private String mTag;
        private MacAddress mDeviceMacAddress;
        private CharSequence mDisplayName;
        private String mDeviceProfile;
        private AssociatedDevice mAssociatedDevice;
        private boolean mSelfManaged;
        private boolean mNotifyOnDeviceNearby;
        private boolean mRevoked;
        private boolean mPending;
        private long mTimeApprovedMs;
        private long mLastTimeConnectedMs;
        private int mSystemDataSyncFlags;

        /** @hide */
        @TestApi
        public Builder(int id, int userId, @NonNull String packageName) {
            mId = id;
            mUserId = userId;
            mPackageName = packageName;
        }

        /** @hide */
        @TestApi
        public Builder(@NonNull AssociationInfo info) {
            mId = info.mId;
            mUserId = info.mUserId;
            mPackageName = info.mPackageName;
            mTag = info.mTag;
            mDeviceMacAddress = info.mDeviceMacAddress;
            mDisplayName = info.mDisplayName;
            mDeviceProfile = info.mDeviceProfile;
            mAssociatedDevice = info.mAssociatedDevice;
            mSelfManaged = info.mSelfManaged;
            mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
            mRevoked = info.mRevoked;
            mPending = info.mPending;
            mTimeApprovedMs = info.mTimeApprovedMs;
            mLastTimeConnectedMs = info.mLastTimeConnectedMs;
            mSystemDataSyncFlags = info.mSystemDataSyncFlags;
        }

        /**
         * This builder is used specifically to create a new association to be restored to a device
         * that is potentially using a different user ID from the backed-up device.
         *
         * @hide
         */
        public Builder(int id, int userId, @NonNull String packageName, AssociationInfo info) {
            mId = id;
            mUserId = userId;
            mPackageName = packageName;
            mTag = info.mTag;
            mDeviceMacAddress = info.mDeviceMacAddress;
            mDisplayName = info.mDisplayName;
            mDeviceProfile = info.mDeviceProfile;
            mAssociatedDevice = info.mAssociatedDevice;
            mSelfManaged = info.mSelfManaged;
            mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
            mRevoked = info.mRevoked;
            mPending = info.mPending;
            mTimeApprovedMs = info.mTimeApprovedMs;
            mLastTimeConnectedMs = info.mLastTimeConnectedMs;
            mSystemDataSyncFlags = info.mSystemDataSyncFlags;
        }

        /** @hide */
        @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG)
        @TestApi
        @NonNull
        public Builder setTag(@Nullable String tag) {
            mTag = tag;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setDeviceMacAddress(@Nullable MacAddress deviceMacAddress) {
            mDeviceMacAddress = deviceMacAddress;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setDisplayName(@Nullable CharSequence displayName) {
            mDisplayName = displayName;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setDeviceProfile(@Nullable String deviceProfile) {
            mDeviceProfile = deviceProfile;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setAssociatedDevice(@Nullable AssociatedDevice associatedDevice) {
            mAssociatedDevice = associatedDevice;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setSelfManaged(boolean selfManaged) {
            mSelfManaged = selfManaged;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        @SuppressLint("MissingGetterMatchingBuilder")
        public Builder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) {
            mNotifyOnDeviceNearby = notifyOnDeviceNearby;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        @SuppressLint("MissingGetterMatchingBuilder")
        public Builder setRevoked(boolean revoked) {
            mRevoked = revoked;
            return this;
        }

        /** @hide */
        @NonNull
        @SuppressLint("MissingGetterMatchingBuilder")
        public Builder setPending(boolean pending) {
            mPending = pending;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        @SuppressLint("MissingGetterMatchingBuilder")
        public Builder setTimeApproved(long timeApprovedMs) {
            if (timeApprovedMs < 0) {
                throw new IllegalArgumentException("timeApprovedMs must be positive. Was given ("
                        + timeApprovedMs + ")");
            }
            mTimeApprovedMs = timeApprovedMs;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        @SuppressLint("MissingGetterMatchingBuilder")
        public Builder setLastTimeConnected(long lastTimeConnectedMs) {
            if (lastTimeConnectedMs < 0) {
                throw new IllegalArgumentException(
                        "lastTimeConnectedMs must not be negative! (Given " + lastTimeConnectedMs
                                + " )");
            }
            mLastTimeConnectedMs = lastTimeConnectedMs;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public Builder setSystemDataSyncFlags(int flags) {
            mSystemDataSyncFlags = flags;
            return this;
        }

        /** @hide */
        @TestApi
        @NonNull
        public AssociationInfo build() {
            if (mId <= 0) {
                throw new IllegalArgumentException("Association ID should be greater than 0");
            }
            if (mDeviceMacAddress == null && mDisplayName == null) {
                throw new IllegalArgumentException("MAC address and the display name must NOT be "
                        + "null at the same time");
            }
            return new AssociationInfo(
                    mId,
                    mUserId,
                    mPackageName,
                    mTag,
                    mDeviceMacAddress,
                    mDisplayName,
                    mDeviceProfile,
                    mAssociatedDevice,
                    mSelfManaged,
                    mNotifyOnDeviceNearby,
                    mRevoked,
                    mPending,
                    mTimeApprovedMs,
                    mLastTimeConnectedMs,
                    mSystemDataSyncFlags
            );
        }
    }
}
