/* *
 *
 *  Grid cell content validator
 *
 *  (c) 2009-2026 Highsoft AS
 *
 *  A commercial license may be required depending on use.
 *  See www.highcharts.com/license
 *
 *
 *  Authors:
 *  - Dawid Dragula
 *
 * */
'use strict';
import AST from '../../../Core/Renderer/HTML/AST.js';
import Globals from '../../Core/Globals.js';
import GridUtils from '../../Core/GridUtils.js';
import { defined } from '../../../Shared/Utilities.js';
const { makeDiv, setHTMLContent } = GridUtils;
/* *
 *
 *  Class
 *
 * */
/**
 * Class for validating cell content.
 */
class Validator {
    /* *
     *
     *  Constructor
     *
     * */
    constructor(viewport) {
        this.viewport = viewport;
        this.notifContainer = makeDiv(Validator.classNames.notifContainer);
        this.viewport.grid.contentWrapper?.appendChild(this.notifContainer);
    }
    /* *
     *
     *  Methods
     *
     * */
    /**
     * Validates the cell content.
     *
     * @param cell
     * Edited cell.
     *
     * @param errors
     * An output array for error messages.
     *
     * @returns
     * Returns true if the value is valid, false otherwise.
     */
    validate(cell, errors = []) {
        const { options, dataType } = cell.column;
        const validationErrors = cell.row.viewport.grid.options?.lang?.validationErrors;
        const rendererType = cell.column.options?.cells?.renderer?.type;
        let rules = Array.from(options?.cells?.editMode?.validationRules || []);
        // Remove duplicates in validationRules
        const isArrayString = rules.every((rule) => typeof rule === 'string');
        if (rules.length > 0 && isArrayString) {
            rules = [...new Set(rules)];
        }
        else {
            const predefined = [
                ...(Validator.predefinedRules.dataType[dataType] ?? [])
            ];
            if (rendererType) {
                predefined.push(...Validator.predefinedRules.renderer[rendererType] ?? []);
            }
            const hasPredefined = rules.some((rule) => typeof rule !== 'string' &&
                typeof rule.validate === 'string' &&
                predefined.includes(rule.validate));
            if (!hasPredefined) {
                rules.push(...predefined);
            }
        }
        for (const rule of rules) {
            let ruleDef;
            let err;
            if (typeof rule === 'string') {
                ruleDef = Validator.rulesRegistry[rule];
                err = validationErrors?.[rule]?.notification;
            }
            else {
                ruleDef = rule;
            }
            let validateFn;
            if (typeof ruleDef.validate === 'string') {
                const predefinedRules = (Validator.rulesRegistry[ruleDef.validate]);
                validateFn =
                    predefinedRules?.validate;
            }
            else {
                validateFn = ruleDef.validate;
            }
            const { editModeContent } = cell.column.viewport.cellEditing || {};
            if (typeof validateFn === 'function' &&
                editModeContent &&
                !validateFn.call(cell, editModeContent)) {
                if (typeof ruleDef.notification === 'function') {
                    err = ruleDef.notification.call(cell, editModeContent);
                }
                errors.push((err || ruleDef.notification));
            }
        }
        return !errors.length;
    }
    /**
     * Set content of notification and adjust the position.
     *
     * @param cell
     * Cell that is currently edited and is not valid.
     *
     * @param errors
     * An array of error messages.
     *
     */
    initErrorBox(cell, errors) {
        const { grid } = this.viewport;
        this.errorCell = cell;
        // Set error container position
        this.reflow();
        // Set width and content
        setHTMLContent(this.notifContainer, errors.join('<br />'));
        // A11y announcement
        if (grid.options?.accessibility?.announcements?.cellEditing) {
            this.viewport.grid.accessibility?.announce((grid.options?.lang?.accessibility?.cellEditing
                ?.announcements?.notValid || '') + ' ' + errors.join('. '), true);
        }
        this.show();
    }
    /**
     * Highlight the non-valid cell and display error in the notification box.
     */
    show() {
        this.errorCell?.htmlElement.classList.add(Validator.classNames.editedCellError);
        this.notifContainer.classList.add(Validator.classNames.notifError, Validator.classNames.notifAnimation);
        // Set for the aria-errormessage attribute.
        this.notifContainer.setAttribute('id', 'notification-error');
    }
    /**
     * Hide the notification, error and unset highlight on cell.
     *
     * @param hideErrorBox
     * The flag that hides the error on edited cell.
     *
     */
    hide(hideErrorBox = true) {
        this.errorCell?.htmlElement.classList.remove(Validator.classNames.editedCellError);
        this.notifContainer.classList.remove(Validator.classNames.notifError, Validator.classNames.notifAnimation);
        this.notifContainer.removeAttribute('id');
        if (hideErrorBox) {
            this.errorCell = void 0;
        }
        this.notifContainer.innerHTML = AST.emptyHTML;
    }
    /**
     * Set the position of the error box.
     */
    reflow() {
        const vp = this.viewport, errorCell = this.errorCell?.htmlElement, tableElement = vp.grid.tableElement, contentWrapper = vp.grid.contentWrapper;
        if (!errorCell || !tableElement || !contentWrapper) {
            return;
        }
        const tableTop = tableElement.offsetTop, tableHeight = tableElement.offsetHeight, middlePoint = tableTop + (tableHeight / 2), errorCellTop = errorCell.offsetTop - tableTop;
        if (errorCellTop > middlePoint) {
            this.notifContainer.style.top = // Avoid header overlap
                tableTop + (vp.theadElement?.offsetHeight || 0) + 'px';
            this.notifContainer.style.bottom = 'auto';
        }
        else {
            this.notifContainer.style.top = 'auto';
            this.notifContainer.style.bottom =
                contentWrapper.offsetHeight - tableTop - tableHeight + 'px';
        }
    }
    /**
     * Destroy validator.
     */
    destroy() {
        this.errorCell = void 0;
        this.notifContainer.remove();
    }
}
/**
 * The class names used by the validator functionality.
 */
Validator.classNames = {
    notifContainer: Globals.classNamePrefix + 'notification',
    notifError: Globals.classNamePrefix + 'notification-error',
    notifAnimation: Globals.classNamePrefix + 'notification-animation',
    editedCellError: Globals.classNamePrefix + 'edited-cell-error'
};
/**
 * Definition of default validation rules.
 */
Validator.rulesRegistry = {
    notEmpty: {
        validate: ({ value, rawValue }) => (defined(value) && rawValue.length > 0),
        notification: 'Value cannot be empty.'
    },
    number: {
        validate: ({ rawValue }) => !isNaN(+rawValue),
        notification: 'Value has to be a number.'
    },
    datetime: {
        validate: ({ value }) => !defined(value) || !isNaN(+value),
        notification: 'Value has to be parsed to a valid timestamp.'
    },
    'boolean': {
        validate: ({ rawValue }) => (rawValue === 'true' || rawValue === 'false' ||
            Number(rawValue) === 1 || Number(rawValue) === 0),
        notification: 'Value has to be a boolean.'
    },
    arrayNumber: {
        validate: function ({ rawValue }) {
            return rawValue
                .split(',')
                .every((item) => !Number.isNaN(Number(item.trim())));
        },
        notification: 'Value should be a list of numbers separated by commas.'
    },
    json: {
        validate: function ({ rawValue }) {
            try {
                JSON.parse(rawValue);
                return true;
            }
            catch {
                return false;
            }
        },
        notification: 'Value should be a valid JSON.'
    },
    sparkline: {
        validate: function ({ rawValue }) {
            const arrayNumberValidate = Validator.rulesRegistry.arrayNumber.validate;
            const jsonValidate = Validator.rulesRegistry.json.validate;
            return arrayNumberValidate({ rawValue }) ||
                jsonValidate({ rawValue });
        },
        notification: 'Value should be a valid JSON or a list of numbers ' +
            'separated by commas.'
    },
    ignoreCaseUnique: {
        validate: function ({ rawValue }) {
            const oldValue = String(this.value).toLowerCase();
            const rowValueString = rawValue.toLowerCase();
            if (oldValue === rowValueString) {
                return true;
            }
            // Local dataset only
            // TODO(enhancement): Implement this for remote dataset.
            const columnData = this.column.data;
            const isDuplicate = columnData?.some((value) => (String(value).toLowerCase() ===
                rowValueString));
            return !isDuplicate;
        },
        notification: 'Value must be unique within this column (case-insensitive).'
    },
    unique: {
        validate: function ({ rawValue }) {
            const oldValue = this.value;
            if (oldValue === rawValue) {
                return true;
            }
            // Local dataset only
            // TODO(enhancement): Implement this for remote dataset.
            const columnData = this.column.data;
            const isDuplicate = columnData?.some((value) => value === rawValue);
            return !isDuplicate;
        },
        notification: 'Value must be unique within this column (case-sensitive).'
    }
};
/**
 * Default validation rules for each dataType.
 */
Validator.predefinedRules = {
    dataType: {
        'boolean': ['boolean'],
        datetime: ['datetime'],
        number: ['number'],
        string: []
    },
    renderer: {
        sparkline: ['sparkline']
    }
};
/* *
 *
 *  Default Export
 *
 * */
export default Validator;
