/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */

import {
  AttributePartInfo,
  DirectiveClass,
  DirectiveResult,
  PartInfo,
} from './directive.js';
import {_Σ, DirectiveParent, Part, TemplateResult} from './lit-html.js';

type Primitive = null | undefined | boolean | number | string | symbol | bigint;

const {_ChildPart: ChildPart} = _Σ;

type ChildPart = InstanceType<typeof ChildPart>;

const ENABLE_SHADYDOM_NOPATCH = true;

const wrap =
  ENABLE_SHADYDOM_NOPATCH &&
  window.ShadyDOM?.inUse &&
  window.ShadyDOM?.noPatch === true
    ? window.ShadyDOM!.wrap
    : (node: Node) => node;

/**
 * Tests if a value is a primitive value.
 *
 * See https://tc39.github.io/ecma262/#sec-typeof-operator
 */
export const isPrimitive = (value: unknown): value is Primitive =>
  value === null || (typeof value != 'object' && typeof value != 'function');

export const TemplateResultType = {
  HTML: 1,
  SVG: 2,
} as const;

export type TemplateResultType = typeof TemplateResultType[keyof typeof TemplateResultType];

/**
 * Tests if a value is a TemplateResult.
 */
export const isTemplateResult = (
  value: unknown,
  type?: TemplateResultType
): value is TemplateResult =>
  type === undefined
    ? (value as TemplateResult)?._$litType$ !== undefined
    : (value as TemplateResult)?._$litType$ === type;

/**
 * Tests if a value is a DirectiveResult.
 */
export const isDirectiveResult = (value: unknown): value is DirectiveResult =>
  (value as DirectiveResult)?._$litDirective$ !== undefined;

/**
 * Retrieves the Directive class for a DirectiveResult
 */
export const getDirectiveClass = (value: unknown): DirectiveClass | undefined =>
  (value as DirectiveResult)?._$litDirective$;

/**
 * Tests whether a part has only a single-expression with no strings to
 * interpolate between.
 *
 * Only AttributePart and PropertyPart can have multiple expressions.
 * Multi-expression parts have a `strings` property and single-expression
 * parts do not.
 */
export const isSingleExpression = (part: PartInfo) =>
  (part as AttributePartInfo).strings === undefined;

const createMarker = () => document.createComment('');

/**
 * Inserts a ChildPart into the given container ChildPart's DOM, either at the
 * end of the container ChildPart, or before the optional `refPart`.
 *
 * This does not add the part to the containerPart's committed value. That must
 * be done by callers.
 *
 * @param containerPart Part within which to add the new ChildPart
 * @param refPart Part before which to add the new ChildPart; when omitted the
 *     part added to the end of the `containerPart`
 * @param part Part to insert, or undefined to create a new part
 */
export const insertPart = (
  containerPart: ChildPart,
  refPart?: ChildPart,
  part?: ChildPart
): ChildPart => {
  const container = wrap(containerPart._$startNode).parentNode!;

  const refNode =
    refPart === undefined ? containerPart._$endNode : refPart._$startNode;

  if (part === undefined) {
    const startNode = wrap(container).insertBefore(createMarker(), refNode);
    const endNode = wrap(container).insertBefore(createMarker(), refNode);
    part = new ChildPart(
      startNode,
      endNode,
      containerPart,
      containerPart.options
    );
  } else {
    const endNode = wrap(part._$endNode!).nextSibling;
    const parentChanged = part._$parent !== containerPart;
    if (parentChanged) {
      part._$reparentDisconnectables?.(containerPart);
      // Note that although `_$reparentDisconnectables` updates the part's
      // `_$parent` reference after unlinking from its current parent, that
      // method only exists if Disconnectables are present, so we need to
      // unconditionally set it here
      part._$parent = containerPart;
    }
    if (endNode !== refNode || parentChanged) {
      let start: Node | null = part._$startNode;
      while (start !== endNode) {
        const n: Node | null = wrap(start!).nextSibling;
        wrap(container).insertBefore(start!, refNode);
        start = n;
      }
    }
  }

  return part;
};

/**
 * Sets the value of a Part.
 *
 * Note that this should only be used to set/update the value of user-created
 * parts (i.e. those created using `insertPart`); it should not be used
 * by directives to set the value of the directive's container part. Directives
 * should return a value from `update`/`render` to update their part state.
 *
 * For directives that require setting their part value asynchronously, they
 * should extend `AsyncDirective` and call `this.setValue()`.
 *
 * @param part Part to set
 * @param value Value to set
 * @param index For `AttributePart`s, the index to set
 * @param directiveParent Used internally; should not be set by user
 */
export const setChildPartValue = <T extends ChildPart>(
  part: T,
  value: unknown,
  directiveParent: DirectiveParent = part
): T => {
  part._$setValue(value, directiveParent);
  return part;
};

// A sentinal value that can never appear as a part value except when set by
// live(). Used to force a dirty-check to fail and cause a re-render.
const RESET_VALUE = {};

/**
 * Sets the committed value of a ChildPart directly without triggering the
 * commit stage of the part.
 *
 * This is useful in cases where a directive needs to update the part such
 * that the next update detects a value change or not. When value is omitted,
 * the next update will be guaranteed to be detected as a change.
 *
 * @param part
 * @param value
 */
export const setCommittedValue = (part: Part, value: unknown = RESET_VALUE) =>
  (part._$committedValue = value);

/**
 * Returns the committed value of a ChildPart.
 *
 * The committed value is used for change detection and efficient updates of
 * the part. It can differ from the value set by the template or directive in
 * cases where the template value is transformed before being commited.
 *
 * - `TemplateResult`s are committed as a `TemplateInstance`
 * - Iterables are committed as `Array<ChildPart>`
 * - All other types are committed as the template value or value returned or
 *   set by a directive.
 *
 * @param part
 */
export const getCommittedValue = (part: ChildPart) => part._$committedValue;

/**
 * Removes a ChildPart from the DOM, including any of its content.
 *
 * @param part The Part to remove
 */
export const removePart = (part: ChildPart) => {
  part._$setChildPartConnected?.(false, true);
  let start: ChildNode | null = part._$startNode;
  const end: ChildNode | null = wrap(part._$endNode!).nextSibling;
  while (start !== end) {
    const n: ChildNode | null = wrap(start!).nextSibling;
    (wrap(start!) as ChildNode).remove();
    start = n;
  }
};

export const clearPart = (part: ChildPart) => {
  part._$clear();
};
