/**
 * @license
 * Copyright (c) 2020 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 {AttributePart, BooleanAttributePart, directive, EventPart, NodePart, PropertyPart} from '../lit-html.js';

/**
 * Checks binding values against live DOM values, instead of previously bound
 * values, when determining whether to update the value.
 *
 * This is useful for cases where the DOM value may change from outside of
 * lit-html, such as with a binding to an `<input>` element's `value` property,
 * a content editable elements text, or to a custom element that changes it's
 * own properties or attributes.
 *
 * In these cases if the DOM value changes, but the value set through lit-html
 * bindings hasn't, lit-html won't know to update the DOM value and will leave
 * it alone. If this is not what you want—if you want to overwrite the DOM
 * value with the bound value no matter what—use the `live()` directive:
 *
 *     html`<input .value=${live(x)}>`
 *
 * `live()` performs a strict equality check agains the live DOM value, and if
 * the new value is equal to the live value, does nothing. This means that
 * `live()` should not be used when the binding will cause a type conversion. If
 * you use `live()` with an attribute binding, make sure that only strings are
 * passed in, or the binding will update every render.
 */
export const live = directive(
    (value: unknown) => (part: AttributePart|PropertyPart|
                         BooleanAttributePart) => {
      let previousValue: unknown;
      if (part instanceof EventPart || part instanceof NodePart) {
        throw new Error(
            'The `live` directive is not allowed on text or event bindings');
      }
      if (part instanceof BooleanAttributePart) {
        checkStrings(part.strings);
        previousValue = part.element.hasAttribute(part.name);
        // This is a hack needed because BooleanAttributePart doesn't have a
        // committer and does its own dirty checking after directives
        part.value = previousValue;
      } else {
        const {element, name, strings} = part.committer;
        checkStrings(strings);
        if (part instanceof PropertyPart) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          previousValue = (element as any)[name];
          if (previousValue === value) {
            return;
          }
        } else if (part instanceof AttributePart) {
          previousValue = element.getAttribute(name);
        }
        if (previousValue === String(value)) {
          return;
        }
      }
      part.setValue(value);
    });

const checkStrings = (strings: readonly string[]) => {
  if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
    throw new Error('`live` bindings can only contain a single expression');
  }
};
