<?php

class ddg{
	
	public function __construct(){
		
		include "lib/backend.php";
		$this->backend = new backend("ddg");
		
		include "lib/fuckhtml.php";
		$this->fuckhtml = new fuckhtml();
	}
	
	/*
		curl functions
	*/
	private const req_web = 0;
	private const req_xhr = 1;
	
	private function get($proxy, $url, $get = [], $reqtype = self::req_web){
		
		$curlproc = curl_init();
		
		if($get !== []){
			$get = http_build_query($get);
			$url .= "?" . $get;
		}
		
		curl_setopt($curlproc, CURLOPT_URL, $url);
		
		// http2 bypass
		curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
		
		switch($reqtype){
			case self::req_web:
				$headers =
					["User-Agent: " . config::USER_AGENT,
					"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
					"Accept-Language: en-US,en;q=0.5",
					"Accept-Encoding: gzip",
					"DNT: 1",
					"Sec-GPC: 1",
					"Connection: keep-alive",
					"Upgrade-Insecure-Requests: 1",
					"Sec-Fetch-Dest: document",
					"Sec-Fetch-Mode: navigate",
					"Sec-Fetch-Site: same-origin",
					"Sec-Fetch-User: ?1",
					"Priority: u=0, i",
					"TE: trailers"];
				break;
			
			case self::req_xhr:
				$headers =
					["User-Agent: " . config::USER_AGENT,
					"Accept: */*",
					"Accept-Language: en-US,en;q=0.5",
					"Accept-Encoding: gzip",
					"Referer: https://duckduckgo.com/",
					"DNT: 1",
					"Sec-GPC: 1",
					"Connection: keep-alive",
					"Sec-Fetch-Dest: script",
					"Sec-Fetch-Mode: no-cors",
					"Sec-Fetch-Site: same-site",
					"Priority: u=1"];
				break;
		}
		
		$this->backend->assign_proxy($curlproc, $proxy);
		
		curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
		curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
		
		curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
		curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
		curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
		
		$data = curl_exec($curlproc);
		
		if(curl_errno($curlproc)){
			throw new Exception(curl_error($curlproc));
		}
		
		curl_close($curlproc);
		return $data;
	}
	
	public function getfilters($pagetype){
		
		$base = [
			"country" => [
				"display" => "Country",
				"option" => [
					"us-en" => "US (English)",
					"ar-es" => "Argentina",
					"au-en" => "Australia",
					"at-de" => "Austria",
					"be-fr" => "Belgium (fr)",
					"be-nl" => "Belgium (nl)",
					"br-pt" => "Brazil",
					"bg-bg" => "Bulgaria",
					"ca-en" => "Canada (en)",
					"ca-fr" => "Canada (fr)",
					"ct-ca" => "Catalonia",
					"cl-es" => "Chile",
					"cn-zh" => "China",
					"co-es" => "Colombia",
					"hr-hr" => "Croatia",
					"cz-cs" => "Czech Republic",
					"dk-da" => "Denmark",
					"ee-et" => "Estonia",
					"fi-fi" => "Finland",
					"fr-fr" => "France",
					"de-de" => "Germany",
					"gr-el" => "Greece",
					"hk-tzh" => "Hong Kong",
					"hu-hu" => "Hungary",
					"in-en" => "India (en)",
					"id-en" => "Indonesia (en)",
					"ie-en" => "Ireland",
					"il-en" => "Israel (en)",
					"it-it" => "Italy",
					"jp-jp" => "Japan",
					"kr-kr" => "Korea",
					"lv-lv" => "Latvia",
					"lt-lt" => "Lithuania",
					"my-en" => "Malaysia (en)",
					"mx-es" => "Mexico",
					"nl-nl" => "Netherlands",
					"nz-en" => "New Zealand",
					"no-no" => "Norway",
					"pk-en" => "Pakistan (en)",
					"pe-es" => "Peru",
					"ph-en" => "Philippines (en)",
					"pl-pl" => "Poland",
					"pt-pt" => "Portugal",
					"ro-ro" => "Romania",
					"ru-ru" => "Russia",
					"xa-ar" => "Saudi Arabia",
					"sg-en" => "Singapore",
					"sk-sk" => "Slovakia",
					"sl-sl" => "Slovenia",
					"za-en" => "South Africa",
					"es-ca" => "Spain (ca)",
					"es-es" => "Spain (es)",
					"se-sv" => "Sweden",
					"ch-de" => "Switzerland (de)",
					"ch-fr" => "Switzerland (fr)",
					"tw-tzh" => "Taiwan",
					"th-en" => "Thailand (en)",
					"tr-tr" => "Turkey",
					"us-es" => "US (Spanish)",
					"ua-uk" => "Ukraine",
					"uk-en" => "United Kingdom",
					"vn-en" => "Vietnam (en)"
				]
			]
		];
		
		switch($pagetype){
			
			case "web":
				$base["country"]["option"] =
					array_merge(["any" => "All Regions"], $base["country"]["option"]);
				
				return array_merge($base,
					[
						"nsfw" => [
							"display" => "NSFW",
							"option" => [
								"yes" => "Yes",
								"maybe" => "Maybe",
								"no" => "No"
							]
						],
						"newer" => [
							"display" => "Newer than",
							"option" => "_DATE"
						],
						"older" => [
							"display" => "Older than",
							"option" => "_DATE"
						],
						"extendedsearch" => [
							// undefined display
							"option" => [
								"yes" => "Yes",
								"no" => "No",
							]
						]
					]
				);
				break;
			
			case "images":
				return array_merge($base,
					[
						"nsfw" => [
							"display" => "NSFW",
							"option" => [
								"yes" => "Yes",
								"no" => "No"
							]
						],
						"date" => [
							"display" => "Time posted",
							"option" => [
								"any" => "Any time",
								"Day" => "Past day",
								"Week" => "Past week",
								"Month" => "Past month"
							]
						],
						"size" => [
							"display" => "Size",
							"option" => [
								"any" => "Any size",
								"Small" => "Small",
								"Medium" => "Medium",
								"Large" => "Large",
								"Wallpaper" => "Wallpaper"
							]
						],
						"color" => [
							"display" => "Colors",
							"option" => [
								"any" => "All colors",
								"Monochrome" => "Black and white",
								"Red" => "Red",
								"Orange" => "Orange",
								"Yellow" => "Yellow",
								"Green" => "Green",
								"Blue" => "Blue",
								"Purple" => "Purple",
								"Pink" => "Pink",
								"Brown" => "Brown",
								"Black" => "Black",
								"Gray" => "Gray",
								"Teal" => "Teal",
								"White" => "White"
							]
						],
						"type" => [
							"display" => "Type",
							"option" => [
								"any" => "All types",
								"photo" => "Photograph",
								"clipart" => "Clipart",
								"gif" => "Animated GIF",
								"transparent" => "Transparent"
							]
						],
						"layout" => [
							"display" => "Layout",
							"option" => [
								"any" => "All layouts",
								"Square" => "Square",
								"Tall" => "Tall",
								"Wide" => "Wide"
							]
						],
						"license" => [
							"display" => "License",
							"option" => [
								"any" => "All licenses",
								"Any" => "All Creative Commons",
								"Public" => "Public domain",
								"Share" => "Free to Share and Use",
								"ShareCommercially" => "Free to Share and Use Commercially",
								"Modify" => "Free to Modify, Share, and Use",
								"ModifyCommercially" => "Free to Modify, Share, and Use Commercially"
							]
						]
					]
				);
				break;
			
			case "videos":
				return array_merge($base,
					[
						"nsfw" => [
							"display" => "NSFW",
							"option" => [
								"yes" => "Yes",
								"no" => "No"
							]
						],
						"date" => [
							"display" => "Time fetched",
							"option" => [
								"any" => "Any time",
								"d" => "Past day",
								"w" => "Past week",
								"m" => "Past month"
							]
						],
						"resolution" => [ //videoDefinition
							"display" => "Resolution",
							"option" => [
								"any" => "Any resolution",
								"high" => "High definition",
								"standard" => "Standard definition"
							]
						],
						"duration" => [ // videoDuration
							"display" => "Duration",
							"option" => [
								"any" => "Any duration",
								"short" => "Short (>5min)",
								"medium" => "Medium (5-20min)",
								"long" => "Long (<20min)"
							]
						],
						"license" => [
							"display" => "License",
							"option" => [
								"any" => "Any license",
								"creativeCommon" => "Creative Commons",
								"youtube" => "YouTube Standard"
							]
						]
					]
				);
				break;
				
			case "news":
				return array_merge($base,
					[
						"nsfw" => [
							"display" => "NSFW",
							"option" => [
								"yes" => "Yes",
								"maybe" => "Maybe",
								"no" => "No"
							]
						],
						"date" => [
							"display" => "Time posted",
							"option" => [
								"any" => "Any time",
								"d" => "Past day",
								"w" => "Past week",
								"m" => "Past month"
							]
						]
					]
				);
				break;
		}
	}
	
	public function web($get){
		
		$out = [
			"status" => "ok",
			"spelling" => [
				"type" => "no_correction",
				"using" => null,
				"correction" => null
			],
			"npt" => null,
			"answer" => [],
			"web" => [],
			"image" => [],
			"video" => [],
			"news" => [],
			"related" => []
		];
		
		if($get["npt"]){
			
			[$js_link, $proxy] = $this->backend->get($get["npt"], "web");
			$js_link = "https://links.duckduckgo.com" . $js_link;
			
			$html = "";
			$get["extendedsearch"] = "no";
			
		}else{
			if(strlen($get["s"]) === 0){
				
				throw new Exception("Search term is empty!");
			}
			
			$proxy = $this->backend->get_ip();
			
			// generate filters
			$get_filters = [
				"q" => $get["s"]
			];
			
			if($get["country"] == "any"){
				
				$get_filters["kl"] = "wt-wt";
			}else{
				
				$get_filters["kl"] = $get["country"];
			}
			
			switch($get["nsfw"]){
				
				case "yes": $get_filters["kp"] = "-2"; break;
				case "maybe": $get_filters["kp"] = "-1"; break;
				case "no": $get_filters["kp"] = "1"; break;
			}
			
			$df = true;
			
			if($get["newer"] === false){
				
				if($get["older"] !== false){
					
					$start = 36000;
					$end = $get["older"];
				}else{
					
					$df = false;
				}
			}else{
				
				$start = $get["newer"];
				
				if($get["older"] !== false){
					
					$end = $get["older"];
				}else{
					
					$end = time();
				}
			}
			
			if($df === true){
				$get_filters["df"] = date("Y-m-d", $start) . ".." . date("Y-m-d", $end);
			}
			
			//
			// Get HTML
			//
			try{
				$html = $this->get(
					$proxy,
					"https://duckduckgo.com/",
					$get_filters
				);
			}catch(Exception $e){
				
				throw new Exception("Failed to fetch search page");
			}
			
			$this->fuckhtml->load($html);
			
			$script =
				$this->fuckhtml
				->getElementById(
					"deep_preload_link",
					"link"
				);
			
			if(
				$script === null ||
				!isset($script["attributes"]["href"])
			){
				
				throw new Exception("Failed to grep d.js");
			}
			
			$js_link =
				$this->fuckhtml
				->getTextContent(
					$script["attributes"]["href"]
				);
		}
		
		//
		// Get d.js
		//
		try{
			$js = $this->get(
				$proxy,
				$js_link,
				[],
				ddg::req_xhr
			);
			
		}catch(Exception $e){
			
			throw new Exception("Failed to fetch d.js");
		}
		
		//echo htmlspecialchars($js);
		
		$js_tmp =
			preg_split(
				'/DDG\.pageLayout\.load\(\s*\'d\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) <= 1){
			
			throw new Exception("Failed to grep pageLayout(d)");
		}
		
		$json =
			json_decode(
				$this->fuckhtml
				->extract_json(
					$js_tmp[1]
				),
				true
			);
		
		if($json === null){
			
			throw new Exception("Failed to decode JSON");
		}
		
		//
		// Get search results + NPT token
		//
		foreach($json as $item){
			
			if(isset($item["c"])){
				
				$table = [];
				
				// get youtube video information
				if(isset($item["video"]["thumbnail_url_template"])){
					
					$thumb =
						[
							"ratio" => "16:9",
							"url" => $this->bingimg($item["video"]["thumbnail_url_template"])
						];
				}else{
					
					$thumb =
						[
							"ratio" => null,
							"url" => null
						];
				}
				
				// get table items
				if(isset($item["rf"])){
					
					foreach($item["rf"] as $hint){
						
						if(
							!isset($hint["label"]["text"]) ||
							!isset($hint["items"][0]["text"])
						){
							
							continue;
						}
						
						$text = [];
						
						foreach($hint["items"] as $text_part){
							
							$text[] = $text_part["text"];
						}
						
						$text = implode(", ", $text);
						
						if(is_numeric($text)){
							
							$text = number_format((string)$text);
						}
						
						$table[$hint["label"]["text"]] = $text;
					}
				}
				
				// get ratings
				if(isset($item["ar"])){
					
					foreach($item["ar"] as $rating){
						
						if(
							isset($rating["aggregateRating"]["bestRating"]) &&
							isset($rating["aggregateRating"]["ratingValue"])
						){
							
							$text = $rating["aggregateRating"]["ratingValue"] . "/" . $rating["aggregateRating"]["bestRating"];
							
							if(isset($rating["aggregateRating"]["reviewCount"])){
								
								$text .= " (" . number_format($rating["aggregateRating"]["reviewCount"]) . " votes)";
							}
							
							$table["Rating"] = $text;
						}
					}
				}
				
				// get sublinks
				$sublinks = [];
				
				if(isset($item["l"])){
					
					foreach($item["l"] as $sublink){
						
						$sublinks[] = [
							"title" => $this->titledots($sublink["text"]),
							"description" => $this->titledots($sublink["snippet"]),
							"url" => $sublink["targetUrl"],
							"date" => null
						];
					}
				}
				
				$title =
					$this->titledots(
						$this->fuckhtml
						->getTextContent(
							$item["t"]
						)
					);
				
				if(
					$title == "EOF" &&
					strpos(
						$item["c"],
						"google"
					)
				){
					
					continue;
				}
				
				// parse search result
				$out["web"][] = [
					"title" =>
						$this->titledots(
							$this->fuckhtml
							->getTextContent(
								$item["t"]
							)
						),
					"description" =>
						isset($item["a"]) ?
						$this->titledots(
							$this->fuckhtml
							->getTextContent(
								$item["a"]
							)
						) : null,
					"url" => $this->unshiturl($item["c"]),
					"date" =>
						isset($item["e"]) ?
						strtotime($item["e"]) : null,
					"type" => "web",
					"thumb" => $thumb,
					"sublink" => $sublinks,
					"table" => $table
				];
				continue;
			}
			
			if(isset($item["n"])){
				
				// get NPT
				$out["npt"] =
					$this->backend->store(
						$item["n"],
						"web",
						$proxy
					);
				continue;
			}
		}
		
		//
		// Get spelling
		//
		$js_tmp =
			preg_split(
				'/DDG\.page\.showMessage\(\s*\'spelling\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				// parse spelling
				// qc=2: including
				
				switch((int)$json["qc"]){
					
					case 2:
						$type = "including";
						break;
					
					default:
						$type = "not_many";
						break;
				}
				
				$out["spelling"] = [
					"type" => $type,
					"using" =>
						$this->fuckhtml
						->getTextContent(
							$json["suggestion"]
						),
					"correction" => $json["recourseText"]
				];
			}
		}
		
		//
		// Get images
		//
		$js_tmp =
			preg_split(
				'/DDG\.duckbar\.load\(\s*\'images\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				foreach($json["results"] as $image){
					
					$ratio = $this->bingratio((int)$image["width"], (int)$image["height"]);
					
					$out["image"][] = [
						"title" => $image["title"],
						"source" =>	[
							[
								"url" => $image["image"],
								"width" => (int)$image["width"],
								"height" => (int)$image["height"]
							],
							[
								"url" => $this->bingimg($image["thumbnail"]),
								"width" => $ratio[0],
								"height" => $ratio[1]
							]
						],
						"url" => $this->unshiturl($image["url"])
					];
				}
			}
		}
		
		//
		// Get videos
		//
		$js_tmp =
			preg_split(
				'/DDG\.duckbar\.load\(\s*\'videos\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				foreach($json["results"] as $video){
					
					$thumb = [
						"ratio" => null,
						"url" => null
					];
					
					foreach(["large", "medium", "small"] as $contender){
						
						if(isset($video["images"][$contender])){
							
							$thumb = [
								"ratio" => "16:9",
								"url" => $this->bingimg($video["images"][$contender])
							];
							break;
						}
					}
					
					$out["video"][] = [
						"title" => $this->titledots($video["title"]),
						"description" =>
							$video["description"] != "" ?
							$this->titledots($video["description"]) : null,
						"date" => 
							isset($video["published"]) ?
							strtotime($video["published"]) : null,
						"duration" =>
							$video["duration"] != "" ?
							$this->hms2int($video["duration"]) : null,
						"views" =>
							isset($video["statistics"]["viewCount"]) ?
							(int)$video["statistics"]["viewCount"] : null,
						"thumb" => $thumb,
						"url" => $this->unshiturl($video["content"])
					];
				}
			}
		}
		
		//
		// Get news
		//
		$js_tmp =
			preg_split(
				'/DDG\.duckbar\.load\(\s*\'news\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				foreach($json["results"] as $news){
					
					if(isset($news["image"])){
						
						$thumb = [
							"ratio" => "16:9",
							"url" => $news["image"]
						];
					}else{
						
						$thumb = [
							"ratio" => null,
							"url" => null
						];
					}
					
					$out["news"][] = [
						"title" => $news["title"],
						"description" =>
							$this->fuckhtml
							->getTextContent(
								$news["excerpt"]
							),
						"date" => (int)$news["date"],
						"thumb" => $thumb,
						"url" => $news["url"]
					];
				}
			}
		}
		
		//
		// Get related searches
		//
		$js_tmp =
			preg_split(
				'/DDG\.duckbar\.loadModule\(\s*\'related_searches\'\s*,\s*/',
				$js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				foreach($json["results"] as $related){
					
					$out["related"][] = $related["text"];
				}
			}
		}
		
		//
		// Get instant answers
		//
		$js_tmp =
			preg_split(
				'/DDG\.duckbar\.add\(\s*/',
				$html . $js,
				2
			);
		
		if(count($js_tmp) > 1){
			
			$json =
				json_decode(
					$this->fuckhtml
					->extract_json(
						$js_tmp[1]
					),
					true
				);
			
			if($json !== null){
				
				$json = $json["data"];
				$table = [];
				$sublinks = [];
				$description = [];
				
				// get official website
				if(
					isset($json["OfficialWebsite"]) &&
					$json["OfficialWebsite"] !== null
				){
					
					$sublinks["Website"] = $json["OfficialWebsite"];
				}
				
				// get sublinks & table elements
				if(isset($json["Infobox"]["content"])){
					foreach($json["Infobox"]["content"] as $info){
						
						if($info["data_type"] == "string"){
							
							// add table element
							$table[$info["label"]] = $info["value"];
							continue;
						}
						
						if($info["data_type"] == "wd_description"){
							
							$description[] = [
								"type" => "quote",
								"value" => $info["value"]
							];
							continue;
						}
						
						// add sublink
						switch($info["data_type"]){
							
							case "official_site":
							case "official_website":
								$type = "Website";
								break;
							
							case "wikipedia": $type = "Wikipedia"; break;
							case "itunes": $type = "iTunes"; break;
							case "amazon": $type = "Amazon"; break;
							
							case "imdb_title_id":
							case "imdb_id":
							case "imdb_name_id":
								$type = "IMDb";
								$delim = substr($info["value"], 0, 2);
								
								if($delim == "nm"){
									
									$prefix = "https://www.imdb.com/name/";
								}elseif($delim == "tt"){
									
									$prefix = "https://www.imdb.com/title/";
								}elseif($delim == "co"){
									
									$prefix = "https://www.imdb.com/search/title/?companies=";
								}else{
									
									$prefix = "https://www.imdb.com/title/";
								}
								break;

							case "imdb_name_id": $prefix = "https://www.imdb.com/name/"; $type = "IMDb"; break;
							case "twitter_profile": $prefix = "https://twitter.com/"; $type = "Twitter"; break;
							case "instagram_profile": $prefix = "https://instagram.com/"; $type = "Instagram"; break;
							case "facebook_profile": $prefix = "https://facebook.com/"; $type = "Facebook"; break;
							case "spotify_artist_id": $prefix = "https://open.spotify.com/artist/"; $type = "Spotify"; break;
							case "itunes_artist_id": $prefix = "https://music.apple.com/us/artist/"; $type = "iTunes"; break;
							case "rotten_tomatoes": $prefix = "https://rottentomatoes.com/"; $type = "Rotten Tomatoes"; break;
							case "youtube_channel": $prefix = "https://youtube.com/channel/"; $type = "YouTube"; break;
							case "soundcloud_id": $prefix = "https://soundcloud.com/"; $type = "SoundCloud"; break;
							
							default:							
								$prefix = null;
								$type = false;
						}
						
						if($type !== false){
							
							if($prefix === null){
								
								$sublinks[$type] = $info["value"];
							}else{
								
								$sublinks[$type] = $prefix . $info["value"];
							}
						}
					}
				}
				
				if(isset($json["Abstract"])){
					
					$description[] =
						[
							"type" => "text",
							"value" => $json["Abstract"]
						];
				}
				
				$out["answer"][] = [
					"title" => $json["Heading"],
					"description" => $description,
					"url" => $json["AbstractURL"],
					"thumb" =>
						(isset($json["Image"]) && $json["Image"]) !== null ?
						"https://duckduckgo.com" . $json["Image"] : null,
					"table" => $table,
					"sublink" => $sublinks
				];
			}
		}
		
		if($get["extendedsearch"] == "no"){
			
			return $out;
		}
		
		//
		// Get wordnik definition
		//
		//nrj('/js/spice/dictionary/definition/create', null, null, null, null, 'dictionary_definition');
		
		preg_match(
			'/nrj\(\s*\'([^\']+)\'/',
			$js,
			$nrj
		);
		
		if(isset($nrj[1])){
			
			$nrj = $nrj[1];
			
			preg_match(
				'/\/js\/spice\/dictionary\/definition\/([^\/]+)/',
				$nrj,
				$word
			);
			
			if(isset($word[1])){
				
				$word = $word[1];
				
				// found wordnik definition & word
				try{
					$nik =
						$this->get(
							$proxy,
							"https://duckduckgo.com/js/spice/dictionary/definition/" . $word,
							[],
							ddg::req_xhr
						);
					
				}catch(Exception $e){
					
					// fail gracefully
					return $out;
				}
				
				// remove javascript
				$js_tmp =
					preg_split(
						'/ddg_spice_dictionary_definition\(\s*/',
						$nik,
						2
					);
				
				if(count($js_tmp) > 1){
					
					$nik =
						json_decode(
							$this->fuckhtml
							->extract_json(
								$js_tmp[1]
							),
							true
						);
				}
				
				if($nik === null){
					
					return $out;
				}
				
				$answer_cat = [];
				$answer = [];
				
				foreach($nik as $snippet){
					
					if(!isset($snippet["partOfSpeech"])){ continue; }
					
					$push = [];
					
					// add text snippet
					if(isset($snippet["text"])){
						
						$push[] = [
							"type" => "text",
							"value" =>
								$this->fuckhtml
								->getTextContent(
									$snippet["text"]
								)
						];
					}
					
					// add example uses
					if(isset($snippet["exampleUses"])){
						
						foreach($snippet["exampleUses"] as $example){
							
							$push[] = [
								"type" => "quote",
								"value" => "\"" .
									$this->fuckhtml
									->getTextContent(
										$example["text"]
									) . "\""
							];
						}
					}
					
					// add citations
					if(isset($snippet["citations"])){
						
						foreach($snippet["citations"] as $citation){
							
							if(!isset($citation["cite"])){ continue; }
							
							$text =
								$this->fuckhtml
								->getTextContent(
									$citation["cite"]
								);
							
							if(isset($citation["source"])){
								
								$text .=
									" - " .
									$this->fuckhtml
									->getTextContent(
										$citation["source"]
									);
							}
							
							$push[] = [
								"type" => "quote",
								"value" => $text
							];
						}
					}
					
					// add related words
					if(isset($snippet["relatedWords"])){
						
						$relations = [];
						
						foreach($snippet["relatedWords"] as $related){
							
							$words = [];
							foreach($related["words"] as $wrd){
								
								$words[] =
									$this->fuckhtml
									->getTextContent(
										$wrd
									);
							}
							
							if(
								count($words) !== 0 &&
								isset($related["relationshipType"])
							){
								
								$relations[ucfirst($related["relationshipType"]) . "s"] =
									implode(", ", $words);
							}
						}
						
						foreach($relations as $relation_title => $relation_content){
							
							$push[] = [
								"type" => "quote",
								"value" => $relation_title . ": " . $relation_content
							];
						}
					}
					
					
					if(count($push) !== 0){
						
						// push data to answer_cat
						if(!isset($answer_cat[$snippet["partOfSpeech"]])){
							
							$answer_cat[$snippet["partOfSpeech"]] = [];
						}
						
						$answer_cat[$snippet["partOfSpeech"]] =
							array_merge(
								$answer_cat[$snippet["partOfSpeech"]],
								$push
							);
					}
				}
				
				foreach($answer_cat as $answer_title => $answer_content){
					
					$i = 0;
					$answer[] = [
						"type" => "title",
						"value" => $answer_title
					];
					
					$old_type = $answer[count($answer) - 1]["type"];
					
					foreach($answer_content as $ans){
						
						if(
							$ans["type"] == "text" &&
							$old_type == "text"
						){
							
							$i++;
							$c = count($answer) - 1;
							
							// append text to existing textfield
							$answer[$c] = [
								"type" => "text",
								"value" => $answer[$c]["value"] . "\n" . $i . ". " . $ans["value"]
							];
							
						}elseif($ans["type"] == "text"){
							
							$i++;
							$answer[] = [
								"type" => "text",
								"value" => $i . ". " . $ans["value"]
							];
						}else{
							
							// append normally
							$answer[] = $ans;
						}
						
						$old_type = $ans["type"];
					}
				}
				
				// yeah.. sometimes duckduckgo doesnt give us a definition back
				if(count($answer) !== 0){
					
					$out["answer"][] = [
						"title" => ucfirst($word),
						"description" => $answer,
						"url" => "https://www.wordnik.com/words/" . $word,
						"thumb" => null,
						"table" => [],
						"sublink" => []
					];
				}
			}
		}
		
		return $out;
	}
	
	public function image($get){
		
		if($get["npt"]){
			
			[$js_link, $proxy] = $this->backend->get($get["npt"], "images");
			
		}else{
			if(strlen($get["s"]) === 0){
				
				throw new Exception("Search term is empty!");
			}
			
			$proxy = $this->backend->get_ip();
			
			$filters = [];
			
			if($get["date"] != "any"){ $filters[] = "time:{$get["date"]}"; }
			if($get["size"] != "any"){ $filters[] = "size:{$get["size"]}"; }
			if($get["color"] != "any"){ $filters[] = "color:{$get["color"]}"; }
			if($get["type"] != "any"){ $filters[] = "type:{$get["type"]}"; }
			if($get["layout"] != "any"){ $filters[] = "layout:{$get["layout"]}"; }
			if($get["license"] != "any"){ $filters[] = "license:{$get["license"]}"; }
			
			$filters = implode(",", $filters);
			
			$get_filters = [
				"q" => $get["s"],
				"iax" => "images",
				"ia" => "images"
			];
			
			if($filters != ""){
				
				$get_filters["iaf"] = $filters;
			}
			
			$nsfw = $get["nsfw"] == "yes" ? "-2" : "-1";
			$get_filters["kp"] = $nsfw;
			
			try{
				
				$html = $this->get(
					$proxy,
					"https://duckduckgo.com",
					$get_filters,
					ddg::req_web
				);
			}catch(Exception $err){
				
				throw new Exception("Failed to fetch search page");
			}
			
			preg_match(
				'/vqd="([0-9-]+)"/',
				$html,
				$vqd
			);
			
			if(!isset($vqd[1])){
				
				throw new Exception("Failed to grep VQD token");
			}
			
			$js_link =
				"i.js?" .
				http_build_query([
					"l" => $get["country"],
					"o" => "json",
					"q" => $get["s"],
					"vqd" => $vqd[1],
					"f" => $filters,
					"p" => $nsfw
				]);
		}
		
		try{
			
			$json =
				$this->get(
					$proxy,
					"https://duckduckgo.com/" . $js_link,
					[],
					ddg::req_xhr
				);
		}catch(Exception $error){
			
			throw new Exception("Failed to get i.js");
		}
		
		$json = json_decode($json, true);
		
		if($json === null){
			
			throw new Exception("Failed to decode JSON");
		}
		
		$out = [
			"status" => "ok",
			"npt" => null,
			"image" => []
		];
		
		if(!isset($json["results"])){
			
			return $out;
		}
		
		// get npt
		if(
			isset($json["next"]) &&
			$json["next"] !== null
		){
			
			$vqd = null;
			
			if(isset($vqd[1])){
				
				$vqd = $vqd[1];
			}else{
				
				$vqd = array_values($json["vqd"]);
				
				if(count($vqd) > 0){
					
					$vqd = $vqd[0];
				}
			}
			
			if($vqd !== null){
				
				$out["npt"] =
					$this->backend->store(
						$json["next"] . "&vqd=" . $vqd,
						"images",
						$proxy
					);
			}
		}
		
		// get images
		foreach($json["results"] as $image){
			
			$ratio =
				$this->bingratio(
					(int)$image["width"],
					(int)$image["height"]
				);
			
			$out["image"][] = [
				"title" => $this->titledots($image["title"]),
				"source" => [
					[
						"url" => $image["image"],
						"width" => (int)$image["width"],
						"height" => (int)$image["height"]
					],
					[
						"url" => $this->bingimg($image["thumbnail"]),
						"width" => $ratio[0],
						"height" => $ratio[1]
					]
				],
				"url" => $this->unshiturl($image["url"])
			];
		}
		
		return $out;
	}
	
	public function video($get){
		
		if($get["npt"]){
			
			[$js_link, $proxy] = $this->backend->get($get["npt"], "videos");
			
		}else{
			if(strlen($get["s"]) === 0){
				
				throw new Exception("Search term is empty!");
			}
			
			$proxy = $this->backend->get_ip();
			
			$get_filters = [
				"q" => $get["s"],
				"iax" => "videos",
				"ia" => "videos"
			];
			
			$nsfw = $get["nsfw"] == "yes" ? "-2" : "-1";
			$get_filters["kp"] = $nsfw;
			
			$filters = [];
			
			if($get["date"] != "any"){ $filters[] = "publishedAfter:{$date}"; }
			if($get["resolution"] != "any"){ $filters[] = "videoDefinition:{$resolution}"; }
			if($get["duration"] != "any"){ $filters[] = "videoDuration:{$duration}"; }
			if($get["license"] != "any"){ $filters[] = "videoLicense:{$license}"; }
			
			$filters = implode(",", $filters);
			
			if($filters != ""){
				
				$get_filters["iaf"] = $filters;
			}
			
			try{
				
				$html =
					$this->get(
						$proxy,
						"https://duckduckgo.com/",
						$get_filters,
						ddg::req_web
					);
			}catch(Exception $error){
				
				throw new Exception("Failed to fetch search page");
			}
			
			preg_match(
				'/vqd="([0-9-]+)"/',
				$html,
				$vqd
			);
			
			if(!isset($vqd[1])){
				
				throw new Exception("Failed to grep VQD token");
			}
			
			$js_link =
				"v.js?" .
				http_build_query([
					"l" => $get["country"],
					"o" => "json",
					"sr" => "1",
					"q" => $get["s"],
					"vqd" => $vqd[1],
					"f" => $filters,
					"p" => $nsfw
				]);
		}
		
		try{
			
			$json =
				$this->get(
					$proxy,
					"https://duckduckgo.com/" . $js_link,
					[],
					ddg::req_xhr
				);
		}catch(Exception $error){
			
			throw new Exception("Failed to fetch JSON");
		}
		
		$json = json_decode($json, true);
		
		if($json === null){
			
			throw new Exception("Failed to decode JSON");
		}
		
		$out = [
			"status" => "ok",
			"npt" => null,
			"video" => [],
			"author" => [],
			"livestream" => [],
			"playlist" => [],
			"reel" => []
		];
		
		if(!isset($json["results"])){
			
			return $out;
		}
		
		// get NPT
		if(
			isset($json["next"]) &&
			$json["next"] !== null
		){
			
			$out["npt"] =
				$this->backend->store(
					$json["next"],
					"videos",
					$proxy
				);
		}
		
		foreach($json["results"] as $video){
			
			$thumb = [
				"ratio" => null,
				"url" => null
			];
			
			foreach(["large", "medium", "small"] as $contender){
				
				if(isset($video["images"][$contender])){
					
					$thumb = [
						"ratio" => "16:9",
						"url" => $this->bingimg($video["images"][$contender])
					];
					break;
				}
			}
			
			$out["video"][] = [
				"title" => $this->titledots($video["title"]),
				"description" => $this->titledots($video["description"]),
				"author" => [
					"name" =>
						(
							isset($video["uploader"]) &&
							$video["uploader"] != ""
						) ?
						$video["uploader"] : null,
					"url" => null,
					"avatar" => null
				],
				"date" =>
					(
						isset($video["published"]) &&
						$video["published"] != ""
					) ?
					strtotime($video["published"]) : null,
				"duration" =>
					(
						isset($video["duration"]) &&
						$video["duration"] != ""
					) ?
					$this->hms2int($video["duration"]) : null,
				"views" =>
					isset($video["statistics"]["viewCount"]) ?
					(int)$video["statistics"]["viewCount"] : null,
				"thumb" => $thumb,
				"url" => $this->unshiturl($video["content"])
			];
		}
		
		return $out;
	}
	
	public function news($get){
		
		if($get["npt"]){
			
			[$js_link, $proxy] = $this->backend->get($get["npt"], "news");
			
		}else{
			if(strlen($get["s"]) === 0){
				
				throw new Exception("Search term is empty!");
			}
			
			$proxy = $this->backend->get_ip();
			
			$get_filters = [
				"q" => $get["s"],
				"iar" => "news",
				"ia" => "news"
			];
			
			if($get["date"] != "any"){
				
				$date = $get["date"];
				$get_filters["df"] = $date;
			}else{
				
				$date = "";
			}
			
			switch($get["nsfw"]){
				
				case "yes": $get_filters["kp"] = "-2"; break;
				case "maybe": $get_filters["kp"] = "-1"; break;
				case "no": $get_filters["kp"] = "1"; break;
			}
			
			try{
				
				$html =
					$this->get(
						$proxy,
						"https://duckduckgo.com/",
						$get_filters,
						ddg::req_web
					);
			}catch(Exception $error){
				
				throw new Exception("Failed to fetch search page");
			}
			
			preg_match(
				'/vqd="([0-9-]+)"/',
				$html,
				$vqd
			);
			
			if(!isset($vqd[1])){
				
				throw new Exception("Failed to grep VQD token");
			}
			
			$js_link =
				"news.js?" .
				http_build_query([
					"l" => $get["country"],
					"o" => "json",
					"noamp" => "1",
					"m" => "30",
					"q" => $get["s"],
					"vqd" => $vqd[1],
					"p" => $get_filters["kp"],
					"df" => $date,
					"u" => "bing"
				]);
		}
		
		try{
			
			$json =
				$this->get(
					$proxy,
					"https://duckduckgo.com/" . $js_link,
					[],
					ddg::req_xhr
				);
		}catch(Exception $error){
			
			throw new Exception("Failed to fetch JSON");
		}
		
		$json = json_decode($json, true);
		
		if($json === null){
			
			throw new Exception("Failed to decode JSON");
		}
		
		$out = [
			"status" => "ok",
			"npt" => null,
			"news" => []
		];
		
		if(!isset($json["results"])){
			
			return $out;
		}
		
		// get NPT
		if(
			isset($json["next"]) &&
			$json["next"] !== null
		){
			
			$out["npt"] =
				$this->backend->store(
					$json["next"],
					"news",
					$proxy
				);
		}
		
		foreach($json["results"] as $news){
			
			if(
				isset($news["image"]) &&
				$news["image"] != ""
			){
				
				$thumb = [
					"ratio" => "16:9",
					"url" => $news["image"]
				];
			}else{
				
				$thumb = [
					"ratio" => null,
					"url" => null
				];
			}
			
			$out["news"][] = [
				"title" => $news["title"],
				"author" =>
					(
						isset($news["source"]) &&
						$news["source"] != ""
					) ?
					$news["source"] : null,
				"description" =>
					(
						isset($news["excerpt"]) &&
						$news["excerpt"] != ""
					) ?
					$this->fuckhtml
					->getTextContent(
						$news["excerpt"]
					) : null,
				"date" =>
					isset($news["date"]) ?
					(int)$news["date"] : null,
				"thumb" => $thumb,
				"url" => $this->unshiturl($news["url"])
			];
		}
		
		return $out;
	}
	
	private function titledots($title){
		
		$substr = substr($title, -3);
		
		if(
			$substr == "..." ||
			$substr == "…"
		){
						
			return trim(substr($title, 0, -3));
		}
		
		return trim($title);
	}
	
	private function hms2int($time){
		
		$parts = explode(":", $time, 3);
		$time = 0;
		
		if(count($parts) === 3){
			
			// hours
			$time = $time + ((int)$parts[0] * 3600);
			array_shift($parts);
		}
		
		if(count($parts) === 2){
			
			// minutes
			$time = $time + ((int)$parts[0] * 60);
			array_shift($parts);
		}
		
		// seconds
		$time = $time + (int)$parts[0];
		
		return $time;
	}

	
	private function unshiturl($url){
		
		// check for domains w/out first short subdomain (ex: www.)
		
		$domain = parse_url($url, PHP_URL_HOST);
		
		$subdomain = preg_replace(
			'/^[A-z0-9]{1,3}\./',
			"",
			$domain
		);
		
		switch($subdomain){
			case "ebay.com.au":
			case "ebay.at":
			case "ebay.ca":
			case "ebay.fr":
			case "ebay.de":
			case "ebay.com.hk":
			case "ebay.ie":
			case "ebay.it":
			case "ebay.com.my":
			case "ebay.nl":
			case "ebay.ph":
			case "ebay.pl":
			case "ebay.com.sg":
			case "ebay.es":
			case "ebay.ch":
			case "ebay.co.uk":
			case "cafr.ebay.ca":
			case "ebay.com":
			case "community.ebay.com":
			case "pages.ebay.com":
				
				// remove ebay tracking elements
				$old_params = parse_url($url, PHP_URL_QUERY);
				parse_str($old_params, $params);
				
				if(isset($params["mkevt"])){ unset($params["mkevt"]); }
				if(isset($params["mkcid"])){ unset($params["mkcid"]); }
				if(isset($params["mkrid"])){ unset($params["mkrid"]); }
				if(isset($params["campid"])){ unset($params["campid"]); }
				if(isset($params["customid"])){ unset($params["customid"]); }
				if(isset($params["toolid"])){ unset($params["toolid"]); }
				if(isset($params["_sop"])){ unset($params["_sop"]); }
				if(isset($params["_dcat"])){ unset($params["_dcat"]); }
				if(isset($params["epid"])){ unset($params["epid"]); }
				if(isset($params["epid"])){ unset($params["oid"]); }
				
				$params = http_build_query($params);
				
				if(strlen($params) === 0){
					$replace = "\?";
				}else{
					$replace = "";
				}
				
				$url = preg_replace(
					"/" . $replace . preg_quote($old_params, "/") . "$/",
					$params,
					$url
				);
				break;
		}
		
		return $url;
	}
	
	private function bingimg($url){
		
		$parse = parse_url($url);
		parse_str($parse["query"], $parts);
		
		return "https://" . $parse["host"] . "/th?id=" . urlencode($parts["id"]);
	}
	
	private function bingratio($width, $height){
		
		$ratio = [
			474 / $width,
			474 / $height
		];
		
		if($ratio[0] < $ratio[1]){
			
			$ratio = $ratio[0];
		}else{
			
			$ratio = $ratio[1];
		}
		
		return [
			floor($width * $ratio),
			floor($height * $ratio)
		];
	}
}