package dev.exhq.ajarc.vm;

import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import dev.exhq.ajarc.Ajar;
import dev.exhq.ajarc.computer.AjarDirectory;
import dev.exhq.ajarc.computer.AjarFile;
import dev.exhq.ajarc.computer.AjarRegularFile;
import dev.exhq.ajarc.computer.AjarSymlinkFile;
import dev.exhq.ajarc.computer.ComputerBlockEntity;
import dev.exhq.ajarc.computer.ComputerTerminal;

import javax.script.ScriptException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class JsVm {
	private final NashornSandbox sandbox;
	private final ComputerBlockEntity entity;

	public JsVm(ComputerBlockEntity entity) {
		this.sandbox = NashornSandboxes.create();
		this.entity = entity;
		this.sandbox.inject("screen", new ScreenHelper(entity));
		this.sandbox.inject("fs", new FileSystemHelper(entity));
		this.sandbox.inject("js", new Js(entity));
		this.sandbox.allow(ProcessHelper.class);
	}

	public record ProcessHelper(String[] args) {
	}

	private record BoundFile(AjarDirectory parent, String name) {
		public boolean exists() {
			return parent.listing().containsKey(name);
		}

		public AjarFile resolve() {
			return parent.listing().get(name);
		}

		public void set(AjarFile file) {
			parent.listing().put(name, file);
		}

		public void unlink() {
			parent.listing().remove(name);
		}
	}

	public record FileSystemHelper(ComputerBlockEntity entity) {
		private List<String> splitPath(String path) {
			var array = new ArrayList<>(Arrays.asList(path.split("[/\\\\]")));
			for (int i = 0; i < array.size(); i++) {
				var str = array.get(i);
				if ("..".equals(str) && i > 1) {
					array.remove(i);
					array.remove(i - 1);
					i--;
				} else if (".".equals(str) || "".equals(str)) {
					array.remove(i);
					i--;
				}
			}
			return array;
		}

		private BoundFile resolveFile(
				String path
		) {
			AjarDirectory node = entity.getFileSystem().root();
			List<String> splitPath = splitPath(path);
			if (splitPath.isEmpty())
				return null;
			for (int i = 0; i < splitPath.size(); i++) {
				var segment = splitPath.get(i);
				var child = node.listing().get(segment);
				if (child instanceof AjarSymlinkFile symlink) {
					// TODO: symlink infinite recursion protection
					if (symlink.target().startsWith("/")) {
						return resolveFile(symlink.target() + "/" + String.join("/", splitPath.subList(i + 1, splitPath.size())));
					} else {
						return resolveFile(String.join("/", splitPath.subList(0, i)) + "/" + symlink.target() + splitPath.subList(i + 1, splitPath.size()));
					}
				}
				if (i == splitPath.size() - 1) break;
				if (child == null) return null;
				if (!(child instanceof AjarDirectory directory)) {
					throw new RuntimeException("No such file or directory " + path);
				}
				node = directory;
			}
			return new BoundFile(node, splitPath.getLast());
		}

		public boolean unlink(String path) {
			var boundFile = resolveFile(path);
			if (boundFile == null || !boundFile.exists())
				return false;
			boundFile.unlink();
			return true;
		}

		public String readFile(String path) {
			// TODO: resolve relative to working directory
			var boundFile = resolveFile(path);
			if (boundFile == null || !boundFile.exists())
				throw new RuntimeException("No such file: " + path);
			var file = boundFile.resolve();
			if (file instanceof AjarRegularFile regularFile) {
				return regularFile.getTextContent();
			}
			throw new RuntimeException("File is not a text file");
		}

		public void writeFile(String path, String content) {
			var boundFile = resolveFile(path);
			if (boundFile == null) {
				throw new RuntimeException("Could not resolve file (parent directory does not exist): " + path);
			}
			var existingFile = boundFile.resolve();
			if (existingFile != null && !(existingFile instanceof AjarRegularFile)) {
				throw new RuntimeException("Cannot overwrite non text file: " + path);
			}
			boundFile.set(new AjarRegularFile(content.getBytes(StandardCharsets.UTF_8)));
		}

	}

	public record ScreenHelper(ComputerBlockEntity entity) {
		public void setScreenSize(int rows, int cols) {
			entity.setTerminal(ComputerTerminal.ofSize(rows, cols));
		}

		public void print(String line) {
			entity.addLine(line);
		}
	}

	public class Js {
		private final ComputerBlockEntity entity;

		public Js(ComputerBlockEntity entity) {this.entity = entity;}
		public void run(String path){
			var fs = new FileSystemHelper(entity);
			entity.getVm().executeScript(fs.readFile(path), new String[0]);
		}
	}

	public void executeScript(String source, String[] args) {
		try {
			// TODO: provide an import facility of some sort
			var bindings = sandbox.createBindings();
			bindings.put("process", new ProcessHelper(args));
			sandbox.eval(source, bindings);
		} catch (ScriptException e) {
			Ajar.LOGGER.error("Could not execute script", e);
			entity.addLine("Could not execute script: " + e.getMessage());
		}
	}
}