import { html } from 'lit';
import { ApiSchemaValues } from '@api-client/core/amf/ApiSchemaValues.js';
import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js';
import { cancelEvent } from '@api-client/core/lib/events/Utils.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { classMap } from 'lit/directives/class-map.js';
import * as InputCache from './InputCache.js';
import { readLabelValue } from './Utils.js';
import '../../md/icons/ui-icon.js';
import '../../md/text-field/ui-text-field.js';
import '../../md/list/ui-list.js';
import '../../md/list/ui-list-item.js';
import '../../md/dropdown-list/ui-dropdown-list.js';
import '../../md/checkbox/ui-checkbox.js';
import '../../md/icon-button/ui-icon-button.js';
import '../../md/button/ui-filled-tonal-button.js';
/**
 * A class that works with UI editor that capture user input for AMF parameters.
 *
 * @fires paramchange - When a parameter value change. The detail object is the key value of changed parameter.
 * @fires render - When the UI should re-render.
 * @fires change - When the UI should notify the hosting application about the change.
 */
export class AmfParameters extends EventTarget {
    parametersValue;
    nilValues;
    target;
    globalCache;
    /**
     * @param target The element that initializes this class.
     */
    constructor(target, init = {}) {
        super();
        const { globalCache = false } = init;
        this.globalCache = globalCache;
        this.target = target;
        this.parametersValue = [];
        this.nilValues = [];
        this.addArrayValueHandler = this.addArrayValueHandler.bind(this);
        this.paramChangeHandler = this.paramChangeHandler.bind(this);
        this.booleanHandler = this.booleanHandler.bind(this);
        this.deleteParamHandler = this.deleteParamHandler.bind(this);
        this.enumSelectionHandler = this.enumSelectionHandler.bind(this);
        this.nilHandler = this.nilHandler.bind(this);
        this.customParamChangeHandler = this.customParamChangeHandler.bind(this);
    }
    paramChanged(key) {
        this.dispatchEvent(new CustomEvent('paramchange', { detail: key }));
    }
    notifyUpdate() {
        this.dispatchEvent(new Event('render'));
    }
    notifyChange() {
        this.dispatchEvent(new Event('change'));
    }
    /**
     * Clears previously set values in the cache storage.
     */
    clearCache() {
        const { parametersValue = [], target, globalCache } = this;
        parametersValue.forEach((param) => {
            InputCache.remove(target, param.paramId, globalCache);
        });
    }
    addArrayValueHandler(e) {
        const button = e.target;
        const { id } = button.dataset;
        if (!id) {
            return;
        }
        const { target, globalCache } = this;
        if (!InputCache.has(target, id, globalCache)) {
            InputCache.set(target, id, [], globalCache);
        }
        const items = InputCache.get(target, id, globalCache);
        items.push(undefined);
        this.notifyUpdate();
        this.paramChanged(id);
    }
    /**
     * Reads the value to be set on an input.
     *
     * @param parameter
     * @param schema
     * @param isArray Whether the value should be read for an array.
     * @returns The value to set on the input. Note, it is not cast to the type.
     */
    readInputValue(parameter, schema, isArray = false) {
        const { target, globalCache } = this;
        const { id } = parameter;
        if (InputCache.has(target, id, globalCache)) {
            return InputCache.get(target, id, globalCache);
        }
        let result;
        if (parameter.required) {
            const opts = { fromExamples: true };
            if (isArray) {
                result = ApiSchemaValues.readInputValues(parameter, schema, opts);
            }
            else {
                result = ApiSchemaValues.readInputValue(parameter, schema, opts);
            }
            if (result !== undefined) {
                InputCache.set(target, id, result, globalCache);
            }
        }
        return result;
    }
    paramChangeHandler(e) {
        const input = e.target;
        const { value, dataset } = input;
        const { domainId, isArray, index } = dataset;
        if (!domainId) {
            return;
        }
        const param = this.parametersValue.find((p) => p.paramId === domainId);
        if (!param) {
            return;
        }
        // sets cached value of the input.
        const typed = ApiSchemaValues.parseUserInput(value, param.schema);
        InputCache.set(this.target, domainId, typed, this.globalCache, isArray === 'true', index ? Number(index) : undefined);
        this.notifyChange();
        this.paramChanged(domainId);
    }
    booleanHandler(e) {
        const input = e.target;
        const { checked, dataset } = input;
        const { domainId, isArray, index } = dataset;
        if (!domainId) {
            return;
        }
        const param = this.parametersValue.find((p) => p.paramId === domainId);
        if (!param) {
            return;
        }
        InputCache.set(this.target, domainId, checked, this.globalCache, isArray === 'true', index ? Number(index) : undefined);
        this.notifyChange();
        this.paramChanged(domainId);
    }
    /**
     * A handler for the remove param button click.
     */
    deleteParamHandler(e) {
        const button = e.currentTarget;
        const { dataset } = button;
        const { domainId, index, deleteParam } = dataset;
        if (!domainId) {
            return;
        }
        let forceUpdate = false;
        if (deleteParam === 'true') {
            const pIndex = this.parametersValue.findIndex((p) => p.paramId === domainId);
            this.parametersValue.splice(pIndex, 1);
            forceUpdate = true;
        }
        if (InputCache.has(this.target, domainId, this.globalCache)) {
            InputCache.remove(this.target, domainId, this.globalCache, index ? Number(index) : undefined);
            forceUpdate = true;
        }
        if (forceUpdate) {
            this.notifyUpdate();
            this.notifyChange();
            this.paramChanged(domainId);
        }
    }
    enumSelectionHandler(e) {
        const { item } = e.detail;
        const dropDown = e.target;
        const { domainId, isArray, index } = dropDown.dataset;
        const { value } = item.dataset;
        if (!domainId) {
            return;
        }
        const param = this.parametersValue.find((p) => p.paramId === domainId);
        if (!param) {
            return;
        }
        const enumValues = param.schema.values;
        if (!enumValues) {
            return;
        }
        const typed = ApiSchemaValues.parseUserInput(value, param.schema);
        InputCache.set(this.target, domainId, typed, this.globalCache, isArray === 'true', index ? Number(index) : undefined);
        this.notifyChange();
        this.paramChanged(domainId);
        this.notifyUpdate();
    }
    /**
     * Handler for the nil value toggle.
     */
    nilHandler(e) {
        const button = e.target;
        const { checked, dataset } = button;
        const { domainId } = dataset;
        if (!domainId) {
            return;
        }
        const list = this.nilValues;
        const has = list.includes(domainId);
        if (checked && !has) {
            list.push(domainId);
            this.notifyUpdate();
        }
        else if (!checked && has) {
            const index = list.indexOf(domainId);
            list.splice(index, 1);
            this.notifyUpdate();
        }
        this.notifyChange();
        this.paramChanged(domainId);
    }
    /**
     * @param param The parameter to render.
     * @param opts Render options
     * @returns The template for the request parameter form control.
     */
    parameterTemplate(param, opts = {}) {
        const { schema, parameter, source } = param;
        if (source === 'custom') {
            return this.customParamTemplate(param);
        }
        if (!schema) {
            return '';
        }
        return this.parameterSchemaTemplate(parameter, schema, opts);
    }
    /**
     * @param parameter
     * @param schema
     * @param opts Internal Process options
     * @returns The template for the request parameter form control.
     */
    parameterSchemaTemplate(parameter, schema, opts = {}) {
        const { types } = schema;
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.ScalarShape)) {
            return this.scalarShapeTemplate(parameter, schema, opts);
        }
        if (types.includes(AmfNamespace.w3.shacl.NodeShape)) {
            return this.nodeShapeTemplate();
        }
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.UnionShape)) {
            return this.unionShapeTemplate(parameter, schema);
        }
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.FileShape)) {
            return this.fileShapeTemplate();
        }
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.SchemaShape)) {
            return this.schemaShapeTemplate();
        }
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.ArrayShape) ||
            types.includes(AmfNamespace.aml.vocabularies.shapes.MatrixShape)) {
            return this.arrayShapeTemplate(parameter, schema);
        }
        if (types.includes(AmfNamespace.aml.vocabularies.shapes.TupleShape)) {
            return this.tupleShapeTemplate(parameter, schema);
        }
        return this.anyShapeTemplate(parameter, schema);
    }
    /**
     * @param parameter
     * @param schema
     * @param opts
     * @returns The template for the schema parameter.
     */
    scalarShapeTemplate(parameter, schema, opts = {}) {
        const { readOnly, values, dataType = 'text' } = schema;
        if (readOnly) {
            return '';
        }
        if (values && values.length) {
            return this.enumTemplate(parameter, schema, opts);
        }
        const inputType = ApiSchemaValues.readInputType(dataType);
        if (inputType === 'boolean') {
            return this.booleanTemplate(parameter, schema, opts);
        }
        return this.textInputTemplate(parameter, schema, inputType, opts);
    }
    /**
     * Corrects the `http://www.w3.org/2001/XMLSchema#dateTime` schema with the `date-time` or `rfc3339` formats
     * to remove the last `.ZZZ` part which is not recognizable by the `<input>` element.
     */
    correctDateTimeParameter(schema, value) {
        if (!value) {
            return value;
        }
        if (schema.dataType === AmfNamespace.w3.xmlSchema.dateTime) {
            if (['date-time', 'rfc3339'].includes(schema.format || '')) {
                return String(value).replace(/(\.\d\d\d)Z/, '$1');
            }
        }
        return value;
    }
    /**
     * @param parameter
     * @param schema
     * @param type The input type.
     * @param opts
     * @returns A template for an input form item for the given type and schema
     */
    textInputTemplate(parameter, schema, type, opts = {}) {
        const { id, binding } = parameter;
        const { pattern, minimum, minLength, maxLength, maximum, multipleOf } = schema;
        let required = false;
        if (typeof opts.required === 'boolean') {
            required = opts.required;
        }
        else {
            required = parameter.required || false;
        }
        const label = readLabelValue(parameter, schema);
        const { allowEmptyValue } = parameter;
        let notifyValueChange = false;
        let value;
        if (opts.arrayItem) {
            value = opts.value || '';
        }
        else {
            value = this.readInputValue(parameter, schema);
            notifyValueChange = true;
        }
        if (value) {
            value = ApiSchemaValues.parseScalarInput(value, schema);
            value = this.correctDateTimeParameter(schema, value);
        }
        if (notifyValueChange && value !== undefined && value !== '') {
            const typed = ApiSchemaValues.parseUserInput(value, schema);
            InputCache.set(this.target, id, typed, this.globalCache);
            this.paramChanged(id);
        }
        let step;
        if (['time', 'datetime-local'].includes(type || '')) {
            step = 1;
        }
        else if (typeof multipleOf !== 'undefined') {
            step = Number(multipleOf);
        }
        const title = parameter.description || schema.description;
        const nillable = this.nilValues;
        const nillDisabled = !!opts.nillable && nillable.includes(id);
        const classes = {
            'form-item': true,
            required,
            'optional': !required,
        };
        const finalValue = value === undefined ? '' : String(value);
        return html `
      <div class="${classMap(classes)}">
        <ui-text-field
          data-domain-id="${id}"
          data-is-array="${ifDefined(opts.arrayItem)}"
          data-index="${ifDefined(opts.index)}"
          data-binding="${ifDefined(binding)}"
          name="${parameter.name || schema.name || ''}"
          class="form-input"
          ?required="${required && !allowEmptyValue}"
          .value="${finalValue}"
          pattern="${ifDefined(pattern)}"
          min="${ifDefined(minimum)}"
          minLength="${ifDefined(minLength)}"
          max="${ifDefined(maximum)}"
          maxLength="${ifDefined(maxLength)}"
          step="${ifDefined(step)}"
          type="${ifDefined(type)}"
          title="${ifDefined(title)}"
          @change="${this.paramChangeHandler}"
          ?disabled="${nillDisabled}"
          label="${label}"
        ></ui-text-field>
        ${opts.nillable ? this.nillInputTemplate(parameter) : ''}
        ${opts.arrayItem ? this.deleteParamTemplate(id, opts.index) : ''}
      </div>
    `;
    }
    /**
     * @param paramId The IApiParameter id.
     * @param arrayIndex When this is an array item, the index on the array.
     * @returns The template for the param remove button.
     */
    deleteParamTemplate(paramId, arrayIndex) {
        const title = 'Removes this parameter.';
        return html `
      <ui-icon-button
        title="${title}"
        data-domain-id="${paramId}"
        data-index="${ifDefined(arrayIndex)}"
        data-delete-param="true"
        @click="${this.deleteParamHandler}"
      >
        <ui-icon icon="deleteOutline" role="presentation"></ui-icon>
      </ui-icon-button>
    `;
    }
    /**
     * @param parameter
     * @param schema
     * @param opts
     * @returns The template for the enum input.
     */
    enumTemplate(parameter, schema, opts = {}) {
        let required = false;
        if (typeof opts.required === 'boolean') {
            required = opts.required;
        }
        else {
            required = parameter.required || false;
        }
        const label = readLabelValue(parameter, schema);
        const enumValues = (schema.values || []);
        const selectedValue = this.readInputValue(parameter, schema);
        const selected = enumValues.findIndex((i) => i.value === selectedValue);
        const { id, binding } = parameter;
        const title = parameter.description || schema.description;
        const nillable = this.nilValues;
        const nillDisabled = !!opts.nillable && nillable.includes(id);
        const classes = {
            'form-item': true,
            required,
            'optional': !required,
        };
        const item = enumValues[selected];
        const finalValue = item ? item.value || '' : '';
        return html `
      <div class="${classMap(classes)}">
        <ui-dropdown-list
          class="param-selector"
          closeOnOutsideClick
          matchTriggerWidth
          @select="${this.enumSelectionHandler}"
          data-binding="${ifDefined(binding)}"
          data-domain-id="${id}"
          data-is-array="${ifDefined(opts.arrayItem)}"
        >
          <ui-text-field
            class="dropdown-trigger"
            label="${label}"
            readOnly
            ?required="${required}"
            .value="${finalValue || ''}"
            @select="${cancelEvent}"
            name="${parameter.name || schema.name || ''}"
            ?disabled="${nillDisabled}"
            title="${ifDefined(title)}"
          >
            <ui-icon icon="arrowDropDown" role="presentation" slot="suffix"></ui-icon>
          </ui-text-field>
          <ui-list slot="dropdown" role="menu" selector="[data-value]" selectActive>
            ${enumValues.map((value) => this.renderEnumItem(value))}
          </ui-list>
        </ui-dropdown-list>
        ${opts.nillable ? this.nillInputTemplate(parameter) : ''}
      </div>
    `;
    }
    renderEnumItem(item) {
        const { dataType, value } = item;
        return html `
      <ui-list-item role="menuitem" data-type="${ifDefined(dataType)}" data-value="${ifDefined(value)}"
        >${value}</ui-list-item
      >
    `;
    }
    /**
     * @returns The template for the checkbox input.
     */
    booleanTemplate(parameter, schema, opts = {}) {
        const label = readLabelValue(parameter, schema);
        const { id } = parameter;
        let value;
        if (opts.arrayItem) {
            value = opts.value || false;
        }
        else {
            value = this.readInputValue(parameter, schema);
        }
        const title = parameter.description || schema.description;
        const nillable = this.nilValues;
        const nillDisabled = !!opts.nillable && nillable.includes(id);
        const classes = {
            'form-item': true,
            'required': parameter.required || false,
            'optional': !parameter.required,
        };
        // note, don't mark a checkbox as required. A checkbox also produces `false` value
        // which should be passed to the transport. The request data processor should take care of
        // producing the right output.
        return html `
      <div class="${classMap(classes)}">
        <label class="label-large">
          <ui-checkbox
            class="boolean-param"
            name="${parameter.name || schema.name || ''}"
            data-binding="${ifDefined(parameter.binding)}"
            .checked="${value}"
            title="${ifDefined(title)}"
            data-domain-id="${id}"
            data-is-array="${ifDefined(opts.arrayItem)}"
            ?disabled="${nillDisabled}"
            @change="${this.booleanHandler}"
            aria-label="${label}"
          ></ui-checkbox>
          ${label}
        </label>
        ${opts.nillable ? this.nillInputTemplate(parameter) : ''}
      </div>
    `;
    }
    /**
     * @returns The template for the nil checkbox input.
     */
    nillInputTemplate(parameter) {
        return html `
      <label class="label-large">
        <ui-checkbox
          class="nil-option"
          data-domain-id="${parameter.id}"
          data-binding="${ifDefined(parameter.binding)}"
          title="Makes the property nillable (e.g. inserts null into the schema)"
          @change="${this.nilHandler}"
          aria-label="Nil"
        ></ui-checkbox>
        Nil
      </label>
    `;
    }
    /**
     * or now we do not support union shapes. There's no way to learn how to serialize
     * the Node shape to a string.
     * @returns {TemplateResult|string} The template for the node shape.
     */
    nodeShapeTemplate() {
        return '';
    }
    /**
     * @returns The template for the union shape.
     */
    unionShapeTemplate(parameter, schema) {
        const { anyOf } = schema;
        if (!anyOf || !anyOf.length) {
            return '';
        }
        const nil = anyOf.find((shape) => shape.types.includes(AmfNamespace.aml.vocabularies.shapes.NilShape));
        if (nil && anyOf.length === 2) {
            // this is a case where a scalar is marked as nillable instead of not required
            // (which for some reason is a common practice among RAML developers).
            const scalar = anyOf.find((shape) => shape !== nil);
            return this.parameterSchemaTemplate(parameter, scalar, {
                nillable: true,
            });
        }
        const hasComplex = anyOf.some((i) => !i.types.includes(AmfNamespace.aml.vocabularies.shapes.ScalarShape));
        const hasScalar = anyOf.some((i) => i.types.includes(AmfNamespace.aml.vocabularies.shapes.ScalarShape));
        if (hasComplex && !hasScalar) {
            // We quit here as this is the same problem as with the Node shape - unclear how to serialize the types
            return '';
        }
        // at this point only scalars are possible. For that we render a regular text field
        // and while serializing the values we figure out what type it should be giving the
        // value provided and available types in the union.
        let opts;
        if (nil) {
            opts = { nillable: true };
        }
        return this.textInputTemplate(parameter, schema, 'text', opts);
    }
    /**
     * This situation makes not sense as there's no mechanism to describe how to
     * put a file into a path, query, or headers.
     * @returns The template for the file shape.
     */
    fileShapeTemplate() {
        return '';
    }
    /**
     * For now we do not support union shapes. There's no way to learn how to serialize
     * the Schema shape to a string.
     * @returns The template for the schema shape.
     */
    schemaShapeTemplate() {
        return '';
    }
    /**
     * @returns The template for the array shape.
     */
    arrayShapeTemplate(parameter, schema) {
        const { items } = schema;
        if (!items) {
            return '';
        }
        const { id } = parameter;
        const label = readLabelValue(parameter, schema);
        const values = this.readInputValue(parameter, schema, true);
        const options = { arrayItem: true };
        const inputs = (values || []).map((value, index) => this.parameterSchemaTemplate(parameter, items, { ...options, value, index }));
        const classes = {
            'array-form-item': true,
            'required': !!parameter.required,
            'optional': !parameter.required,
        };
        return html `
      <div class="${classMap(classes)}" data-param-id="${id}" data-param-label="${label}">
        <div class="array-title"><span class="label">${label}</span></div>
        ${inputs} ${this.addArrayItemTemplate(id)}
      </div>
    `;
    }
    /**
     * @returns The template for the tuple shape.
     */
    tupleShapeTemplate(parameter, schema) {
        const { items } = schema;
        if (!items) {
            return '';
        }
        // TODO: not sure how to process this value...
        return '';
    }
    /**
     * @returns The template for the Any shape.
     */
    anyShapeTemplate(parameter, schema) {
        return this.textInputTemplate(parameter, schema, 'text');
    }
    /**
     * @param id The id of the parameter to add the value to.
     * @returns The template for the adding an array item button
     */
    addArrayItemTemplate(id) {
        return html `
      <ui-filled-tonal-button data-id="${id}" @click="${this.addArrayValueHandler}" class="add-item-button"
        >Add new value</ui-filled-tonal-button
      >
    `;
    }
    /**
     * Handler for a custom parameter input change.
     */
    customParamChangeHandler(e) {
        const input = e.target;
        const { value, name, dataset } = input;
        const { domainId } = dataset;
        if (!domainId || !['paramName', 'paramValue'].includes(name || '')) {
            return;
        }
        if (name === 'paramName') {
            const param = this.parametersValue.find((item) => item.paramId === domainId);
            if (!param) {
                return;
            }
            param.parameter.name = value;
        }
        else {
            InputCache.set(this.target, domainId, value, this.globalCache);
        }
        this.notifyChange();
        this.paramChanged(domainId);
    }
    /**
     * @param param The parameter to render
     * @returns The template for a custom parameter
     */
    customParamTemplate(param) {
        const value = InputCache.get(this.target, param.paramId, this.globalCache);
        const classes = {
            'form-item': true,
            'custom-item': true,
        };
        let nameLabel;
        let nameTitle;
        let valueLabel;
        let valueTitle;
        if (param.binding === 'query') {
            nameLabel = 'Query parameter';
            nameTitle = 'The name of this query parameter';
            valueLabel = 'Parameter value';
            valueTitle = 'The value of this query parameter';
        }
        else if (param.binding === 'header') {
            nameLabel = 'Header name';
            nameTitle = 'The name of this header';
            valueLabel = 'Header value';
            valueTitle = 'The value of this header';
        }
        else {
            nameLabel = 'Parameter name';
            nameTitle = 'The name of the parameter';
            valueLabel = 'Parameter value';
            valueTitle = 'The value of the parameter';
        }
        return html `
      <div class="${classMap(classes)}">
        <ui-text-field
          data-domain-id="${param.paramId}"
          data-binding="${param.binding}"
          class="form-input custom-name"
          .value="${param.parameter.name || ''}"
          title="${nameTitle}"
          name="paramName"
          @change="${this.customParamChangeHandler}"
          noFloating
          label="${nameLabel}"
        ></ui-text-field>
        <ui-text-field
          data-domain-id="${param.paramId}"
          data-binding="${param.binding}"
          class="form-input custom-value"
          .value="${value}"
          title="${valueTitle}"
          name="paramValue"
          @change="${this.customParamChangeHandler}"
          noFloating
          label="${valueLabel}"
        ></ui-text-field>
        ${this.deleteParamTemplate(param.paramId)}
      </div>
    `;
    }
}
//# sourceMappingURL=AmfParameters.js.map