/*
 * Copyright (C) 2022, Tencent.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.internal.storage.io;

import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ProgressMonitor;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.security.MessageDigest;

/**
 * An OutputStream that keeps a digest and checks every N bytes for
 * cancellation.
 */
public class CancellableDigestOutputStream extends OutputStream {

	/** The OutputStream checks every this value for cancellation **/
	public static final int BYTES_TO_WRITE_BEFORE_CANCEL_CHECK = 128 * 1024;

	private final ProgressMonitor writeMonitor;

	private final OutputStream out;

	private final MessageDigest md = Constants.newMessageDigest();

	private long count;

	private long checkCancelAt;

	/**
	 * Initialize a CancellableDigestOutputStream.
	 *
	 * @param writeMonitor
	 *            monitor to update on output progress and check cancel.
	 * @param out
	 *            target stream to receive all contents.
	 */
	public CancellableDigestOutputStream(ProgressMonitor writeMonitor,
			OutputStream out) {
		this.writeMonitor = writeMonitor;
		this.out = out;
		this.checkCancelAt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK;
	}

	/**
	 * Get the monitor which is used to update on output progress and check
	 * cancel.
	 *
	 * @return the monitor
	 */
	public final ProgressMonitor getWriteMonitor() {
		return writeMonitor;
	}

	/**
	 * Obtain the current SHA-1 digest.
	 *
	 * @return SHA-1 digest
	 */
	public final byte[] getDigest() {
		return md.digest();
	}

	/**
	 * Get total number of bytes written since stream start.
	 *
	 * @return total number of bytes written since stream start.
	 */
	public final long length() {
		return count;
	}

	@Override
	public final void write(int b) throws IOException {
		if (checkCancelAt <= count) {
			if (writeMonitor.isCancelled()) {
				throw new InterruptedIOException();
			}
			checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK;
		}

		out.write(b);
		md.update((byte) b);
		count++;
	}

	@Override
	public final void write(byte[] b, int off, int len) throws IOException {
		while (0 < len) {
			if (checkCancelAt <= count) {
				if (writeMonitor.isCancelled()) {
					throw new InterruptedIOException();
				}
				checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK;
			}

			int n = Math.min(len, BYTES_TO_WRITE_BEFORE_CANCEL_CHECK);
			out.write(b, off, n);
			md.update(b, off, n);
			count += n;

			off += n;
			len -= n;
		}
	}

	@Override
	public void flush() throws IOException {
		out.flush();
	}
}
