content: package thebetweenlands.client.handler.gallery;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.lang.reflect.Type;
  import java.net.HttpURLConnection;
  import java.net.Proxy;
  import java.net.URL;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.StandardCopyOption;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.ThreadFactory;
  import java.util.concurrent.atomic.AtomicInteger;
  
  import javax.annotation.Nullable;
  
  import com.google.gson.Gson;
  import com.google.gson.JsonArray;
  import com.google.gson.JsonElement;
  import com.google.gson.JsonObject;
  import com.google.gson.JsonParser;
  import com.google.gson.JsonPrimitive;
  import com.google.gson.reflect.TypeToken;
  
  import net.minecraft.util.JsonUtils;
  import thebetweenlands.common.TheBetweenlands;
  import thebetweenlands.common.config.BetweenlandsConfig;
  import thebetweenlands.common.lib.ModInfo;
  
  public final class GalleryManager {
  	private static final ExecutorService DOWNLOADER = Executors.newFixedThreadPool(1, new ThreadFactory() {
  		@Override
  		public Thread newThread(Runnable r) {
  			Thread thread = new Thread(r);
  			thread.setName("BL Gallery Downloader #" + GALLERY_DOWNLOADER_THREAD_ID.getAndIncrement());
  			thread.setDaemon(true);
  			return thread;
  		}
  	});
  
  	private static final AtomicInteger GALLERY_DOWNLOADER_THREAD_ID = new AtomicInteger(0);
  
  	public static final Gson GSON = new Gson();
  	public static final Type STRING_ARRAY_TYPE = new TypeToken<String[]>() {}.getType();
  
  	public static final GalleryManager INSTANCE = new GalleryManager();
  
  	private Map<String, GalleryEntry> entries = new HashMap<>();
  
  	private GalleryManager() {
  
  	}
  
  	public Map<String, GalleryEntry> getEntries() {
  		return this.entries;
  	}
  
  	private synchronized void loadLocalIndex(File folder) {
  		File file = new File(folder, "index.json");
  
  		if(file.exists()) {
  			try(InputStreamReader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
  				JsonParser parser = new JsonParser();
  
  				final JsonElement json = parser.parse(reader);
  
  				synchronized(this.entries) {
  					this.entries = this.parseLocalIndex(folder, json);
  				}
  			} catch(Exception ex) {
  				TheBetweenlands.logger.error("Failed loading local gallery index", ex);
  			}
  		}
  	}
  
  	private synchronized void saveLocalIndex(File folder) {
  		File file = new File(folder, "index.json");
  
  		file.delete();
  
  		try(FileWriter fileWriter = new FileWriter(file)) {
  			JsonArray indexArr = new JsonArray();
  
  			synchronized(this.entries) {
  				for(GalleryEntry entry : this.entries.values()) {
  					indexArr.add(this.writeEntry(entry));
  				}
  			}
  
  			GSON.toJson(indexArr, fileWriter);
  		} catch (IOException e) {
  			TheBetweenlands.logger.error("Failed saving local gallery index", e);
  		}
  	}
  
  	public void checkAndUpdate(File folder) {
  		this.loadLocalIndex(folder);
  
  		if(BetweenlandsConfig.GENERAL.onlineGallery) {
  			final Proxy proxy = TheBetweenlands.proxy.getNetProxy();
  			DOWNLOADER.submit(new Runnable() {
  				@Override
  				public void run() {
  					try {
  						TheBetweenlands.logger.info("Updating gallery");
  
  						URL url = new URL("https://raw.githubusercontent.com/Angry-Pixel/The-Betweenlands/online_picture_gallery/index.json");
  						HttpURLConnection request = null;
  						try {
  							request = GalleryManager.this.createHttpConnection(url, proxy);
  							request.setRequestProperty("Content-Type", "application/json; charset=utf-8");
  							request.connect();
  
  							if (request.getResponseCode() == HttpURLConnection.HTTP_OK) {
  								JsonParser parser = new JsonParser();
  								final JsonElement jsonElement = parser.parse(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
  								GalleryManager.this.updateLocalIndexFromOnline(folder, jsonElement, proxy);
  							}
  						} finally {
  							if(request != null) {
  								request.disconnect();
  							}
  						}
  					} catch(Exception ex) {
  						TheBetweenlands.logger.error("Failed downloading gallery data", ex);
  					}
  				}
  			});
  		}
  	}
  
  	private HttpURLConnection createHttpConnection(URL url, @Nullable Proxy proxy) throws IOException {
  		HttpURLConnection request = null;
  		if(proxy != null) {
  			request = (HttpURLConnection) url.openConnection(proxy);
  		} else {
  			request = (HttpURLConnection) url.openConnection();
  		}
  		request.setDoInput(true);
  		request.setDoOutput(false);
  		return request;
  	}
  
  	private void updateLocalIndexFromOnline(File folder, JsonElement json, @Nullable Proxy proxy) {
  		Map<String, GalleryEntry> onlineEntries = this.parseOnlineIndex(folder, json);
  
  		Map<String, GalleryEntry> newLocalEntries = new HashMap<>();
  
  		boolean changed = false;
  
  		Map<String, GalleryEntry> localEntriesCopy = new HashMap<>();
  		synchronized(this.entries) {
  			localEntriesCopy.putAll(this.entries);
  		}
  
  		for(GalleryEntry localEntry : localEntriesCopy.values()) {
  			GalleryEntry onlineEntry = onlineEntries.get(localEntry.getUrl());
  
  			String localSha256 = localEntry.getLocalSha256();
  
  			if(onlineEntry == null) {
  				//Doesn't exist anymore online, just keep it
  				newLocalEntries.put(localEntry.getUrl(), localEntry);
  			} else if(localSha256 == null || !localSha256.equals(onlineEntry.getSha256())) {
  				TheBetweenlands.logger.info("Removing gallery entry '" + localEntry.getSha256() + "'/'" + localEntry.getUrl() + "' because the local hash (" + localSha256 + ") does not match");
  				this.deleteLocalPicture(folder, localEntry);
  				changed = true;
  			} else {
  				newLocalEntries.put(onlineEntry.getUrl(), onlineEntry);
  			}
  		}
  
  		for(GalleryEntry onlineEntry : onlineEntries.values()) {
  			if(!newLocalEntries.containsKey(onlineEntry.getUrl())) {
  				try {
  					if(this.downloadPicture(folder, onlineEntry, proxy)) {
  						String localSha256 = onlineEntry.getLocalSha256();
  
  						if(localSha256 != null && !onlineEntry.getSha256().equals(localSha256)) {
  							TheBetweenlands.logger.info("Downloaded gallery picture '" + onlineEntry.getUrl() + "' SHA256 hash does not match (Expected: " + onlineEntry.getSha256() + " Got: " + localSha256 + ")! Please report this to the mod authors.");
  						}
  
  						newLocalEntries.put(onlineEntry.getUrl(), onlineEntry);
  						changed = true;
  					} else {
  						TheBetweenlands.logger.error("Failed downloading gallery picture '" + onlineEntry.getUrl() + "'");
  					}
  				} catch(Exception ex) {
  					TheBetweenlands.logger.error("Failed downloading gallery picture '" + onlineEntry.getUrl() + "'", ex);
  				}
  			}
  		}
  
  		synchronized(this.entries) {
  			this.entries = newLocalEntries;
  		}
  
  		if(changed) {
  			this.saveLocalIndex(folder);
  		}
  	}
  
  	private File getPictureFile(File folder, GalleryEntry entry) {
  		return this.getPictureFile(folder, entry.getSha256());
  	}
  
  	private File getPictureFile(File folder, String sha256) {
  		return new File(folder, sha256.toLowerCase() + ".png");
  	}
  
  	private boolean downloadPicture(File folder, GalleryEntry entry, @Nullable Proxy proxy) throws IOException {
  		TheBetweenlands.logger.info("Downloading gallery picture '" + entry.getSha256() + "'/'" + entry.getUrl() + "'");
  
  		URL url = new URL(entry.getUrl());
  		HttpURLConnection request = null;
  		try {
  			request = GalleryManager.this.createHttpConnection(url, proxy);
  			request.connect();
  
  			if (request.getResponseCode() == HttpURLConnection.HTTP_OK) {
  				Files.copy(request.getInputStream(), this.getPictureFile(folder, entry).toPath(), StandardCopyOption.REPLACE_EXISTING);
  				return true;
  			}
  		} finally {
  			if(request != null) {
  				request.disconnect();
  			}
  		}
  
  		return false;
  	}
  
  	private void deleteLocalPicture(File folder, GalleryEntry entry) {
  		try {
  			File file = this.getPictureFile(folder, entry);
  			if(file.exists()) {
  				file.delete();
  			}
  		} catch(Exception ex) {
  			TheBetweenlands.logger.error("Failed deleting gallery picture", ex);
  		}
  	}
  
  	private Map<String, GalleryEntry> parseLocalIndex(File folder, JsonElement json) {
  		Map<String, GalleryEntry> entries = new HashMap<>();
  
  		if(json.isJsonArray()) {
  			JsonArray index = json.getAsJsonArray();
  			for(int j = 0; j < index.size(); j++) {
  				try {
  					JsonObject entryJson = index.get(j).getAsJsonObject();
  					GalleryEntry entry = this.parseEntry(folder, entryJson);
  					entries.put(entry.getUrl(), entry);
  				} catch(Exception ex) {
  					if(BetweenlandsConfig.DEBUG.debug) {
  						TheBetweenlands.logger.error("Failed parsing local gallery index entry: " + j, ex);
  					}
  				}
  			}
  		}
  
  		return entries;
  	}
  
  	private Map<String, GalleryEntry> parseOnlineIndex(File folder, JsonElement json) {
  		Map<String, GalleryEntry> entries = new HashMap<>();
  
  		if(json.isJsonArray()) {
  			JsonArray jsonArr = json.getAsJsonArray();
  			for(int i = 0; i < jsonArr.size(); i++) {
  				try {
  					JsonObject element = jsonArr.get(i).getAsJsonObject();
  					boolean isVersionValid;
  					if(element.has("versions")) {
  						isVersionValid = false;
  						String[] versions = GSON.fromJson(element.get("versions"), STRING_ARRAY_TYPE);
  						for(String version : versions) {
  							if(ModInfo.GALLERY_VERSION.equals(version)) {
  								isVersionValid = true;
  								break;
  							}
  						}
  					} else {
  						isVersionValid = true;
  					}
  					if(isVersionValid) {
  						JsonArray index = element.get("index").getAsJsonArray();
  						for(int j = 0; j < index.size(); j++) {
  							try {
  								JsonObject entryJson = index.get(j).getAsJsonObject();
  								GalleryEntry entry = this.parseEntry(folder, entryJson);
  								entries.put(entry.getUrl(), entry);
  							} catch(Exception ex) {
  								if(BetweenlandsConfig.DEBUG.debug) {
  									TheBetweenlands.logger.error("Failed parsing gallery index entry: " + j, ex);
  								}
  							}
  						}
  					}
  				} catch(Exception ex) {
  					if(BetweenlandsConfig.DEBUG.debug) {
  						TheBetweenlands.logger.error("Failed parsing gallery version entry: " + i, ex);
  					}
  				}
  			}
  		}
  
  		return entries;
  	}
  
  	private JsonObject writeEntry(GalleryEntry entry) {
  		JsonObject json = new JsonObject();
  		json.add("sha256", new JsonPrimitive(entry.getSha256()));
  		json.add("url", new JsonPrimitive(entry.getUrl()));
  		json.add("title", new JsonPrimitive(entry.getTitle()));
  		json.add("author", new JsonPrimitive(entry.getAuthor()));
  		if(entry.getDescription() != null) {
  			json.add("description", new JsonPrimitive(entry.getDescription()));
  		}
  		if(entry.getSourceUrl() != null) {
  			json.add("source_url", new JsonPrimitive(entry.getSourceUrl()));
  		}
  		return json;
  	}
  
  	private GalleryEntry parseEntry(File folder, JsonObject json) {
  		String sha256 = JsonUtils.getString(json, "sha256");
  		String url = JsonUtils.getString(json, "url");
  		String title = JsonUtils.getString(json, "title");
  		String author = JsonUtils.getString(json, "author");
  		String description = json.has("description") ? JsonUtils.getString(json, "description") : null;
  		String sourceUrl = json.has("source_url") ? JsonUtils.getString(json, "source_url") : null;
  		return new GalleryEntry(sha256, url, title, author, description, sourceUrl, this.getPictureFile(folder, sha256));
  	}
  }
  