/*******************************************************************************
 * Copyright (c) 2002, 2016 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:
 * Rational Software - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.core.model;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;

import org.eclipse.cdt.core.ICLogConstants;
import org.eclipse.cdt.core.model.BufferChangedEvent;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.IBuffer;
import org.eclipse.cdt.core.model.IBufferChangedListener;
import org.eclipse.cdt.core.model.ICModelStatusConstants;
import org.eclipse.cdt.core.model.IOpenable;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;

/**
  * @see IBuffer
  * This class is similar to the JDT Buffer class.
  */
public class Buffer implements IBuffer {
	protected IFile file;
	protected int flags;
	protected char[] contents;
	protected ArrayList<IBufferChangedListener> changeListeners;
	protected IOpenable owner;
	protected int gapStart = -1;
	protected int gapEnd = -1;

	protected Object lock = new Object();

	protected static final int F_HAS_UNSAVED_CHANGES = 1;
	protected static final int F_IS_READ_ONLY = 2;
	protected static final int F_IS_CLOSED = 4;

	/**
	 * Creates a new buffer on an underlying resource.
	 */
	protected Buffer(IFile file, IOpenable owner, boolean readOnly) {
		this.file = file;
		this.owner = owner;
		if (file == null) {
			setReadOnly(readOnly);
		}
	}

	/**
	 * @see IBuffer
	 */
	@Override
	public void addBufferChangedListener(IBufferChangedListener listener) {
		if (this.changeListeners == null) {
			this.changeListeners = new ArrayList<>(5);
		}
		if (!this.changeListeners.contains(listener)) {
			this.changeListeners.add(listener);
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#append(char[])
	 */
	@Override
	public void append(char[] text) {
		if (!isReadOnly()) {
			if (text == null || text.length == 0) {
				return;
			}
			int length = getLength();
			moveAndResizeGap(length, text.length);
			System.arraycopy(text, 0, this.contents, length, text.length);
			this.gapStart += text.length;
			this.flags |= F_HAS_UNSAVED_CHANGES;
			notifyChanged(new BufferChangedEvent(this, length, 0, new String(text)));
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#append(java.lang.String)
	 */
	@Override
	public void append(String text) {
		if (text == null) {
			return;
		}
		this.append(text.toCharArray());
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#close()
	 */
	@Override
	public void close() {
		BufferChangedEvent event = null;
		synchronized (this.lock) {
			if (isClosed())
				return;
			event = new BufferChangedEvent(this, 0, 0, null);
			this.contents = null;
			this.flags |= F_IS_CLOSED;
		}
		notifyChanged(event); // notify outside of synchronized block
		this.changeListeners = null;
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getChar(int)
	 */
	@Override
	public char getChar(int position) {
		synchronized (this.lock) {
			if (position < this.gapStart) {
				return this.contents[position];
			}
			int gapLength = this.gapEnd - this.gapStart;
			return this.contents[position + gapLength];
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getCharacters()
	 */
	@Override
	public char[] getCharacters() {
		if (this.contents == null)
			return null;
		synchronized (this.lock) {
			if (this.gapStart < 0) {
				return this.contents;
			}
			int length = this.contents.length;
			char[] newContents = new char[length - this.gapEnd + this.gapStart];
			System.arraycopy(this.contents, 0, newContents, 0, this.gapStart);
			System.arraycopy(this.contents, this.gapEnd, newContents, this.gapStart, length - this.gapEnd);
			return newContents;
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getContents()
	 */
	@Override
	public String getContents() {
		if (this.contents == null)
			return null;
		return new String(this.getCharacters());
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getLength()
	 */
	@Override
	public int getLength() {
		synchronized (this.lock) {
			int length = this.gapEnd - this.gapStart;
			return (this.contents.length - length);
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getOwner()
	 */
	@Override
	public IOpenable getOwner() {
		return this.owner;
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getText(int, int)
	 */
	@Override
	public String getText(int offset, int length) {
		if (this.contents == null)
			return ""; //$NON-NLS-1$
		synchronized (this.lock) {
			if (offset + length < this.gapStart)
				return new String(this.contents, offset, length);
			if (this.gapStart < offset) {
				int gapLength = this.gapEnd - this.gapStart;
				return new String(this.contents, offset + gapLength, length);
			}
			StringBuilder buf = new StringBuilder();
			buf.append(this.contents, offset, this.gapStart - offset);
			buf.append(this.contents, this.gapEnd, offset + length - this.gapStart);
			return buf.toString();
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#getUnderlyingResource()
	 */
	@Override
	public IResource getUnderlyingResource() {
		return this.file;
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#hasUnsavedChanges()
	 */
	@Override
	public boolean hasUnsavedChanges() {
		return (this.flags & F_HAS_UNSAVED_CHANGES) != 0;
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#isClosed()
	 */
	@Override
	public boolean isClosed() {
		return (this.flags & F_IS_CLOSED) != 0;
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#isReadOnly()
	 */
	@Override
	public boolean isReadOnly() {
		if (this.file == null) {
			return (this.flags & F_IS_READ_ONLY) != 0;
		}
		return this.file.isReadOnly();
	}

	/**
	 * Notify the listeners that this buffer has changed.
	 * To avoid deadlock, this should not be called in a synchronized block.
	 */
	protected void notifyChanged(final BufferChangedEvent event) {
		if (this.changeListeners != null) {
			for (int i = 0, size = this.changeListeners.size(); i < size; ++i) {
				final IBufferChangedListener listener = this.changeListeners.get(i);
				SafeRunner.run(new ISafeRunnable() {
					@Override
					public void handleException(Throwable exception) {
						Util.log(exception, "Exception occurred in listener of buffer change notification", //$NON-NLS-1$
								ICLogConstants.CDT);
					}

					@Override
					public void run() throws Exception {
						listener.bufferChanged(event);
					}
				});
			}
		}
	}

	/**
	 * @see IBuffer
	 */
	@Override
	public void removeBufferChangedListener(IBufferChangedListener listener) {
		if (this.changeListeners != null) {
			this.changeListeners.remove(listener);
			if (this.changeListeners.size() == 0) {
				this.changeListeners = null;
			}
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#replace(int, int, char[])
	 */
	@Override
	public void replace(int position, int length, char[] text) {
		if (!isReadOnly()) {
			int textLength = text == null ? 0 : text.length;
			synchronized (this.lock) {
				// move gap
				moveAndResizeGap(position + length, textLength - length);

				// overwrite
				int min = Math.min(textLength, length);
				if (min > 0) {
					System.arraycopy(text, 0, this.contents, position, min);
				}
				if (length > textLength) {
					// enlarge the gap
					this.gapStart -= length - textLength;
				} else if (textLength > length) {
					// shrink gap
					this.gapStart += textLength - length;
					System.arraycopy(text, 0, this.contents, position, textLength);
				}
			}
			this.flags |= F_HAS_UNSAVED_CHANGES;
			String string = null;
			if (textLength > 0) {
				string = new String(text);
			}
			notifyChanged(new BufferChangedEvent(this, position, length, string));
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#replace(int, int, java.lang.String)
	 */
	@Override
	public void replace(int position, int length, String text) {
		this.replace(position, length, text == null ? null : text.toCharArray());
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#save(org.eclipse.core.runtime.IProgressMonitor, boolean)
	 */
	@Override
	public void save(IProgressMonitor progress, boolean force) throws CModelException {
		// determine if saving is required
		if (isReadOnly() || this.file == null) {
			return;
		}
		synchronized (this.lock) {
			if (!hasUnsavedChanges())
				return;

			// use a platform operation to update the resource contents
			try {
				String encoding = null;
				try {
					encoding = this.file.getCharset();
				} catch (CoreException ce) {
					// use no encoding
				}
				String contents = this.getContents();
				if (contents == null)
					return;
				byte[] bytes = encoding == null ? contents.getBytes() : contents.getBytes(encoding);
				ByteArrayInputStream stream = new ByteArrayInputStream(bytes);

				if (this.file.exists()) {
					this.file.setContents(stream,
							force ? IResource.FORCE | IResource.KEEP_HISTORY : IResource.KEEP_HISTORY, null);
				} else {
					this.file.create(stream, force, null);
				}
			} catch (IOException e) {
				throw new CModelException(e, ICModelStatusConstants.IO_EXCEPTION);
			} catch (CoreException e) {
				throw new CModelException(e);
			}

			// the resource no longer has unsaved changes
			this.flags &= ~(F_HAS_UNSAVED_CHANGES);
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#setContents(char[])
	 */
	@Override
	public void setContents(char[] newContents) {
		// allow special case for first initialization
		// after creation by buffer factory
		if (this.contents == null) {
			this.contents = newContents;
			this.flags &= ~(F_HAS_UNSAVED_CHANGES);
			return;
		}

		if (!isReadOnly()) {
			String string = null;
			if (newContents != null) {
				string = new String(newContents);
			}
			BufferChangedEvent event = new BufferChangedEvent(this, 0, this.getLength(), string);
			synchronized (this.lock) {
				this.contents = newContents;
				this.flags |= F_HAS_UNSAVED_CHANGES;
				this.gapStart = -1;
				this.gapEnd = -1;
			}
			notifyChanged(event);
		}
	}

	/**
	 * @see org.eclipse.cdt.core.model.IBuffer#setContents(java.lang.String)
	 */
	@Override
	public void setContents(String newContents) {
		this.setContents(newContents.toCharArray());
	}

	/**
	 * Moves the gap to location and adjust its size to the
	 * anticipated change size. The size represents the expected
	 * range of the gap that will be filled after the gap has been moved.
	 * Thus the gap is resized to actual size + the specified size and
	 * moved to the given position.
	 */
	protected void moveAndResizeGap(int position, int size) {
		char[] content = null;
		int oldSize = this.gapEnd - this.gapStart;
		if (size < 0) {
			if (oldSize > 0) {
				content = new char[this.contents.length - oldSize];
				System.arraycopy(this.contents, 0, content, 0, this.gapStart);
				System.arraycopy(this.contents, this.gapEnd, content, this.gapStart, content.length - this.gapStart);
				this.contents = content;
			}
			this.gapStart = this.gapEnd = position;
			return;
		}
		content = new char[this.contents.length + (size - oldSize)];
		int newGapStart = position;
		int newGapEnd = newGapStart + size;
		if (oldSize == 0) {
			System.arraycopy(this.contents, 0, content, 0, newGapStart);
			System.arraycopy(this.contents, newGapStart, content, newGapEnd, content.length - newGapEnd);
		} else if (newGapStart < this.gapStart) {
			int delta = this.gapStart - newGapStart;
			System.arraycopy(this.contents, 0, content, 0, newGapStart);
			System.arraycopy(this.contents, newGapStart, content, newGapEnd, delta);
			System.arraycopy(this.contents, this.gapEnd, content, newGapEnd + delta,
					this.contents.length - this.gapEnd);
		} else {
			int delta = newGapStart - this.gapStart;
			System.arraycopy(this.contents, 0, content, 0, this.gapStart);
			System.arraycopy(this.contents, this.gapEnd, content, this.gapStart, delta);
			System.arraycopy(this.contents, this.gapEnd + delta, content, newGapEnd, content.length - newGapEnd);
		}
		this.contents = content;
		this.gapStart = newGapStart;
		this.gapEnd = newGapEnd;
	}

	/**
	 * Sets this <code>Buffer</code> to be read only.
	 */
	protected void setReadOnly(boolean readOnly) {
		if (readOnly) {
			this.flags |= F_IS_READ_ONLY;
		} else {
			this.flags &= ~(F_IS_READ_ONLY);
		}
	}

	@Override
	public String toString() {
		StringBuilder buffer = new StringBuilder();
		buffer.append("Owner: ").append(((CElement) this.owner).toString()); //$NON-NLS-1$
		buffer.append("\nHas unsaved changes: ").append(this.hasUnsavedChanges()); //$NON-NLS-1$
		buffer.append("\nIs readonly: ").append(this.isReadOnly()); //$NON-NLS-1$
		buffer.append("\nIs closed: ").append(this.isClosed()); //$NON-NLS-1$
		buffer.append("\nContents:\n"); //$NON-NLS-1$
		char[] contents = this.getCharacters();
		if (contents == null) {
			buffer.append("<null>"); //$NON-NLS-1$
		} else {
			int length = contents.length;
			for (int i = 0; i < length; i++) {
				char car = contents[i];
				switch (car) {
				case '\n':
					buffer.append("\\n\n"); //$NON-NLS-1$
					break;
				case '\r':
					if (i < length - 1 && this.contents[i + 1] == '\n') {
						buffer.append("\\r\\n\n"); //$NON-NLS-1$
						i++;
					} else {
						buffer.append("\\r\n"); //$NON-NLS-1$
					}
					break;
				default:
					buffer.append(car);
					break;
				}
			}
		}
		return buffer.toString();
	}
}
