/*
 * Copyright (C) 2016 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.documentsui.archives;

import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.GuardedBy;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.compressors.CompressorException;

/**
 * Loads an instance of Archive lazily.
 */
public class Loader {
    private static final String TAG = "Loader";

    public static final int STATUS_OPENING = 0;
    public static final int STATUS_OPENED = 1;
    public static final int STATUS_FAILED = 2;
    public static final int STATUS_CLOSING = 3;
    public static final int STATUS_CLOSED = 4;

    private final Context mContext;
    private final Uri mArchiveUri;
    private final int mAccessMode;
    private final Uri mNotificationUri;
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private int mStatus = STATUS_OPENING;
    @GuardedBy("mLock")
    private int mRefCount = 0;
    private Archive mArchive = null;

    Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) {
        this.mContext = context;
        this.mArchiveUri = archiveUri;
        this.mAccessMode = accessMode;
        this.mNotificationUri = notificationUri;

        // Start loading the archive immediately in the background.
        mExecutor.submit(this::get);
    }

    synchronized Archive get() {
        synchronized (mLock) {
            if (mStatus == STATUS_OPENED) {
                return mArchive;
            }
        }

        synchronized (mLock) {
            if (mStatus != STATUS_OPENING) {
                throw new IllegalStateException(
                        "Trying to perform an operation on an archive which is invalidated.");
            }
        }

        try {
            if (ReadableArchive.supportsAccessMode(mAccessMode)) {
                final ContentResolver contentResolver = mContext.getContentResolver();
                final String archiveMimeType = contentResolver.getType(mArchiveUri);
                mArchive = ReadableArchive.createForParcelFileDescriptor(
                        mContext,
                        contentResolver.openFileDescriptor(
                                mArchiveUri, "r", null /* signal */),
                        mArchiveUri, archiveMimeType, mAccessMode, mNotificationUri);
            } else if (WriteableArchive.supportsAccessMode(mAccessMode)) {
                mArchive = WriteableArchive.createForParcelFileDescriptor(
                        mContext,
                        mContext.getContentResolver().openFileDescriptor(
                                mArchiveUri, "w", null /* signal */),
                        mArchiveUri, mAccessMode, mNotificationUri);
            } else {
                throw new IllegalStateException("Access mode not supported.");
            }
            synchronized (mLock) {
                if (mRefCount == 0) {
                    mArchive.close();
                    mStatus = STATUS_CLOSED;
                } else {
                    mStatus = STATUS_OPENED;
                }
            }
        } catch (IOException | RuntimeException | ArchiveException | CompressorException e) {
            Log.e(TAG, "Failed to open the archive.", e);
            synchronized (mLock) {
                mStatus = STATUS_FAILED;
            }
            throw new IllegalStateException("Failed to open the archive.", e);
        } finally {
            synchronized (mLock) {
                // Only notify when there might be someone listening.
                if (mRefCount > 0) {
                    // Notify observers that the root directory is loaded (or failed)
                    // so clients reload it.
                    mContext.getContentResolver().notifyChange(
                            ArchivesProvider.buildUriForArchive(mArchiveUri, mAccessMode),
                            null /* observer */, false /* syncToNetwork */);
                }
            }
        }

        return mArchive;
    }

    int getStatus() {
        synchronized (mLock) {
            return mStatus;
        }
    }

    void acquire() {
        synchronized (mLock) {
            mRefCount++;
        }
    }

    void release() {
        synchronized (mLock) {
            mRefCount--;
            if (mRefCount == 0) {
                assert(mStatus == STATUS_OPENING
                        || mStatus == STATUS_OPENED
                        || mStatus == STATUS_FAILED);

                switch (mStatus) {
                    case STATUS_OPENED:
                        try {
                            mArchive.close();
                            mStatus = STATUS_CLOSED;
                        } catch (IOException e) {
                            Log.e(TAG, "Failed to close the archive on release.", e);
                        }
                        break;
                    case STATUS_FAILED:
                        mStatus = STATUS_CLOSED;
                        break;
                    case STATUS_OPENING:
                        mStatus = STATUS_CLOSING;
                        // ::get() will close the archive once opened.
                        break;
                }
            }
        }
    }
}
