/*
 * 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.telephony.mbms;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

/**
 * Describes a request to download files over cell-broadcast. Instances of this class should be
 * created by the app when requesting a download, and instances of this class will be passed back
 * to the app when the middleware updates the status of the download.
 */
public final class DownloadRequest implements Parcelable {
    // Version code used to keep token calculation consistent.
    private static final int CURRENT_VERSION = 1;
    private static final String LOG_TAG = "MbmsDownloadRequest";

    /** @hide */
    public static final int MAX_APP_INTENT_SIZE = 50000;

    /** @hide */
    public static final int MAX_DESTINATION_URI_SIZE = 50000;

    /** @hide */
    private static class SerializationDataContainer implements Externalizable {
        private String fileServiceId;
        private Uri source;
        private Uri destination;
        private int subscriptionId;
        private String appIntent;
        private int version;

        public SerializationDataContainer() {}

        SerializationDataContainer(DownloadRequest request) {
            fileServiceId = request.fileServiceId;
            source = request.sourceUri;
            destination = request.destinationUri;
            subscriptionId = request.subscriptionId;
            appIntent = request.serializedResultIntentForApp;
            version = request.version;
        }

        @Override
        public void writeExternal(ObjectOutput objectOutput) throws IOException {
            objectOutput.write(version);
            objectOutput.writeUTF(fileServiceId);
            objectOutput.writeUTF(source.toString());
            objectOutput.writeUTF(destination.toString());
            objectOutput.write(subscriptionId);
            objectOutput.writeUTF(appIntent);
        }

        @Override
        public void readExternal(ObjectInput objectInput) throws IOException {
            version = objectInput.read();
            fileServiceId = objectInput.readUTF();
            source = Uri.parse(objectInput.readUTF());
            destination = Uri.parse(objectInput.readUTF());
            subscriptionId = objectInput.read();
            appIntent = objectInput.readUTF();
            // Do version checks here -- future versions may have other fields.
        }
    }

    public static class Builder {
        private String fileServiceId;
        private Uri source;
        private Uri destination;
        private int subscriptionId;
        private String appIntent;
        private int version = CURRENT_VERSION;

        /**
         * Constructs a {@link Builder} from a {@link DownloadRequest}
         * @param other The {@link DownloadRequest} from which the data for the {@link Builder}
         *              should come.
         * @return An instance of {@link Builder} pre-populated with data from the provided
         *         {@link DownloadRequest}.
         */
        public static Builder fromDownloadRequest(DownloadRequest other) {
            Builder result = new Builder(other.sourceUri, other.destinationUri)
                    .setServiceId(other.fileServiceId)
                    .setSubscriptionId(other.subscriptionId);
            result.appIntent = other.serializedResultIntentForApp;
            // Version of the result is going to be the current version -- as this class gets
            // updated, new fields will be set to default values in here.
            return result;
        }

        /**
         * This method constructs a new instance of {@link Builder} based on the serialized data
         * passed in.
         * @param data A byte array, the contents of which should have been originally obtained
         *             from {@link DownloadRequest#toByteArray()}.
         */
        public static Builder fromSerializedRequest(byte[] data) {
            Builder builder;
            try {
                ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
                SerializationDataContainer dataContainer =
                        (SerializationDataContainer) stream.readObject();
                builder = new Builder(dataContainer.source, dataContainer.destination);
                builder.version = dataContainer.version;
                builder.appIntent = dataContainer.appIntent;
                builder.fileServiceId = dataContainer.fileServiceId;
                builder.subscriptionId = dataContainer.subscriptionId;
            } catch (IOException e) {
                // Really should never happen
                Log.e(LOG_TAG, "Got IOException trying to parse opaque data");
                throw new IllegalArgumentException(e);
            } catch (ClassNotFoundException e) {
                Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data");
                throw new IllegalArgumentException(e);
            }
            return builder;
        }

        /**
         * Builds a new DownloadRequest.
         * @param sourceUri the source URI for the DownloadRequest to be built. This URI should
         *     never be null.
         * @param destinationUri The final location for the file(s) that are to be downloaded. It
         *     must be on the same filesystem as the temp file directory set via
         *     {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}.
         *     The provided path must be a directory that exists. An
         *     {@link IllegalArgumentException} will be thrown otherwise.
         */
        public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) {
            if (sourceUri == null || destinationUri == null) {
                throw new IllegalArgumentException("Source and destination URIs must be non-null.");
            }
            source = sourceUri;
            destination = destinationUri;
        }

        /**
         * Sets the service from which the download request to be built will download from.
         * @param serviceInfo
         * @return
         */
        public Builder setServiceInfo(FileServiceInfo serviceInfo) {
            fileServiceId = serviceInfo.getServiceId();
            return this;
        }

        /**
         * Set the service ID for the download request. For use by the middleware only.
         * @hide
         */
        @SystemApi
        public Builder setServiceId(String serviceId) {
            fileServiceId = serviceId;
            return this;
        }

        /**
         * Set the subscription ID on which the file(s) should be downloaded.
         * @param subscriptionId
         */
        public Builder setSubscriptionId(int subscriptionId) {
            this.subscriptionId = subscriptionId;
            return this;
        }

        /**
         * Set the {@link Intent} that should be sent when the download completes or fails. This
         * should be an intent with a explicit {@link android.content.ComponentName} targeted to a
         * {@link android.content.BroadcastReceiver} in the app's package.
         *
         * The middleware should not use this method.
         * @param intent
         */
        public Builder setAppIntent(Intent intent) {
            this.appIntent = intent.toUri(0);
            if (this.appIntent.length() > MAX_APP_INTENT_SIZE) {
                throw new IllegalArgumentException("App intent must not exceed length " +
                        MAX_APP_INTENT_SIZE);
            }
            return this;
        }

        public DownloadRequest build() {
            return new DownloadRequest(fileServiceId, source, destination,
                    subscriptionId, appIntent, version);
        }
    }

    private final String fileServiceId;
    private final Uri sourceUri;
    private final Uri destinationUri;
    private final int subscriptionId;
    private final String serializedResultIntentForApp;
    private final int version;

    private DownloadRequest(String fileServiceId,
            Uri source, Uri destination, int sub,
            String appIntent, int version) {
        this.fileServiceId = fileServiceId;
        sourceUri = source;
        subscriptionId = sub;
        destinationUri = destination;
        serializedResultIntentForApp = appIntent;
        this.version = version;
    }

    private DownloadRequest(Parcel in) {
        fileServiceId = in.readString();
        sourceUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class);
        destinationUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class);
        subscriptionId = in.readInt();
        serializedResultIntentForApp = in.readString();
        version = in.readInt();
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeString(fileServiceId);
        out.writeParcelable(sourceUri, flags);
        out.writeParcelable(destinationUri, flags);
        out.writeInt(subscriptionId);
        out.writeString(serializedResultIntentForApp);
        out.writeInt(version);
    }

    /**
     * @return The ID of the file service to download from.
     */
    public String getFileServiceId() {
        return fileServiceId;
    }

    /**
     * @return The source URI to download from
     */
    public Uri getSourceUri() {
        return sourceUri;
    }

    /**
     * @return The destination {@link Uri} of the downloaded file.
     */
    public Uri getDestinationUri() {
        return destinationUri;
    }

    /**
     * @return The subscription ID on which to perform MBMS operations.
     */
    public int getSubscriptionId() {
        return subscriptionId;
    }

    /**
     * For internal use -- returns the intent to send to the app after download completion or
     * failure.
     * @hide
     */
    public Intent getIntentForApp() {
        try {
            return Intent.parseUri(serializedResultIntentForApp, 0);
        } catch (URISyntaxException e) {
            return null;
        }
    }

    /**
     * This method returns a byte array that may be persisted to disk and restored to a
     * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
     * may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
     * @return A byte array of data to persist.
     */
    public byte[] toByteArray() {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
            SerializationDataContainer container = new SerializationDataContainer(this);
            stream.writeObject(container);
            stream.flush();
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            // Really should never happen
            Log.e(LOG_TAG, "Got IOException trying to serialize opaque data");
            return null;
        }
    }

    /** @hide */
    public int getVersion() {
        return version;
    }

    public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR =
            new Parcelable.Creator<DownloadRequest>() {
        public DownloadRequest createFromParcel(Parcel in) {
            return new DownloadRequest(in);
        }
        public DownloadRequest[] newArray(int size) {
            return new DownloadRequest[size];
        }
    };

    /**
     * Maximum permissible length for the app's destination path, when serialized via
     * {@link Uri#toString()}.
     */
    public static int getMaxAppIntentSize() {
        return MAX_APP_INTENT_SIZE;
    }

    /**
     * Maximum permissible length for the app's download-completion intent, when serialized via
     * {@link Intent#toUri(int)}.
     */
    public static int getMaxDestinationUriSize() {
        return MAX_DESTINATION_URI_SIZE;
    }

    /**
     * Retrieves the hash string that should be used as the filename when storing a token for
     * this DownloadRequest.
     * @hide
     */
    public String getHash() {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Could not get sha256 hash object");
        }
        if (version >= 1) {
            // Hash the source, destination, and the app intent
            digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
            digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
            if (serializedResultIntentForApp != null) {
                digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
            }
        }
        // Add updates for future versions here
        return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP);
    }

    @Override
    public boolean equals(@Nullable Object o) {
        if (this == o) return true;
        if (o == null) {
            return false;
        }
        if (!(o instanceof DownloadRequest)) {
            return false;
        }
        DownloadRequest request = (DownloadRequest) o;
        return subscriptionId == request.subscriptionId &&
                version == request.version &&
                Objects.equals(fileServiceId, request.fileServiceId) &&
                Objects.equals(sourceUri, request.sourceUri) &&
                Objects.equals(destinationUri, request.destinationUri) &&
                Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
    }

    @Override
    public int hashCode() {
        return Objects.hash(fileServiceId, sourceUri, destinationUri,
                subscriptionId, serializedResultIntentForApp, version);
    }
}
