/*
	GoToSocial
	Copyright (C) GoToSocial Authors admin@gotosocial.org
	SPDX-License-Identifier: AGPL-3.0-or-later

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import React, { useCallback, useMemo } from "react";
import {
	useDefaultInteractionPoliciesQuery,
	useResetDefaultInteractionPoliciesMutation,
	useUpdateDefaultInteractionPoliciesMutation,
} from "../../../../lib/query/user";
import Loading from "../../../../components/loading";
import { Error } from "../../../../components/error";
import MutationButton from "../../../../components/form/mutation-button";
import type {
	DefaultInteractionPolicies,
	InteractionPolicy,
	InteractionPolicyEntry,
	InteractionPolicyValue} from "../../../../lib/types/interaction";
import {
	PolicyValueAuthor,
	PolicyValueFollowers,
	PolicyValueFollowing,
	PolicyValueMentioned,
	PolicyValuePublic,
} from "../../../../lib/types/interaction";
import { useTextInput } from "../../../../lib/form";
import { Select } from "../../../../components/form/inputs";
import type { TextFormInputHook } from "../../../../lib/form/types";
import { useBasicFor } from "./basic";
import type { PolicyFormSomethingElse} from "./something-else";
import { useSomethingElseFor } from "./something-else";
import type { Action, PolicyFormSub, SomethingElseValue, Visibility } from "./types";

export default function InteractionPolicySettings() {
	const {
		data: defaultPolicies,
		isLoading,
		isFetching,
		isError,
		error,
	} = useDefaultInteractionPoliciesQuery();

	if (isLoading || isFetching) {
		return <Loading />;
	}

	if (isError) {
		return <Error error={error} />;
	}

	if (!defaultPolicies) {
		throw "default policies undefined";
	}

	return (
		<InteractionPoliciesForm defaultPolicies={defaultPolicies} />
	);
}

interface InteractionPoliciesFormProps {
	defaultPolicies: DefaultInteractionPolicies;
}

function InteractionPoliciesForm({ defaultPolicies }: InteractionPoliciesFormProps) {
	// Sub-form for visibility "public".
	const formPublic = useFormForVis(defaultPolicies.public, "public");
	const assemblePublic = useCallback(() => {
		return {
			can_favourite: assemblePolicyEntry("public", "favourite", formPublic),
			can_reply: assemblePolicyEntry("public", "reply", formPublic),
			can_reblog: assemblePolicyEntry("public", "reblog", formPublic),
		};
	}, [formPublic]);
	
	// Sub-form for visibility "unlisted".
	const formUnlisted = useFormForVis(defaultPolicies.unlisted, "unlisted");
	const assembleUnlisted = useCallback(() => {
		return {
			can_favourite: assemblePolicyEntry("unlisted", "favourite", formUnlisted),
			can_reply: assemblePolicyEntry("unlisted", "reply", formUnlisted),
			can_reblog: assemblePolicyEntry("unlisted", "reblog", formUnlisted),
		};
	}, [formUnlisted]);
	
	// Sub-form for visibility "private".
	const formPrivate = useFormForVis(defaultPolicies.private, "private");
	const assemblePrivate = useCallback(() => {
		return {
			can_favourite: assemblePolicyEntry("private", "favourite", formPrivate),
			can_reply: assemblePolicyEntry("private", "reply", formPrivate),
			can_reblog: assemblePolicyEntry("private", "reblog", formPrivate),
		};
	}, [formPrivate]);

	const selectedVis = useTextInput("selectedVis", { defaultValue: "public" });
	
	const [updatePolicies, updateResult] = useUpdateDefaultInteractionPoliciesMutation();
	const [resetPolicies, resetResult] = useResetDefaultInteractionPoliciesMutation();

	const onSubmit = (e) => {
		e.preventDefault();
		updatePolicies({
			public: assemblePublic(),
			unlisted: assembleUnlisted(),
			private: assemblePrivate(),
			// Always use the
			// default for direct.
			direct: null,
		});
	};

	return (
		<form className="interaction-default-settings" onSubmit={onSubmit}>
			<div className="form-section-docs">
				<h3>Default Interaction Policies</h3>
				<p>
					You can use this section to customize the default interaction
					policy for posts created by you, per visibility setting.
					<br/>
					These settings apply only for new posts created by you <em>after</em> applying
					these settings; they do not apply retroactively.
					<br/>
					The word "anyone" in the below options means <em>anyone with
						permission to see the post</em>, taking account of blocks.
					<br/>
					Bear in mind that no matter what you set below, you will always
					be able to like, reply-to, and boost your own posts.
				</p>
				<a
					href="https://docs.gotosocial.org/en/latest/user_guide/settings#default-interaction-policies"
					target="_blank"
					className="docslink"
					rel="noreferrer"
				>
					Learn more about these settings (opens in a new tab)
				</a>
			</div>
			<div className="tabbable-sections">
				<PolicyPanelsTablist selectedVis={selectedVis} />
				<PolicyPanel
					policyForm={formPublic}
					forVis={"public"}
					isActive={selectedVis.value === "public"}
				/>
				<PolicyPanel
					policyForm={formUnlisted}
					forVis={"unlisted"}
					isActive={selectedVis.value === "unlisted"}
				/>
				<PolicyPanel
					policyForm={formPrivate}
					forVis={"private"}
					isActive={selectedVis.value === "private"}
				/>
			</div>

			<div className="action-buttons row">
				<MutationButton
					disabled={false}
					label="Save policies"
					result={updateResult}
				/>

				<MutationButton
					disabled={false}
					type="button"
					onClick={() => resetPolicies()}
					label="Reset to defaults"
					result={resetResult}
					className="button danger"
					showError={false}
				/>
			</div>

		</form>
	);
}

// A tablist of tab buttons, one for each visibility.
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
	return (
		<div className="tab-buttons" role="tablist">
			<Tab
				thisVisibility="public"
				label="Public"
				selectedVis={selectedVis}
			/>
			<Tab
				thisVisibility="unlisted"
				label="Unlisted"
				selectedVis={selectedVis}
			/>
			<Tab
				thisVisibility="private"
				label="Followers-only"
				selectedVis={selectedVis}
			/>
		</div>
	);
}

interface TabProps {
	thisVisibility: string;
	label: string,
	selectedVis: TextFormInputHook
}

// One tab in a tablist, corresponding to the given thisVisibility.
function Tab({ thisVisibility, label, selectedVis }: TabProps) {
	const selected = useMemo(() => {
		return selectedVis.value === thisVisibility;
	}, [selectedVis, thisVisibility]);

	return (
		<button
			id={`tab-${thisVisibility}`}
			title={label}
			role="tab"
			className={`tab-button ${selected && "active"}`}
			onClick={(e) => {
				e.preventDefault();
				selectedVis.setter(thisVisibility);
			}}
			aria-selected={selected}
			aria-controls={`panel-${thisVisibility}`}
			tabIndex={selected ? 0 : -1}
		>
			{label}
		</button>
	);
}

interface PolicyPanelProps {
	policyForm: PolicyForm;
	forVis: Visibility;
	isActive: boolean;
}

// Tab panel for one policy form of the given visibility.
function PolicyPanel({ policyForm, forVis, isActive }: PolicyPanelProps) {
	return (
		<div
			className={`interaction-policy-section ${isActive && "active"}`}
			role="tabpanel"
			hidden={!isActive}
		>
			<PolicyComponent
				form={policyForm.favourite}
				forAction="favourite"
			/>
			<PolicyComponent
				form={policyForm.reply}
				forAction="reply"
			/>
			{ forVis !== "private" &&
				<PolicyComponent
					form={policyForm.reblog}
					forAction="reblog"
				/>
			}
		</div>
	);
}

interface PolicyComponentProps {
	form: {
		basic: PolicyFormSub;
		somethingElse: PolicyFormSomethingElse;
	};
	forAction: Action;
}

// A component of one policy of the given
// visibility, corresponding to the given action.
function PolicyComponent({ form, forAction }: PolicyComponentProps) {	
	const legend = useLegend(forAction);
	return (
		<fieldset>
			<legend>{legend}</legend>
			{ forAction === "reply" &&
				<div className="info">
					<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
					<b>Mentioned accounts can always reply.</b>
				</div>	
			}
			<Select
				field={form.basic.field}
				label={form.basic.label}
				options={form.basic.options}
			/>
			{/* Include advanced "something else" options if appropriate */}
			{ (form.basic.field.value === "something_else") &&
				<>
					<hr />
					<div className="something-else">
						<Select
							field={form.somethingElse.followers.field}
							label={form.somethingElse.followers.label}
							options={form.somethingElse.followers.options}
						/>
						<Select
							field={form.somethingElse.following.field}
							label={form.somethingElse.following.label}
							options={form.somethingElse.following.options}
						/>
						{/*
							Skip mentioned accounts field for reply action,
							since mentioned accounts can always reply.
						*/}
						{ forAction !== "reply" &&
							<Select
								field={form.somethingElse.mentioned.field}
								label={form.somethingElse.mentioned.label}
								options={form.somethingElse.mentioned.options}
							/>
						}
						<Select
							field={form.somethingElse.everyoneElse.field}
							label={form.somethingElse.everyoneElse.label}
							options={form.somethingElse.everyoneElse.options}
						/>
					</div>
				</>
			}
		</fieldset>
	);
}

/*
	UTILITY FUNCTIONS
*/

// useLegend returns an appropriate
// fieldset legend for the given action.
function useLegend(action: Action) {
	return useMemo(() => {
		switch (action) {
			case "favourite":
				return (
					<>
						<i className="fa fa-fw fa-star" aria-hidden="true"></i>
						<span>Like</span>
					</>
				);
			case "reply":
				return (
					<>
						<i className="fa fa-fw fa-reply-all" aria-hidden="true"></i>
						<span>Reply</span>
					</>
				);
			case "reblog":
				return (
					<>
						<i className="fa fa-fw fa-retweet" aria-hidden="true"></i>
						<span>Boost</span>
					</>
				);
		}
	}, [action]);
}

// Form encapsulating the different
// actions for one visibility.
interface PolicyForm {
	favourite: {
		basic: PolicyFormSub,
		somethingElse: PolicyFormSomethingElse,
	}
	reply: {
		basic: PolicyFormSub,
		somethingElse: PolicyFormSomethingElse,
	}
	reblog: {
		basic: PolicyFormSub,
		somethingElse: PolicyFormSomethingElse,
	}
}

// Return a PolicyForm for the given visibility,
// set already to whatever the defaultPolicies value is.
function useFormForVis(
	currentPolicy: InteractionPolicy,
	forVis: Visibility,
): PolicyForm {	
	return {
		favourite: {
			basic: useBasicFor(
				forVis,
				"favourite",
				currentPolicy.can_favourite.always,
				currentPolicy.can_favourite.with_approval,
			),
			somethingElse: useSomethingElseFor(
				forVis,
				"favourite",
				currentPolicy.can_favourite.always,
				currentPolicy.can_favourite.with_approval,
			),
		},
		reply: {
			basic: useBasicFor(
				forVis,
				"reply",
				currentPolicy.can_reply.always,
				currentPolicy.can_reply.with_approval,
			),
			somethingElse: useSomethingElseFor(
				forVis,
				"reply",
				currentPolicy.can_reply.always,
				currentPolicy.can_reply.with_approval,
			),
		},
		reblog: {
			basic: useBasicFor(
				forVis,
				"reblog",
				currentPolicy.can_reblog.always,
				currentPolicy.can_reblog.with_approval,
			),
			somethingElse: useSomethingElseFor(
				forVis,
				"reblog",
				currentPolicy.can_reblog.always,
				currentPolicy.can_reblog.with_approval,
			),
		},
	};
}

function assemblePolicyEntry(
	forVis: Visibility,
	forAction: Action,
	policyForm: PolicyForm,
): InteractionPolicyEntry {
	const basic = policyForm[forAction].basic;
	
	// If this is followers visibility then
	// "anyone" only means followers, not public.
	const anyone: InteractionPolicyValue =
		(forVis === "private")
			? PolicyValueFollowers
			: PolicyValuePublic;
	
	// If this is a reply action then "just me"
	// must include mentioned accounts as well,
	// since they can always reply.
	const justMe: InteractionPolicyValue[] =
		(forAction === "reply")
			? [PolicyValueAuthor, PolicyValueMentioned]
			: [PolicyValueAuthor];

	switch (basic.field.value) {
		case "anyone":
			return {
				// Anyone can do this.
				always: [anyone],
				with_approval: [],
			};
		case "anyone_with_approval":
			return {
				// Author and maybe mentioned can do
				// this, everyone else needs approval.
				always: justMe,
				with_approval: [anyone],
			};
		case "just_me":
			return {
				// Only author and maybe
				// mentioned can do this.
				always: justMe,
				with_approval: [],
			};
	}

	// Something else!
	const somethingElse = policyForm[forAction].somethingElse;
	
	// Start with basic "always"
	// and "with_approval" values.
	let always: InteractionPolicyValue[] = justMe;
	let withApproval: InteractionPolicyValue[] = [];
	
	// Add PolicyValueFollowers depending on choices made.
	switch (somethingElse.followers.field.value as SomethingElseValue) {
		case "always":
			always.push(PolicyValueFollowers);
			break;
		case "with_approval":
			withApproval.push(PolicyValueFollowers);
			break;
	}

	// Add PolicyValueFollowing depending on choices made.
	switch (somethingElse.following.field.value as SomethingElseValue) {
		case "always":
			always.push(PolicyValueFollowing);
			break;
		case "with_approval":
			withApproval.push(PolicyValueFollowing);
			break;
	}

	// Add PolicyValueMentioned depending on choices made.
	// Note: mentioned can always reply, and that's already
	// included above, so only do this if action is not reply.
	if (forAction !== "reply") {
		switch (somethingElse.mentioned.field.value as SomethingElseValue) {
			case "always":
				always.push(PolicyValueMentioned);
				break;
			case "with_approval":
				withApproval.push(PolicyValueMentioned);
				break;
		}
	}

	// Add anyone depending on choices made.
	switch (somethingElse.everyoneElse.field.value as SomethingElseValue) {
		case "with_approval":
			withApproval.push(anyone);
			break;
	}

	// Simplify a bit after
	// all the parsing above.
	if (always.includes(anyone)) {
		always = [anyone];
	}

	if (withApproval.includes(anyone)) {
		withApproval = [anyone];
	}

	return {
		always: always,
		with_approval: withApproval,
	};
}
