/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

import type iItems from 'traits/i-items/i-items';
import type { CreateFromItemFn } from 'traits/i-items/i-items';

import type bVirtualScrollNew from 'base/b-virtual-scroll-new/b-virtual-scroll-new';

import type {

	VirtualScrollState,
	ComponentDb,
	RequestQueryFn,
	ShouldPerform,
	ItemsProcessors,

	ComponentItemFactory,
	ComponentItemType,
	ComponentItemMeta

} from 'base/b-virtual-scroll-new/interface';

import { defaultShouldProps, componentItemType, itemsProcessors } from 'base/b-virtual-scroll-new/const';

import iData, { component, prop } from 'super/i-data/i-data';

@component()
export default abstract class iVirtualScrollProps extends iData {
	/** {@link iItems.item} */
	readonly Item!: object;

	/** {@link iItems.Items} */
	readonly Items!: Array<this['Item']>;

	/** {@link iItems.item} */
	@prop({type: [String, Function]})
	readonly item?: iItems['item'];

	/** {@link iItems.items} */
	@prop({type: [String, Function]})
	readonly items: iItems['items'];

	/** {@link iItems.itemKey} */
	@prop({type: [String, Function]})
	readonly itemKey?: CreateFromItemFn<object, string>;

	/** {@link ComponentItemType} */
	@prop({type: [String, Function]})
	readonly itemType: keyof ComponentItemType | CreateFromItemFn<object, ComponentItemType> = componentItemType.item;

	/** {@link iItems.itemProps} */
	@prop({type: [Function, Object], default: () => ({})})
	readonly itemProps!: iItems['itemProps'];

	/**
	 * Meta information for a component that will not be used during rendering,
	 * but will be available for reading/changing in `itemsProcessors`.
	 *
	 * If a function is provided, it will be called; otherwise, the value will be preserved "as is".
	 *
	 * @example
	 * ```typescript
	 * const itemMeta = (data) => ({
	 *   componentData: data
	 * })
	 * ```
	 */
	@prop()
	readonly itemMeta?: CreateFromItemFn<object, ComponentItemMeta>;

	/**
	 * Specifies the number of times the `tombstone` component will be rendered.
	 *
	 * This prop can be useful if you want to render multiple `tombstone` components
	 * using a single specified element. For example, if you set `tombstoneCount` to 3,
	 * then three `tombstone` components will be rendered on your page.
	 *
	 * @example
	 * ```
	 * < b-virtual-scroll-new :tombstoneCount = 3
	 *   < template #tombstone
	 *     < .&__skeleton
	 *       Skeleton
	 * ```
	 *
	 * ```html
	 * <div class="..__skeleton">Skeleton</div>
	 * <div class="..__skeleton">Skeleton</div>
	 * <div class="..__skeleton">Skeleton</div>
	 * ```
	 */
	@prop(Number)
	readonly tombstoneCount?: number;

	/**
	 * This factory function is used to pass information about the components that need to be rendered.
	 * The function should return an array of arbitrary length consisting of objects that satisfy the
	 * {@link ComponentItem} interface.
	 *
	 * By default, the rendering strategy is based on the `chunkSize` and `iItems` trait.
	 * In other words, the default implementation takes a data slice of length `chunkSize`
	 * and calls the `iItems` functions to generate a `ComponentItem` object.
	 *
	 * However, the client can implement any required strategy by overriding this function.
	 *
	 * For example, it is possible to define a function
	 * that takes the last loaded data and renders twice as many components:
	 *
	 * @example
	 * ```typescript
	 * const itemsFactory = (state) => {
	 *   const data = state.lastLoadedData;
	 *
	 *   const items = data.map<ComponentItem>((item) => ({
	 *     item: 'section',
	 *     key: Object.cast(undefined),
	 *     type: 'item',
	 *     children: [],
	 *     props: {
	 *       'data-index': item.i
	 *     }
	 *   }));
	 *
	 *   return [...items, ...items];
	 * }
	 * ```
	 */
	@prop({
		type: Function,
		default: (state: VirtualScrollState, ctx: bVirtualScrollNew) => {
			const descriptors = ctx.getNextDataSlice(state, ctx.getChunkSize(state)).map((data, i) => ({
				key: ctx.itemKey?.(data, i),

				item: Object.isFunction(ctx.item) ? ctx.item(data, i) : ctx.item,
				type: Object.isFunction(ctx.itemType) ? ctx.itemType(data, i) : ctx.itemType,

				meta: {
					data,
					...Object.isFunction(ctx.itemMeta) ? ctx.itemMeta(data, i) : ctx.itemMeta
				},

				props: Object.isFunction(ctx.itemProps) ?
					ctx.itemProps(data, i, {
						key: ctx.itemKey?.(data, i),
						ctx
					}) :
					ctx.itemProps
			}));

			return descriptors;
		}
	})

	readonly itemsFactory!: ComponentItemFactory;

	/**
	 * This processor function enables you to manipulate previously compiled
	 * {@link ComponentItem}s via {@link bVirtualScroll.itemsFactory}. It allows you to add components to render,
	 * mutate props, and add children. It acts as middleware for rendering components.
	 *
	 * Scenarios where you might use this functionality:
	 *
	 * **Scenario**: Add an advertisement component after each rendered component
	 * in `b-virtual-scroll-new` throughout the app.
	 *
	 * **Solution**: Instead of overriding {@link bVirtualScroll.itemsFactory} inline,
	 * use {@link bVirtualScroll.itemsProcessors} for a centralized solution.
	 *
	 * @example
	 * ```typescript
	 * const addAds = (items: ComponentItem[]) => {
	 *   const newItems = [];
	 *
	 *   items.forEach((item) => {
	 *     newItems.push(item);
	 *
	 *     if (item.type === 'item') {
	 *       newItems.push({
	 *         type: 'separator',
	 *         item: 'b-ads-component',
	 *         props: { prop: 'val' },
	 *         key: 'uniqueKey'
	 *       });
	 *     }
	 *   });
	 *
	 *   return newItems;
	 * }
	 * ```
	 *
	 * To set this function as the global component processor in `b-virtual-scroll-new`,
	 * override the `itemsProcessors` constant (in `base/b-virtual-scroll-new/const.ts`) of your layer and export it.
	 *
	 * @example
	 * ```typescript
	 * export const itemsProcessors = {
	 *   addAds
	 * }
	 * ```
	 *
	 * After redefining this, `b-virtual-scroll-new` renders `b-ads-component` after
	 * each `item` component.
	 *
	 * **Scenario**: Replace `b-card` components with `b-mega-card` throughout the app
	 * and modify props.
	 *
	 * **Solution**: Add a processor function that changes the component name and mutates props.
	 *
	 * @example
	 * ```typescript
	 * const itemsProcessors = {
	 *   addAds,
	 *   migrateCardComponent: (items: ComponentItem[]) => {
	 *     return items.map((item) => {
	 *       if (item.item === 'b-card') {
	 *         console.warn('Deprecation: b-card is deprecated.');
	 *
	 *         return {
	 *           ...item,
	 *           props: convertProps(item.props),
	 *           item: 'b-mega-card'
	 *         };
	 *       }
	 *
	 *       return item;
	 *     });
	 *   }
	 * }
	 * ```
	 */
	@prop({
		type: [Function, Object, Array],
		default: itemsProcessors
	})

	readonly itemsProcessors?: ItemsProcessors;

	override readonly DB!: ComponentDb;

	/**
	 * A function that returns the GET parameters for a request. This function is called for each request. It receives the
	 * current component state and should return the request parameters. These parameters are merged with the parameters
	 * from the `request` prop in favor of the second one.
	 *
	 * This function is useful when you need to pass pagination parameters or any other parameters that should not trigger
	 * a component's state reload, unlike changing the `request` prop.
	 *
	 * {@link RequestQueryFn}
	 */
	@prop({type: Function})
	readonly requestQuery?: RequestQueryFn;

	/**
	 * The amount of data required to perform one cycle of item rendering.
	 *
	 * This prop is primarily used to determine whether a specific action with the data needs to be performed
	 * ({@link bVirtualScroll.renderGuard}), and only secondarily for component rendering.
	 *
	 * By default, this prop is used in {@link bVirtualScroll.itemsFactory} to slice the data
	 * according to the {@link bVirtualScroll.chunkSize} and render components based on it.
	 * However, it is possible to define a custom {@link bVirtualScroll.itemsFactory} and render as many components
	 * as desired in one cycle of rendering. In this case, the `chunkSize` will only have significance for the data.
	 *
	 * This prop can also be a function that should return the amount of data required to perform one cycle of rendering.
	 * For example, different values can be specified depending on the rendering page:
	 *
	 * @example
	 * ```typescript
	 * const chunkSize = (state: VirtualScrollState) => {
	 *   return [6, 12, 18][state.renderPage] ?? 18;
	 * }
	 * ```
	 */
	@prop({type: [Number, Function]})
	readonly chunkSize: number | ShouldPerform<number> = 10;

	/**
	 * The amount of data that the component can preload and use afterwards.
	 * By default, `b-virtual-scroll-new` requests data only when it is not enough to render a chunk,
	 * but often it is necessary to have a behavior where data is preloaded in advance.
	 *
	 * This prop allows you to configure data preloading and allows `b-virtual-scroll-new`
	 * to preload as much data as you specify.
	 *
	 * The prop can also be a function, for example, you can configure data preloading depending on loadPage:
	 *
	 * ```typescript
	 * preloadAmount(state: VirtualScrollState, _ctx: bVirtualScrollNew): number {
	 *   const
	 *     chunkSize = this.getRequestChunkSize(feed),
	 *    {loadPage} = v;
	 *
	 *   return loadPage < 4 ? chunkSize : chunkSize * 4;
	 * }
	 * ```
	 */
	@prop({type: [Number, Function]})
	readonly preloadAmount: number | ShouldPerform<number> = 0;

	/**
	 * When this function returns true the component will stop to request new data.
	 * This function will be called on each data loading cycle.
	 */
	@prop({
		type: Function,
		default: defaultShouldProps.shouldStopRequestingData
	})

	readonly shouldStopRequestingData!: ShouldPerform;

	/**
	 * This function is called in the {@link bVirtualScroll.renderGuard} after other checks are completed.
	 *
	 * This function receives the component state as input, based on which the client
	 * should determine whether the component should render the next chunk of components.
	 *
	 * For example, if we want to render the next data chunk only when the client
	 * has seen all the main (`type=item`) components, we can implement the following function:
	 *
	 * @example
	 * ```typescript
	 * const shouldPerformDataRender = (state) => {
	 *   return state.isInitialRender || state.remainingItems === 0;
	 * }
	 * ```
	 */
	@prop({type: Function, default: defaultShouldProps.shouldPerformDataRender})
	readonly shouldPerformDataRender?: ShouldPerform<boolean>;

	/**
	 * Setting this property to false will disable the {@link Observer observation module}. This is useful when you
	 * want to implement lazy rendering not based on scrolling but on some other event, such as a click. In this case,
	 * you should use manual invocation of the `initLoadNext` method to render chunks.
	 */
	@prop(Boolean)
	readonly disableObserver: boolean = false;
}
