import {Member} from "./member.js";
import {MarkDown} from "./markdown.js";
import {Contextmenu} from "./contextmenu.js";
import {Localuser} from "./localuser.js";
import {Guild} from "./guild.js";
import {SnowFlake} from "./snowflake.js";
import {ConnectionJson, highMemberJSON, presencejson, relationJson, userjson} from "./jsontypes.js";
import {Role} from "./role.js";
import {Search} from "./search.js";
import {I18n} from "./i18n.js";
import {Hover} from "./hover.js";
import {Dialog, Float, Options} from "./settings.js";
import {createImg, removeAni, safeImg} from "./utils/utils.js";
import {Direct} from "./direct.js";
import {Permissions} from "./permissions.js";
import {Channel} from "./channel.js";
import {getDeveloperSettings} from "./utils/storage/devSettings";
import {ReportMenu} from "./reporting/report.js";
import {CDNParams} from "./utils/cdnParams.js";
import {ImagesDisplay} from "./disimg.js";
import {File} from "./file.js";
export const userVolMenu = new Contextmenu<Localuser, string>("user vol stacked", true);
userVolMenu.addSlider(
	() => I18n.Voice.userVol(),
	(local, id, e) => {
		local.setUserAudio(id, e);
	},
	undefined,
	"default",
	{
		startVal: (local, id) => {
			return local.getUserAudio(id);
		},
	},
);
class User extends SnowFlake {
	owner: Localuser;
	hypotheticalpfp!: boolean;
	avatar!: string | null;
	uid: string;
	username!: string;
	nickname: string | null = null;
	relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0;
	relationshipID?: string;
	bio!: MarkDown;
	discriminator!: string;
	pronouns?: string;
	bot!: boolean;
	public_flags!: number;
	accent_color!: number;
	banner: string | undefined;
	hypotheticalbanner!: boolean;
	premium_since!: string;
	premium_type!: number;
	theme_colors: [number, number] | null = null;
	badge_ids!: string[];
	members = new WeakMap<Guild, Member | undefined | Promise<Member | undefined>>();
	status!: string;
	avatar_decoration_data?: {
		asset: string;
		sku_id: string;
	} | null;
	webhook_id?: string;

	resolving: false | Promise<any> = false;
	get headers() {
		return this.localuser.headers;
	}

	constructor(userjson: userjson, owner: Localuser, dontclone: boolean = false) {
		super(userjson.id);
		this.owner = owner;
		if (getDeveloperSettings().logBannedFields && owner.user && owner.user.id !== userjson.id) {
			this.checkfortmi(userjson);
		}
		if (!owner) {
			console.error("missing localuser");
		}
		this.uid = userjson.id;
		if (userjson.webhook_id) {
			this.uid += ":::" + userjson.username;
			console.log(this.uid);
		}
		userjson.uid = this.uid;
		if (dontclone) {
			this.userupdate(userjson);
			this.hypotheticalpfp = false;
		} else {
			return User.checkuser(userjson, owner);
		}
	}
	static makeSelector(
		opt: Options,
		doneText: string,
		options: User[],
		{single = false, addText = I18n.add()} = {},
	): Promise<Set<User> | void> {
		return new Promise<Set<User> | void>((res) => {
			const div = document.createElement("div");
			div.classList.add("flexttb", "friendGroupSelect");

			const invited = new Set<User>();

			const makeList = (search: string) => {
				const list = options
					.map((user) => [user, user.compare(search)] as const)
					.filter((_) => _[1] !== 0)
					.sort((a, b) => a[1] - b[1])
					.map((_) => _[0]);
				div.innerHTML = "";
				div.append(
					...list.map((user) => {
						const div = document.createElement("div");
						div.classList.add("flexltr");

						//TODO implement status stuff here once spacebar really supports it
						div.append(user.buildpfp(), user.name);
						if (single) {
							const button = document.createElement("button");
							button.textContent = addText;
							div.append(button);
							button.onclick = () => res(new Set([user]));
						} else {
							const check = document.createElement("input");
							check.type = "checkbox";
							check.checked = invited.has(user);
							check.onchange = () => {
								if (check.checked) {
									invited.add(user);
								} else {
									invited.delete(user);
								}
							};
							div.append(check);
						}
						return div;
					}),
				);
			};
			opt.addTextInput("", () => {}).onchange = makeList;
			opt.addHTMLArea(div);
			const buttons = opt.addOptions("", {ltr: true});
			buttons.addButtonInput("", I18n.cancel(), () => {
				res();
			});
			buttons.addButtonInput("", doneText, async () => {
				res(invited);
			});

			makeList("");
			buttons.container.deref()?.classList.add("expandButtons");
		});
	}
	compare(str: string) {
		function similar(str2: string | null | undefined) {
			if (!str2) return 0;
			const strl = Math.max(str.length, 1);
			if (str2.includes(str)) {
				return strl / str2.length;
			} else if (str2.toLowerCase().includes(str.toLowerCase())) {
				return strl / str2.length / 1.2;
			}
			return 0;
		}
		return Math.max(
			similar(this.name),
			similar(this.nickname),
			similar(this.username),
			similar(this.id) / 1.5,
		);
	}
	/**
	 * function is meant to check if userjson contains too much information IE non-public stuff
	 *
	 *
	 */
	checkfortmi(json: any) {
		if (json.data) {
			console.error("Server sent *way* too much info, this is really bad, it sent data");
		}
		const bad = new Set([
			"fingerprints",
			"extended_settings",
			"mfa_enabled",
			"nsfw_allowed",
			"premium_usage_flags",
			"totp_last_ticket",
			"totp_secret",
			"webauthn_enabled",
		]);
		if (!this.localuser.rights.getPermission("OPERATOR")) {
			//Unless the user is an operator, we really shouldn't ever see this
			bad.add("rights");
		}
		for (const thing of bad) {
			if (json.hasOwnProperty(thing)) {
				console.error(thing + " should not be exposed to the client");
			}
		}
	}
	tojson(): userjson {
		return {
			username: this.username,
			id: this.id,
			public_flags: this.public_flags,
			discriminator: this.discriminator,
			avatar: this.avatar,
			accent_color: this.accent_color,
			banner: this.banner,
			bio: this.bio.rawString,
			premium_since: this.premium_since,
			premium_type: this.premium_type,
			bot: this.bot,
			theme_colors: this.theme_colors,
			pronouns: this.pronouns,
			badge_ids: this.badge_ids,
		};
	}

	clone(): User {
		const json = this.tojson();
		json.id += "#clone";
		return new User(json, this.owner);
	}

	public getPresence(presence: presencejson | undefined): void {
		if (presence) {
			this.setstatus(presence.status);
		} else {
			this.setstatus("offline");
		}
	}
	get online() {
		return this.status && this.status != "offline";
	}
	setstatus(status: string): void {
		this.status = status;
		const has = this.statusChange();
		if (has) this.localstatusUpdate();
	}

	getStatus(): string {
		return this.status || "offline";
	}

	static contextmenu = new Contextmenu<User, Member | undefined>("User Menu");
	async opendm(message?: string) {
		for (const dm of (this.localuser.guilds.get("@me") as Direct).channels) {
			if ((dm.type === 1 || dm.type === undefined) && dm.users[0].id === this.id) {
				await this.localuser.goToChannel(dm.id);
				if (message) {
					await dm.sendMessage(message, {
						attachments: [],
						embeds: [],
						replyingto: null,
						sticker_ids: [],
					});
				}

				return;
			}
		}

		await fetch(this.info.api + "/users/@me/channels", {
			method: "POST",
			body: JSON.stringify({recipients: [this.id]}),
			headers: this.localuser.headers,
		})
			.then((res) => res.json())
			.then((json) => {
				return this.localuser.goToChannel(json.id);
			});
		if (message) {
			for (const dm of (this.localuser.guilds.get("@me") as Direct).channels) {
				if ((dm.type === 1 || dm.type === undefined) && dm.users[0].id === this.id) {
					dm.sendMessage(message, {
						attachments: [],
						embeds: [],
						replyingto: null,
						sticker_ids: [],
					});
					return;
				}
			}
		}
		return;
	}
	async changeRelationship(type: 0 | 1 | 2 | 3 | 4 | 5) {
		const relChange = this.localuser.relationChange(this.id);
		if (type !== 0) {
			await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
				method: "PUT",
				headers: this.owner.headers,
				body: JSON.stringify({
					type,
				}),
			});
		} else {
			await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
				method: "DELETE",
				headers: this.owner.headers,
			});
		}
		await relChange;
	}
	static setUpContextMenu(): void {
		this.contextmenu.addGroup("dm");
		this.contextmenu.addSeperator();

		this.contextmenu.addButton(
			() => I18n.user.message(),
			function (this: User) {
				this.opendm();
			},
			{
				icon: {
					css: "svg-frmessage",
				},
				group: "dmPerson",
			},
		);

		this.contextmenu.addSeperator();

		this.contextmenu.addButton(
			() => I18n.user.block(),
			function (this: User) {
				this.block();
			},
			{
				visible: function () {
					return this.relationshipType !== 2 && this.id !== this.localuser.user.id;
				},
				color: "red",
			},
		);

		this.contextmenu.addButton(
			() => I18n.user.unblock(),
			function (this: User) {
				this.unblock();
			},
			{
				visible: function () {
					return this.relationshipType === 2 && this.id !== this.localuser.user.id;
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.user.friendReq(),
			function (this: User) {
				this.changeRelationship(1);
			},
			{
				visible: function () {
					return (
						(this.relationshipType === 0 || this.relationshipType === 3) &&
						this.id !== this.localuser.user.id &&
						!this.bot
					);
				},
				icon: {
					css: "svg-addfriend",
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.friends.removeFriend(),
			function (this: User) {
				this.changeRelationship(0);
			},
			{
				visible: function () {
					return this.relationshipType === 1 && this.id !== this.localuser.user.id;
				},
			},
		);
		this.contextmenu.addButton(
			function () {
				switch (this.relationshipType) {
					case 1:
						return I18n.user.nick.friend();
					case 2:
						return I18n.user.nick.foe();
					case 3:
						return I18n.user.nick.stalker();
					case 4:
						return I18n.user.nick.stalking();
					default:
						return "You shouldn't see this";
				}
			},
			function (this: User) {
				this.setFriendNick();
			},
			{
				visible: function () {
					return new Set([1, 2, 3, 4]).has(this.relationshipType);
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.user.viewProfile(),
			function (this: User, m, e) {
				this.buildprofile(e.clientX, e.clientY, m, 100000);
			},
		);

		this.contextmenu.addSeperator();

		this.contextmenu.addButton(
			() => I18n.user.editServerProfile(),
			function (this: User, member: Member | undefined) {
				if (!member) return;
				member.showEditProfile();
			},
			{
				visible: function (member) {
					return member?.id === this.localuser.user.id;
				},
			},
		);

		this.contextmenu.addButton(
			() => I18n.user.editNick(),
			function (this: User, member: Member | undefined) {
				if (!member) return;
				member.showEditNick();
			},
			{
				visible: function (member) {
					return (
						!!member &&
						member?.id !== this.localuser.user.id &&
						member.guild.member.hasPermission("MANAGE_NICKNAMES")
					);
				},
			},
		);

		this.contextmenu.addButton(
			() => I18n.user.timeout(),
			function (this: User, member: Member | undefined) {
				member?.timeout();
			},
			{
				visible: function (member) {
					if (!member) return false;
					if (member.hasPermission("MODERATE_MEMBERS")) return false;

					return (
						!member.commuicationDisabledLeft() &&
						member.guild.member.hasPermission("MODERATE_MEMBERS")
					);
				},
				color: "red",
				icon: {
					css: "svg-timeout",
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.user.unTimeout(),
			function (memb) {
				memb?.removeTimeout();
			},
			{
				visible: function (member) {
					if (!member) return false;

					return (
						!!member.commuicationDisabledLeft() &&
						member.guild.member.hasPermission("MODERATE_MEMBERS")
					);
				},
				color: "red",
			},
		);

		//TODO kick icon
		this.contextmenu.addButton(
			() => I18n.user.kick(),
			function (this: User, member: Member | undefined) {
				member?.kick();
			},
			{
				visible: function (member) {
					if (!member) return false;
					const us = member.guild.member;
					if (member.id === us.id) {
						return false;
					}
					if (member.id === member.guild.properties.owner_id) {
						return false;
					}
					return us.hasPermission("KICK_MEMBERS") && this.id !== this.localuser.user.id;
				},
				color: "red",
			},
		);

		//TODO ban icon
		this.contextmenu.addButton(
			() => I18n.user.ban(),
			function (this: User, member: Member | undefined) {
				member?.ban();
			},
			{
				visible: function (member) {
					if (!member) return false;
					const us = member.guild.member;
					if (member.id === us.id) {
						return false;
					}
					if (member.id === member.guild.properties.owner_id) {
						return false;
					}
					return us.hasPermission("BAN_MEMBERS") && this.id !== this.localuser.user.id;
				},
				color: "red",
			},
		);

		this.contextmenu.addSeperator();

		this.contextmenu.addButton(
			() => I18n.user.addRole(),
			async function (this: User, member: Member | undefined, e) {
				if (member) {
					e.stopPropagation();
					const roles: [Role, string[]][] = [];
					for (const role of member.guild.roles) {
						if (!role.canManage() || member.roleIds.has(role.id)) {
							continue;
						}
						roles.push([role, [role.name]]);
					}
					const search = new Search(roles);
					const result = await search.find(e.x, e.y);
					if (!result) return;
					member.addRole(result);
				}
			},
			{
				visible: (member) => {
					if (!member) return false;
					const us = member.guild.member;
					console.log(us.hasPermission("MANAGE_ROLES"));
					return us.hasPermission("MANAGE_ROLES") || false;
				},
			},
		);
		this.contextmenu.addButton(
			() => I18n.user.removeRole(),
			async function (this: User, member: Member | undefined, e) {
				if (member) {
					e.stopPropagation();
					const roles: [Role, string[]][] = [];
					for (const role of member.roles) {
						if (!role.canManage()) {
							continue;
						}
						roles.push([role, [role.name]]);
					}
					const search = new Search(roles);
					const result = await search.find(e.x, e.y);
					if (!result) return;
					member.removeRole(result);
				}
			},
			{
				visible: (member) => {
					if (!member) return false;
					const us = member.guild.member;
					console.log(us.hasPermission("MANAGE_ROLES"));
					return us.hasPermission("MANAGE_ROLES") || false;
				},
			},
		);

		this.contextmenu.addSeperator();
		this.contextmenu.addButton(
			() => I18n.user.copyId(),
			function (this: User) {
				navigator.clipboard.writeText(this.id);
			},
		);
		this.contextmenu.addGroup("id");

		this.contextmenu.addSeperator();

		this.contextmenu.addButton(
			() => I18n.user.report(),
			async function (member) {
				const menu = await ReportMenu.makeReport("user", this.localuser, {user: this, member});
				menu?.spawnMenu();
			},
			{
				visible: function () {
					const settings = getDeveloperSettings();
					return this.id !== this.localuser.user.id && settings.reportSystem;
				},
				color: "red",
			},
		);
		this.contextmenu.addButton(
			() => I18n.user.reportApp(),
			async function () {
				const menu = await ReportMenu.makeReport("application", this.localuser, {
					application_id: this.id,
				});
				menu?.spawnMenu();
			},
			{
				visible: function () {
					const settings = getDeveloperSettings();
					return this.bot && settings.reportSystem;
				},
				color: "red",
			},
		);

		this.contextmenu.addButton(
			() => I18n.user.instanceBan(),
			function (this: User) {
				const params = {
					reason: "",
					persistInstanceBan: true,
				};
				const menu = new Dialog("");
				const options = menu.float.options;
				options.addTitle(I18n.user.confirmInstBan(this.name));
				options.addTextInput(I18n.member["reason:"](), () => {}, {}).onchange = (txt) => {
					params.reason = txt;
				};
				options.addCheckboxInput(I18n.member.persist(), () => {}, {
					initState: false,
				}).onchange = (checked) => {
					params.persistInstanceBan = !checked;
				};
				const opt = options.addOptions("", {ltr: true});
				opt.addButtonInput("", I18n.yes(), () => {
					fetch(this.info.api + "/users/" + this.id + "/delete", {
						headers: this.localuser.headers,
						method: "POST",
						body: JSON.stringify(params),
					});
					menu.hide();
				});
				opt.addButtonInput("", I18n.no(), () => {
					menu.hide();
				});
				menu.show();
			},
			{
				visible: function () {
					return this.localuser.rights.hasPermission("MANAGE_USERS");
				},
				color: "red",
			},
		);
		console.warn("this ran");
	}
	setFriendNick() {
		const dio = new Dialog("");
		const form = dio.options.addForm(
			"",
			() => {
				dio.hide();
			},
			{
				fetchURL: this.info.api + `/users/@me/relationships/${this.id}`,
				method: "PATCH",
				headers: this.headers,
			},
		);
		form.addTextInput(I18n.member["nick:"](), "nickname", {
			initText: this.nickname || "",
		});
		dio.show();
	}
	getMembersSync() {
		return [...this.localuser.guilds.values()]
			.map((guild) => {
				const m = this.members.get(guild);
				return m instanceof Member ? m : undefined;
			})
			.filter((m) => m !== undefined);
	}

	elms = new Set<WeakRef<HTMLElement>>();
	subName(elm: HTMLElement) {
		this.elms.add(new WeakRef(elm));
	}
	nameChange() {
		this.getMembersSync().forEach((memb) => {
			memb.nameChange();
		});

		for (const ref of this.elms) {
			const elm = ref.deref();
			if (!elm || !document.contains(elm)) {
				this.elms.delete(ref);
				continue;
			}
			elm.textContent = this.name;
		}
	}

	static checkuser(user: User | userjson, owner: Localuser): User {
		const tempUser = owner.userMap.get(user.uid || user.id);
		if (tempUser) {
			if (!(user instanceof User)) {
				tempUser.userupdate(user);
			}
			return tempUser;
		} else {
			const tempuser = new User(user as userjson, owner, true);
			owner.userMap.set(user.uid || user.id, tempuser);
			return tempuser;
		}
	}

	get info() {
		return this.owner.info;
	}

	get localuser() {
		return this.owner;
	}

	get name() {
		return this.nickname || (this.relationshipType === 2 ? I18n.friends.bu() : this.username);
	}

	async resolvemember(guild: Guild): Promise<Member | undefined> {
		return await Member.resolveMember(this, guild);
	}

	async getUserProfile(): Promise<any> {
		return await fetch(
			`${this.info.api}/users/${this.id.replace(
				"#clone",
				"",
			)}/profile?with_mutual_guilds=true&with_mutual_friends=true`,
			{
				headers: this.localuser.headers,
			},
		).then((res) => res.json());
	}

	async getBadge(id: string) {
		if (this.localuser.badges.has(id)) {
			return this.localuser.badges.get(id);
		} else {
			if (this.resolving) {
				await this.resolving;
				return this.localuser.badges.get(id);
			}

			const prom = await this.getUserProfile();
			this.resolving = prom;
			const badges = prom.badges;
			this.resolving = false;
			for (const badge of badges) {
				this.localuser.badges.set(badge.id, badge);
			}
			return this.localuser.badges.get(id);
		}
	}

	buildpfp(guild: Guild | void | Member | null, hoverElm: void | HTMLElement): HTMLDivElement {
		const div = document.createElement("div");
		div.classList.add("pfpDiv");
		hoverElm ??= div;
		const pfp = createImg(this.getpfpsrc(), undefined, hoverElm);
		pfp.loading = "lazy";
		pfp.classList.add("pfp");
		if (!this.webhook_id) pfp.classList.add("userid:" + this.id);
		if (guild) {
			(async () => {
				if (guild instanceof Guild) {
					const memb = await Member.resolveMember(this, guild);
					if (!memb) return;
					pfp.setSrcs(memb.getpfpsrc());
				} else {
					pfp.setSrcs(guild.getpfpsrc());
				}
			})();
		}
		if (this.avatar_decoration_data && this.localuser.perminfo.user.decorations) {
			const dec = createImg(
				this.info.cdn +
					`/avatar-decoration-presets/${this.avatar_decoration_data.asset}.png` +
					new CDNParams({expectedSize: 96}),
				void 0,
				hoverElm,
			);
			dec.classList.add("avDec");
			div.append(dec);
		}
		div.append(pfp);
		return div;
	}
	createWidget(guild?: Guild) {
		guild = this.localuser.guilds.get("@me") as Guild;
		const div = document.createElement("div");
		div.classList.add("mutFriends", "flexltr");
		//TODO make sure this is something I can actually do here
		const name = document.createElement("b");
		name.textContent = this.name;

		const nameBox = document.createElement("div");
		nameBox.classList.add("flexttb");
		nameBox.append(name);
		const pfp = this.buildpfp(undefined, div);
		div.append(pfp, nameBox);
		Member.resolveMember(this, guild).then((_) => {
			if (_) {
				_.subName(name);
				name.textContent = _.name;
				(pfp.children[0] as HTMLImageElement).src = _.getpfpsrc();
			} else if (guild.id !== "@me") {
				this.subName(name);
				const notFound = document.createElement("span");
				notFound.textContent = I18n.webhooks.notFound();
				nameBox.append(notFound);
			}
		});
		this.bind(div, guild, undefined);
		return div;
	}
	updateStatusSet = new Set<WeakRef<HTMLDivElement>>();
	contextMap = new WeakMap<HTMLDivElement, Guild | Channel>();
	localstatusUpdate = () => {};
	registerStatus(status: HTMLDivElement, thing: Guild | void | Member | null | Channel) {
		if (thing) {
			if (thing instanceof Member) {
				this.contextMap.set(status, thing.guild);
			} else {
				this.contextMap.set(status, thing);
			}
		}
		this.updateStatusSet.add(new WeakRef(status));
		this.updateStatus(status);
	}
	updateStatus(status: HTMLDivElement) {
		status.classList.remove("offlinestatus", "dndstatus", "onlinestatus", "typingstatus");
		switch (this.getStatus()) {
			case "offline":
			case "invisible":
				status.classList.add("offlinestatus");
				break;
			case "dnd":
				status.classList.add("dndstatus");
				break;
			case "online":
			default:
				status.classList.add("onlinestatus");
				break;
		}
		const m = this.contextMap.get(status);
		if (m) {
			let guild: Guild;
			let channel: Channel | void = undefined;
			if ("guild" in m) {
				channel = m;
				guild = m.guild;
			} else {
				guild = m;
			}
			const memb = this.members.get(guild);
			if (memb && !(memb instanceof Promise) && channel) {
				const typing = channel.typingmap.get(memb);

				if (typing) {
					status.classList.add("typingstatus");
				}
			}
		}
	}
	statusChange() {
		let has = false;
		for (const ref of this.updateStatusSet) {
			const elm = ref.deref();
			if (!elm || !document.body.contains(elm)) {
				this.updateStatusSet.delete(ref);
				continue;
			}
			has = true;
			this.updateStatus(elm);
		}
		return has;
	}
	buildstatuspfp(guild: Guild | void | Member | null | Channel): HTMLDivElement {
		const isChannel = !!(guild && "guild" in guild && !(guild instanceof Member));
		const div = this.buildpfp(isChannel ? guild.guild : guild);

		const status = document.createElement("div");
		this.registerStatus(status, guild);
		status.classList.add("statusDiv");
		status.append(document.createElement("div"));

		div.append(status);
		return div;
	}

	userupdate(json: userjson): void {
		const up = json.username !== this.username;
		if (json.avatar !== this.avatar) {
			if (json.avatar) json.avatar = json.avatar.match(/[^\/]*$/gm)![0];
			Array.from(document.getElementsByClassName("userid:" + this.id)).forEach((element) => {
				const img = element as safeImg;
				if ("setSrcs" in element) {
					img.setSrcs(this.getpfpsrc());
				} else {
					img.src = this.getpfpsrc();
				}
			});
		}
		for (const key of Object.keys(json)) {
			if (key === "bio") {
				this.bio = new MarkDown(json[key], this.localuser);
				continue;
			}
			if (key === "id") {
				continue;
			}
			(this as any)[key] = (json as any)[key];
		}
		if ("rights" in this) {
			if (
				this === this.localuser.user &&
				(typeof this.rights == "string" || typeof this.rights == "number")
			) {
				this.localuser.updateRights(this.rights);
			}
		}
		if (up) {
			this.nameChange();
		}
	}

	bind(
		html: HTMLElement,
		guild: Guild | null = null,
		error = true,
		button: "right" | "left" | "none" = "right",
	): void {
		if (guild && guild.id !== "@me") {
			Member.resolveMember(this, guild)
				.then((member) => {
					User.contextmenu.bindContextmenu(html, this, member);
					if (member === undefined && error) {
						if (this.webhook_id) return;
						const errorSpan = document.createElement("span");
						errorSpan.textContent = "!";
						errorSpan.classList.add("membererror");
						html.after(errorSpan);
						return;
					}
					if (member) {
						member.bind(html);
					} else {
						if (button !== "none")
							User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button);
					}
				})
				.catch((err) => {
					console.log(err);
				});
		} else {
			if (button !== "none")
				User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button);
		}
		if (button !== "none")
			if (guild) {
				this.profileclick(html, guild);
			} else {
				this.profileclick(html);
			}
	}

	static async resolve(id: string, localuser: Localuser): Promise<User> {
		const time = SnowFlake.stringToUnixTime(id);

		if (time < 1420070400000 + 100)
			return new User(
				{
					id: "0",
					public_flags: 0,
					username: I18n.friends.notfound(),
					avatar: null,
					discriminator: "0000",
					bio: "",
					bot: false,
					premium_type: 0,
					premium_since: "",
					accent_color: 0,
					theme_colors: null,
					badge_ids: [],
				},
				localuser,
			);
		let user: User | undefined;
		if ((user = localuser.userMap.get(id))) return user;
		const json = await fetch(localuser.info.api.toString() + "/users/" + id + "/profile", {
			headers: localuser.headers,
		}).then((res) => res.json());
		if (json.code === 404) {
			return new User(
				{
					id: "0",
					public_flags: 0,
					username: I18n.friends.notfound(),
					avatar: null,
					discriminator: "0000",
					bio: "",
					bot: false,
					premium_type: 0,
					premium_since: "",
					accent_color: 0,
					theme_colors: null,
					badge_ids: [],
				},
				localuser,
			);
		}
		return new User(json.user, localuser);
	}

	changepfp(update: string | null): void {
		this.avatar = update;
		this.hypotheticalpfp = false;
	}

	async block() {
		await this.changeRelationship(2);
		const channel = this.localuser.focusChannel;
		if (channel) {
			for (const message of channel.messages) {
				message[1].generateMessage();
			}
		}
	}

	async unblock() {
		await this.changeRelationship(0);
		const channel = this.localuser.focusChannel;
		if (channel) {
			for (const message of channel.messages) {
				message[1].generateMessage();
			}
		}
	}
	/**
	 * @param guild this is an optional thing that'll get the src of the member if it exists, otherwise ignores it, this is meant to be fast, not accurate
	 */
	getpfpsrc(guild: Guild | void): string {
		if (this.hypotheticalpfp && this.avatar) {
			return this.avatar;
		}
		if (guild) {
			const member = this.members.get(guild);
			if (member instanceof Member) {
				return member.getpfpsrc();
			}
		}
		if (this.avatar !== null) {
			return (
				`${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${this.avatar}.png` +
				new CDNParams({expectedSize: 96})
			);
		} else {
			const int = Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n);
			return `${this.info.cdn}/embed/avatars/${int}.png`;
		}
	}
	async getBadges() {
		let i = 0;
		let flagbits = this.public_flags;
		const ids = [
			"staff",
			"partner",
			"certified_moderator",
			"hypesquad",
			"hypesquad_house_1",
			"hypesquad_house_2",
			"hypesquad_house_3",
			"bug_hunter_level_1",
			"bug_hunter_level_2",
			"active_developer",
			"verified_developer",
			"early_supporter",
			"premium",
			"guild_booster_lvl1",
			"guild_booster_lvl2",
			"guild_booster_lvl3",
			"guild_booster_lvl4",
			"guild_booster_lvl5",
			"guild_booster_lvl6",
			"guild_booster_lvl7",
			"guild_booster_lvl8",
			"guild_booster_lvl9",
			"bot_commands",
			"automod",
			"application_guild_subscription",
			"legacy_username",
			"quest_completed",
		];
		let badgeids: string[] = [];
		while (flagbits !== 0) {
			if (flagbits & 1) {
				badgeids.push(ids[i]);
			}
			flagbits >>= 1;
			i++;
		}
		if (this.badge_ids) {
			badgeids = badgeids.concat(this.badge_ids);
		}

		let badges: {
			id: string;
			description: string;
			icon: string;
			link?: string;
			translate?: boolean;
		}[] = [];

		const b = (await Promise.all(badgeids.map((_) => this.getBadge(_)))).filter(
			(_) => _ !== undefined,
		);
		badges = b;

		return badges;
	}
	async highInfo() {
		return (await (
			await fetch(
				this.info.api +
					"/users/" +
					this.id +
					"/profile?with_mutual_guilds=true&with_mutual_friends=true",
				{headers: this.localuser.headers},
			)
		).json()) as highMemberJSON;
	}
	handleRelationship(relation: relationJson) {
		const nickChange = this.nickname !== relation.nickname;
		this.nickname = relation.nickname;
		this.relationshipType = relation.type;
		this.localuser.inrelation.add(this);
		this.relationshipID = relation.id;
		if (nickChange) {
			this.nameChange();
		}
	}
	removeRelation() {
		const nickChange = this.nickname;
		this.nickname = null;
		this.relationshipType = 0;
		this.localuser.inrelation.delete(this);
		if (nickChange) {
			this.nameChange();
		}
	}
	static getLink(con: ConnectionJson): string | void {
		switch (con.type) {
			case "steam": {
				return `https://steamcommunity.com/profiles/${con.external_id}`;
			}
			case "domain": {
				return `https://${con.external_id}`;
			}
		}
	}
	note?: string;
	async saveNotes(note: string) {
		this.note = note;
		await fetch(`${this.info.api}/users/@me/notes/${this.id}`, {
			method: "PUT",
			headers: this.headers,
			body: JSON.stringify({note}),
		});
	}
	async getNotes() {
		try {
			if (this.note === undefined) {
				this.note =
					(
						await (
							await fetch(`${this.info.api}/users/@me/notes/${this.id}`, {
								method: "GET",
								headers: this.headers,
							})
						).json()
					).note || "";
			}
		} catch {
			this.note = "";
		}
		return this.note as string;
	}
	async fullProfile(guild: Guild | null | Member = null) {
		console.log(guild);
		const membres = (async () => {
			if (!guild) return;
			let member: Member | undefined;
			if (guild instanceof Guild) {
				member = await Member.resolveMember(this, guild);
			} else {
				member = guild;
			}
			return member;
		})();
		const background = document.createElement("div");
		background.classList.add("background");
		background.onclick = () => {
			removeAni(background);
		};
		const div = document.createElement("div");
		div.onclick = (e) => e.stopImmediatePropagation();
		div.classList.add("centeritem", "profile");

		if (this.accent_color) {
			div.style.setProperty(
				"--accent_color",
				`#${this.accent_color.toString(16).padStart(6, "0")}`,
			);
		} else {
			div.style.setProperty("--accent_color", "transparent");
		}
		const banner = this.getBanner(guild);
		banner.onclick = async () => {
			const m = await membres;
			const disp = new ImagesDisplay([
				new File(
					{
						id: "fakeID",
						filename: "avatar.webp",
						content_type: "image/webp",
						url: m?.getBannerUrl() ?? this.getBannerUrl()!,
						size: 1,
					},
					null,
				),
			]);
			disp.show();
		};
		div.append(banner);
		membres.then((member) => {
			if (!member) return;
			if (member.accent_color && member.accent_color !== 0) {
				div.style.setProperty(
					"--accent_color",
					`#${member.accent_color.toString(16).padStart(6, "0")}`,
				);
			}
		});

		const badgediv = document.createElement("div");
		badgediv.classList.add("badges");
		(async () => {
			const badges = await this.getBadges();
			for (const badgejson of badges) {
				const badge = document.createElement(badgejson.link ? "a" : "div");
				badge.classList.add("badge");
				let src: string;
				if (URL.canParse(badgejson.icon)) {
					src = badgejson.icon;
				} else {
					src =
						this.info.cdn +
						"/badge-icons/" +
						badgejson.icon +
						".png" +
						new CDNParams({expectedSize: 32});
				}
				const img = createImg(src, undefined, badgediv);

				badge.append(img);
				let hovertxt: string;
				if (badgejson.translate) {
					//@ts-ignore
					hovertxt = I18n.badge[badgejson.description]();
				} else {
					hovertxt = badgejson.description;
				}
				const hover = new Hover(hovertxt);
				hover.addEvent(badge);
				if (badgejson.link && badge instanceof HTMLAnchorElement) {
					MarkDown.safeLink(badge, badgejson.link);
				}
				badgediv.append(badge);
			}
		})();

		const pfp = this.buildstatuspfp(guild);
		div.appendChild(pfp);
		pfp.onclick = () => {
			const disp = new ImagesDisplay([
				new File(
					{
						id: "fakeID",
						filename: "avatar.webp",
						content_type: "image/webp",
						url: this.getpfpsrc(guild instanceof Member ? guild.guild : (guild ?? undefined)),
						size: 1,
					},
					null,
				),
			]);
			disp.show();
		};
		const userbody = document.createElement("div");
		userbody.classList.add("flexttb", "infosection");
		div.appendChild(userbody);

		const usernamehtml = document.createElement("h2");
		usernamehtml.textContent = this.name;

		userbody.appendChild(usernamehtml);

		if (this.bot) {
			const username = document.createElement("span");
			username.classList.add("bot");
			username.textContent = this.webhook_id ? I18n.webhook() : I18n.bot();
			usernamehtml.appendChild(username);
		}

		userbody.appendChild(badgediv);
		const discrimatorhtml = document.createElement("h3");
		discrimatorhtml.classList.add("tag");
		discrimatorhtml.textContent = `${this.username}#${this.discriminator}`;
		userbody.appendChild(discrimatorhtml);

		const pronounshtml = document.createElement("p");
		pronounshtml.textContent = this.pronouns || "";
		pronounshtml.classList.add("pronouns");
		userbody.appendChild(pronounshtml);

		membres.then((member) => {
			if (!member) return;
			if (member.pronouns && member.pronouns !== "") {
				pronounshtml.textContent = member.pronouns;
			}
		});

		const rule = document.createElement("hr");
		userbody.appendChild(rule);
		const float = new Float("");
		const infoDiv = document.createElement("div");
		infoDiv.classList.add("flexttb");
		const buttons = float.options.addButtons("", {top: true, titles: false});
		{
			const info = buttons.add(I18n.profile.userInfo());

			infoDiv.append(I18n.profile.bio(), document.createElement("hr"));
			const biohtml = this.bio.makeHTML();
			infoDiv.appendChild(biohtml);

			membres.then((member) => {
				if (!member) return;
				if (member.bio && member.bio !== "") {
					//TODO make markdown take Guild
					infoDiv.insertBefore(new MarkDown(member.bio, this.localuser).makeHTML(), biohtml);
					biohtml.remove();
				}
			});
			info.addHTMLArea(infoDiv);

			const roles = document.createElement("div");
			const joined = document.createElement("div");
			joined.textContent = I18n.profile.joined(new Date(this.getUnixTime()).toLocaleString());
			infoDiv.append(roles, document.createElement("hr"), joined);

			if (guild) {
				membres.then((member) => {
					if (!member) {
						this.subName(usernamehtml);
						return;
					}
					const p = document.createElement("p");
					p.textContent = I18n.profile.joinedMember(
						member.guild.properties.name,
						new Date(member.joined_at).toLocaleString(),
					);
					joined.append(p);

					usernamehtml.textContent = member.name;
					member.subName(usernamehtml);
					if (this.bot) {
						const username = document.createElement("span");
						username.classList.add("bot");
						username.textContent = this.webhook_id ? I18n.webhook() : I18n.bot();
						usernamehtml.appendChild(username);
					}

					roles.classList.add("flexltr", "rolesbox");
					for (const role of member.roles) {
						if (role.id === member.guild.id) continue;
						const roleDiv = document.createElement("div");
						roleDiv.classList.add("rolediv");
						const color = document.createElement("div");
						roleDiv.append(color);

						color.style.setProperty("--role-color", role.getColorStyle(true));
						color.classList.add("colorrolediv");
						const span = document.createElement("span");
						roleDiv.append(span);
						span.textContent = role.name;
						roles.append(roleDiv);
					}
				});
			}
		}

		(async () => {
			const memb = await membres;
			if (!memb) return;
			const perms = buttons.add(I18n.profile.permInfo());
			const permDiv = document.createElement("div");
			permDiv.classList.add("permbox");
			const permsL = Permissions.info()
				.filter((_) => memb.hasPermission(_.name, false))
				.map((_) => _.readableName);
			for (const perm of permsL) {
				const span = document.createElement("span");
				span.textContent = perm;
				permDiv.append(span);
			}
			perms.addHTMLArea(permDiv);
		})();

		const fhtml = float.generateHTML();
		fhtml.style.overflow = "auto";
		userbody.append(fhtml);

		document.body.append(background);
		background.append(div);
		console.log(background);
		const notes = buttons.add(I18n.profile.notes());
		const textArea = document.createElement("textarea");
		textArea.classList.add("userNote");
		notes.addHTMLArea(textArea);
		this.getNotes().then((_) => (textArea.textContent = _));
		textArea.onblur = () => {
			this.saveNotes(textArea.value);
		};
		(async () => {
			const high = await this.highInfo();
			const mut = buttons.add(I18n.profile.mut(high.mutual_guilds.length + ""));
			const mutDiv = document.createElement("div");

			mutDiv.append(
				...high.mutual_guilds
					.map((_) => [this.localuser.guilds.get(_.id), _.nick] as const)
					.map(([guild, nick]) => {
						if (!guild) return;
						const icon = guild.generateGuildIcon(false);

						const box = document.createElement("div");
						box.classList.add("mutGuildBox", "flexltr");

						const info = document.createElement("div");
						info.classList.add("flexttb");
						const gname = document.createElement("span");
						gname.textContent = guild.properties.name;
						info.append(gname);
						box.append(icon, info);
						box.onclick = () => {
							removeAni(background);
							guild.loadGuild();
							guild.loadChannel();
						};
						if (nick) info.append(nick);
						return box;
					})
					.filter((_) => _ !== undefined),
			);
			mut.addHTMLArea(mutDiv);

			if (high.mutual_friends) {
				const friends = buttons.add(I18n.profile.mutFriends());
				const div = document.createElement("div");
				div.classList.add("mutFriends");
				div.append(
					...high.mutual_friends
						.map((_) => new User(_, this.localuser))
						.map((user) => {
							const html = user.createWidget(this.localuser.focusGuild);
							html.onclick = (e) => {
								e.stopImmediatePropagation();
								e.preventDefault();
								user.fullProfile(guild);
								removeAni(background);
							};
							return html;
						}),
				);
				friends.addHTMLArea(div);
			}
			if (high.connected_accounts.length) {
				const cons = await this.localuser.getConnections();

				for (const con of high.connected_accounts) {
					const conic = cons[con.type];
					if (!conic) continue;
					const conDiv = document.createElement("div");
					conDiv.classList.add("flexltr", "conProfDiv");
					const url = User.getLink(con);
					if (url) {
						MarkDown.safeLink(conDiv, url);
						conDiv.style.cursor = "pointer";
					}

					if (conic.icon_url) {
						const span = document.createElement("span");
						span.classList.add("conImg", "svgicon", "conProfImg");
						span.style.setProperty("mask", `url("${conic.icon_url}")`);
						//span.alt = key;
						conDiv.append(span);
					} else {
						conDiv.textContent = con.type.charAt(0).toUpperCase() + con.type.slice(1);
					}

					const nameDateBox = document.createElement("div");
					nameDateBox.classList.add("flexttb");
					const username = document.createElement("span");
					username.textContent = con.name;
					nameDateBox.append(username);
					if (con.metadata && con.metadata.created_at) {
						const len = document.createElement("span");
						len.textContent = I18n.connections.since(
							new Date(con.metadata.created_at).toDateString(),
						);
						nameDateBox.append(len);
					}
					conDiv.append(nameDateBox);
					infoDiv.append(conDiv);
				}
			}
		})();
		return background;
	}

	async buildprofile(
		x: number,
		y: number,
		guild: Guild | null | Member = null,
		zIndex = -1,
	): Promise<HTMLDivElement> {
		const members = (async () => {
			if (!guild) return;
			let member: Member | undefined;
			if (guild instanceof Guild) {
				member = await Member.resolveMember(this, guild);
			} else {
				member = guild;
			}
			return member;
		})();
		const div = document.createElement("div");
		if (zIndex !== -1) {
			div.style.zIndex = zIndex + "";
		}
		if (this.accent_color) {
			div.style.setProperty(
				"--accent_color",
				`#${this.accent_color.toString(16).padStart(6, "0")}`,
			);
		} else {
			div.style.setProperty("--accent_color", "transparent");
		}
		const banner = this.getBanner(guild);
		div.append(banner);
		members.then((member) => {
			if (!member) return;
			if (member.accent_color && member.accent_color !== 0) {
				div.style.setProperty(
					"--accent_color",
					`#${member.accent_color.toString(16).padStart(6, "0")}`,
				);
			}
		});

		if (x !== -1) {
			div.style.left = `${x}px`;
			div.style.top = `${y}px`;
			div.classList.add("profile", "flexttb");
		} else {
			this.setstatus("online");
			div.classList.add("hypoprofile", "profile", "flexttb");
		}
		const badgediv = document.createElement("div");
		badgediv.classList.add("badges");
		(async () => {
			const badges = await this.getBadges();
			for (const badgejson of badges) {
				const badge = document.createElement(badgejson.link ? "a" : "div");
				badge.classList.add("badge");
				let src: string;
				if (URL.canParse(badgejson.icon)) {
					src = badgejson.icon;
				} else {
					src =
						this.info.cdn +
						"/badge-icons/" +
						badgejson.icon +
						".png" +
						new CDNParams({expectedSize: 32});
				}
				const img = createImg(src, undefined, badgediv);

				badge.append(img);
				let hovertxt: string;
				if (badgejson.translate) {
					//@ts-ignore
					hovertxt = I18n.badge[badgejson.description]();
				} else {
					hovertxt = badgejson.description;
				}
				const hover = new Hover(hovertxt);
				hover.addEvent(badge);
				if (badgejson.link && badge instanceof HTMLAnchorElement) {
					badge.href = badgejson.link;
				}
				badgediv.append(badge);
			}
		})();
		const pfp = this.buildstatuspfp(guild);
		pfp.onclick = (e) => {
			if (this.id.includes("clone")) return;
			this.fullProfile(guild);
			div.remove();
			e.stopImmediatePropagation();
			e.preventDefault();
		};
		div.appendChild(pfp);
		const userbody = document.createElement("div");
		userbody.classList.add("flexttb", "infosection");
		div.appendChild(userbody);
		const usernamehtml = document.createElement("h2");
		usernamehtml.textContent = this.username;

		const friendDiv = document.createElement("div");
		friendDiv.classList.add("friendProf");

		const friendSpan = document.createElement("span");
		friendSpan.classList.add("svgicon");
		const updateIcon = () => {
			friendSpan.classList.remove("svg-addfriend", "svg-waitfriend");
			console.log(this.relationshipType);
			if (this.relationshipType === 0 || this.relationshipType === 3) {
				friendSpan.classList.add("svg-addfriend");
			} else if (this.relationshipType === 4) {
				friendSpan.classList.add("svg-waitfriend");
			} else {
				friendSpan.classList.add("svg-hasfriend");
			}
		};
		if (this !== this.localuser.user && !this.bot && x !== -1) {
			friendDiv.append(friendSpan);
			div.append(friendDiv);
			updateIcon();
			const queForUpdates = async () => {
				while (document.body.contains(friendDiv)) {
					updateIcon();
					await this.localuser.relationChange(this.id);
				}
			};
			setTimeout(queForUpdates, 100);
			friendDiv.onclick = async () => {
				if (this.relationshipType === 0 || this.relationshipType === 3) {
					await this.changeRelationship(1);
				} else if (this.relationshipType === 4) {
					//nothing
				} else {
					//also nothing
				}
			};
		}

		userbody.appendChild(usernamehtml);
		if (this.bot) {
			const username = document.createElement("span");
			username.classList.add("bot");
			username.textContent = this.webhook_id ? I18n.webhook() : I18n.bot();
			usernamehtml.appendChild(username);
		}
		userbody.appendChild(badgediv);
		const discrimatorhtml = document.createElement("h3");
		discrimatorhtml.classList.add("tag");
		discrimatorhtml.textContent = `${this.username}#${this.discriminator}`;
		userbody.appendChild(discrimatorhtml);

		const pronounshtml = document.createElement("p");
		pronounshtml.textContent = this.pronouns || "";
		pronounshtml.classList.add("pronouns");
		userbody.appendChild(pronounshtml);

		members.then((member) => {
			if (!member) return;
			if (member.pronouns && member.pronouns !== "") {
				pronounshtml.textContent = member.pronouns;
			}
		});

		const rule = document.createElement("hr");
		userbody.appendChild(rule);
		const biohtml = this.bio.makeHTML();
		userbody.appendChild(biohtml);

		members.then((member) => {
			if (!member) return;
			if (member.bio && member.bio !== "") {
				//TODO make markdown take Guild
				userbody.insertBefore(new MarkDown(member.bio, this.localuser).makeHTML(), biohtml);
				biohtml.remove();
			}
		});

		const send = document.createElement("input");
		if (!this.id.includes("#clone")) div.append(send);
		send.placeholder = I18n.user.sendMessage(this.name);
		send.onkeyup = (e) => {
			if (e.key === "Enter") {
				this.opendm(send.value);
				div.remove();
			}
		};

		if (guild) {
			members.then((member) => {
				if (!member) return;
				send.placeholder = I18n.user.sendMessage(member.name);
				usernamehtml.textContent = member.name;
				if (this.bot) {
					const username = document.createElement("span");
					username.classList.add("bot");
					username.textContent = this.webhook_id ? I18n.webhook() : I18n.bot();
					usernamehtml.appendChild(username);
				}
				const roles = document.createElement("div");
				roles.classList.add("flexltr", "rolesbox");
				for (const role of member.roles) {
					if (role.id === member.guild.id) continue;
					const roleDiv = document.createElement("div");
					roleDiv.classList.add("rolediv");
					const color = document.createElement("div");
					roleDiv.append(color);
					color.style.setProperty("--role-color", role.getColorStyle(true));
					color.classList.add("colorrolediv");
					const span = document.createElement("span");
					roleDiv.append(span);
					span.textContent = role.name;
					roles.append(roleDiv);
				}
				userbody.append(roles);
			});
		}

		if (x !== -1) {
			Contextmenu.declareMenu(div);
			document.body.appendChild(div);
			Contextmenu.keepOnScreen(div);
		}
		return div;
	}
	getBanner(guild: Guild | null | Member): HTMLImageElement {
		const banner = createImg(undefined);

		const bsrc = this.getBannerUrl();
		if (bsrc) {
			banner.setSrcs(bsrc);
			banner.classList.add("banner");
		}

		if (guild) {
			if (guild instanceof Member) {
				const bsrc = guild.getBannerUrl();
				if (bsrc) {
					banner.setSrcs(bsrc);
					banner.classList.add("banner");
				}
			} else {
				Member.resolveMember(this, guild).then((memb) => {
					if (!memb) return;
					const bsrc = memb.getBannerUrl();
					if (bsrc) {
						banner.setSrcs(bsrc);
						banner.classList.add("banner");
					}
				});
			}
		}
		return banner;
	}
	getBannerUrl(): string | undefined {
		if (this.banner) {
			if (!this.hypotheticalbanner) {
				return (
					`${this.info.cdn}/banners/${this.id.replace("#clone", "")}/${this.banner}.png` +
					new CDNParams({expectedSize: 160})
				);
			} else {
				return this.banner;
			}
		} else {
			return undefined;
		}
	}
	profileclick(obj: HTMLElement, guild?: Guild): void {
		const getIndex = (elm: HTMLElement) => {
			const index = getComputedStyle(elm).zIndex;
			if (index === "auto") {
				if (elm.parentElement) {
					return getIndex(elm.parentElement);
				}
			}
			return +index;
		};
		obj.onclick = (e: MouseEvent) => {
			const index = 1 + getIndex(obj);
			this.buildprofile(e.clientX, e.clientY, guild, index);
			e.stopPropagation();
		};
	}
}

User.setUpContextMenu();
export {User};
