public class Tuba.Network : GLib.Object {
	public signal void started ();
	public signal void finished ();

	public delegate void ErrorCallback (int32 code, string reason);
	public delegate void SuccessCallback (InputStream in_stream, Soup.MessageHeaders? response_headers = null) throws Error;
	public delegate void NodeCallback (Json.Node node) throws Error;
	public delegate void ObjectCallback (Json.Object node) throws Error;

	public Soup.Session session { get; set; }
	private Soup.Cache cache;
	int requests_processing = 0;

	public void clear_cache () {
		cache.clear ();
		Tuba.Helper.Image.clear_cache ();
	}

	public void flush_cache () {
		this.cache.flush ();
		this.cache.dump ();
		Tuba.Helper.Image.flush_cache ();
	}

	construct {
		cache = new Soup.Cache (
			GLib.Path.build_path (GLib.Path.DIR_SEPARATOR_S, Tuba.cache_path, "soup", "misc"),
			Soup.CacheType.SINGLE_USER
		);
		cache.load ();
		cache.set_max_size (1024 * 1024 * 100);

		session = new Soup.Session.with_options ("max-conns", 64, "max-conns-per-host", 64) {
			user_agent = @"$(Build.NAME)/$(Build.VERSION) libsoup/$(Soup.get_major_version()).$(Soup.get_minor_version()).$(Soup.get_micro_version()) ($(Soup.MAJOR_VERSION).$(Soup.MINOR_VERSION).$(Soup.MICRO_VERSION))" // vala-lint=line-length
		};
		session.add_feature (cache);
		session.request_unqueued.connect (msg => {
			requests_processing--;
			if (requests_processing <= 0)
				finished ();
		});

		app.notify ["proxy"].connect (on_proxy_change);
	}

	private void on_proxy_change () {
		session.set_proxy_resolver (app.proxy);
	}

	public enum ExtraData {
		RESPONSE_HEADERS
	}

	//  public void queue (
	//  	owned Soup.Message msg,
	//  	GLib.Cancellable? cancellable,
	//  	owned SuccessCallback cb,
	//  	owned ErrorCallback? ecb,
	//  	ExtraData? extra_data = null
	//  ) {
	//  	requests_processing++;
	//  	started ();

	//  	debug (@"$(msg.method): $(msg.uri.to_string ())");

	//  	session.send_async.begin (msg, 0, cancellable, (obj, res) => {
	//  		try {
	//  			var in_stream = session.send_async.end (res);

	//  			var status = msg.status_code;
	//  			if (status >= 200 && status < 300) {
	//  				try {
	//  					if (cb != null)
	//  						cb (in_stream, extra_data == ExtraData.RESPONSE_HEADERS ? msg.response_headers : null);
	//  				} catch (Error e) {
	//  					warning (@"Error in session: $(e.message)");
	//  				}
	//  			} else if (status == GLib.IOError.CANCELLED) {
	//  				debug ("Message is cancelled. Ignoring callback invocation.");
	//  			} else {
	//  				if (ecb == null) {
	//  					critical (@"Request \"$(msg.uri.to_string ())\" failed: $status $(msg.reason_phrase)");
	//  				} else {
	//  					string error_msg = msg.reason_phrase;

	//  					try {
	//  						var parser = Network.get_parser_from_inputstream (in_stream);
	//  						var root = network.parse (parser);
	//  						if (root != null) {
	//  							error_msg = root.has_member ("message")
	//  							? root.get_string_member_with_default ("message", msg.reason_phrase)
	//  							: root.get_string_member_with_default ("error", msg.reason_phrase);
	//  						}
	//  					} catch {}

	//  					ecb ((int32) status, error_msg);
	//  				}
	//  			}
	//  		} catch (GLib.Error e) {
	//  			warning (e.message);
	//  			if (ecb != null) {
	//  				ecb ((int32) e.code, e.message);
	//  			}
	//  		}
	//  	});
	//  }

	public async GLib.InputStream queue_v2 (
		owned Soup.Message msg,
		GLib.Cancellable? cancellable,
		out Soup.MessageHeaders response_headers
	) throws GLib.Error, Oopsie {
		requests_processing++;

		debug (@"$(msg.method): $(msg.uri.to_string ())");
		GLib.InputStream in_stream = yield session.send_async (msg, 0, cancellable);
		var status = msg.status_code;
		response_headers = msg.response_headers;

		if (status >= 200 && status < 300)
			return in_stream;

		unowned string error_msg = msg.reason_phrase;
		try {
			var parser = yield Network.get_parser_from_inputstream_async (in_stream);
			var root = network.parse (parser);
			if (root != null) {
				error_msg = root.has_member ("message")
				? root.get_string_member_with_default ("message", msg.reason_phrase)
				: root.get_string_member_with_default ("error", msg.reason_phrase);
			}
		} catch {}

		critical (@"Request \"$(msg.uri.to_string ())\" failed: $status $(msg.reason_phrase) $error_msg");
		switch (status) {
			case 401: throw new Oopsie.HTTP_401 (error_msg);
			case 403: throw new Oopsie.HTTP_403 (error_msg);
			case 404: throw new Oopsie.HTTP_404 (error_msg);
			case 410: throw new Oopsie.HTTP_410 (error_msg);
			case 422: throw new Oopsie.HTTP_422 (error_msg);
			case 429: throw new Oopsie.HTTP_429 (error_msg);
			case 500: throw new Oopsie.HTTP_500 (error_msg);
			case 503: throw new Oopsie.HTTP_503 (error_msg);
		}

		throw new Oopsie.INSTANCE (error_msg);
	}

	public void on_error (int32 code, string message) {
		warning (message);
		app.toast (message, 0);
	}

	public Json.Node? parse_node (Json.Parser parser) {
		return parser.get_root ();
	}

	public Json.Object? parse (Json.Parser parser) {
		var node = parse_node (parser);
		if (node == null) return null;

		return node.get_object ();
	}

	//  public static Json.Parser get_parser_from_inputstream (InputStream in_stream) throws Error {
	//  	var parser = new Json.Parser ();
	//  	parser.load_from_stream (in_stream);
	//  	return parser;
	//  }

	public static async Json.Parser get_parser_from_inputstream_async (InputStream in_stream, Cancellable? cancellable = null) throws Error {
		var mem_out = new GLib.MemoryOutputStream.resizable ();
		yield mem_out.splice_async (
			in_stream,
			GLib.OutputStreamSpliceFlags.NONE,
			GLib.Priority.DEFAULT,
			cancellable
		);
		yield mem_out.close_async (GLib.Priority.DEFAULT, cancellable);
		var mem_in = new GLib.MemoryInputStream.from_bytes (mem_out.steal_as_bytes ());

		var parser = new Json.Parser ();
		yield parser.load_from_stream_async (mem_in);
		return parser;
	}

	public static Json.Array? get_array_mstd (Json.Parser parser) {
		return parser.get_root ().get_array ();
	}

	public static uint get_array_size (Json.Parser parser) {
		return get_array_mstd (parser).get_length ();
	}

	public static void parse_array (Json.Parser parser, owned NodeCallback cb) throws Error {
		get_array_mstd (parser).foreach_element ((array, i, node) => {
			try {
				cb (node);
			} catch (Error e) {
				warning (@"Error parsing array: $(e.message)");
			}
		});
	}

}
