import { get, set, computed } from '@ember/object';
import Changeset from 'ember-changeset';
import lookupValidator from 'ember-changeset-validations';

// Keep these here for now so forms are easy to make
// TODO: Probably move this to utils/form/parse-element-name
import parseElementName from 'consul-ui/utils/get-form-name-property';
// TODO: Currently supporting ember-data nicely like this
// Unfortunately since post-ember 2.18, the only way to get this to work
// is to hang stuff off the prototype (which then makes it available everywhere)
// we should either try and figure out another way of doing this, or move this code
// somewhere where it is more 'global' like an initializer
Changeset.prototype.isSaving = computed('data.isSaving', function() {
  return this.data.isSaving;
});
const defaultChangeset = function(data, validators) {
  return new Changeset(data, lookupValidator(validators), validators);
};
/**
 * Form builder/Form factory
 * Deals with handling (generally change) events and updating data in response to the change
 * in a typical data down event up manner
 * validations are included currently using ember-changeset-validations
 *
 * @param {string} name - The name of the form, generally this is the name of your model
 *   Generally (until view building is complete) you should name your form elements as `name="modelName[property]"`
 *   or pass this name through using you action and create an Event-like object instead
 *   You can also just not set a name and use `name="property"`, but if you want to use combinations
 *   if multiple forms at least form children should use names
 *
 * @param {object} config - Form configuration object. Just a plain object to configure the form should be a hash
 *   with property names relating to the form data. Each property is the configuration for that model/data property
 *   currently the only supported property of these configuration objects is `type` which currently allows you to
 *   set a property as 'array-like'
 */
export default function(changeset = defaultChangeset, getFormNameProperty = parseElementName) {
  return function(name = '', obj = {}) {
    const _children = {};
    let _validators = null;
    // TODO make this into a class to reuse prototype
    const form = {
      data: null,
      name: name,
      getName: function() {
        return this.name;
      },
      setData: function(data) {
        // Array check temporarily for when we get an empty array from repo.status
        if (_validators && !Array.isArray(data)) {
          data = changeset(data, _validators);
        }
        set(this, 'data', data);
        return this;
      },
      getData: function() {
        return this.data;
      },
      add: function(child) {
        _children[child.getName()] = child;
        return this;
      },
      handleEvent: function(e, targetName) {
        const target = e.target;
        // currently we only use targetName in {{form-component}} for handling deeply
        // nested forms, once {{form-component}} handles deeply nested forms targetName can go
        const parts = getFormNameProperty(targetName || target.name);
        // split the form element name from `name[prop]`
        const name = parts[0];
        const prop = parts[1];
        //
        let config = obj;
        // if the name (usually the name of the model) isn't this form, look at its children
        if (name !== this.getName()) {
          if (this.has(name)) {
            // is its a child form then use the child form
            return this.form(name).handleEvent(e);
          }
          // should probably throw here, unless we support having a name
          // even if you are referring to this form
          config = config[name];
        }
        const data = this.getData();
        // ember-data/changeset dance
        // TODO: This works for ember-data RecordSets and Changesets but not for plain js Objects
        // see settings
        const json = typeof data.toJSON === 'function' ? data.toJSON() : get(data, 'data').toJSON();
        // if the form doesn't include a property then throw so it can be
        // caught outside, therefore the user can deal with things that aren't in the data
        // TODO: possibly need to add support for deeper properties using `get` here
        // for example `client.blocking` instead of just `blocking`
        if (!Object.keys(json).includes(prop)) {
          const error = new Error(`${prop} property doesn't exist`);
          error.target = target;
          throw error;
        }
        // deal with the change of property
        let currentValue = get(data, prop);
        // if the value is an array-like or config says its an array
        if (
          Array.isArray(currentValue) ||
          (typeof config[prop] !== 'undefined' &&
            typeof config[prop].type === 'string' &&
            config[prop].type.toLowerCase() === 'array')
        ) {
          // array specific set
          if (currentValue == null) {
            currentValue = [];
          }
          const method = target.checked ? 'pushObject' : 'removeObject';
          currentValue[method](target.value);
          set(data, prop, currentValue);
        } else {
          // deal with booleans
          // but only booleans that aren't checkboxes/radios with values
          if (
            typeof target.checked !== 'undefined' &&
            (target.value.toLowerCase() === 'on' || target.value.toLowerCase() === 'off')
          ) {
            set(data, prop, target.checked);
          } else {
            // text and non-boolean checkboxes/radios
            set(data, prop, target.value);
          }
        }
        // validate everything
        return this.validate();
      },
      reset: function() {
        const data = this.getData();
        if (typeof data.rollbackAttributes === 'function') {
          this.getData().rollbackAttributes();
        }
        return this;
      },
      clear: function(cb = {}) {
        if (typeof cb === 'function') {
          return (this.clearer = cb);
        } else {
          return this.setData(this.clearer(cb)).getData();
        }
      },
      submit: function(cb = {}) {
        if (typeof cb === 'function') {
          return (this.submitter = cb);
        } else {
          this.submitter(this.getData());
        }
      },
      setValidators: function(validators) {
        _validators = validators;
        return this;
      },
      validate: function() {
        const data = this.getData();
        // just pass along to the Changeset for now
        if (typeof data.validate === 'function') {
          data.validate();
        }
        return this;
      },
      addError: function(name, message) {
        const data = this.getData();
        if (typeof data.addError === 'function') {
          data.addError(...arguments);
        }
      },
      form: function(name) {
        if (name == null) {
          return this;
        }
        return _children[name];
      },
      has: function(name) {
        return typeof _children[name] !== 'undefined';
      },
    };
    form.submit = form.submit.bind(form);
    form.reset = form.reset.bind(form);
    return form;
  };
}
