import { Component } from "@odoo/owl";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { Domain } from "@web/core/domain";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import {
    deserializeDate,
    deserializeDateTime,
    formatDate,
    formatDateTime,
    serializeDate,
    serializeDateTime,
} from "@web/core/l10n/dates";
import { _t } from "@web/core/l10n/translation";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useService } from "@web/core/utils/hooks";
import { formatInteger, formatMany2one, formatMonetary } from "@web/views/fields/formatters";
import { formatFloat } from "@web/core/utils/numbers";
import { parseFloat, parseInteger, parseMonetary } from "@web/views/fields/parsers";
import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
import { PropertyTags } from "./property_tags";
import { PropertyText } from "./property_text";
import { imageUrl } from "@web/core/utils/urls";
import { getCurrency } from "@web/core/currency";
import { nbsp } from "@web/core/utils/strings";

function extractData(record) {
    let name;
    if ("display_name" in record) {
        name = record.display_name;
    } else if ("name" in record) {
        name = record.name.id ? record.name.display_name : record.name;
    }
    return { id: record.id, display_name: name };
}

/**
 * Represent one property value.
 * Supports many types and instantiates the appropriate component for it.
 * - Text
 * - Integer
 * - Boolean
 * - Selection
 * - Datetime & Date
 * - Many2one
 * - Many2many
 * - Monetary
 * - Tags
 * - ...
 */
export class PropertyValue extends Component {
    static template = "web.PropertyValue";
    static components = {
        Dropdown,
        DropdownItem,
        CheckBox,
        DateTimeInput,
        Many2XAutocomplete,
        TagsList,
        PropertyTags,
        PropertyText,
    };

    static props = {
        id: { type: String, optional: true },
        type: { type: String, optional: true },
        comodel: { type: String, optional: true },
        currencyField: { type: String, optional: true },
        domain: { type: String, optional: true },
        string: { type: String, optional: true },
        value: { optional: true },
        context: { type: Object },
        readonly: { type: Boolean, optional: true },
        canChangeDefinition: { type: Boolean, optional: true },
        selection: { type: Array, optional: true },
        tags: { type: Array, optional: true },
        onChange: { type: Function, optional: true },
        onTagsChange: { type: Function, optional: true },
        record: { type: Object, optional: true },
    };

    setup() {
        this.nbsp = nbsp;

        this.orm = useService("orm");
        this.action = useService("action");

        this.openMany2X = useOpenMany2XRecord({
            resModel: this.props.model,
            activeActions: {
                create: false,
                createEdit: false,
                write: true,
            },
            isToMany: false,
            onRecordSaved: async (record) => {
                if (!record) {
                    return;
                }
                // maybe the record display name has changed
                const records = await this.orm.read(
                    record.resModel,
                    [record.resId],
                    ["display_name"],
                    {
                        context: this.context,
                    }
                );
                const recordData = extractData(records[0]);
                await this.onValueChange([recordData]);
            },
            fieldString: this.props.string,
        });
    }

    /* --------------------------------------------------------
     * Public methods / Getters
     * -------------------------------------------------------- */

    get currency() {
        if (!isNaN(this.currencyId)) {
            return getCurrency(this.currencyId) || null;
        }
        return null;
    }

    get currencyId() {
        const currency = this.props.record.data[this.props.currencyField];
        return currency && currency.id;
    }

    /**
     * Return the value of the current property,
     * that will be used by the sub-components.
     *
     * @returns {object}
     */
    get propertyValue() {
        const value = this.props.value;

        if (this.props.type === "float") {
            // force to show at least 1 digit, even for integers
            return value;
        } else if (this.props.type === "datetime") {
            const datetimeValue = typeof value === "string" ? deserializeDateTime(value) : value;
            return datetimeValue && !datetimeValue.invalid ? datetimeValue : false;
        } else if (this.props.type === "date") {
            const dateValue = typeof value === "string" ? deserializeDate(value) : value;
            return dateValue && !dateValue.invalid ? dateValue : false;
        } else if (this.props.type === "boolean") {
            return !!value;
        } else if (this.props.type === "selection") {
            const options = this.props.selection || [];
            const option = options.find((option) => option[0] === value);
            return option && option.length === 2 && option[0] ? option[0] : "";
        } else if (this.props.type === "many2one") {
            return !value || !value.id || !value.display_name ? false : value;
        } else if (this.props.type === "many2many") {
            if (!value || !value.length) {
                return [];
            }

            // Convert to TagsList component format
            return value.map((many2manyValue) => {
                const hasAccess = many2manyValue[1] !== null;
                return {
                    id: many2manyValue[0],
                    comodel: this.props.comodel,
                    text: hasAccess ? many2manyValue[1] : _t("No Access"),
                    onClick:
                        hasAccess &&
                        this.clickableRelational &&
                        (async () => await this._openRecord(this.props.comodel, many2manyValue[0])),
                    onDelete:
                        !this.props.readonly &&
                        hasAccess &&
                        (() => this.onMany2manyDelete(many2manyValue[0])),
                    colorIndex: 0,
                    img:
                        this.showAvatar && hasAccess
                            ? imageUrl(this.props.comodel, many2manyValue[0], "avatar_128")
                            : null,
                };
            });
        } else if (this.props.type === "tags") {
            return value || [];
        }

        return value;
    }

    /**
     * Return the model domain (related to many2one and many2many properties).
     *
     * @returns {array}
     */
    get propertyDomain() {
        if (!this.props.domain || !this.props.domain.length) {
            return [];
        }
        let domain = new Domain(this.props.domain);
        if (this.props.type === "many2many" && this.props.value) {
            domain = Domain.and([
                domain,
                [["id", "not in", this.props.value.map((rec) => rec[0])]],
            ]);
        }
        return domain.toList();
    }

    /**
     * Formatted value displayed in readonly mode.
     *
     * @returns {string}
     */
    get displayValue() {
        const value = this.propertyValue;

        if (this.props.type === "many2one" && value && value.length === 2) {
            return formatMany2one(value);
        } else if (this.props.type === "integer") {
            return formatInteger(value || 0);
        } else if (this.props.type === "float") {
            return formatFloat(value || 0);
        } else if (this.props.type === "monetary") {
            return formatMonetary(value || 0, {
                digits: this.currency?.digits,
                currencyId: this.currencyId,
                noSymbol: !this.props.readonly,
            });
        } else if (!value) {
            return false;
        } else if (this.props.type === "datetime" && value) {
            return formatDateTime(value);
        } else if (this.props.type === "date" && value) {
            return formatDate(value);
        } else if (this.props.type === "selection") {
            return this.props.selection.find((option) => option[0] === value)[1];
        }
        return value.toString();
    }

    /**
     * Return true if the relational properties are clickable.
     *
     * @returns {boolean}
     */
    get clickableRelational() {
        return !this.env.config || this.env.config.viewType !== "kanban";
    }

    /**
     * Return True if we need to display a avatar for the current property.
     *
     * @returns {boolean}
     */
    get showAvatar() {
        return (
            ["many2one", "many2many"].includes(this.props.type) &&
            ["res.users", "res.partner"].includes(this.props.comodel)
        );
    }

    /* --------------------------------------------------------
     * Event handlers
     * -------------------------------------------------------- */

    /**
     * Parse the value received by the sub-components and trigger an onChange event.
     *
     * @param {object} newValue
     */
    async onValueChange(newValue) {
        if (this.props.type === "datetime") {
            newValue = newValue && serializeDateTime(newValue);
        } else if (this.props.type === "date") {
            newValue = newValue && serializeDate(newValue);
        } else if (this.props.type === "integer") {
            try {
                newValue = parseInteger(newValue) || 0;
            } catch {
                newValue = 0;
            }
        } else if (this.props.type === "float") {
            try {
                newValue = parseFloat(newValue) || 0;
            } catch {
                newValue = 0;
            }
        } else if (["many2one", "many2many"].includes(this.props.type)) {
            newValue = newValue[0];
            if (newValue && newValue.id && newValue.display_name === undefined) {
                // The "Search more" option in the Many2XAutocomplete component
                // only return the record ID, and not the name. But we need to name
                // in the component props to be able to display it.
                // Make a RPC call to resolve the display name of the record.
                newValue = await this._nameGet(newValue.id);
            }

            if (this.props.type === "many2many" && newValue) {
                // add the record in the current many2many list
                const currentValue = this.props.value || [];
                const recordId = newValue.id;
                const exists = currentValue.find((rec) => rec.id === recordId);
                if (exists) {
                    return;
                }
                newValue = [...currentValue, [newValue.id, newValue.display_name]];
            }
        } else if (this.props.type === "monetary") {
            try {
                newValue = parseMonetary(newValue) || 0;
            } catch {
                newValue = 0;
            }
        }

        // trigger the onchange event to notify the parent component
        this.props.onChange(newValue);
    }

    /**
     * Open the form view of the current record.
     *
     * @param {event} event
     */
    async onMany2oneClick(event) {
        if (this.props.readonly) {
            event.stopPropagation();
            await this._openRecord(this.props.comodel, this.propertyValue.id);
        }
    }

    /**
     * Open the current many2one record form view in a modal.
     */
    onExternalLinkClick() {
        return this.openMany2X({
            resId: this.propertyValue.id,
            forceModel: this.props.comodel,
            context: this.context,
        });
    }

    /**
     * Removed a record from the many2many list.
     *
     * @param {integer} many2manyId
     */
    onMany2manyDelete(many2manyId) {
        // deep copy
        const currentValue = JSON.parse(JSON.stringify(this.props.value || []));
        const newValue = currentValue.filter((value) => value[0] !== many2manyId);
        this.props.onChange(newValue);
    }

    /**
     * Ask to create a record from a relational property.
     *
     * @param {string} name
     */
    async onQuickCreate(name) {
        const result = await this.orm.call(this.props.comodel, "name_create", [name], {
            context: this.props.context,
        });
        this.onValueChange([{ id: result[0], display_name: result[1] }]);
    }

    /* --------------------------------------------------------
     * Private methods
     * -------------------------------------------------------- */

    /**
     * Open the form view of the given record id / model.
     *
     * @param {string} recordModel
     * @param {integer} recordId
     */
    async _openRecord(recordModel, recordId) {
        const action = await this.orm.call(recordModel, "get_formview_action", [[recordId]], {
            context: this.props.context,
        });

        this.action.doAction(action);
    }

    /**
     * Get the display name of the given record.
     * Model is taken from the current selected model.
     *
     * @param {string} recordId
     * @returns {array} [record id, record name]
     */
    async _nameGet(recordId) {
        const result = await this.orm.read(this.props.comodel, [recordId], ["display_name"], {
            context: this.props.context,
        });
        return result[0];
    }
}
