public static final String MANUAL_AUTO_VERIFY = "manual.auto.verify";
 public static final String MANUAL_SEARCH_BLACKLIST = "manual.search.blacklist";
 public static boolean getBooleanProperty(String key, boolean defaultValue) {
	String propety = getPropety(key, String.valueOf(defaultValue));
	return Boolean.parseBoolean(propety);
}
 public static boolean notBlacklisted(String key, SearchResultItem item) {
	String blacklistStr = Config.getPropety(key, "");
	boolean isBlacklisted = asList(blacklistStr.split(","))
			.stream()
			.anyMatch(s -> s.equalsIgnoreCase(item.seller()));
	return !isBlacklisted;
}
 public class Command {

	public Command(String line) {
		this.input = line;
	}

	String input;
	Status status;
	public List<SearchResultItem> itemResults = Collections.emptyList();
	public String league;
	
	String errorMessage;
	String errorStackTrace;
	
	public List<String> invalidSearchTerms;
	
	public Long searchDuration;
	
	public static enum Status {
		SUCCESS, ERROR, EXIT, INVALID
	}

	public String toJson() {
		return Util.toJsonPretty(this);
	}
}
 public class Main {
	
	private final static Logger logger = LoggerFactory.getLogger(Main.class.getName());

	BackendClient backendClient = new BackendClient();
	SessProp sessProp = new SessProp();
	Long searchDuration = null; 
	List<String> invalidSearchTerms = null; 
	
	public static void main(String[] args) throws Exception {
		try {
			reloadConfig();
			new Main(args);
		} catch (Exception e) {
			JOptionPane.showMessageDialog(null, "Error occured: " + e.getMessage());
			throw e;
		}
    }

	public static void reloadConfig() throws Exception, FileNotFoundException {
		Config.loadConfig();
	}
	
	public static void reloadLookAndFeel() {
		if (Config.getBooleanProperty(Config.LOOK_AND_FEEL_DECORATED, false)) {
			JFrame.setDefaultLookAndFeelDecorated(true); 
			JDialog.setDefaultLookAndFeelDecorated(true);
			System.setProperty("sun.awt.noerasebackground", "true");
		}
		try {
			String lookAndFeel = Config.getPropety(Config.LOOK_AND_FEEL, "com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
			if (lookAndFeel.startsWith("Substance")) {
				lookAndFeel = "org.pushingpixels.substance.api.skin." + lookAndFeel;
			}
			UIManager.setLookAndFeel(lookAndFeel);
		} catch (Exception e) {
		    try {
		        UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
		    } catch (Exception ex) {
		    	throw new RuntimeException(ex);
		    }
		}
		
		for (Window window : Window.getWindows()) {
			SwingUtilities.updateComponentTreeUI(window);
		}
	}

	public Main(String[] args) throws Exception {
		CommandLine cmd = new CommandLine(args);
		boolean guiEnabled = cmd.hasFlag("-gui");
		guiEnabled = guiEnabled || cmd.getNumberOfArguments() == 0;

		logger.info("guiEnabled: " + guiEnabled);
		
		if (guiEnabled) {
			showGui(cmd.getArgument(0));
		} else {
			if (cmd.getNumberOfArguments() == 0) {
				throw new IllegalArgumentException("First arguement needed, and should be the query. e.g. 'search chest 100life 6s5L'. "
						+ "Enclosed in double quoutes if needed.");
			}
			String query = cmd.getArgument(0);
			logger.info("Query: " + query);
			
			Command command = processLine(query);
			String json = command.toJson();
			writeToFile(json);
		}
	}

	private void showGui(String query) {
		reloadLookAndFeel();
	    SwingUtilities.invokeLater(() -> new QicFrame(Main.this, query));
	}

	public void writeToFile(String contents) throws IOException {
		Util.overwriteFile("results.json", contents);
	}

	public Command processLine(String line) throws Exception {
		Command command = new Command(line);
		searchDuration = null;
		invalidSearchTerms = new LinkedList<>();
		try {
			if (line.startsWith("sort")&& !sessProp.getLocation().isEmpty()) {
				command.itemResults = runSearch(line, true);
				command.searchDuration = searchDuration;
			} else if (line.startsWith("s ")) {
				String terms = substringAfter(line, "s ").trim();
				if (!terms.isEmpty()) {
					command.itemResults = runSearch(terms, false);
					command.searchDuration = searchDuration;
				}
			}
			command.league = sessProp.getLeague();
			command.invalidSearchTerms = invalidSearchTerms;
			command.status = Status.SUCCESS;
		} catch (Exception e) {
			e.printStackTrace();
			command.status = ERROR;
			command.errorMessage = e.getMessage();
			command.errorStackTrace = ExceptionUtils.getStackTrace(e);
			throw e;
		}
		setGuildInfo(command.itemResults);
		FmJS fmJS = new FmJS(command.itemResults);
		fmJS.process();
		return command;
	}

	private void setGuildInfo(List<SearchResultItem> itemResults) throws IOException {
		String nameList = FileUtils.readFileToString(new File(GUILD_LIST_FILENAME));
		if (!nameList.isEmpty()) {
			List<String> names = Arrays.asList(StringUtils.split(nameList));
			itemResults.stream().forEach(item -> {
				boolean guildmate = names.stream().anyMatch(name -> StringUtils.equalsIgnoreCase(name, item.seller()));
				item.guildItem(guildmate);
				item.guildDiscount(Config.getPropety(Config.GUILD_DISCOUNT_STRING, Config.GUILD_DISCOUNT_STRING_DEFAULT));
			});
		}
	}

	private List<SearchResultItem> runSearch(String terms, boolean sortOnly) throws Exception {
		String html = downloadHtml(terms, sortOnly);
		if (html == null) {
			return Collections.emptyList();
		}
		SearchPageScraper scraper = new SearchPageScraper(html);
		List<SearchResultItem> items = scraper.parse();
		logger.info("items found: " + items.size());
		return items;
	}

	public String downloadHtml(String terms, boolean sortOnly) throws Exception {
		long start = System.currentTimeMillis();
		
		String regex = "([^\\s]*=\".*?\")";
		List<String> customHttpKeyVals = Util.regexMatches(regex, terms, 1);
		String customHttpKeyVal = customHttpKeyVals.stream()
				.map(s -> StringUtils.remove(s, '"'))
				.collect(Collectors.joining("&")); 
		String query = terms.replaceAll(regex, " ");
		
		BlackmarketLanguage language = new BlackmarketLanguage();
		ParseResult sortParseResult = language.parseSortToken(query);
		String sort = sortParseResult.result;
		sort = sort == null ? "price_in_chaos" : sort;
		invalidSearchTerms.addAll(sortParseResult.invalidSearchTerms);
		
		if (!sortOnly) {
			logger.info("Query: " + query);
			ParseResult queryParseResult = language.parse(query);
			String payload = queryParseResult.result;
			invalidSearchTerms.addAll(queryParseResult.invalidSearchTerms);
			if (invalidSearchTerms.isEmpty()) {
				payload = asList(payload, customHttpKeyVal, "capquality=x").stream().filter(StringUtils::isNotBlank).collect(joining("&"));
				logger.info("Unencoded payload: " + payload);
				payload = asList(payload.split("&")).stream().map(Util::encodeQueryParm).collect(joining("&"));
				String location  = submitSearchForm(payload);
				String league = language.parseLeagueToken(query);
				sessProp.setLocation(location);
				sessProp.setLeague(league);
				sessProp.saveToFile();
			}
		}

		String searchPage = null;
		if (invalidSearchTerms.isEmpty()) {
			logger.info("sort: " + sort);
			searchPage = ajaxSort(sort);
		} else {
			logger.info("invalidSearchTerms: " + invalidSearchTerms.toString());
		}
		
		long end = System.currentTimeMillis();
		long duration = end - start;
		logger.info("Took " + duration + " ms");
		searchDuration = Long.valueOf(duration);
		return searchPage;
	}

	private String ajaxSort(String sort) throws Exception {
		String searchPage = "";
		sort = URLEncoder.encode(sort, "UTF-8");
		sort = "sort=" + sort + "&bare=true";
		searchPage = backendClient.postXMLHttpRequest(sessProp.getLocation(), sort);
		return searchPage;
	}

	private String submitSearchForm(String payload) throws Exception {
		String url = "http://poe.trade/search";
		String location = backendClient.post(url, payload);
		return location;
	}

} 	public static class SearchResultItem {
		
		private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
		
		public String id; // the id in the search result html page
		public String buyout;
		public String name;
		public String ign;
		public boolean corrupted;
		public boolean identified;
		public Rarity rarity;
		
		public String dataHash; // use for verify
		
		public String socketsRaw;
		public String stackSize;
		
		public String quality;
		
		public String physDmgRangeAtMaxQuality;
		public String physDmgAtMaxQuality;
		public String eleDmgRange;
		public String attackSpeed;
		public String dmgAtMaxQuality;
		public String crit;
		public String level;
		public String eleDmg;
		
		public String armourAtMaxQuality;
		public String evasionAtMaxQuality;
		public String energyShieldAtMaxQuality;
		public String block;
		
		public String reqLvl;
		public String reqStr;
		public String reqInt;
		public String reqDex;
		public String mapQuantity;

		public String ageAndHighLvl;
		public String league;
		public String seller;
		public String thread;
		public String sellerid;
		public String threadUrl;
		public String online;
		
		public String imageUrl;

		public Mod implicitMod;
		public List<Mod> explicitMods = new ArrayList<>();
		public Verify verified = Verify.UKNOWN;
		public boolean guildItem = false;
		public String wtb = null;
		public String guildDiscount = null;
		public boolean newInAutomated = false;
		
		public List<Mod> getMods() {
			List<Mod> mods = explicitMods.stream().collect(Collectors.toList());
			if (implicitMod != null) {
				mods.add(0, new Mod("--------------", null));
				mods.add(0, implicitMod);
			}
			return mods;
		}
		
		public List<String> getReqs() {
			return labelList(
					labelVal("Lvl", reqLvl), 
					labelVal("Str", reqStr),
					labelVal("Dex", reqDex),
					labelVal("Int", reqInt));
		}
		
		public List<String> getItem() {
			return labelList(
					labelVal("No", idFinal()),
					labelVal("Name", nameFinal()), 
					labelVal("League", league),
					labelVal("Quality", quality), 
					labelVal("Identified", String.valueOf(identified)), 
					labelVal("Corrupted", String.valueOf(corrupted)), 
					labelVal("SocketsRaw", socketsRaw),
					labelVal("StackSize", stackSize),
					labelVal("MapQuantity", mapQuantity));
		}
		
		public List<String> getOffense() {
			return labelList(
					labelVal("pDPS", physDmgAtMaxQuality), 
					labelVal("eDPS", eleDmg), 
					labelVal("DPS", dmgAtMaxQuality), 
					labelVal("APS", attackSpeed), 
					labelVal("ele", eleDmgRange),
					labelVal("phys", physDmgRangeAtMaxQuality));
		}
		
		public List<String> getDefense() {
			return labelList(
					labelVal("Armour", armourAtMaxQuality), 
					labelVal("Evasion", evasionAtMaxQuality), 
					labelVal("ES", energyShieldAtMaxQuality), 
					labelVal("Block", block), 
					labelVal("Crit", crit),
					labelVal("Map-Gem Lvl", level));
		}
		
		private List<String> labelList(String ... labels) {
			return asList(labels).stream()
					.filter(Objects::nonNull)
					.collect(toList());
		}

		public List<String> getSeller() {
			String highestLvl = substringAfter(ageAndHighLvl, "h");
			String age = substringBetween(ageAndHighLvl, "a", "h");
			age = StringUtils.isNumeric(age) ? now().minusDays(parseInt(age)).format(ofPattern("MMM dd uuuu")) : age;
			return labelList(
					labelVal("IGN", ign),
					labelVal("Joined", age),
					labelVal("HighestLvl", highestLvl),
					labelVal("Account", seller),
					labelVal("Thread", thread),
					labelVal("Verified", verified.name()),
//					labelVal("", threadUrl),
					labelVal("Online", String.valueOf(containsIgnoreCase(online, "online"))));
		}
		
		private String labelVal(String label, String val) {
			return trimToNull(val) == null ? null : label + ": " + val;
		}

		public String wtb() {
			if (wtb != null) {
				return wtb;
			}
			return String.format(
					"@%s Hi, WTB your \"%s\" listed for %s in %s league.",
					ign, name, buyout, league);
		}
		
		public void wtb(String wtb) {
			this.wtb = wtb;
		}
		
		public void guildDiscount(String guildDiscount) {
			this.guildDiscount = guildDiscount;
		}
		
		public String guildDiscount() {
			return guildDiscount;
		}
		
		public boolean newInAutomated() {
			return newInAutomated;
		}

		/**
		 * @author thirdy
		 *
		 */
		public static class Mod {
			String name;
			String value;
			String forgottenMod;

			public Mod(String name, String value) {
				this.name = name;
				this.value = value;
			}
			
			public String getName() {
				return name;
			}

			public String getValue() {
				return value;
			}
			
			public String getForgottenMod() {
				return forgottenMod;
			}

			public void setForgottenMod(String forgottenMod) {
				this.forgottenMod = forgottenMod;
			}

			@Override
			public String toString() {
//				return System.lineSeparator() + "Mod [name=" + name + ", value=" + value + "]";
				if (forgottenMod != null) {
					return forgottenMod;
				}
				return toStringDisplay();
			}

			/*  Convert the ff into human readable form:
			    #Socketed Gems are Supported by level # Increased Area of Effect
				##% increased Physical Damage
				#+# to Strength
				#+# to Accuracy Rating
				#+# Mana Gained on Kill
				#+# to Weapon range
			 */
			public String toStringDisplay() {
				String display = name;
				if (StringUtils.startsWith(name, "#") || StringUtils.startsWith(name, "$")) {
					display = StringUtils.removeStart(display, "#");
					display = StringUtils.removeStart(display, "$");
					String val = StringUtils.endsWith(value, ".0") ? StringUtils.substringBefore(value, ".0") : value;
					display = StringUtils.replaceOnce(display, "#", val);
				}
				return display;
			}
			
			public String toUUID() {
				return name + ":" + value;
			}
		}
		
		public String toDisplay(String newLine) {
			StringBuilder builder = new StringBuilder();
			String _quality  = isNotBlank(quality) ? " " + quality + "%" : "";
			String linksocks = isNotBlank(socketsRaw) ? " " + socketsRaw : "";
			String strCorrupt = corrupted ? " Corrupted " : "";
			strCorrupt += !identified ? " Unidentified " : "";
			builder.append(format("[%s] %s%s%s", id, name, linksocks, _quality));
			builder.append(format("%s -----%s------ ", newLine, strCorrupt));
			builder.append(newLine);
			if (implicitMod != null) {
				builder.append(format("%s", implicitMod.toStringDisplay()));
				builder.append(newLine + " ----------- " + newLine);
			}
			if (explicitMods.size() > 0) {
				for (Mod mod : explicitMods) {
					builder.append(format("%s", mod.toStringDisplay()));
					builder.append(newLine);
				}
				builder.append("-----------" + newLine);
			}
			String _physDmg 	= isNotBlank(physDmgAtMaxQuality) ? ("pDPS " + physDmgAtMaxQuality) : "";
			String _eleDmg 		= isNotBlank(eleDmg) 			  ? ("eDPS " + eleDmg) : "";
			String _attackSpeed = isNotBlank(attackSpeed) 		  ? ("APS " + attackSpeed) : "";
			String _crit 		= isNotBlank(crit) 				  ? ("Cc " + crit) : "";
			String offense = format("%s %s %s %s", _physDmg, _eleDmg, _attackSpeed, _crit).trim();
			offense = offense.isEmpty() ? "" : (offense + newLine);
			builder.append(offense);
			
			String _armour 		= isNotBlank(armourAtMaxQuality) 	? ("Ar " + armourAtMaxQuality) : "";
			String _evasion 	= isNotBlank(evasionAtMaxQuality) 	? ("Ev " + evasionAtMaxQuality) : "";
			String _energyShield = isNotBlank(energyShieldAtMaxQuality) ? ("Es " + energyShieldAtMaxQuality) : "";
			String _block 		= isNotBlank(block) 				? ("Bk " + block) : "";
			String defense = format("%s %s %s %s", _armour, _evasion, _energyShield, _block).trim();
			defense = defense.isEmpty() ? "" : (defense + newLine);
			builder.append(defense);
			
			builder.append(format("%s IGN: %s", buyout, ign));
			return builder.toString();
		}
		
		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			String lineSeparator = System.lineSeparator();
			builder.append(lineSeparator);
			builder.append("id=");
			builder.append(id);
			builder.append(lineSeparator);
			builder.append("buyout=");
			builder.append(buyout);
			builder.append(lineSeparator);
			builder.append("name=");
			builder.append(name);
			builder.append(lineSeparator);
			builder.append("corrupted=");
			builder.append(corrupted);
			builder.append(lineSeparator);
			builder.append("identified=");
			builder.append(identified);
			builder.append(lineSeparator);
			builder.append("ign=");
			builder.append(ign);
			builder.append(lineSeparator);
			builder.append("socketsRaw=");
			builder.append(socketsRaw);
			builder.append(lineSeparator);
			builder.append("stackSize=");
			builder.append(stackSize);
			builder.append(lineSeparator);
			builder.append("quality=");
			builder.append(quality);
			builder.append(lineSeparator);
			builder.append("physDmgRangeAtMaxQuality=");
			builder.append(physDmgRangeAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("physDmgAtMaxQuality=");
			builder.append(physDmgAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("eleDmgRange=");
			builder.append(eleDmgRange);
			builder.append(lineSeparator);
			builder.append("attackSpeed=");
			builder.append(attackSpeed);
			builder.append(lineSeparator);
			builder.append("dmgAtMaxQuality=");
			builder.append(dmgAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("crit=");
			builder.append(crit);
			builder.append(lineSeparator);
			builder.append("level=");
			builder.append(level);
			builder.append(lineSeparator);
			builder.append("eleDmg=");
			builder.append(eleDmg);
			builder.append(lineSeparator);
			builder.append("armourAtMaxQuality=");
			builder.append(armourAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("evasionAtMaxQuality=");
			builder.append(evasionAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("energyShieldAtMaxQuality=");
			builder.append(energyShieldAtMaxQuality);
			builder.append(lineSeparator);
			builder.append("block=");
			builder.append(block);
			builder.append(lineSeparator);
			builder.append("reqLvl=");
			builder.append(reqLvl);
			builder.append(lineSeparator);
			builder.append("reqStr=");
			builder.append(reqStr);
			builder.append(lineSeparator);
			builder.append("reqInt=");
			builder.append(reqInt);
			builder.append(lineSeparator);
			builder.append("reqDex=");
			builder.append(reqDex);
			builder.append(lineSeparator);
			builder.append("mapQuantity=");
			builder.append(mapQuantity);
			builder.append(lineSeparator);
			builder.append("ageAndHighLvl=");
			builder.append(ageAndHighLvl);
			builder.append(lineSeparator);
			builder.append("league=");
			builder.append(league);
			builder.append(lineSeparator);
			builder.append("seller=");
			builder.append(seller);
			builder.append(lineSeparator);
			builder.append("thread=");
			builder.append(thread);
			builder.append(lineSeparator);
			builder.append("sellerid=");
			builder.append(sellerid);
			builder.append(lineSeparator);
			builder.append("threadUrl=");
			builder.append(threadUrl);
			builder.append(lineSeparator);
			builder.append("imageUrl=");
			builder.append(imageUrl);
			builder.append(lineSeparator);
			builder.append("implicitMod=");
			builder.append(implicitMod);
			builder.append(lineSeparator);
			builder.append("explicitMods=");
			builder.append(explicitMods);
			builder.append(lineSeparator);
			builder.append("online=");
			builder.append(online);
			builder.append(lineSeparator);
			return builder.toString();
		}

//		public String getId() {
//			return id;
//		}

		public String getBo() {
			String str = buyout;
			str = StringUtils.replace(str, "alteration", "alt");
			str = StringUtils.replace(str, "fusing", "fuse");
			str = StringUtils.replace(str, "jewellers", "jew");
			str = StringUtils.replace(str, "exalted", "ex");
			str = StringUtils.replace(str, "alchemy", "alch");
			str = StringUtils.replace(str, "chaos", "ch");
			str = StringUtils.replace(str, "chromatic", "chrm");
			str = StringUtils.replace(str, "chance", "chan");
			return str;
		}

//		public String getPseudoEleResistance() {
//			return getExplicitModValueByName("#(pseudo) +#% total Elemental Resistance");
//		}
//
//		public String getPseudoLife() {
//			return getExplicitModValueByName("#(pseudo) (total) +# to maximum Life");
//		}
//		
//		public String getFireRes() {
//			return getExplicitModValueByName("#+#% to Fire Resistance");
//		}
//		
//		public String getColdRes() {
//			return getExplicitModValueByName("#+#% to Cold Resistance");
//		}
//		
//		public String getLightRes() {
//			return getExplicitModValueByName("#+#% to Light Resistance");
//		}
		
//		private String getExplicitModValueByName(String name) {
//			for (Mod mod : explicitMods) {
//				if (mod.getName().equalsIgnoreCase(name)) {
//					return mod.getValue();
//				}
//			}
//			return "";
//		}

		/**
		 * Used for showing exceptions in the result table.
		 */
		public static SearchResultItem exceptionItem(Exception e) {
			SearchResultItem item = new SearchResultItem();
			item.name = e.getMessage();
			item.ign = e.getClass().getName();
			item.socketsRaw = e.getCause().getMessage();
			return item;
		}

		public String seller() {
			return seller;
		}
		
		public String dataHash() {
			return dataHash;
		}
		
		public String thread() {
			return thread;
		}

		public String toShortDebugInfo() {
			return String.format("id=%s name=%s account=%s thread=%s", id, name, seller, thread);
		}

		public void verified(Verify verified) {
			this.verified = verified;
		}
		
		public void guildItem(boolean guildItem) {
			this.guildItem = guildItem;
		}
		
		public String nameFinal() {
			return name;
		}
		
		public String idFinal() {
			if (corrupted) {
				return id + " [CORRUPTED]";
			}
			return id;
		}
		
		public ImageIcon getArt() {
			ImageIcon imageIcon = null;
			try {
				boolean artEnabled = Config.getBooleanProperty(Config.RESULT_TABLE_ART_ENABLED, true);
				if (artEnabled) {
					URL url = new URL(imageUrl);
					imageIcon = ImageCache.getInstance().get(url.toString());
				}
			} catch (MalformedURLException e) {
				logger.error("Error while grabbing art from: " + imageUrl, e);
			}
			return imageIcon;
		}
		
		public int toUUID() {
			String explicitModsUUID = explicitMods.stream().map(Mod::toUUID).collect(Collectors.joining(":"));
			String implicitModuuid = implicitMod != null ? implicitMod.toUUID() : null;
			List<String> uuidList = asList(thread, name, buyout, String.valueOf(corrupted), socketsRaw, quality, 
					physDmgRangeAtMaxQuality, physDmgAtMaxQuality, eleDmgRange, attackSpeed, dmgAtMaxQuality, crit, level, eleDmg,
					armourAtMaxQuality, evasionAtMaxQuality, energyShieldAtMaxQuality, block, mapQuantity, implicitModuuid, explicitModsUUID);
			String uuid = uuidList.stream().collect(Collectors.joining(":"));
			return Integer.valueOf(uuid.hashCode());
		}
		
		public boolean guildItem() {
			return guildItem;
		}

		public static enum Rarity {
			normal,
			magic,
			rare,
			unique,
			gem,
			currency,
			divinationcard, 
			unknown;
			
			public static Rarity valueOf(int ordinal) {
				for (Rarity r : values()) {
					if (r.ordinal() == ordinal) return r;
				}
				return unknown;
			}
		}
	} public class Worker<T> extends SwingWorker<T, Void> {
	Supplier<T> supplier;
	Consumer<T> consumer;

	public Worker(Supplier<T> supplier, Consumer<T> consumer) {
		super();
		this.supplier = supplier;
		this.consumer = consumer;
	}

	@Override
	public T doInBackground() {
		return supplier.get();
	}

	@Override
	protected void done() {
		try {
			consumer.accept(get());
		} catch (InterruptedException | ExecutionException e) {
			throw new RuntimeException(e);
		}
	}
} public class Config {
	
	public static final String CONFIG_PROPERTIES_FILENAME = "config.properties";
	public static final String GUILD_LIST_FILENAME = "guild.txt";
	
	public static final String AUTOMATED_SEARCH_WAIT_MINUTES = "automated.search.wait.minutes";
	public static final String AUTOMATED_SEARCH_INBETWEEN_WAIT_SECONDS = "automated.search.inbetween.wait.seconds";
	public static final String MANUAL_SEARCH_PREFIX = "manual.search.prefix";
	public static final String AUTOMATED_SEARCH_PREFIX = "automated.search.prefix";
	public static final String AUTOMATED_SEARCH_SOUND_FILENAME = "automated.search.sound.filename";
	public static final String AUTOMATED_SEARCH_SOUND_VOLUME = "automated.search.sound.volume";
	public static final String AUTOMATED_SEARCH_SOUND_MODE = "automated.search.sound.mode";
	public static final String AUTOMATED_SEARCH_ENABLED = "automated.search.enabled";
	public static final String AUTOMATED_SEARCH_NOTIFY_NEWONLY = "automated.search.notify.newonly";
	public static final String AUTOMATED_SEARCH_NOTIFY_NEWONLY_COLOR_HIGHLIGHT = "automated.search.notify.newonly.color.highlight";
	public static final String RESULT_TABLE_ART_ENABLED = "result.table.art.enabled";
	public static final String RESULT_TABLE_BG_COLOR = "result.table.bg.color";
	
	public static final String GUILD_DISCOUNT_STRING = "guild.discount.string";
	public static final String GUILD_COLOR_HIGHLIGHT = "guild.color.highlight";
	public static final String GUILD_DISCOUNT_STRING_DEFAULT = "1ch";

	public static final String CORRUPTED_COLOR_HIGHLIGHT = "corrupted.color.highlight";
	
	public static final String AUTOMATED_SEARCH_BLACKLIST = "automated.search.blacklist";
	public static final String MANUAL_SEARCH_BLACKLIST = "manual.search.blacklist";
	
	public static final String AUTOMATED_AUTO_VERIFY = "automated.auto.verify";
	public static final String MANUAL_AUTO_VERIFY = "manual.auto.verify";
	
	public static final String AUTOMATED_AUTO_VERIFY_SLEEP = "automated.auto.verify.wait.millisec";
	public static final String MANUAL_AUTO_VERIFY_SLEEP = "manual.auto.verify.wait.millisec";

	public static final String LOOK_AND_FEEL = "lookandfeel";
	public static final String LOOK_AND_FEEL_DECORATED = "lookandfeel.decorated.enabled";

	private static Properties config;
	
	public static void loadConfig() throws IOException, FileNotFoundException {
		config = new Properties();
		try (BufferedReader br = new BufferedReader(new FileReader(new File(CONFIG_PROPERTIES_FILENAME)))) {
			config.load(br);
		}
	}
    
    public static String getPropety(String key, String defaultValue) {
    	return StringUtils.defaultIfBlank(config.getProperty(key, defaultValue), defaultValue);
    }

	public static boolean getBooleanProperty(String key, boolean defaultValue) {
		String propety = getPropety(key, String.valueOf(defaultValue));
		return Boolean.parseBoolean(propety);
	}

	public static int getIntegerProperty(String key, int i) {
		String propety = getPropety(key, String.valueOf(i));
		return Integer.parseInt(propety);
	}
	
	public static long getLongProperty(String key, long i) {
		String propety = getPropety(key, String.valueOf(i));
		return Long.parseLong(propety);
	}

	public static double getDoubleProperty(String key, double i) {
		String propety = getPropety(key, String.valueOf(i));
		return Double.parseDouble(propety);
	}
	
	public static SoundMode getSoundMode() {
		String propety = getPropety(AUTOMATED_SEARCH_SOUND_MODE, SoundMode.EACH_SEARCH.name());
		return SoundMode.valueOf(propety);
	}
	
	public static enum SoundMode {
		EACH_SEARCH, ONCE
	}

	public static Properties getProperties() {
		return config;
	}
}
 public class SwingUtil {
	
	public static void openUrlViaBrowser(String url) {
		String s = url;
		if (Desktop.isDesktopSupported()) {
			try {
				Desktop.getDesktop().browse(new URI(s));
			} catch (Exception e) {
				showError(new BlackmarketException(
						"Error on opening browser, address: " + s + ": " + e.getMessage(), e
							));
			}
		} else {
			showError(new BlackmarketException("Launch browser failed, please manually visit: " + s));
		}
	}
	
	public static void copyToClipboard(String s) {
		StringSelection stringSelection = new StringSelection(s);
			Clipboard clpbrd = Toolkit.getDefaultToolkit().getSystemClipboard();
			clpbrd.setContents(stringSelection, null);
	}

	public static void showError(Throwable e) {
		new qic.util.SimpleExceptionHandler().uncaughtException(Thread.currentThread(), e);
	}

} public class Util {

	private static final Logger logger = LoggerFactory.getLogger(Util.class.getName());

	public static String removeThoseDamnWhiteSpace(String s) {
		s = StringUtils.deleteWhitespace(s);
		StringBuilder sb = new StringBuilder();
		char[] charArray = s.toCharArray();
		for (char c : charArray) {
			if (!Character.isSpaceChar(c)) {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	public static String regexMatch(String regex, String s, int group) {
		if (s != null) {
			List<String> matches = regexMatches(regex, s, group, Pattern.CASE_INSENSITIVE);
			if (!matches.isEmpty()) {
				return matches.get(0);
			}
		}
		return null;
	}
	
	public static List<String> regexMatches(String regex, String s, int group) {
		return regexMatches(regex, s, group, Pattern.CASE_INSENSITIVE);
	}

	public static List<String> regexMatches(String regex, String s, int group, int flags) {
		List<String> allMatches = new ArrayList<String>();
		Matcher m = Pattern.compile(regex, flags).matcher(s);
		while (m.find()) {
			allMatches.add(m.group(group));
		}
		return allMatches;
	}

	public static String encodeQueryParm(String queryParam) {
		String key = substringBefore(queryParam, "=");
		String value = substringAfter(queryParam, "=");
		try {
			value = URLEncoder.encode(value, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
		return key + "=" + value;
	}

	public static void overwriteFile(String fileName, String contents) throws IOException {
		File file = new File(fileName);
		overwriteFile(file, contents);
	}

	public static void overwriteFile(File file, String contents) throws IOException {
		FileUtils.writeStringToFile(file, contents, "UTF-8", false);
	}

	public static String toJsonPretty(Object obj) {
		Gson gson = new GsonBuilder().setPrettyPrinting().create();
		return gson.toJson(obj);
	}

	public static List<String> loadSearchList(String filename) {
		List<String> lines;
		try {
			lines = FileUtils.readLines(new File(filename));
			lines = lines.stream().filter(s -> isNotBlank(s) && !s.startsWith(";")).collect(toList());
			return lines;
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	public static void sleep(long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			logger.error(e.getMessage(), e);
			throw new RuntimeException(e);
		}
	}
}

import static java.lang.System.lineSeparator;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static qic.util.Config.MANUAL_AUTO_VERIFY;
import static qic.util.Config.MANUAL_SEARCH_BLACKLIST;
import static qic.util.Config.getBooleanProperty;
import static qic.util.DurianUtils.notBlacklisted;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qic.Command;
import qic.Main;
import qic.SearchPageScraper.SearchResultItem;
import qic.ui.extra.Worker;
import qic.util.Config;
import qic.util.SwingUtil;
import qic.util.Util;

/*
 * Copyright (C) 2015 thirdy
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package qic.ui;






/**
 * @author thirdy
 *
 */
public class ManualPanel extends JPanel {
	private static final String MANUAL_TXT_FILENAME = "manual.txt";

	private static final long serialVersionUID = 1L;
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());

	private JList<String> searchJList = new JList<>();
	private DefaultListModel<String> searchJListModel = new DefaultListModel<>();

	private JSplitPane splitPane;

	private SearchResultTable table = new SearchResultTable();

	@SuppressWarnings("serial")
	public ManualPanel(Main main) {
		super(new BorderLayout(5, 5));
		
		table.setDoubleBuffered(true);

		JTextField searchTf = new JTextField(100);
		JButton runBtn = new JButton("Run");
		runBtn.setPreferredSize(new Dimension(200, 10));
		JLabel invalidTermsLblLbl = new JLabel();
		invalidTermsLblLbl.setFont(invalidTermsLblLbl.getFont().deriveFont(Font.BOLD));
		JLabel invalidTermsLbl = new JLabel();
		invalidTermsLbl.setForeground(Color.RED);
		JPanel northPanel = new JPanel();
		northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.X_AXIS));
		JLabel searchLbl = new JLabel(" Search: ");
		searchLbl.setFont(searchLbl.getFont().deriveFont(Font.BOLD));
		northPanel.add(searchLbl);
		northPanel.add(searchTf);
		northPanel.add(invalidTermsLblLbl);
		northPanel.add(invalidTermsLbl);
		northPanel.add(runBtn);
		this.add(northPanel, BorderLayout.NORTH);
		
		List<String> searchList = Util.loadSearchList(MANUAL_TXT_FILENAME);
		searchList.stream().forEach(searchJListModel::addElement);
		searchJList.setModel(searchJListModel);
		
		searchJList.addListSelectionListener(e -> {
			if (e.getValueIsAdjusting()) {
				searchTf.setText(trimToEmpty(searchJList.getSelectedValue()));
			}
		});
		searchJList.addMouseListener(new MouseAdapter() {
		    public void mouseClicked(MouseEvent evt) {
		        if (evt.getClickCount() == 2) {
		        	int index = searchJList.locationToIndex(evt.getPoint());
		            if (index != -1) {
		            	String search = trimToEmpty(searchJListModel.getElementAt(index));
						searchTf.setText(search);
						runBtn.doClick();
					}
		        }
		    }
		});
		
		searchJList.getInputMap().put(KeyStroke.getKeyStroke("DELETE"),
                "doSomething");
		searchJList.getActionMap().put("doSomething", new AbstractAction() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				int selectedIndex = searchJList.getSelectedIndex();
 				if (selectedIndex != -1) {
 					searchJListModel.remove(selectedIndex);
 				}				
			}
		});
		
		ActionListener runCommand = e -> {
			String tfText = searchTf.getText().trim();
			if (!tfText.isEmpty()) {

				Worker<Command> worker = new Worker<Command>(
