package speiger.src.data;

import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import javax.imageio.ImageIO;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonWriter;

public class Registry {
	public static final Registry INSTANCE = new Registry();
	ExecutorService service = Executors.newFixedThreadPool(4);
	ExecutorCompletionService<DownloadedImage> queue = new ExecutorCompletionService<>(service);
	Map<UUID, Collectable> collectables = new LinkedHashMap<>();
	Set<UUID> completed = new HashSet<>();
	Object syncObj = new Object();
	boolean bulkOperationActive = false;
	
	public void load() {
		parseRegistry("/assets/data/medals.json", CollectableType.MEDAL);
		parseRegistry("/assets/data/panels.json", CollectableType.PANEL);
		parseRegistry("/assets/data/pswitches.json", CollectableType.PSWITCH);
		parseRegistry("/assets/data/chucks.json", CollectableType.CHUCKS);
		dumpData();
		loadCompleted();
	}
	
	private void dumpData() {
		Path path = getOrigin().resolve("data");
		if(Files.exists(path.resolve("images.zip"))) return;
		try(InputStream stream = Registry.class.getResourceAsStream("/assets/images/images.zip")) {
			if(Files.notExists(path)) Files.createDirectories(path);
			Files.copy(stream, path.resolve("images.zip"));
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public void importSave(List<UUID> ids) {
		completed.clear();
		completed.addAll(ids);
		save();
	}
	
	private void loadCompleted() {
		completed.clear();
		Path path = getOrigin().resolve("data/save.json");
		if(Files.notExists(path)) return;
		try(BufferedReader reader = Files.newBufferedReader(path)) {
			for(JsonElement element : JsonParser.parseReader(reader).getAsJsonObject().getAsJsonArray("completed")) {
				completed.add(UUID.fromString(element.getAsString()));
			}
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public void save() {
		JsonArray array = new JsonArray();
		completed.forEach(T -> array.add(T.toString()));
		Path path = getOrigin().resolve("data");
		if(Files.notExists(path)) {
			try {
				Files.createDirectories(path);
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
		try(JsonWriter writer = new JsonWriter(Files.newBufferedWriter(path.resolve("save.json")))) {
			writer.setIndent("\t");
			JsonObject obj = new JsonObject();
			obj.add("completed", array);
			Streams.write(obj, writer);
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public void processQueue() {
		List<DownloadedImage> images = new ArrayList<>();
		Future<DownloadedImage> data = null;
		while((data = queue.poll()) != null) {
			try {
				images.add(data.get());
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
		synchronized(syncObj) {
			try(FileSystem system = FileSystems.newFileSystem(getOrigin().resolve("data/images.zip"), Map.of("create", "true"))) {
				for(DownloadedImage image : images) {
					if(image.id() == null) continue;
					Path path = system.getPath(image.id().toString().replace("-", "_")+".jpg");
					try(OutputStream stream = Files.newOutputStream(path)) {
						ImageIO.write(image.image(), "jpg", new BufferedOutputStream(stream));
					}
					catch(Exception e) {
						e.printStackTrace();
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	private void parseRegistry(String path, CollectableType type) {
		try(InputStreamReader reader = new InputStreamReader(Registry.class.getResourceAsStream(path))) {
			for(JsonElement element : JsonParser.parseReader(reader).getAsJsonObject().getAsJsonArray("entries")) {
				try {
					Collectable entry = parse(element.getAsJsonObject(), type);
					collectables.put(entry.id(), entry);
				}
				catch(Exception e) {
					e.printStackTrace();
				}
			}
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public BufferedImage getImage(TaggedImage image) {
		Path path = getOrigin().resolve("data/images.zip");
		if(Files.exists(path)) {
			synchronized(syncObj) {
				try(FileSystem system = FileSystems.newFileSystem(path)) {
					Path resolve = system.getPath(image.fileId().toString().replace("-", "_")+".jpg");
					if(Files.exists(resolve)) {
						return ImageIO.read(new ByteArrayInputStream(Files.readAllBytes(resolve))); 
					}
				}
				catch(Exception e) {
					e.printStackTrace();
				}
			}
		}
		try {
			return queue.submit(new DownloadTask(image)).get().image();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public Collectable get(UUID id) {
		return collectables.get(id);
	}
	
	public int completed(CollectableType type) {
		return (int)collectables.values().stream().filter(T -> completed.contains(T.id())).filter(T -> T.type() == type).count();
	}
	
	public int total(CollectableType type) {
		return (int)collectables.values().stream().filter(T -> T.type() == type).count();
	}
	
	public void markComplete(UUID id) {
		completed.add(id);
		if (bulkOperationActive) save();
	}
	
	public void unmarkComplete(UUID id) {
		completed.remove(id);
		if (bulkOperationActive) save();
	}
	
	public void setBulkOperation(boolean bulk) {
		bulkOperationActive = bulk;

		if (!bulk) {
			save();
		}
	}

	public boolean isCompleted(UUID id) {
		return completed.contains(id);
	}
	
	public List<Collectable> collectables() {
		return new ArrayList<>(collectables.values());
	}
	
	public void downloadMissingIcons(Consumer<int[]> progressTracker, AtomicBoolean cancel, Runnable completion) {
		ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
		ExecutorCompletionService<DownloadedImage> queue = new ExecutorCompletionService<>(service);
		synchronized(syncObj) {
			Map<String, String> done = Map.of();
			if(Files.notExists(getOrigin().resolve("data/images.zip"))) {
				done = Map.of("create", "true");
			}
			
			try(FileSystem system = FileSystems.newFileSystem(getOrigin().resolve("data/images.zip"), done)) {
				int toDo = 0;
				for(Collectable collect : Registry.INSTANCE.collectables()) {
					for(TaggedImage image : collect.images().values()) {
						Path path = system.getPath(image.fileId().toString().replace("-", "_")+".jpg");
						if(Files.exists(path)) continue;
						queue.submit(new DownloadTask(image, cancel));
						toDo++;
					}
				}
				int total = toDo;
				progressTracker.accept(new int[] {0, total});
				service.shutdown();
				while(toDo > 0) {
					Future<DownloadedImage> future = null;
					while((future = queue.poll()) != null) {
						DownloadedImage image = future.get();
						toDo--;
						if(image.id() == null) continue;
						Path path = system.getPath(image.id().toString().replace("-", "_")+".jpg");
						try(OutputStream stream = Files.newOutputStream(path)) {
							ImageIO.write(image.image(), "jpg", new BufferedOutputStream(stream));
						}
						catch(Exception e) {
							e.printStackTrace();
						}
						progressTracker.accept(new int[] {(total - toDo), total});
						Thread.sleep(Duration.ofMillis(50));
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
		completion.run();
	}
	
	private static Collectable parse(JsonObject obj, CollectableType type) {
		String name = obj.get("name").getAsString();
		UUID id = UUID.fromString(obj.get("id").getAsString());
		Point pos = parsePosition(obj.getAsJsonObject("position"));
		EndPoint point = parseEnd(obj.getAsJsonObject("end"));
		Map<String, TaggedImage> images = new HashMap<>();
		for(JsonElement el : obj.getAsJsonArray("images")) {
			TaggedImage image = parseImage(el.getAsJsonObject());
			images.put(image.tag(), image);
		}
		return new Collectable(name, id, type, pos, point, images);
	}
	
	private static TaggedImage parseImage(JsonObject obj) {
		return new TaggedImage(UUID.fromString(obj.get("id").getAsString()), obj.get("url").getAsString(), obj.get("type").getAsString());
	}
	
	private static EndPoint parseEnd(JsonObject obj) {
		return obj == null ? null : new EndPoint(parsePosition(obj), obj.get("flying").getAsBoolean());
	}
	
	private static Point parsePosition(JsonObject obj) {
		return new Point(obj.get("x").getAsInt(), obj.get("y").getAsInt());
	}
	
	public static Path getOrigin() {
		try {
			Path data = Path.of(Registry.class.getProtectionDomain().getCodeSource().getLocation().toURI());
			if(!Files.isDirectory(data)) return data.getParent();
		}
		catch(URISyntaxException e) {
			e.printStackTrace();
		}
		return Path.of("");
	}
	
	private static class DownloadTask implements Callable<DownloadedImage> {
		TaggedImage image;
		AtomicBoolean cancel;
		
		public DownloadTask(TaggedImage image) {
			this.image = image;
		}
		
		public DownloadTask(TaggedImage image, AtomicBoolean cancel) {
			this.image = image;
			this.cancel = cancel;
		}
		
		@Override
		public DownloadedImage call() throws Exception {
			if(cancel != null && cancel.get()) {
				return new DownloadedImage(null, null);
			}
			try {
				ByteArrayOutputStream stream = new ByteArrayOutputStream();
				WritableByteChannel output = Channels.newChannel(stream);
				ReadableByteChannel input = Channels.newChannel(new URI(image.url().replace(" ", "%20")).toURL().openStream());
				ByteBuffer buffer = ByteBuffer.allocate(8192);
				while(input.read(buffer) > 0) {
					output.write(buffer.flip());
					buffer.flip();
				}
				input.close();
				output.close();
				return new DownloadedImage(this.image.fileId(), ImageIO.read(new ByteArrayInputStream(stream.toByteArray())));
			}
			catch(Exception e) {
				e.printStackTrace();
			}
			return new DownloadedImage(null, null);
		}
		
	}
	
	private record DownloadedImage(UUID id, BufferedImage image) {}
}
