import {
    iterateCursor
} from '#core/dom';
import {
    getFormAsObject,
    getSubmitButtonUsed
} from '#core/dom/form';
import {
    map
} from '#core/types/object';

import {
    Services
} from '#service';

/**
 * Create a form data wrapper. The wrapper is necessary to provide a common
 * API for FormData objects on all browsers. For example, not all browsers
 * support the FormData#entries or FormData#delete functions.
 *
 * @param {!Window} win
 * @param {!HTMLFormElement=} opt_form
 * @return {!FormDataWrapperInterface}
 */
export function createFormDataWrapper(win, opt_form) {
    const platform = Services.platformFor(win);

    if (platform.isIos() && platform.getMajorVersion() == 11) {
        return new Ios11NativeFormDataWrapper(opt_form);
    } else if (FormData.prototype.entries && FormData.prototype.delete) {
        return new NativeFormDataWrapper(opt_form);
    } else {
        return new PolyfillFormDataWrapper(opt_form);
    }
}

/**
 * Check if the given object is a FormDataWrapper instance
 * @param {*} o
 * @return {boolean} True if the object is a FormDataWrapper instance.
 */
export function isFormDataWrapper(o) {
    // instanceof doesn't work as expected, so we detect with duck-typing.
    return !!o && typeof o.getFormData == 'function';
}

/**
 * A polyfill wrapper for a `FormData` object.
 *
 * If there's no native `FormData#entries`, chances are there are no native
 * methods to read the content of the `FormData` after construction, so the
 * only way to implement `entries` in this class is to capture the fields in
 * the form passed to the constructor (and the arguments passed to the
 * `append` method).
 *
 * For more details on this, see http://mdn.io/FormData.
 *
 * @implements {FormDataWrapperInterface}
 * @visibleForTesting
 */
export class PolyfillFormDataWrapper {
    /** @override */
    constructor(opt_form = undefined) {
        /** @private @const {!Object<string, !Array<string>>} */
        this.fieldValues_ = opt_form ? getFormAsObject(opt_form) : map();
    }

    /**
     * @param {string} name
     * @param {string|!File} value
     * @param {string=} opt_filename
     * @override
     */
    append(name, value, opt_filename) {
        // Coercion to string is required to match
        // the native FormData.append behavior
        const nameString = String(name);
        this.fieldValues_[nameString] = this.fieldValues_[nameString] || [];
        this.fieldValues_[nameString].push(String(value));
    }

    /** @override */
    delete(name) {
        delete this.fieldValues_[name];
    }

    /** @override */
    entries() {
        const fieldEntries = [];
        Object.keys(this.fieldValues_).forEach((name) => {
            const values = this.fieldValues_[name];
            values.forEach((value) => fieldEntries.push([name, value]));
        });

        // Generator functions are not supported by the current Babel configuration,
        // so we must manually implement the iterator interface.
        let nextIndex = 0;
        return /** @type {!Iterator<!Array<string>>} */ ({
            next() {
                return nextIndex < fieldEntries.length ?
                    {
                        value: fieldEntries[nextIndex++],
                        done: false
                    } :
                    {
                        value: undefined,
                        done: true
                    };
            },
        });
    }

    /** @override */
    getFormData() {
        const formData = new FormData();

        Object.keys(this.fieldValues_).forEach((name) => {
            const values = this.fieldValues_[name];
            values.forEach((value) => formData.append(name, value));
        });

        return formData;
    }
}

/**
 * Wrap the native `FormData` implementation.
 *
 * NOTE: This differs from the standard `FormData` constructor. This constructor
 * includes a submit button if it was used to submit the `opt_form`, where
 * the native `FormData` constructor does not include the submit button used to
 * submit the form.
 * {@link https://xhr.spec.whatwg.org/#dom-formdata}
 * @implements {FormDataWrapperInterface}
 */
class NativeFormDataWrapper {
    /** @override */
    constructor(opt_form) {
        /** @private @const {!FormData} */
        this.formData_ = new FormData(opt_form);

        this.maybeIncludeSubmitButton_(opt_form);
    }

    /**
     * If a submit button is focused (because it was used to submit the form),
     * or was the first submit button present, add its name and value to the
     * `FormData`, since publishers expect the submit button to be present.
     * @param {!HTMLFormElement=} opt_form
     * @private
     */
    maybeIncludeSubmitButton_(opt_form) {
        // If a form is not passed to the constructor,
        // we are not in a submitting code path.
        if (!opt_form) {
            return;
        }

        const button = getSubmitButtonUsed(opt_form);
        if (button && button.name) {
            this.append(button.name, button.value);
        }
    }

    /**
     * @param {string} name
     * @param {string|!File} value
     * @param {string=} opt_filename
     * @override
     */
    append(name, value, opt_filename) {
        this.formData_.append(name, value);
    }

    /** @override */
    delete(name) {
        this.formData_.delete(name);
    }

    /** @override */
    entries() {
        return this.formData_.entries();
    }

    /** @override */
    getFormData() {
        return this.formData_;
    }
}

/**
 * iOS 11 has a bug when submitting empty file inputs.
 * This works around the bug by replacing the empty files with Blob objects.
 */
class Ios11NativeFormDataWrapper extends NativeFormDataWrapper {
    /** @override */
    constructor(opt_form) {
        super(opt_form);

        if (opt_form) {
            iterateCursor(opt_form.elements, (input) => {
                if (input.type == 'file' && input.files.length == 0) {
                    this.formData_.delete(input.name);
                    this.formData_.append(input.name, new Blob([]), '');
                }
            });
        }
    }

    /**
     * @param {string} name
     * @param {string|!File} value
     * @param {string=} opt_filename
     * @override
     */
    append(name, value, opt_filename) {
        // Safari 11 breaks on submitting empty File values.
        if (value && typeof value == 'object' && isEmptyFile(value)) {
            this.formData_.append(name, new Blob([]), opt_filename || '');
        } else {
            this.formData_.append(name, value);
        }
    }
}

/**
 * A wrapper for a native `FormData` object that allows the retrieval of entries
 * in the form data after construction even on browsers that don't natively
 * support `FormData.prototype.entries`.
 *
 * @interface
 * Subclassing `FormData` doesn't work in this case as the transpiler
 *     generates code that calls the super constructor directly using
 *     `Function.prototype.call`. WebKit (Safari) doesn't allow this and
 *     enforces that constructors be called with the `new` operator.
 */
class FormDataWrapperInterface {
    /**
     * Creates a new wrapper for a `FormData` object.
     *
     * If there's no native `FormData#entries`, chances are there are no native
     * methods to read the content of the `FormData` after construction, so the
     * only way to implement `entries` in this class is to capture the fields in
     * the form passed to the constructor (and the arguments passed to the
     * `append` method).
     *
     * This constructor should also add the submitter element as defined in the
     * HTML spec for Form Submission Algorithm, but is not defined by the standard
     * when using the `FormData` constructor directly.
     *
     * For more details on this, see http://mdn.io/FormData.
     *
     * @param {!HTMLFormElement=} opt_form An HTML `<form>` element — when
     *     specified, the `FormData` object will be populated with the form's
     *     current keys/values using the name property of each element for the
     *     keys and their submitted value for the values. It will also encode file
     *     input content.
     */
    constructor(opt_form) {}

    /**
     * Appends a new value onto an existing key inside a `FormData` object, or
     * adds the key if it does not already exist.
     *
     * Appending a `File` object is not yet supported and the `filename`
     * parameter is ignored for this wrapper.
     *
     * For more details on this, see http://mdn.io/FormData/append.
     *
     * TODO(cvializ): Update file support
     *
     * @param {string} unusedName The name of the field whose data is contained in
     *     `value`.
     * @param {string|!File} unusedValue The field's value.
     * @param {string=} opt_filename The filename to use if the value is a file.
     */
    append(unusedName, unusedValue, opt_filename) {}

    /**
     * Remove the given value from the FormData.
     *
     * For more details on this, see http://mdn.io/FormData/delete.
     *
     * @param {string} unusedName The name of the field to remove from the FormData.
     */
    delete(unusedName) {}

    /**
     * Returns an iterator of all key/value pairs contained in this object.
     *
     * For more details on this, see http://mdn.io/FormData/entries.
     *
     * @return {!Iterator<!Array<string|!File>>}
     */
    entries() {}

    /**
     * Returns the wrapped native `FormData` object.
     *
     * @return {!FormData}
     */
    getFormData() {}
}

/**
 * Check if the given file is an empty file, which is the result of submitting
 * an empty `<input type="file">`. These cause errors when submitting forms
 * in Safari 11.
 *
 * @param {!File} file
 * @return {boolean}
 */
function isEmptyFile(file) {
    return file.name == '' && file.size == 0;
}