/*******************************************************************************
 * Copyright (c) 2022 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
 *******************************************************************************/
package org.eclipse.core.internal.filesystem.memory;

import java.io.*;
import java.util.ArrayList;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.provider.FileInfo;
import org.eclipse.core.runtime.*;

/**
 * An in-memory file system.
 */
public class MemoryTree {
	static class DirNode extends Node {
		private final ArrayList<Node> children = new ArrayList<>();

		DirNode(Node parent, String name) {
			super(parent, name);
		}

		void add(Node child) {
			children.add(child);
		}

		public String[] childNames() {
			String[] names = new String[children.size()];
			for (int i = 0, imax = children.size(); i < imax; i++) {
				Node child = children.get(i);
				names[i] = child.getInfo(false).getName();
			}
			return names;
		}

		/**
		 * Returns the child with the given name, or null if not found.
		 * @param name
		 * @return
		 */
		Node getChild(String name) {
			for (int i = 0, imax = children.size(); i < imax; i++) {
				Node child = children.get(i);
				if (child.getInfo(false).getName().equals(name))
					return child;
			}
			return null;
		}

		@Override
		protected void initializeInfo(FileInfo fileInfo) {
			super.initializeInfo(fileInfo);
			fileInfo.setDirectory(true);
		}

		@Override
		boolean isFile() {
			return false;
		}

		void remove(String name) {
			Node child = getChild(name);
			if (child != null)
				children.remove(child);
		}

		@Override
		public String toString() {
			return super.toString() + ' ' + children;
		}
	}

	static class FileNode extends Node {
		byte[] contents = EMPTY_CONTENTS;

		FileNode(Node parent, String name) {
			super(parent, name);
		}

		@Override
		boolean isFile() {
			return true;
		}

		public InputStream openInputStream() {
			return new ByteArrayInputStream(contents);
		}

		public OutputStream openOutputStream(final int options) {
			return new ByteArrayOutputStream() {
				@Override
				public void close() throws IOException {
					super.close();
					setContents(toByteArray(), options);
				}
			};
		}

		protected void setContents(byte[] bytes, int options) {
			if ((options & EFS.APPEND) != 0) {
				//create reference in case of concurrent modification
				byte[] oldContents = this.contents;
				byte[] newContents = new byte[oldContents.length + bytes.length];
				System.arraycopy(oldContents, 0, newContents, 0, oldContents.length);
				System.arraycopy(bytes, 0, newContents, oldContents.length, bytes.length);
				this.contents = newContents;
			} else
				this.contents = bytes;
			info.setLastModified(System.currentTimeMillis());
			((FileInfo) info).setLength(bytes.length);
		}
	}

	static abstract class Node extends PlatformObject {
		protected IFileInfo info;

		Node(Node parent, String name) {
			if (parent != null)
				((DirNode) parent).add(this);
			FileInfo fileInfo = new FileInfo(name);
			initializeInfo(fileInfo);
			this.info = fileInfo;
		}

		IFileInfo getInfo(boolean copy) {
			return (IFileInfo) (copy ? ((FileInfo) info).clone() : info);
		}

		protected void initializeInfo(FileInfo fileInfo) {
			fileInfo.setExists(true);
			fileInfo.setLastModified(System.currentTimeMillis());
		}

		abstract boolean isFile();

		void putInfo(IFileInfo newInfo, int options) {
			if ((options & EFS.SET_ATTRIBUTES) != 0) {
				for (int i = 0; i < ALL_ATTRIBUTES.length; i++)
					info.setAttribute(ALL_ATTRIBUTES[i], newInfo.getAttribute(ALL_ATTRIBUTES[i]));
			}
			if ((options & EFS.SET_LAST_MODIFIED) != 0) {
				info.setLastModified(newInfo.getLastModified());
			}
		}

		/**
		 * For debugging purposes only.
		 */
		@Override
		public String toString() {
			return info.getName();
		}
	}

	static final int[] ALL_ATTRIBUTES = new int[] {EFS.ATTRIBUTE_ARCHIVE, EFS.ATTRIBUTE_EXECUTABLE, EFS.ATTRIBUTE_HIDDEN, EFS.ATTRIBUTE_READ_ONLY,};

	public static final MemoryTree TREE = new MemoryTree();

	static final byte[] EMPTY_CONTENTS = new byte[0];

	private static final String ROOT_NAME = "<root>";

	private Node root = new DirNode(null, ROOT_NAME);

	private MemoryTree() {
		// TREE singleton should be used rather than direct instantiation
	}

	public String[] childNames(IPath path) {
		Node node = findNode(path);
		if (node == null || node.isFile())
			return null;
		return ((DirNode) node).childNames();
	}

	public void delete(IPath path) {
		//cannot delete the root
		if (path.segmentCount() == 0)
			return;
		Node parent = findNode(path.removeLastSegments(1));
		if (parent == null || parent.isFile())
			return;
		((DirNode) parent).remove(path.lastSegment());

	}

	/**
	 * Deletes the entire memory tree. Used during debugging and testing only.
	 */
	public void deleteAll() {
		this.root = new DirNode(null, ROOT_NAME);
	}

	/**
	 * Returns the file info for the given path. Never returns null.
	 * @param path
	 * @return
	 */
	public synchronized IFileInfo fetchInfo(IPath path) {
		Node node = findNode(path);
		if (node == null)
			return new FileInfo(path.lastSegment());
		return node.getInfo(true);
	}

	/**
	 * Returns the node at the given path, or null if not found.
	 * 
	 * @param path
	 * @return
	 */
	private Node findNode(IPath path) {
		Node current = root;
		for (int i = 0, imax = path.segmentCount(); i < imax; i++) {
			if (current == null || current.isFile())
				return null;
			current = ((DirNode) current).getChild(path.segment(i));
		}
		return current;
	}

	public Node mkdir(IPath path, boolean deep) throws CoreException {
		Node dir = findNode(path);
		if (dir != null) {
			if (dir.isFile())
				Policy.error("A file exists with this name: " + path);
			return dir;
		}
		final IPath parentPath = path.removeLastSegments(1);
		Node parent = findNode(parentPath);
		if (parent != null) {
			if (parent.isFile())
				Policy.error("Parent is a file: " + path);
		} else {
			if (!deep)
				Policy.error("Parent does not exist: " + parentPath);
			parent = mkdir(parentPath, deep);
		}
		//create the child directory
		return new DirNode(parent, path.lastSegment());
	}

	public InputStream openInputStream(IPath path) throws CoreException {
		Node node = findNode(path);
		if (node == null)
			Policy.error("File not found: " + path);
		if (!node.isFile())
			Policy.error("Cannot open stream on directory: " + path);
		return ((FileNode) node).openInputStream();
	}

	public OutputStream openOutputStream(IPath path, int options) throws CoreException {
		Node node = findNode(path);
		//if we already have such a file, just open a stream on it
		if (node instanceof DirNode)
			Policy.error("Could not create file: " + path);
		if (node instanceof FileNode)
			return ((FileNode) node).openOutputStream(options);
		//if the parent exists we can create the file
		Node parent = findNode(path.removeLastSegments(1));
		if (!(parent instanceof DirNode))
			Policy.error("Could not create file: " + path);
		node = new FileNode(parent, path.lastSegment());
		return ((FileNode) node).openOutputStream(options);
	}

	public void putInfo(IPath path, IFileInfo info, int options) throws CoreException {
		Node node = findNode(path);
		if (node == null)
			Policy.error("File not found: " + path);
		node.putInfo(info, options);
	}
}
