/*
 * 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.builder.internal.packaging.zip.utils;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;

/**
 * Closeable byte source that delegates to another byte source.
 */
public class CloseableDelegateByteSource extends CloseableByteSource {

    /**
     * The byte source we delegate all operations to. {@code null} if disposed.
     */
    @Nullable
    private ByteSource mInner;

    /**
     * Size of the byte source. This is the same as {@code mInner.size()} (when {@code mInner}
     * is not {@code null}), but we keep it separate to avoid calling {@code mInner.size()}
     * because it might throw {@code IOException}.
     */
    private final long mSize;

    /**
     * Creates a new byte source.
     *
     * @param inner the inner byte source
     * @param size the size of the source
     */
    public CloseableDelegateByteSource(@NonNull ByteSource inner, long size) {
        mInner = inner;
        mSize = size;
    }

    /**
     * Obtains the inner byte source. Will throw an exception if the inner by byte source has
     * been disposed of.
     *
     * @return the inner byte source
     */
    @NonNull
    private synchronized ByteSource get() {
        if (mInner == null) {
            throw new ByteSourceDisposedException();
        }

        return mInner;
    }

    /**
     * Mark the byte source as disposed.
     */
    @Override
    protected synchronized void innerClose() throws IOException {
        if (mInner == null) {
            return;
        }

        mInner = null;
    }

    /**
     * Obtains the size of this byte source. Equivalent to {@link #size()} but not throwing
     * {@code IOException}.
     *
     * @return the size of the byte source
     */
    public long sizeNoException() {
        return mSize;
    }

    @Override
    public CharSource asCharSource(Charset charset) {
        return get().asCharSource(charset);
    }

    @Override
    public InputStream openBufferedStream() throws IOException {
        return get().openBufferedStream();
    }

    @Override
    public ByteSource slice(long offset, long length) {
        return get().slice(offset, length);
    }

    @Override
    public boolean isEmpty() throws IOException {
        return get().isEmpty();
    }

    @Override
    public long size() throws IOException {
        return get().size();
    }

    @Override
    public long copyTo(@NonNull OutputStream output) throws IOException {
        return get().copyTo(output);
    }

    @Override
    public long copyTo(@NonNull ByteSink sink) throws IOException {
        return get().copyTo(sink);
    }

    @Override
    public byte[] read() throws IOException {
        return get().read();
    }

    @Override
    public <T> T read(@NonNull ByteProcessor<T> processor) throws IOException {
        return get().read(processor);
    }

    @Override
    public HashCode hash(HashFunction hashFunction) throws IOException {
        return get().hash(hashFunction);
    }

    @Override
    public boolean contentEquals(@NonNull ByteSource other) throws IOException {
        return get().contentEquals(other);
    }

    @Override
    public InputStream openStream() throws IOException {
        return get().openStream();
    }

    /**
     * Exception thrown when trying to use a byte source that has been disposed.
     */
    private static class ByteSourceDisposedException extends RuntimeException {

        /**
         * Creates a new exception.
         */
        private ByteSourceDisposedException() {
            super("Byte source was created by a ByteTracker and is now disposed. If you see "
                    + "this message, then there is a bug.");
        }
    }
}
