/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Andrey Loskutov <loskutov@gmx.de> - Bug 468787
 *******************************************************************************/
package org.eclipse.core.internal.preferences;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

/**
 * This class should be used when there's a file already in the
 * destination and we don't want to lose its contents if a
 * failure writing this stream happens.
 * Basically, the new contents are written to a temporary location.
 * If everything goes OK, it is moved to the right place.
 * This class handles buffering of output stream contents.
 *
 * Copied from org.eclipse.core.internal.localstore.SafeFileOutputStream
 */
public class SafeFileOutputStream extends OutputStream {
	protected File temp;
	protected File target;
	protected OutputStream output;
	protected boolean failed;
	protected static final String EXTENSION = ".bak"; //$NON-NLS-1$

	/**
	 * Creates an output stream on a file at the given location
	 * @param file The file to be written to
	 */
	public SafeFileOutputStream(File file) throws IOException {
		failed = false;
		target = file;
		temp = new File(target.getAbsolutePath() + EXTENSION);
		if (!target.exists()) {
			if (!temp.exists()) {
				output = new BufferedOutputStream(new FileOutputStream(target));
				return;
			}
			// If we do not have a file at target location, but we do have at temp location,
			// it probably means something wrong happened the last time we tried to write it.
			// So, try to recover the backup file. And, if successful, write the new one.
			copy(temp, target);
		}
		output = new BufferedOutputStream(new FileOutputStream(temp));
	}

	@Override
	public void close() throws IOException {
		try {
			output.close();
		} catch (IOException e) {
			failed = true;
			throw e; // rethrow
		}
		if (failed)
			temp.delete();
		else
			commit();
	}

	protected void commit() throws IOException {
		if (!temp.exists())
			return;
		target.delete();
		copy(temp, target);
		temp.delete();
	}

	protected void copy(File sourceFile, File destinationFile) throws IOException {
		if (!sourceFile.exists())
			return;
		try {
			Files.move(sourceFile.toPath(), destinationFile.toPath(), new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING});
		} catch (IOException e) {
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=468787
			if (!sourceFile.exists() && destinationFile.exists()) {
				return;
			}
			InputStream source = null;
			OutputStream destination = null;
			try {
				source = new BufferedInputStream(new FileInputStream(sourceFile));
				destination = new BufferedOutputStream(new FileOutputStream(destinationFile));
				transferStreams(source, destination);
			} finally {
				try {
					if (source != null)
						source.close();
				} finally {
					//ignore secondary exception
				}
				try {
					if (destination != null)
						destination.close();
				} finally {
					//ignore secondary exception
				}
			}
		}
	}

	@Override
	public void flush() throws IOException {
		try {
			output.flush();
		} catch (IOException e) {
			failed = true;
			throw e; // rethrow
		}
	}

	public String getTempFilePath() {
		return temp.getAbsolutePath();
	}

	protected void transferStreams(InputStream source, OutputStream destination) throws IOException {
		byte[] buffer = new byte[8192];
		while (true) {
			int bytesRead = source.read(buffer);
			if (bytesRead == -1)
				break;
			destination.write(buffer, 0, bytesRead);
		}
	}

	@Override
	public void write(int b) throws IOException {
		try {
			output.write(b);
		} catch (IOException e) {
			failed = true;
			throw e; // rethrow
		}
	}
}
