/**
 * @fileoverview
 * @suppress {checkPrototypalTypes}
 * @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
 * Google as part of the polymer project is also subject to an additional IP
 * rights grant found at http://polymer.github.io/PATENTS.txt
 */
import { ElementMixin } from './element-mixin.js';
import { dedupingMixin } from '../utils/mixin.js';
import { wrap } from '../utils/wrap.js';

const DISABLED_ATTR = 'disable-upgrade';

export const findObservedAttributesGetter = (ctor) => {
  while (ctor) {
    const desc = Object.getOwnPropertyDescriptor(ctor, 'observedAttributes');
    if (desc) {
      return desc.get;
    }
    ctor = Object.getPrototypeOf(ctor.prototype).constructor;
  }
  return () => [];
};

/**
 * Element class mixin that allows the element to boot up in a non-enabled
 * state when the `disable-upgrade` attribute is present. This mixin is
 * designed to be used with element classes like PolymerElement that perform
 * initial startup work when they are first connected. When the
 * `disable-upgrade` attribute is removed, if the element is connected, it
 * boots up and "enables" as it otherwise would; if it is not connected, the
 * element boots up when it is next connected.
 *
 * Using `disable-upgrade` with PolymerElement prevents any data propagation
 * to the element, any element DOM from stamping, or any work done in
 * connected/disconnctedCallback from occuring, but it does not prevent work
 * done in the element constructor.
 *
 * Note, this mixin must be applied on top of any element class that
 * itself implements a `connectedCallback` so that it can control the work
 * done in `connectedCallback`. For example,
 *
 *     MyClass = DisableUpgradeMixin(class extends BaseClass {...});
 *
 * @mixinFunction
 * @polymer
 * @appliesMixin ElementMixin
 * @template T
 * @param {function(new:T)} superClass Class to apply mixin to.
 * @return {function(new:T)} superClass with mixin applied.
 */
export const DisableUpgradeMixin = dedupingMixin((base) => {
  /**
   * @constructor
   * @implements {Polymer_ElementMixin}
   * @extends {HTMLElement}
   * @private
   */
  const superClass = ElementMixin(base);

  // Work around for closure bug #126934458. Using `super` in a property
  // getter does not work so instead we search the Base prototype for an
  // implementation of observedAttributes so that we can override and call
  // the `super` getter. Note, this is done one time ever because we assume
  // that `Base` is always comes from `Polymer.LegacyElementMixn`.
  let observedAttributesGetter = findObservedAttributesGetter(superClass);

  /**
   * @polymer
   * @mixinClass
   * @implements {Polymer_DisableUpgradeMixin}
   */
  class DisableUpgradeClass extends superClass {

    constructor() {
      super();
      /** @type {boolean|undefined} */
      this.__isUpgradeDisabled;
    }

    static get observedAttributes() {
      return observedAttributesGetter.call(this).concat(DISABLED_ATTR);
    }

    // Prevent element from initializing properties when it's upgrade disabled.
    /** @override */
    _initializeProperties() {
      if (this.hasAttribute(DISABLED_ATTR)) {
        this.__isUpgradeDisabled = true;
      } else {
        super._initializeProperties();
      }
    }

    // Prevent element from enabling properties when it's upgrade disabled.
    // Normally overriding connectedCallback would be enough, but dom-* elements
    /** @override */
    _enableProperties() {
      if (!this.__isUpgradeDisabled) {
        super._enableProperties();
      }
    }

    // If the element starts upgrade-disabled and a property is set for
    // which an accessor exists, the default should not be applied.
    // This additional check is needed because defaults are applied via
    // `_initializeProperties` which is called after initial properties
    // have been set when the element starts upgrade-disabled.
    /** @override */
    _canApplyPropertyDefault(property) {
      return super._canApplyPropertyDefault(property) &&
        !(this.__isUpgradeDisabled && this._isPropertyPending(property));
    }

    /**
     * @override
     * @param {string} name Attribute name.
     * @param {?string} old The previous value for the attribute.
     * @param {?string} value The new value for the attribute.
     * @param {?string} namespace The XML namespace for the attribute.
     * @return {void}
     */
    attributeChangedCallback(name, old, value, namespace) {
      if (name == DISABLED_ATTR) {
        // When disable-upgrade is removed, initialize properties and
        // provoke connectedCallback if the element is already connected.
        if (this.__isUpgradeDisabled && value == null) {
          super._initializeProperties();
          this.__isUpgradeDisabled = false;
          if (wrap(this).isConnected) {
            super.connectedCallback();
          }
        }
      } else {
        super.attributeChangedCallback(
            name, old, value, /** @type {null|string} */ (namespace));
      }
    }

    // Prevent element from connecting when it's upgrade disabled.
    // This prevents user code in `attached` from being called.
    /** @override */
    connectedCallback() {
      if (!this.__isUpgradeDisabled) {
        super.connectedCallback();
      }
    }

    // Prevent element from disconnecting when it's upgrade disabled.
    // This avoids allowing user code `detached` from being called without a
    // paired call to `attached`.
    /** @override */
    disconnectedCallback() {
      if (!this.__isUpgradeDisabled) {
        super.disconnectedCallback();
      }
    }

  }

  return DisableUpgradeClass;
});
