/**
 * A `TcbOp` which generates code to check "unclaimed inputs" - bindings on an element which were
 * not attributed to any directive or component, and are instead processed against the HTML element
 * itself.
 *
 * Currently, only the expressions of these bindings are checked. The targets of the bindings are
 * checked against the DOM schema via a `TcbDomSchemaCheckerOp`.
 *
 * Executing this operation returns nothing.
 */
class TcbUnclaimedInputsOp extends TcbOp {
  constructor(
      private tcb: Context, private scope: Scope, private element: TmplAstElement,
      private claimedInputs: Set<string>) {
    super();
  }

  execute(): null {
    // `this.inputs` contains only those bindings not matched by any directive. These bindings go to
    // the element itself.
    const elId = this.scope.resolve(this.element);

    // TODO(alxhub): this could be more efficient.
    for (const binding of this.element.inputs) {
      if (binding.type === BindingType.Property && this.claimedInputs.has(binding.name)) {
        // Skip this binding as it was claimed by a directive.
        continue;
      }

      let expr = tcbExpression(binding.value, this.tcb, this.scope);
      if (!this.tcb.env.config.checkTypeOfInputBindings) {
        // If checking the type of bindings is disabled, cast the resulting expression to 'any'
        // before the assignment.
        expr = tsCastToAny(expr);
      } else if (!this.tcb.env.config.strictNullInputBindings) {
        // If strict null checks are disabled, erase `null` and `undefined` from the type by
        // wrapping the expression in a non-null assertion.
        expr = ts.createNonNullExpression(expr);
      }

      if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
        if (binding.name !== 'style' && binding.name !== 'class') {
          // A direct binding to a property.
          const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
          const prop = ts.createElementAccess(elId, ts.createStringLiteral(propertyName));
          const stmt = ts.createBinary(prop, ts.SyntaxKind.EqualsToken, wrapForDiagnostics(expr));
          addParseSpanInfo(stmt, binding.sourceSpan);
          this.scope.addStatement(ts.createExpressionStatement(stmt));
        } else {
          this.scope.addStatement(ts.createExpressionStatement(expr));
        }
      } else {
        // A binding to an animation, attribute, class or style. For now, only validate the right-
        // hand side of the expression.
        // TODO: properly check class and style bindings.
        this.scope.addStatement(ts.createExpressionStatement(expr));
      }
    }

    return null;
  }
}


/**
 * A `TcbOp` which generates code to check event bindings on an element that correspond with the
 * outputs of a directive.
 *
 * Executing this operation returns nothing.
 */
class TcbDirectiveOutputsOp extends TcbOp {
  constructor(
      private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement,
      private dir: TypeCheckableDirectiveMeta) {
    super();
  }

  execute(): null {
    const dirId = this.scope.resolve(this.node, this.dir);

    // `dir.outputs` is an object map of field names on the directive class to event names.
    // This is backwards from what's needed to match event handlers - a map of event names to field
    // names is desired. Invert `dir.outputs` into `fieldByEventName` to create this map.
    const fieldByEventName = new Map<string, string>();
    const outputs = this.dir.outputs;
    for (const key of Object.keys(outputs)) {
      fieldByEventName.set(outputs[key], key);
    }

    for (const output of this.node.outputs) {
      if (output.type !== ParsedEventType.Regular || !fieldByEventName.has(output.name)) {
        continue;
      }
      const field = fieldByEventName.get(output.name)!;

      if (this.tcb.env.config.checkTypeOfOutputEvents) {
        // For strict checking of directive events, generate a call to the `subscribe` method
        // on the directive's output field to let type information flow into the handler function's
        // `$event` parameter.
        //
        // Note that the `EventEmitter<T>` type from '@angular/core' that is typically used for
        // outputs has a typings deficiency in its `subscribe` method. The generic type `T` is not
        // carried into the handler function, which is vital for inference of the type of `$event`.
        // As a workaround, the directive's field is passed into a helper function that has a
        // specially crafted set of signatures, to effectively cast `EventEmitter<T>` to something
        // that has a `subscribe` method that properly carries the `T` into the handler function.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);

        const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field));
        const outputHelper =
            ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
        const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
        const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
        addParseSpanInfo(call, output.sourceSpan);
        this.scope.addStatement(ts.createExpressionStatement(call));
      } else {
        // If strict checking of directive events is disabled, emit a handler function where the
        // `$event` parameter has an explicit `any` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      }

      ExpressionSemanticVisitor.visit(
          output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
    }

    return null;
  }
}


/**
 * A `TcbOp` which generates code to check "unclaimed outputs" - event bindings on an element which
 * were not attributed to any directive or component, and are instead processed against the HTML
 * element itself.
 *
 * Executing this operation returns nothing.
 */
class TcbUnclaimedOutputsOp extends TcbOp {
  constructor(
      private tcb: Context, private scope: Scope, private element: TmplAstElement,
      private claimedOutputs: Set<string>) {
    super();
  }

  execute(): null {
    const elId = this.scope.resolve(this.element);

    // TODO(alxhub): this could be more efficient.
    for (const output of this.element.outputs) {
      if (this.claimedOutputs.has(output.name)) {
        // Skip this event handler as it was claimed by a directive.
        continue;
      }

      if (output.type === ParsedEventType.Animation) {
        // Animation output bindings always have an `$event` parameter of type `AnimationEvent`.
        const eventType = this.tcb.env.config.checkTypeOfAnimationEvents ?
            this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent') :
            EventParamType.Any;

        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      } else if (this.tcb.env.config.checkTypeOfDomEvents) {
        // If strict checking of DOM events is enabled, generate a call to `addEventListener` on
        // the element instance so that TypeScript's type inference for
        // `HTMLElement.addEventListener` using `HTMLElementEventMap` to infer an accurate type for
        // `$event` depending on the event name. For unknown event names, TypeScript resorts to the
        // base `Event` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);

        const call = ts.createCall(
            /* expression */ ts.createPropertyAccess(elId, 'addEventListener'),
            /* typeArguments */ undefined,
            /* arguments */[ts.createStringLiteral(output.name), handler]);
        addParseSpanInfo(call, output.sourceSpan);
        this.scope.addStatement(ts.createExpressionStatement(call));
      } else {
        // If strict checking of DOM inputs is disabled, emit a handler function where the `$event`
        // parameter has an explicit `any` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      }

      ExpressionSemanticVisitor.visit(
          output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
    }

    return null;
  }
}


/**
 * Overall generation context for the type check block.
 *
 * `Context` handles operations during code generation which are global with respect to the whole
 * block. It's responsible for variable name allocation and management of any imports needed. It
 * also contains the template metadata itself.
 */
export class Context {
  private nextId = 1;

  constructor(
      readonly env: Environment, readonly domSchemaChecker: DomSchemaChecker,
      readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: TemplateId,
      readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
      private pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
      readonly schemas: SchemaMetadata[]) {}

  /**
   * Allocate a new variable name for use within the `Context`.
   *
   * Currently this uses a monotonically increasing counter, but in the future the variable name
   * might change depending on the type of data being stored.
   */
  allocateId(): ts.Identifier {
    return ts.createIdentifier(`_t${this.nextId++}`);
  }

  getPipeByName(name: string): ts.Expression|null {
    if (!this.pipes.has(name)) {
      return null;
    }
    return this.env.pipeInst(this.pipes.get(name)!);
  }
}


class TcbEventHandlerTranslator extends TcbExpressionTranslator {
  protected resolve(ast: AST): ts.Expression|null {
    // Recognize a property read on the implicit receiver corresponding with the event parameter
    // that is available in event bindings. Since this variable is a parameter of the handler
    // function that the converted expression becomes a child of, just create a reference to the
    // parameter by its name.
    if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver &&
        ast.name === EVENT_PARAMETER) {
      const event = ts.createIdentifier(EVENT_PARAMETER);
      addParseSpanInfo(event, ast.sourceSpan);
      return event;
    }

    return super.resolve(ast);
  }
}


const enum EventParamType {
  /* Generates code to infer the type of `$event` based on how the listener is registered. */
  Infer,

  /* Declares the type of the `$event` parameter as `any`. */
  Any,
}


type TcbDirectiveInput = {
  type: 'binding'; field: string; expression: ts.Expression; sourceSpan: ParseSourceSpan;
}|{
  type: 'unset';
  field: string;
};


abstract execute(): ts.Expression|null;


/**
   * Replacement value or operation used while this `TcbOp` is executing (i.e. to resolve circular
   * references during its execution).
   *
   * This is usually a `null!` expression (which asks TS to infer an appropriate type), but another
   * `TcbOp` can be returned in cases where additional code generation is necessary to deal with
   * circular references.
   */
circularFallback(): TcbOp|ts.Expression {
    return INFER_TYPE_FOR_CIRCULAR_OP_EXPR;
  }


execute(): ts.Identifier {
    const id = this.tcb.allocateId();
    // Add the declaration of the element using document.createElement.
    const initializer = tsCreateElement(this.element.name);
    addParseSpanInfo(initializer, this.element.startSourceSpan || this.element.sourceSpan);
    this.scope.addStatement(tsCreateVariable(id, initializer));
    return id;
  }


execute(): ts.Identifier {
    // Look for a context variable for the template.
    const ctx = this.scope.resolve(this.template);

    // Allocate an identifier for the TmplAstVariable, and initialize it to a read of the variable
    // on the template context.
    const id = this.tcb.allocateId();
    const initializer = ts.createPropertyAccess(
        /* expression */ ctx,
        /* name */ this.variable.value || '$implicit');
    addParseSpanInfo(initializer, this.variable.sourceSpan);

    // Declare the variable, and return its identifier.
    this.scope.addStatement(tsCreateVariable(id, initializer));
    return id;
  }


execute(): ts.Identifier {
    // Allocate a template ctx variable and declare it with an 'any' type. The type of this variable
    // may be narrowed as a result of template guard conditions.
    const ctx = this.tcb.allocateId();
    const type = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
    this.scope.addStatement(tsDeclareVariable(ctx, type));
    return ctx;
  }


execute(): null {
    // An `if` will be constructed, within which the template's children will be type checked. The
    // `if` is used for two reasons: it creates a new syntactic scope, isolating variables declared
    // in the template's TCB from the outer context, and it allows any directives on the templates
    // to perform type narrowing of either expressions or the template's context.
    //
    // The guard is the `if` block's condition. It's usually set to `true` but directives that exist
    // on the template can trigger extra guard expressions that serve to narrow types within the
    // `if`. `guard` is calculated by starting with `true` and adding other conditions as needed.
    // Collect these into `guards` by processing the directives.
    const directiveGuards: ts.Expression[] = [];

    const directives = this.tcb.boundTarget.getDirectivesOfNode(this.template);
    if (directives !== null) {
      for (const dir of directives) {
        const dirInstId = this.scope.resolve(this.template, dir);
        const dirId =
            this.tcb.env.reference(dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);

        // There are two kinds of guards. Template guards (ngTemplateGuards) allow type narrowing of
        // the expression passed to an @Input of the directive. Scan the directive to see if it has
        // any template guards, and generate them if needed.
        dir.ngTemplateGuards.forEach(guard => {
          // For each template guard function on the directive, look for a binding to that input.
          const boundInput = this.template.inputs.find(i => i.name === guard.inputName) ||
              this.template.templateAttrs.find(
                  (i: TmplAstTextAttribute|TmplAstBoundAttribute): i is TmplAstBoundAttribute =>
                      i instanceof TmplAstBoundAttribute && i.name === guard.inputName);
          if (boundInput !== undefined) {
            // If there is such a binding, generate an expression for it.
            const expr = tcbExpression(boundInput.value, this.tcb, this.scope);

            // The expression has already been checked in the type constructor invocation, so
            // it should be ignored when used within a template guard.
            ignoreDiagnostics(expr);

            if (guard.type === 'binding') {
              // Use the binding expression itself as guard.
              directiveGuards.push(expr);
            } else {
              // Call the guard function on the directive with the directive instance and that
              // expression.
              const guardInvoke = tsCallMethod(dirId, `ngTemplateGuard_${guard.inputName}`, [
                dirInstId,
                expr,
              ]);
              addParseSpanInfo(guardInvoke, boundInput.value.sourceSpan);
              directiveGuards.push(guardInvoke);
            }
          }
        });

        // The second kind of guard is a template context guard. This guard narrows the template
        // rendering context variable `ctx`.
        if (dir.hasNgTemplateContextGuard && this.tcb.env.config.applyTemplateContextGuards) {
          const ctx = this.scope.resolve(this.template);
          const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
          addParseSpanInfo(guardInvoke, this.template.sourceSpan);
          directiveGuards.push(guardInvoke);
        }
      }
    }

    // By default the guard is simply `true`.
    let guard: ts.Expression|null = null;

    // If there are any guards from directives, use them instead.
    if (directiveGuards.length > 0) {
      // Pop the first value and use it as the initializer to reduce(). This way, a single guard
      // will be used on its own, but two or more will be combined into binary AND expressions.
      guard = directiveGuards.reduce(
          (expr, dirGuard) =>
              ts.createBinary(expr, ts.SyntaxKind.AmpersandAmpersandToken, dirGuard),
          directiveGuards.pop()!);
    }

    // Create a new Scope for the template. This constructs the list of operations for the template
    // children, as well as tracks bindings within the template.
    const tmplScope = Scope.forNodes(this.tcb, this.scope, this.template, guard);

    // Render the template's `Scope` into a block.
    let tmplBlock: ts.Statement = ts.createBlock(tmplScope.render());
    if (guard !== null) {
      // The scope has a guard that needs to be applied, so wrap the template block into an `if`
      // statement containing the guard expression.
      tmplBlock = ts.createIf(/* expression */ guard, /* thenStatement */ tmplBlock);
    }
    this.scope.addStatement(tmplBlock);

    return null;
  }


execute(): null {
    const expr = tcbExpression(this.binding.value, this.tcb, this.scope);
    this.scope.addStatement(ts.createExpressionStatement(expr));
    return null;
  }


execute(): ts.Identifier {
    const id = this.tcb.allocateId();
    // Process the directive and construct expressions for each of its bindings.
    const inputs = tcbGetDirectiveInputs(this.node, this.dir, this.tcb, this.scope);

    // Call the type constructor of the directive to infer a type, and assign the directive
    // instance.
    const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, inputs);
    addParseSpanInfo(typeCtor, this.node.sourceSpan);
    this.scope.addStatement(tsCreateVariable(id, typeCtor));
    return id;
  }


circularFallback(): TcbOp {
    return new TcbDirectiveCircularFallbackOp(this.tcb, this.scope, this.node, this.dir);
  }


execute(): ts.Identifier {
    const id = this.tcb.allocateId();
    const typeCtor = this.tcb.env.typeCtorFor(this.dir);
    const circularPlaceholder = ts.createCall(
        typeCtor, /* typeArguments */ undefined, [ts.createNonNullExpression(ts.createNull())]);
    this.scope.addStatement(tsCreateVariable(id, circularPlaceholder));
    return id;
  }


execute(): ts.Expression|null {
    if (this.checkElement) {
      this.tcb.domSchemaChecker.checkElement(this.tcb.id, this.element, this.tcb.schemas);
    }

    // TODO(alxhub): this could be more efficient.
    for (const binding of this.element.inputs) {
      if (binding.type === BindingType.Property && this.claimedInputs.has(binding.name)) {
        // Skip this binding as it was claimed by a directive.
        continue;
      }

      if (binding.type === BindingType.Property) {
        if (binding.name !== 'style' && binding.name !== 'class') {
          // A direct binding to a property.
          const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
          this.tcb.domSchemaChecker.checkProperty(
              this.tcb.id, this.element, propertyName, binding.sourceSpan, this.tcb.schemas);
        }
      }
    }
    return null;
  }


execute(): null {
    // `this.inputs` contains only those bindings not matched by any directive. These bindings go to
    // the element itself.
    const elId = this.scope.resolve(this.element);

    // TODO(alxhub): this could be more efficient.
    for (const binding of this.element.inputs) {
      if (binding.type === BindingType.Property && this.claimedInputs.has(binding.name)) {
        // Skip this binding as it was claimed by a directive.
        continue;
      }

      let expr = tcbExpression(binding.value, this.tcb, this.scope);
      if (!this.tcb.env.config.checkTypeOfInputBindings) {
        // If checking the type of bindings is disabled, cast the resulting expression to 'any'
        // before the assignment.
        expr = tsCastToAny(expr);
      } else if (!this.tcb.env.config.strictNullInputBindings) {
        // If strict null checks are disabled, erase `null` and `undefined` from the type by
        // wrapping the expression in a non-null assertion.
        expr = ts.createNonNullExpression(expr);
      }

      if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
        if (binding.name !== 'style' && binding.name !== 'class') {
          // A direct binding to a property.
          const propertyName = ATTR_TO_PROP[binding.name] || binding.name;
          const prop = ts.createElementAccess(elId, ts.createStringLiteral(propertyName));
          const stmt = ts.createBinary(prop, ts.SyntaxKind.EqualsToken, wrapForDiagnostics(expr));
          addParseSpanInfo(stmt, binding.sourceSpan);
          this.scope.addStatement(ts.createExpressionStatement(stmt));
        } else {
          this.scope.addStatement(ts.createExpressionStatement(expr));
        }
      } else {
        // A binding to an animation, attribute, class or style. For now, only validate the right-
        // hand side of the expression.
        // TODO: properly check class and style bindings.
        this.scope.addStatement(ts.createExpressionStatement(expr));
      }
    }

    return null;
  }


execute(): null {
    const dirId = this.scope.resolve(this.node, this.dir);

    // `dir.outputs` is an object map of field names on the directive class to event names.
    // This is backwards from what's needed to match event handlers - a map of event names to field
    // names is desired. Invert `dir.outputs` into `fieldByEventName` to create this map.
    const fieldByEventName = new Map<string, string>();
    const outputs = this.dir.outputs;
    for (const key of Object.keys(outputs)) {
      fieldByEventName.set(outputs[key], key);
    }

    for (const output of this.node.outputs) {
      if (output.type !== ParsedEventType.Regular || !fieldByEventName.has(output.name)) {
        continue;
      }
      const field = fieldByEventName.get(output.name)!;

      if (this.tcb.env.config.checkTypeOfOutputEvents) {
        // For strict checking of directive events, generate a call to the `subscribe` method
        // on the directive's output field to let type information flow into the handler function's
        // `$event` parameter.
        //
        // Note that the `EventEmitter<T>` type from '@angular/core' that is typically used for
        // outputs has a typings deficiency in its `subscribe` method. The generic type `T` is not
        // carried into the handler function, which is vital for inference of the type of `$event`.
        // As a workaround, the directive's field is passed into a helper function that has a
        // specially crafted set of signatures, to effectively cast `EventEmitter<T>` to something
        // that has a `subscribe` method that properly carries the `T` into the handler function.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);

        const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field));
        const outputHelper =
            ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
        const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
        const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
        addParseSpanInfo(call, output.sourceSpan);
        this.scope.addStatement(ts.createExpressionStatement(call));
      } else {
        // If strict checking of directive events is disabled, emit a handler function where the
        // `$event` parameter has an explicit `any` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      }

      ExpressionSemanticVisitor.visit(
          output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
    }

    return null;
  }


execute(): null {
    const elId = this.scope.resolve(this.element);

    // TODO(alxhub): this could be more efficient.
    for (const output of this.element.outputs) {
      if (this.claimedOutputs.has(output.name)) {
        // Skip this event handler as it was claimed by a directive.
        continue;
      }

      if (output.type === ParsedEventType.Animation) {
        // Animation output bindings always have an `$event` parameter of type `AnimationEvent`.
        const eventType = this.tcb.env.config.checkTypeOfAnimationEvents ?
            this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent') :
            EventParamType.Any;

        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      } else if (this.tcb.env.config.checkTypeOfDomEvents) {
        // If strict checking of DOM events is enabled, generate a call to `addEventListener` on
        // the element instance so that TypeScript's type inference for
        // `HTMLElement.addEventListener` using `HTMLElementEventMap` to infer an accurate type for
        // `$event` depending on the event name. For unknown event names, TypeScript resorts to the
        // base `Event` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);

        const call = ts.createCall(
            /* expression */ ts.createPropertyAccess(elId, 'addEventListener'),
            /* typeArguments */ undefined,
            /* arguments */[ts.createStringLiteral(output.name), handler]);
        addParseSpanInfo(call, output.sourceSpan);
        this.scope.addStatement(ts.createExpressionStatement(call));
      } else {
        // If strict checking of DOM inputs is disabled, emit a handler function where the `$event`
        // parameter has an explicit `any` type.
        const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
        this.scope.addStatement(ts.createExpressionStatement(handler));
      }

      ExpressionSemanticVisitor.visit(
          output.handler, this.tcb.id, this.tcb.boundTarget, this.tcb.oobRecorder);
    }

    return null;
  }


/**
   * Allocate a new variable name for use within the `Context`.
   *
   * Currently this uses a monotonically increasing counter, but in the future the variable name
   * might change depending on the type of data being stored.
   */
allocateId(): ts.Identifier {
    return ts.createIdentifier(`_t${this.nextId++}`);
  }


getPipeByName(name: string): ts.Expression|null {
    if (!this.pipes.has(name)) {
      return null;
    }
    return this.env.pipeInst(this.pipes.get(name)!);
  }


/**
   * Constructs a `Scope` given either a `TmplAstTemplate` or a list of `TmplAstNode`s.
   *
   * @param tcb the overall context of TCB generation.
   * @param parent the `Scope` of the parent template (if any) or `null` if this is the root
   * `Scope`.
   * @param templateOrNodes either a `TmplAstTemplate` representing the template for which to
   * calculate the `Scope`, or a list of nodes if no outer template object is available.
   * @param guard an expression that is applied to this scope for type narrowing purposes.
   */
static forNodes(
      tcb: Context, parent: Scope|null, templateOrNodes: TmplAstTemplate|(TmplAstNode[]),
      guard: ts.Expression|null): Scope {
    const scope = new Scope(tcb, parent, guard);

    let children: TmplAstNode[];

    // If given an actual `TmplAstTemplate` instance, then process any additional information it
    // has.
    if (templateOrNodes instanceof TmplAstTemplate) {
      // The template's variable declarations need to be added as `TcbVariableOp`s.
      const varMap = new Map<string, TmplAstVariable>();

      for (const v of templateOrNodes.variables) {
        // Validate that variables on the `TmplAstTemplate` are only declared once.
        if (!varMap.has(v.name)) {
          varMap.set(v.name, v);
        } else {
          const firstDecl = varMap.get(v.name)!;
          tcb.oobRecorder.duplicateTemplateVar(tcb.id, v, firstDecl);
        }

        const opIndex = scope.opQueue.push(new TcbVariableOp(tcb, scope, templateOrNodes, v)) - 1;
        scope.varMap.set(v, opIndex);
      }
      children = templateOrNodes.children;
    } else {
      children = templateOrNodes;
    }
    for (const node of children) {
      scope.appendNode(node);
    }
    return scope;
  }


/**
   * Look up a `ts.Expression` representing the value of some operation in the current `Scope`,
   * including any parent scope(s).
   *
   * @param node a `TmplAstNode` of the operation in question. The lookup performed will depend on
   * the type of this node:
   *
   * Assuming `directive` is not present, then `resolve` will return:
   *
   * * `TmplAstElement` - retrieve the expression for the element DOM node
   * * `TmplAstTemplate` - retrieve the template context variable
   * * `TmplAstVariable` - retrieve a template let- variable
   *
   * @param directive if present, a directive type on a `TmplAstElement` or `TmplAstTemplate` to
   * look up instead of the default for an element or template node.
   */
resolve(
      node: TmplAstElement|TmplAstTemplate|TmplAstVariable,
      directive?: TypeCheckableDirectiveMeta): ts.Expression {
    // Attempt to resolve the operation locally.
    const res = this.resolveLocal(node, directive);
    if (res !== null) {
      return res;
    } else if (this.parent !== null) {
      // Check with the parent.
      return this.parent.resolve(node, directive);
    } else {
      throw new Error(`Could not resolve ${node} / ${directive}`);
    }
  }


/**
   * Add a statement to this scope.
   */
addStatement(stmt: ts.Statement): void {
    this.statements.push(stmt);
  }


/**
   * Get the statements.
   */
render(): ts.Statement[] {
    for (let i = 0; i < this.opQueue.length; i++) {
      this.executeOp(i);
    }
    return this.statements;
  }


/**
   * Returns an expression of all template guards that apply to this scope, including those of
   * parent scopes. If no guards have been applied, null is returned.
   */
guards(): ts.Expression|null {
    let parentGuards: ts.Expression|null = null;
    if (this.parent !== null) {
      // Start with the guards from the parent scope, if present.
      parentGuards = this.parent.guards();
    }

    if (this.guard === null) {
      // This scope does not have a guard, so return the parent's guards as is.
      return parentGuards;
    } else if (parentGuards === null) {
      // There's no guards from the parent scope, so this scope's guard represents all available
      // guards.
      return this.guard;
    } else {
      // Both the parent scope and this scope provide a guard, so create a combination of the two.
      // It is important that the parent guard is used as left operand, given that it may provide
      // narrowing that is required for this scope's guard to be valid.
      return ts.createBinary(parentGuards, ts.SyntaxKind.AmpersandAmpersandToken, this.guard);
    }
  }


private resolveLocal(
      ref: TmplAstElement|TmplAstTemplate|TmplAstVariable,
      directive?: TypeCheckableDirectiveMeta): ts.Expression|null {
    if (ref instanceof TmplAstVariable && this.varMap.has(ref)) {
      // Resolving a context variable for this template.
      // Execute the `TcbVariableOp` associated with the `TmplAstVariable`.
      return this.resolveOp(this.varMap.get(ref)!);
    } else if (
        ref instanceof TmplAstTemplate && directive === undefined &&
        this.templateCtxOpMap.has(ref)) {
      // Resolving the context of the given sub-template.
      // Execute the `TcbTemplateContextOp` for the template.
      return this.resolveOp(this.templateCtxOpMap.get(ref)!);
    } else if (
        (ref instanceof TmplAstElement || ref instanceof TmplAstTemplate) &&
        directive !== undefined && this.directiveOpMap.has(ref)) {
      // Resolving a directive on an element or sub-template.
      const dirMap = this.directiveOpMap.get(ref)!;
      if (dirMap.has(directive)) {
        return this.resolveOp(dirMap.get(directive)!);
      } else {
        return null;
      }
    } else if (ref instanceof TmplAstElement && this.elementOpMap.has(ref)) {
      // Resolving the DOM node of an element in this template.
      return this.resolveOp(this.elementOpMap.get(ref)!);
    } else {
      return null;
    }
  }


/**
   * Like `executeOp`, but assert that the operation actually returned `ts.Expression`.
   */
private resolveOp(opIndex: number): ts.Expression {
    const res = this.executeOp(opIndex);
    if (res === null) {
      throw new Error(`Error resolving operation, got null`);
    }
    return res;
  }


/**
   * Execute a particular `TcbOp` in the `opQueue`.
   *
   * This method replaces the operation in the `opQueue` with the result of execution (once done)
   * and also protects against a circular dependency from the operation to itself by temporarily
   * setting the operation's result to a special expression.
   */
private executeOp(opIndex: number): ts.Expression|null {
    const op = this.opQueue[opIndex];
    if (!(op instanceof TcbOp)) {
      return op;
    }

    // Set the result of the operation in the queue to its circular fallback. If executing this
    // operation results in a circular dependency, this will prevent an infinite loop and allow for
    // the resolution of such cycles.
    this.opQueue[opIndex] = op.circularFallback();
    const res = op.execute();
    // Once the operation has finished executing, it's safe to cache the real result.
    this.opQueue[opIndex] = res;
    return res;
  }


private appendNode(node: TmplAstNode): void {
    if (node instanceof TmplAstElement) {
      const opIndex = this.opQueue.push(new TcbElementOp(this.tcb, this, node)) - 1;
      this.elementOpMap.set(node, opIndex);
      this.appendDirectivesAndInputsOfNode(node);
      this.appendOutputsOfNode(node);
      for (const child of node.children) {
        this.appendNode(child);
      }
      this.checkReferencesOfNode(node);
    } else if (node instanceof TmplAstTemplate) {
      // Template children are rendered in a child scope.
      this.appendDirectivesAndInputsOfNode(node);
      this.appendOutputsOfNode(node);
      if (this.tcb.env.config.checkTemplateBodies) {
        const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
        this.templateCtxOpMap.set(node, ctxIndex);
        this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node));
      }
      this.checkReferencesOfNode(node);
    } else if (node instanceof TmplAstBoundText) {
      this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node));
    }
  }


private checkReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void {
    for (const ref of node.references) {
      if (this.tcb.boundTarget.getReferenceTarget(ref) === null) {
        this.tcb.oobRecorder.missingReferenceTarget(this.tcb.id, ref);
      }
    }
  }


private appendDirectivesAndInputsOfNode(node: TmplAstElement|TmplAstTemplate): void {
    // Collect all the inputs on the element.
    const claimedInputs = new Set<string>();
    const directives = this.tcb.boundTarget.getDirectivesOfNode(node);
    if (directives === null || directives.length === 0) {
      // If there are no directives, then all inputs are unclaimed inputs, so queue an operation
      // to add them if needed.
      if (node instanceof TmplAstElement) {
        this.opQueue.push(new TcbUnclaimedInputsOp(this.tcb, this, node, claimedInputs));
        this.opQueue.push(
            new TcbDomSchemaCheckerOp(this.tcb, node, /* checkElement */ true, claimedInputs));
      }
      return;
    }

    const dirMap = new Map<TypeCheckableDirectiveMeta, number>();
    for (const dir of directives) {
      const dirIndex = this.opQueue.push(new TcbDirectiveOp(this.tcb, this, node, dir)) - 1;
      dirMap.set(dir, dirIndex);
    }
    this.directiveOpMap.set(node, dirMap);

    // After expanding the directives, we might need to queue an operation to check any unclaimed
    // inputs.
    if (node instanceof TmplAstElement) {
      // Go through the directives and remove any inputs that it claims from `elementInputs`.
      for (const dir of directives) {
        for (const fieldName of Object.keys(dir.inputs)) {
          const value = dir.inputs[fieldName];
          claimedInputs.add(Array.isArray(value) ? value[0] : value);
        }
      }

      this.opQueue.push(new TcbUnclaimedInputsOp(this.tcb, this, node, claimedInputs));
      // If there are no directives which match this element, then it's a "plain" DOM element (or a
      // web component), and should be checked against the DOM schema. If any directives match,
      // we must assume that the element could be custom (either a component, or a directive like
      // <router-outlet>) and shouldn't validate the element name itself.
      const checkElement = directives.length === 0;
      this.opQueue.push(new TcbDomSchemaCheckerOp(this.tcb, node, checkElement, claimedInputs));
    }
  }


private appendOutputsOfNode(node: TmplAstElement|TmplAstTemplate): void {
    // Collect all the outputs on the element.
    const claimedOutputs = new Set<string>();
    const directives = this.tcb.boundTarget.getDirectivesOfNode(node);
    if (directives === null || directives.length === 0) {
      // If there are no directives, then all outputs are unclaimed outputs, so queue an operation
      // to add them if needed.
      if (node instanceof TmplAstElement) {
        this.opQueue.push(new TcbUnclaimedOutputsOp(this.tcb, this, node, claimedOutputs));
      }
      return;
    }

    // Queue operations for all directives to check the relevant outputs for a directive.
    for (const dir of directives) {
      this.opQueue.push(new TcbDirectiveOutputsOp(this.tcb, this, node, dir));
    }

    // After expanding the directives, we might need to queue an operation to check any unclaimed
    // outputs.
    if (node instanceof TmplAstElement) {
      // Go through the directives and register any outputs that it claims in `claimedOutputs`.
      for (const dir of directives) {
        for (const outputField of Object.keys(dir.outputs)) {
          claimedOutputs.add(dir.outputs[outputField]);
        }
      }

      this.opQueue.push(new TcbUnclaimedOutputsOp(this.tcb, this, node, claimedOutputs));
    }
  }


translate(ast: AST): ts.Expression {
    // `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed
    // which interprets specific expression nodes that interact with the `ImplicitReceiver`. These
    // nodes actually refer to identifiers within the current scope.
    return astToTypescript(ast, ast => this.resolve(ast), this.tcb.env.config);
  }


/**
   * Resolve an `AST` expression within the given scope.
   *
   * Some `AST` expressions refer to top-level concepts (references, variables, the component
   * context). This method assists in resolving those.
   */
protected resolve(ast: AST): ts.Expression|null {
    if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
      // Try to resolve a bound target for this expression. If no such target is available, then
      // the expression is referencing the top-level component context. In that case, `null` is
      // returned here to let it fall through resolution so it will be caught when the
      // `ImplicitReceiver` is resolved in the branch below.
      return this.resolveTarget(ast);
    } else if (ast instanceof PropertyWrite && ast.receiver instanceof ImplicitReceiver) {
      const target = this.resolveTarget(ast);
      if (target === null) {
        return null;
      }

      const expr = this.translate(ast.value);
      const result = ts.createParen(ts.createBinary(target, ts.SyntaxKind.EqualsToken, expr));
      addParseSpanInfo(result, ast.sourceSpan);
      return result;
    } else if (ast instanceof ImplicitReceiver) {
      // AST instances representing variables and references look very similar to property reads
      // or method calls from the component context: both have the shape
      // PropertyRead(ImplicitReceiver, 'propName') or MethodCall(ImplicitReceiver, 'methodName').
      //
      // `translate` will first try to `resolve` the outer PropertyRead/MethodCall. If this works,
      // it's because the `BoundTarget` found an expression target for the whole expression, and
      // therefore `translate` will never attempt to `resolve` the ImplicitReceiver of that
      // PropertyRead/MethodCall.
      //
      // Therefore if `resolve` is called on an `ImplicitReceiver`, it's because no outer
      // PropertyRead/MethodCall resolved to a variable or reference, and therefore this is a
      // property read or method call on the component context itself.
      return ts.createIdentifier('ctx');
    } else if (ast instanceof BindingPipe) {
      const expr = this.translate(ast.exp);
      let pipe: ts.Expression|null;
      if (this.tcb.env.config.checkTypeOfPipes) {
        pipe = this.tcb.getPipeByName(ast.name);
        if (pipe === null) {
          // No pipe by that name exists in scope. Record this as an error.
          this.tcb.oobRecorder.missingPipe(this.tcb.id, ast);

          // Return an 'any' value to at least allow the rest of the expression to be checked.
          pipe = NULL_AS_ANY;
        }
      } else {
        pipe = ts.createParen(ts.createAsExpression(
            ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
      }
      const args = ast.args.map(arg => this.translate(arg));
      const result = tsCallMethod(pipe, 'transform', [expr, ...args]);
      addParseSpanInfo(result, ast.sourceSpan);
      return result;
    } else if (ast instanceof MethodCall && ast.receiver instanceof ImplicitReceiver) {
      // Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`.
      if (ast.name === '$any' && ast.args.length === 1) {
        const expr = this.translate(ast.args[0]);
        const exprAsAny =
            ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
        const result = ts.createParen(exprAsAny);
        addParseSpanInfo(result, ast.sourceSpan);
        return result;
      }

      // Attempt to resolve a bound target for the method, and generate the method call if a target
      // could be resolved. If no target is available, then the method is referencing the top-level
      // component context, in which case `null` is returned to let the `ImplicitReceiver` being
      // resolved to the component context.
      const receiver = this.resolveTarget(ast);
      if (receiver === null) {
        return null;
      }

      const method = ts.createPropertyAccess(wrapForDiagnostics(receiver), ast.name);
      const args = ast.args.map(arg => this.translate(arg));
      const node = ts.createCall(method, undefined, args);
      addParseSpanInfo(node, ast.sourceSpan);
      return node;
    } else {
      // This AST isn't special after all.
      return null;
    }
  }


/**
   * Attempts to resolve a bound target for a given expression, and translates it into the
   * appropriate `ts.Expression` that represents the bound target. If no target is available,
   * `null` is returned.
   */
protected resolveTarget(ast: AST): ts.Expression|null {
    const binding = this.tcb.boundTarget.getExpressionTarget(ast);
    if (binding === null) {
      return null;
    }

    // This expression has a binding to some variable or reference in the template. Resolve it.
    if (binding instanceof TmplAstVariable) {
      const expr = ts.getMutableClone(this.scope.resolve(binding));
      addParseSpanInfo(expr, ast.sourceSpan);
      return expr;
    } else if (binding instanceof TmplAstReference) {
      const target = this.tcb.boundTarget.getReferenceTarget(binding);
      if (target === null) {
        // This reference is unbound. Traversal of the `TmplAstReference` itself should have
        // recorded the error in the `OutOfBandDiagnosticRecorder`.
        // Still check the rest of the expression if possible by using an `any` value.
        return NULL_AS_ANY;
      }

      // The reference is either to an element, an <ng-template> node, or to a directive on an
      // element or template.

      if (target instanceof TmplAstElement) {
        if (!this.tcb.env.config.checkTypeOfDomReferences) {
          // References to DOM nodes are pinned to 'any'.
          return NULL_AS_ANY;
        }

        const expr = ts.getMutableClone(this.scope.resolve(target));
        addParseSpanInfo(expr, ast.sourceSpan);
        return expr;
      } else if (target instanceof TmplAstTemplate) {
        if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
          // References to `TemplateRef`s pinned to 'any'.
          return NULL_AS_ANY;
        }

        // Direct references to an <ng-template> node simply require a value of type
        // `TemplateRef<any>`. To get this, an expression of the form
        // `(null as any as TemplateRef<any>)` is constructed.
        let value: ts.Expression = ts.createNull();
        value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
        value = ts.createAsExpression(
            value,
            this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]));
        value = ts.createParen(value);
        addParseSpanInfo(value, ast.sourceSpan);
        return value;
      } else {
        if (!this.tcb.env.config.checkTypeOfNonDomReferences) {
          // References to directives are pinned to 'any'.
          return NULL_AS_ANY;
        }

        const expr = ts.getMutableClone(this.scope.resolve(target.node, target.directive));
        addParseSpanInfo(expr, ast.sourceSpan);
        return expr;
      }
    } else {
      throw new Error(`Unreachable: ${binding}`);
    }
  }


protected resolve(ast: AST): ts.Expression|null {
    // Recognize a property read on the implicit receiver corresponding with the event parameter
    // that is available in event bindings. Since this variable is a parameter of the handler
    // function that the converted expression becomes a child of, just create a reference to the
    // parameter by its name.
    if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver &&
        ast.name === EVENT_PARAMETER) {
      const event = ts.createIdentifier(EVENT_PARAMETER);
      addParseSpanInfo(event, ast.sourceSpan);
      return event;
    }

    return super.resolve(ast);
  }


lastSyncedLabel => this.updateInputAriaLabel(lastSyncedLabel)


result => {
			if (token && token.isCancellationRequested) {
				// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
				return null;
			}

			if (!this.searchResultModel) {
				this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
				this.searchResultModel.setResult(type, result);
				this.tocTreeModel.currentSearchModel = this.searchResultModel;
				this.onSearchModeToggled();
			} else {
				this.searchResultModel.setResult(type, result);
				this.tocTreeModel.update();
			}

			this.tocTree.setFocus([]);
			this.viewState.filterToCategory = undefined;
			this.tocTree.expandAll();

			this.refreshTOCTree();
			this.renderTree(undefined, true);
			return result;
		}


class SyncControls extends Disposable {
	private readonly lastSyncedLabel!: HTMLElement;
	private readonly turnOnSyncButton!: Button;

	private readonly _onDidChangeLastSyncedLabel = this._register(new Emitter<string>());
	public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event;

	constructor(
		container: HTMLElement,
		@ICommandService private readonly commandService: ICommandService,
		@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
		@IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService,
		@IThemeService themeService: IThemeService,
	) {
		super();

		const headerRightControlsContainer = DOM.append(container, $('.settings-right-controls'));
		const turnOnSyncButtonContainer = DOM.append(headerRightControlsContainer, $('.turn-on-sync'));
		this.turnOnSyncButton = this._register(new Button(turnOnSyncButtonContainer, { title: true }));
		this._register(attachButtonStyler(this.turnOnSyncButton, themeService));
		this.lastSyncedLabel = DOM.append(headerRightControlsContainer, $('.last-synced-label'));
		DOM.hide(this.lastSyncedLabel);

		this.turnOnSyncButton.enabled = true;
		this.turnOnSyncButton.label = localize('turnOnSyncButton', "Turn on Settings Sync");
		DOM.hide(this.turnOnSyncButton.element);

		this._register(this.turnOnSyncButton.onDidClick(async () => {
			await this.commandService.executeCommand('workbench.userDataSync.actions.turnOn');
		}));

		this.updateLastSyncedTime();
		this._register(this.userDataSyncService.onDidChangeLastSyncTime(() => {
			this.updateLastSyncedTime();
		}));

		const updateLastSyncedTimer = this._register(new IntervalTimer());
		updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000);

		this.update();
		this._register(this.userDataSyncService.onDidChangeStatus(() => {
			this.update();
		}));

		this._register(this.userDataAutoSyncService.onDidChangeEnablement(() => {
			this.update();
		}));
	}

	private updateLastSyncedTime(): void {
		const last = this.userDataSyncService.lastSyncTime;
		let label: string;
		if (typeof last === 'number') {
			const d = fromNow(last, true);
			label = localize('lastSyncedLabel', "Last synced: {0}", d);
		} else {
			label = '';
		}

		this.lastSyncedLabel.textContent = label;
		this._onDidChangeLastSyncedLabel.fire(label);
	}

	private update(): void {
		if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
			return;
		}

		if (this.userDataAutoSyncService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {
			DOM.show(this.lastSyncedLabel);
			DOM.hide(this.turnOnSyncButton.element);
		} else {
			DOM.hide(this.lastSyncedLabel);
			DOM.show(this.turnOnSyncButton.element);
		}
	}
}


layout(dimension: DOM.Dimension): void {
		this.dimension = dimension;

		if (!this.isVisible()) {
			return;
		}

		this.layoutTrees(dimension);

		const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right;
		// minus padding inside inputbox, countElement width, controls width, extra padding before countElement
		const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - this.controlsElement.clientWidth - 12;
		this.searchWidget.layout({ height: 20, width: monacoWidth });

		this.rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600);
		this.rootElement.classList.toggle('narrow-width', dimension.width < 600);
	}


private updateInputAriaLabel(lastSyncedLabel: string) {
		const label = lastSyncedLabel ?
			`${searchBoxLabel}. ${lastSyncedLabel}` :
			searchBoxLabel;
		this.searchWidget.updateAriaLabel(label);
	}


private createHeader(parent: HTMLElement): void {
		this.headerContainer = DOM.append(parent, $('.settings-header'));

		const searchContainer = DOM.append(this.headerContainer, $('.search-container'));

		const clearInputAction = new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Settings Search Input"), 'codicon-clear-all', false, () => { this.clearSearchResults(); return Promise.resolve(null); });

		this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, {
			triggerCharacters: ['@'],
			provideResults: (query: string) => {
				return SettingsEditor2.SUGGESTIONS.filter(tag => query.indexOf(tag) === -1).map(tag => tag.endsWith(':') ? tag : tag + ' ');
			}
		}, searchBoxLabel, 'settingseditor:searchinput' + SettingsEditor2.NUM_INSTANCES++, {
			placeholderText: searchBoxLabel,
			focusContextKey: this.searchFocusContextKey,
			// TODO: Aria-live
		}));
		this._register(this.searchWidget.onFocus(() => {
			this._currentFocusContext = SettingsFocusContext.Search;
		}));

		this._register(attachSuggestEnabledInputBoxStyler(this.searchWidget, this.themeService, {
			inputBorder: settingsTextInputBorder
		}));

		this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge.long'));
		this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => {
			const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
			const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';
			const foreground = colors.badgeForeground ? colors.badgeForeground.toString() : '';

			this.countElement.style.backgroundColor = background;
			this.countElement.style.color = foreground;

			this.countElement.style.borderWidth = border ? '1px' : '';
			this.countElement.style.borderStyle = border ? 'solid' : '';
			this.countElement.style.borderColor = border;
		}));

		this._register(this.searchWidget.onInputDidChange(() => {
			const searchVal = this.searchWidget.getValue();
			clearInputAction.enabled = !!searchVal;
			this.onSearchInputChanged();
		}));

		const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls'));
		const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container'));
		this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer, { enableRemoteSettings: true }));
		this.settingsTargetsWidget.settingsTarget = ConfigurationTarget.USER_LOCAL;
		this.settingsTargetsWidget.onDidTargetChange(target => this.onDidSettingsTargetChange(target));
		this._register(DOM.addDisposableListener(targetWidgetContainer, DOM.EventType.KEY_DOWN, e => {
			const event = new StandardKeyboardEvent(e);
			if (event.keyCode === KeyCode.DownArrow) {
				this.focusSettings();
			}
		}));

		if (this.userDataSyncWorkbenchService.enabled && this.userDataAutoSyncService.canToggleEnablement()) {
			const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer));
			this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => this.updateInputAriaLabel(lastSyncedLabel)));
		}

		this.controlsElement = DOM.append(searchContainer, DOM.$('.settings-clear-widget'));

		const actionBar = this._register(new ActionBar(this.controlsElement, {
			animated: false,
			actionViewItemProvider: (_action) => { return undefined; }
		}));

		actionBar.push([clearInputAction], { label: false, icon: true });
	}


private notifyNoSaveNeeded() {
		if (!this.storageService.getBoolean(SETTINGS_AUTOSAVE_NOTIFIED_KEY, StorageScope.GLOBAL, false)) {
			this.storageService.store(SETTINGS_AUTOSAVE_NOTIFIED_KEY, true, StorageScope.GLOBAL);
			this.notificationService.info(localize('settingsNoSaveNeeded', "Your changes are automatically saved as you edit."));
		}
	}


private async onConfigUpdate(keys?: string[], forceRefresh = false, schemaChange = false): Promise<void> {
		if (keys && this.settingsTreeModel) {
			return this.updateElementsByKey(keys);
		}

		const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed
		const dividedGroups = collections.groupBy(groups, g => g.extensionInfo ? 'extension' : 'core');
		const settingsResult = resolveSettingsTree(tocData, dividedGroups.core);
		const resolvedSettingsRoot = settingsResult.tree;

		// Warn for settings not included in layout
		if (settingsResult.leftoverSettings.size && !this.hasWarnedMissingSettings) {
			const settingKeyList: string[] = [];
			settingsResult.leftoverSettings.forEach(s => {
				settingKeyList.push(s.key);
			});

			this.logService.warn(`SettingsEditor2: Settings not included in settingsLayout.ts: ${settingKeyList.join(', ')}`);
			this.hasWarnedMissingSettings = true;
		}

		const commonlyUsed = resolveSettingsTree(commonlyUsedData, dividedGroups.core);
		resolvedSettingsRoot.children!.unshift(commonlyUsed.tree);

		resolvedSettingsRoot.children!.push(resolveExtensionsSettings(dividedGroups.extension || []));

		if (this.searchResultModel) {
			this.searchResultModel.updateChildren();
		}

		if (this.settingsTreeModel) {
			this.settingsTreeModel.update(resolvedSettingsRoot);

			if (schemaChange && !!this.searchResultModel) {
				// If an extension's settings were just loaded and a search is active, retrigger the search so it shows up
				return await this.onSearchInputChanged();
			}

			this.refreshTOCTree();
			this.renderTree(undefined, forceRefresh);
		} else {
			this.settingsTreeModel = this.instantiationService.createInstance(SettingsTreeModel, this.viewState);
			this.settingsTreeModel.update(resolvedSettingsRoot);
			this.tocTreeModel.settingsTreeRoot = this.settingsTreeModel.root as SettingsTreeGroupElement;

			const cachedState = this.restoreCachedState();
			if (cachedState && cachedState.searchQuery) {
				await this.onSearchInputChanged();
			} else {
				this.refreshTOCTree();
				this.refreshTree();
				this.tocTree.collapseAll();
			}
		}
	}


private triggerSearch(query: string): Promise<void> {
		this.viewState.tagFilters = new Set<string>();
		this.viewState.extensionFilters = new Set<string>();
		if (query) {
			const parsedQuery = parseQuery(query);
			query = parsedQuery.query;
			parsedQuery.tags.forEach(tag => this.viewState.tagFilters!.add(tag));
			parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId));
		}

		if (query && query !== '@') {
			query = this.parseSettingFromJSON(query) || query;
			return this.triggerFilterPreferences(query);
		} else {
			if ((this.viewState.tagFilters && this.viewState.tagFilters.size) || (this.viewState.extensionFilters && this.viewState.extensionFilters.size)) {
				this.searchResultModel = this.createFilterModel();
			} else {
				this.searchResultModel = null;
			}

			this.localSearchDelayer.cancel();
			this.remoteSearchThrottle.cancel();
			if (this.searchInProgress) {
				this.searchInProgress.cancel();
				this.searchInProgress.dispose();
				this.searchInProgress = null;
			}

			this.tocTree.setFocus([]);
			this.viewState.filterToCategory = undefined;
			this.tocTreeModel.currentSearchModel = this.searchResultModel;
			this.onSearchModeToggled();

			if (this.searchResultModel) {
				// Added a filter model
				this.tocTree.setSelection([]);
				this.tocTree.expandAll();
				this.refreshTOCTree();
				this.renderResultCountMessages();
				this.refreshTree();
			} else {
				// Leaving search mode
				this.tocTree.collapseAll();
				this.refreshTOCTree();
				this.renderResultCountMessages();
				this.refreshTree();
			}
		}

		return Promise.resolve();
	}


private filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise<ISearchResult | null> {
		return this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token).then(result => {
			if (token && token.isCancellationRequested) {
				// Handle cancellation like this because cancellation is lost inside the search provider due to async/await
				return null;
			}

			if (!this.searchResultModel) {
				this.searchResultModel = this.instantiationService.createInstance(SearchResultModel, this.viewState);
				this.searchResultModel.setResult(type, result);
				this.tocTreeModel.currentSearchModel = this.searchResultModel;
				this.onSearchModeToggled();
			} else {
				this.searchResultModel.setResult(type, result);
				this.tocTreeModel.update();
			}

			this.tocTree.setFocus([]);
			this.viewState.filterToCategory = undefined;
			this.tocTree.expandAll();

			this.refreshTOCTree();
			this.renderTree(undefined, true);
			return result;
		});
	}


private renderResultCountMessages() {
		if (!this.currentSettingsModel) {
			return;
		}

		this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
			? 'initial'
			: 'none';

		if (!this.searchResultModel) {
			if (this.countElement.style.display !== 'none') {
				this.countElement.style.display = 'none';
				this.layout(this.dimension);
			}

			this.rootElement.classList.remove('no-results');
			return;
		}

		if (this.tocTreeModel && this.tocTreeModel.settingsTreeRoot) {
			const count = this.tocTreeModel.settingsTreeRoot.count;
			let resultString: string;
			switch (count) {
				case 0: resultString = localize('noResults', "No Settings Found"); break;
				case 1: resultString = localize('oneResult', "1 Setting Found"); break;
				default: resultString = localize('moreThanOneResult', "{0} Settings Found", count);
			}

			this.countElement.innerText = resultString;
			aria.status(resultString);

			if (this.countElement.style.display !== 'block') {
				this.countElement.style.display = 'block';
				this.layout(this.dimension);
			}
			this.rootElement.classList.toggle('no-results', count === 0);
		}
	}


private layoutTrees(dimension: DOM.Dimension): void {
		const listHeight = dimension.height - (76 + 11 /* header height + padding*/);
		const settingsTreeHeight = listHeight - 14;
		this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`;
		this.settingsTree.layout(settingsTreeHeight, dimension.width);

		const tocTreeHeight = listHeight - 17;
		this.tocTreeContainer.style.height = `${tocTreeHeight}px`;
		this.tocTree.layout(tocTreeHeight);
	}


private update(): void {
		if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
			return;
		}

		if (this.userDataAutoSyncService.isEnabled() || this.userDataSyncService.status !== SyncStatus.Idle) {
			DOM.show(this.lastSyncedLabel);
			DOM.hide(this.turnOnSyncButton.element);
		} else {
			DOM.hide(this.lastSyncedLabel);
			DOM.show(this.turnOnSyncButton.element);
		}
	}


(appointmentId: number, identity: string, roomName: string) => {
    console.log(
        `/appointments/${appointmentId}/video-call-token/?identity=${identity}&roomName=${roomName}`
    );

    return APIKit.get(
        `/appointments/${appointmentId}/video-call-token/?identity=${identity}&roomName=${roomName}`
    )
        .then((res) => {
            return res.data.data.token;
        })
        .catch((e) => {
            console.log(e.response.data);
            return '';
        });
}


(res) => {
            return res.data.data.token;
        }


(e) => {
            console.log(e.response.data);
            return '';
        }


amount => amount > 0


results => results.length > 0


t => t.trim()


(e:Error) => {
          this.error.next(new QueryError(text, e));
          return Observable.empty();
        }


@Component({
    selector: 'app-statistics-section',
    templateUrl: './statistics.component.html',
    styleUrls: ['statistics.component.scss']
})
export class StatisticsSectionComponent {

  areas: Observable<FilterItem[]>;
  experiences: Observable<FilterItem[]>;
  inProgress: Observable<boolean>;
  results: Observable<SearchModel[]>;
  hasResults: Observable<boolean>;
  cloudTags: Observable<CloudTag[]>;
  samples: Observable<string>;

  error = new BehaviorSubject<Error>(null);

  selectedExperience: FilterItem = null;
  selectedArea: FilterItem = null;

  constructor(
    private searchService: SearchService,
    private dictionaryService: DictionaryService,
    private apiService: ApiService
  ) {
    this.experiences = this.dictionaryService.experiences;
    this.areas = this.dictionaryService.areas;

    this.inProgress = this.searchService.queriesOnAir.map(amount => amount > 0);
    this.results = this.searchService.results;
    this.hasResults = this.results.map(results => results.length > 0);

    this.cloudTags = apiService.cloudTags;
    this.cloudTags.subscribe();

    this.samples = apiService.samples;
    this.samples.subscribe();
  }

  onSearch(text) {
    for (let token of text.split(',').map(t => t.trim())) {
      this.searchService
        .addSearch([token], this.selectedArea, this.selectedExperience)
        .catch( (e:Error) => {
          this.error.next(new QueryError(text, e));
          return Observable.empty();
        })
        .subscribe();
    }
  }

  onAreaSelected(area) {
    console.log('area', area);
  }

  onExperienceSelected(experience) {
    console.log('experiences', experience);
  }

  onRemoveQuery(query: SearchModel) {
    this.searchService.removeSearch(query);
  }

  onDismiss() {
    this.error.next(null);
  }
}


onSearch(text) {
    for (let token of text.split(',').map(t => t.trim())) {
      this.searchService
        .addSearch([token], this.selectedArea, this.selectedExperience)
        .catch( (e:Error) => {
          this.error.next(new QueryError(text, e));
          return Observable.empty();
        })
        .subscribe();
    }
  }


onAreaSelected(area) {
    console.log('area', area);
  }


onExperienceSelected(experience) {
    console.log('experiences', experience);
  }


onRemoveQuery(query: SearchModel) {
    this.searchService.removeSearch(query);
  }


onDismiss() {
    this.error.next(null);
  }


pkg => pkg.isVisible()


cls => cls.isVisible()


/**
 *
 */
export class Package extends EANodeContainer {
  static umlId = 'uml:Package';

  protected _isVisible: boolean;
  protected _lastSearch: string;
  public isVisible(): boolean {
    const str = EABaseClass.service.searchString;
    if (str && str.length > 0) {
      if (str == this._lastSearch) { return this._isVisible; }
      const meVisible = super.isVisible();
      const pkgVisible = this.packages.some(pkg => pkg.isVisible());
      const clsVisible = this.allClasses.some(cls => cls.isVisible());

      this._lastSearch = str;
      this._isVisible = (meVisible || pkgVisible || clsVisible);
      return this._isVisible;
    }
    return true;
  }

  // Properties for rendering
  width: number = 0;
  height: number = 0;

  constructor() {
    super();
  }
}


public isVisible(): boolean {
    const str = EABaseClass.service.searchString;
    if (str && str.length > 0) {
      if (str == this._lastSearch) { return this._isVisible; }
      const meVisible = super.isVisible();
      const pkgVisible = this.packages.some(pkg => pkg.isVisible());
      const clsVisible = this.allClasses.some(cls => cls.isVisible());

      this._lastSearch = str;
      this._isVisible = (meVisible || pkgVisible || clsVisible);
      return this._isVisible;
    }
    return true;
  }


@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class NgxAuthRoutingModule {
}


function FullnodeSyncThin({
  size = 16,
  color = "currentColor",
}: Props): JSX.Element {
  return (
    <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <Path
        d="M10.8 20.556c.384 0 .768 0 1.128-.024l-.456-.456H10.8c-4.344 0-7.224-1.152-7.224-2.736v-2.712c1.056 1.248 3.792 2.016 7.224 2.016.504 0 .984-.024 1.44-.048l.168-.024h.168c2.592-.216 4.584-.888 5.448-1.92v1.416l.48-.48V5.508c0-1.968-3.216-3.192-7.704-3.192-4.368 0-7.704 1.224-7.704 3.192V17.34c0 1.968 3.264 3.216 7.704 3.216zm-7.224-7.152v-2.712c1.056 1.272 3.792 2.04 7.224 2.04 3.384 0 6.144-.768 7.224-2.064v2.736c0 1.56-2.952 2.76-7.224 2.76-4.344 0-7.224-1.2-7.224-2.76zm0-3.96V6.756C4.632 8.028 7.368 8.748 10.8 8.748c3.384 0 6.144-.744 7.224-2.016v2.712c0 1.608-2.952 2.808-7.224 2.808-4.344 0-7.224-1.2-7.224-2.808zm0-3.936c0-1.584 2.952-2.712 7.224-2.712 4.392 0 7.224 1.128 7.224 2.712 0 1.632-2.952 2.76-7.224 2.76-4.344 0-7.224-1.128-7.224-2.76zm8.76 13.056l3.12 3.12 5.448-5.448-.336-.336-5.112 5.112-2.784-2.784-.336.336z"
        fill={color}
      />
    </Svg>
  );
}


type Props = {
  size?: number | string;
  color?: string;
};


() => {
    resources = new Resources({
        resources: {
            en: new Resource([
                new Message(new Identifier('test'), new Pattern([new TextElement('this is my test')])),
                new Message(new Identifier('hello'), new Pattern([new TextElement('hello there')])),
                new Message(
                    new Identifier('complex'),
                    new Pattern([
                        new TextElement('Hello '),
                        new Placeable(new VariableReference(new Identifier('name'))),
                    ])
                ),
                new Message(new Identifier('extra'), new Pattern([new TextElement('a key without translations ')])),
            ]),
            da: new Resource([
                new Message(new Identifier('test'), new Pattern([new TextElement('dette er min test')])),
                new Message(new Identifier('hello'), new Pattern([new TextElement('hej med dig')])),

                new Message(
                    new Identifier('complex'),
                    new Pattern([new TextElement('Hej '), new Placeable(new VariableReference(new Identifier('name')))])
                ),
            ]),
            de: new Resource([
                new Message(new Identifier('test'), new Pattern([new TextElement('dis ist ein test')])),
                new Message(new Identifier('hello'), new Pattern([new TextElement('hallo mit dich')])),
                new Message(
                    new Identifier('complex'),
                    new Pattern([
                        new TextElement('Hallo '),
                        new Placeable(new VariableReference(new Identifier('name'))),
                    ])
                ),
            ]),
        },
    });
}


() => {
    it('should be truthy when a message exists', () => {
        expect(resources.messageExist({ id: 'test', languageCode: 'en' })).toBeTruthy();
    });

    it('should be falsy when a message not exists', () => {
        expect(resources.messageExist({ id: 'extra', languageCode: 'de' })).toBeFalsy();
    });
}


() => {
        expect(resources.messageExist({ id: 'test', languageCode: 'en' })).toBeTruthy();
    }


() => {
        expect(resources.messageExist({ id: 'extra', languageCode: 'de' })).toBeFalsy();
    }


() => {
    it('simple: should not be undefined and match the expected result', () => {
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('this is my test');
    });

    it('complex(er): should not be undefined and match the expected result', () => {
        const result = resources.getMessage({ id: 'complex', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('complex = Hello { $name }');
    });

    it('should return undefined if a message does not exist', () => {
        const result = resources.getMessage({ id: 'undefined-key', languageCode: 'en' });
        expect(result).toBeUndefined();
    });
}


() => {
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('this is my test');
    }


() => {
        const result = resources.getMessage({ id: 'complex', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('complex = Hello { $name }');
    }


() => {
        const result = resources.getMessage({ id: 'undefined-key', languageCode: 'en' });
        expect(result).toBeUndefined();
    }


() => {
    it('should return all messages', () => {
        const messages = resources.getMessageForAllResources({ id: 'test' });

        const result: Record<string, string> = {};

        for (const languageCode in messages) {
            const message = messages[languageCode];
            if (message) {
                result[languageCode] = serializeEntry(message);
            }
        }

        const match = {
            en: 'test = this is my test',
            da: 'test = dette er min test',
            de: 'test = dis ist ein test',
        };
        expect(result).toEqual(match);
    });

    it('should return an empty object if no messages exist', () => {
        const result = resources.getMessageForAllResources({ id: 'undefined-key' });
        expect(result).toEqual({});
    });
}


() => {
        const messages = resources.getMessageForAllResources({ id: 'test' });

        const result: Record<string, string> = {};

        for (const languageCode in messages) {
            const message = messages[languageCode];
            if (message) {
                result[languageCode] = serializeEntry(message);
            }
        }

        const match = {
            en: 'test = this is my test',
            da: 'test = dette er min test',
            de: 'test = dis ist ein test',
        };
        expect(result).toEqual(match);
    }


() => {
        const result = resources.getMessageForAllResources({ id: 'undefined-key' });
        expect(result).toEqual({});
    }


() => {
    it('retrieving a message should result in undefined after deletion', () => {
        const deletion = resources.deleteMessage({ id: 'test', languageCode: 'en' });
        if (deletion.isErr) {
            fail();
        }
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(result).toBeUndefined();
    });

    it('should fail when key is not found', () => {
        const result = resources.deleteMessage({ id: 'asdf', languageCode: 'en' });
        expect(result.isErr).toBeTruthy();
    });
}


() => {
        const deletion = resources.deleteMessage({ id: 'test', languageCode: 'en' });
        if (deletion.isErr) {
            fail();
        }
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(result).toBeUndefined();
    }


() => {
        const result = resources.deleteMessage({ id: 'asdf', languageCode: 'en' });
        expect(result.isErr).toBeTruthy();
    }


() => {
    it('should delete the message for all resources', () => {
        const result = resources.deleteMessageForAllResources({ id: 'test' });
        if (result.isErr) {
            fail();
        }
        for (const languageCode of resources.containedLanguageCodes()) {
            expect(resources.getMessage({ id: 'test', languageCode })).toBeUndefined();
        }
    });
    // hmmm bad. Since resources is not an immutable class and all actions are mutable
    // sub deletions might succeed and are not reversed.
    it('should return Result.ok if the message does not exist for one or more languages', () => {
        const result = resources.deleteMessageForAllResources({ id: 'dasdsa' });
        expect(result.isOk).toBeTruthy();
    });
}


() => {
        const result = resources.deleteMessageForAllResources({ id: 'test' });
        if (result.isErr) {
            fail();
        }
        for (const languageCode of resources.containedLanguageCodes()) {
            expect(resources.getMessage({ id: 'test', languageCode })).toBeUndefined();
        }
    }


() => {
        const result = resources.deleteMessageForAllResources({ id: 'dasdsa' });
        expect(result.isOk).toBeTruthy();
    }


() => {
    it('should get all ids', () => {
        const result = resources.getMessageIdsForAllResources();
        expect(result).toEqual(new Set(['test', 'hello', 'complex', 'extra']));
    });
}


() => {
        const result = resources.getMessageIdsForAllResources();
        expect(result).toEqual(new Set(['test', 'hello', 'complex', 'extra']));
    }


() => {
    it('should update a message', () => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));

        const update = resources.updateMessage({ id: 'test', languageCode: 'en', with: mockMessage });
        if (update.isErr) {
            fail();
        }
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('test = why not this instead');
    });

    it('should fail when the language code does not exist', () => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));
        const result = resources.updateMessage({ id: 'test', languageCode: 'aa', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    });

    it('should fail when the message id is not found', () => {
        const mockMessage = new Message(
            new Identifier('unknown-id'),
            new Pattern([new TextElement('why not this instead')])
        );
        const result = resources.updateMessage({ id: 'unknown-id', languageCode: 'en', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    });

    it('should fail when the given id does not equal the with.id', () => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));
        const result = resources.updateMessage({ id: 'differnet-id', languageCode: 'en', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    });
}


() => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));

        const update = resources.updateMessage({ id: 'test', languageCode: 'en', with: mockMessage });
        if (update.isErr) {
            fail();
        }
        const result = resources.getMessage({ id: 'test', languageCode: 'en' });
        if (result === undefined) {
            fail();
        }
        expect(serializeEntry(result)).toMatch('test = why not this instead');
    }


() => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));
        const result = resources.updateMessage({ id: 'test', languageCode: 'aa', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    }


() => {
        const mockMessage = new Message(
            new Identifier('unknown-id'),
            new Pattern([new TextElement('why not this instead')])
        );
        const result = resources.updateMessage({ id: 'unknown-id', languageCode: 'en', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    }


() => {
        const mockMessage = new Message(new Identifier('test'), new Pattern([new TextElement('why not this instead')]));
        const result = resources.updateMessage({ id: 'differnet-id', languageCode: 'en', with: mockMessage });
        expect(result.isErr).toBeTruthy();
    }


() => {
    it('should be possible to add a message', () => {
        const add = resources.createMessage({ id: 'extra', pattern: 'en nøgle uden oversættelse', languageCode: 'da' });
        if (add.isErr) {
            fail(add.error);
        }
        const message = resources.getMessage({ id: 'extra', languageCode: 'da' });
        if (message === undefined) {
            fail();
        }
        expect(serializeEntry(message)).toMatch('en nøgle uden oversættelse');
    });

    it('should not be possible to add an empty message', () => {
        const add = resources.createMessage({ id: 'extra', pattern: '', languageCode: 'da' });
        expect(add.isErr).toBeTruthy();
    });

    it('should be return Result.ok when adding a message with an undefined pattern', () => {
        const create = resources.createMessage({
            id: 'extra',
            pattern: undefined,
            languageCode: 'da',
            attributes: [new Attribute(new Identifier('hi'), new Pattern([]))],
        });
        expect(create.isOk).toBeTruthy();
        const message = resources.getMessage({ id: 'extra', languageCode: 'da' });
        // eslint-disable-next-line unicorn/no-null
        expect(message?.value).toBe(null);
    });

    it('should be return Result.err when adding a message with an undefined pattern without attributes', () => {
        const create = resources.createMessage({
            id: 'extra',
            pattern: undefined,
            languageCode: 'da',
            attributes: [],
        });
        expect(create.isErr).toBeTruthy();
    });

    it('should return an error when a message alreaedy exists', () => {
        const result = resources.createMessage({ id: 'extra', pattern: 'this should fail', languageCode: 'en' });
        expect(result.isErr).toBeTruthy();
    });

    it('should return an error when language code does not exist', () => {
        const result = resources.createMessage({ id: 'extra', pattern: 'this should fail', languageCode: 'aa' });
        expect(result.isErr).toBeTruthy();
    });

    it('should return an error when the message id is invalid', () => {
        const result = resources.createMessage({
            id: 'extra.something',
            pattern: 'this should fail',
            languageCode: 'aa',
        });
        expect(result.isErr).toBeTruthy();
    });
}


() => {
        const add = resources.createMessage({ id: 'extra', pattern: 'en nøgle uden oversættelse', languageCode: 'da' });
        if (add.isErr) {
            fail(add.error);
        }
        const message = resources.getMessage({ id: 'extra', languageCode: 'da' });
        if (message === undefined) {
            fail();
        }
        expect(serializeEntry(message)).toMatch('en nøgle uden oversættelse');
    }


() => {
        const add = resources.createMessage({ id: 'extra', pattern: '', languageCode: 'da' });
        expect(add.isErr).toBeTruthy();
    }


() => {
        const create = resources.createMessage({
            id: 'extra',
            pattern: undefined,
            languageCode: 'da',
            attributes: [new Attribute(new Identifier('hi'), new Pattern([]))],
        });
        expect(create.isOk).toBeTruthy();
        const message = resources.getMessage({ id: 'extra', languageCode: 'da' });
        // eslint-disable-next-line unicorn/no-null
        expect(message?.value).toBe(null);
    }


() => {
        const create = resources.createMessage({
            id: 'extra',
            pattern: undefined,
            languageCode: 'da',
            attributes: [],
        });
        expect(create.isErr).toBeTruthy();
    }


() => {
        const result = resources.createMessage({ id: 'extra', pattern: 'this should fail', languageCode: 'en' });
        expect(result.isErr).toBeTruthy();
    }


() => {
        const result = resources.createMessage({ id: 'extra', pattern: 'this should fail', languageCode: 'aa' });
        expect(result.isErr).toBeTruthy();
    }


() => {
        const result = resources.createMessage({
            id: 'extra.something',
            pattern: 'this should fail',
            languageCode: 'aa',
        });
        expect(result.isErr).toBeTruthy();
    }


() => {
    it('should be truthy when an attribute exists', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        expect(resources.attributeExists({ messageId: 'test', id: 'login', languageCode: 'en' })).toBeTruthy();
    });

    it('should be falsy when a message not exists', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        expect(resources.attributeExists({ messageId: 'test', id: 'sfafsafwee', languageCode: 'en' })).toBeFalsy();
    });
}


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        expect(resources.attributeExists({ messageId: 'test', id: 'login', languageCode: 'en' })).toBeTruthy();
    }


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        expect(resources.attributeExists({ messageId: 'test', id: 'sfafsafwee', languageCode: 'en' })).toBeFalsy();
    }


() => {
    it('should create an attribute', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const message = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(message?.attributes[0].id.name).toEqual('login');
        expect(serializePattern(message?.attributes[0].value ?? new Pattern([]))).toEqual(
            'Welcome to this test, please login.'
        );
    });

    it('should return Result.err if the attribute exists already', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        expect(create.isOk).toBeTruthy();
        const create2 = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        expect(create2.isOk).toBeFalsy();
    });

    it('should create the message if the if the message id does not exist', () => {
        const create = resources.createAttribute({
            messageId: 'balbla-nonexistent',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            console.error(create.error);
            fail();
        }
        const message = resources.getMessage({ id: 'balbla-nonexistent', languageCode: 'en' });
        if (message === undefined) {
            fail();
        }
        expect(message.attributes.length > 0).toBeTruthy();
        // eslint-disable-next-line unicorn/no-null
        expect(message.value).toBe(null);
    });

    it('should return Result.err if the pattern is invalid', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this {{{}$ test, please login.',
            languageCode: 'en',
        });
        expect(create.isOk).toBeFalsy();
    });
}


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const message = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(message?.attributes[0].id.name).toEqual('login');
        expect(serializePattern(message?.attributes[0].value ?? new Pattern([]))).toEqual(
            'Welcome to this test, please login.'
        );
    }


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        expect(create.isOk).toBeTruthy();
        const create2 = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        expect(create2.isOk).toBeFalsy();
    }


() => {
        const create = resources.createAttribute({
            messageId: 'balbla-nonexistent',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            console.error(create.error);
            fail();
        }
        const message = resources.getMessage({ id: 'balbla-nonexistent', languageCode: 'en' });
        if (message === undefined) {
            fail();
        }
        expect(message.attributes.length > 0).toBeTruthy();
        // eslint-disable-next-line unicorn/no-null
        expect(message.value).toBe(null);
    }


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this {{{}$ test, please login.',
            languageCode: 'en',
        });
        expect(create.isOk).toBeFalsy();
    }


() => {
    it('should update an attribute', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const update = resources.updateAttribute({
            messageId: 'test',
            id: 'login',
            languageCode: 'en',
            with: new Attribute(
                new Identifier('login'),
                new Pattern([new TextElement('Welcome to this test. Please logout.')])
            ),
        });
        if (update.isErr) {
            fail(update.error);
        }
        const message = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(message?.attributes[0].id.name).toEqual('login');
        expect(serializePattern(message?.attributes[0].value ?? new Pattern([]))).toEqual(
            'Welcome to this test. Please logout.'
        );
    });

    it('should return Result.err if the messageId does not exist', () => {
        const update = resources.updateAttribute({
            messageId: 'afpihsihg',
            id: 'das',
            with: new Attribute(new Identifier('hi'), new Pattern([])),
            languageCode: 'en',
        });
        expect(update.isErr).toBeTruthy();
    });
    it('should return Result.err if the attribute does not exist and options.upsert is undefined', () => {
        const update = resources.updateAttribute({
            messageId: 'test',
            id: 'login',
            with: new Attribute(new Identifier('hi'), new Pattern([])),
            languageCode: 'en',
        });
        expect(update.isErr).toBeTruthy();
    });
}


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const update = resources.updateAttribute({
            messageId: 'test',
            id: 'login',
            languageCode: 'en',
            with: new Attribute(
                new Identifier('login'),
                new Pattern([new TextElement('Welcome to this test. Please logout.')])
            ),
        });
        if (update.isErr) {
            fail(update.error);
        }
        const message = resources.getMessage({ id: 'test', languageCode: 'en' });
        expect(message?.attributes[0].id.name).toEqual('login');
        expect(serializePattern(message?.attributes[0].value ?? new Pattern([]))).toEqual(
            'Welcome to this test. Please logout.'
        );
    }


() => {
        const update = resources.updateAttribute({
            messageId: 'afpihsihg',
            id: 'das',
            with: new Attribute(new Identifier('hi'), new Pattern([])),
            languageCode: 'en',
        });
        expect(update.isErr).toBeTruthy();
    }


() => {
        const update = resources.updateAttribute({
            messageId: 'test',
            id: 'login',
            with: new Attribute(new Identifier('hi'), new Pattern([])),
            languageCode: 'en',
        });
        expect(update.isErr).toBeTruthy();
    }


() => {
    it('should update an attribute', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }

        const attribute = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(serializePattern(attribute?.value ?? new Pattern([]))).toEqual('Welcome to this test, please login.');
    });
}


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }

        const attribute = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(serializePattern(attribute?.value ?? new Pattern([]))).toEqual('Welcome to this test, please login.');
    }


() => {
    // hmmm bad. Since resources is not an immutable class and all actions are mutable
    // sub deletions might succeed and are not reversed.
    it('should retrieve the attribute for all resources', () => {
        for (const languageCode of resources.containedLanguageCodes()) {
            const create = resources.createAttribute({
                messageId: 'test',
                id: 'login',
                pattern: 'Welcome to this test, please login.',
                languageCode: languageCode,
            });
            if (create.isErr) {
                fail(create.error);
            }
        }
        const attributes = resources.getAttributeForAllResources({ messageId: 'test', id: 'login' });
        expect(Object.keys(attributes).length).toBe(resources.containedLanguageCodes().length);
    });
}


() => {
        for (const languageCode of resources.containedLanguageCodes()) {
            const create = resources.createAttribute({
                messageId: 'test',
                id: 'login',
                pattern: 'Welcome to this test, please login.',
                languageCode: languageCode,
            });
            if (create.isErr) {
                fail(create.error);
            }
        }
        const attributes = resources.getAttributeForAllResources({ messageId: 'test', id: 'login' });
        expect(Object.keys(attributes).length).toBe(resources.containedLanguageCodes().length);
    }


() => {
    it('should delete an attribute', () => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const attribute = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(serializePattern(attribute?.value ?? new Pattern([]))).toEqual('Welcome to this test, please login.');
        const deletion = resources.deleteAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(deletion.isOk).toBeTruthy();
        const attributeAfterDeletion = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(attributeAfterDeletion).toBeUndefined();
    });

    it('should return Result.err if the message does not exist', () => {
        const deletion = resources.deleteAttribute({ messageId: 'tadsdasdasdadsest', id: 'login', languageCode: 'en' });
        expect(deletion.isErr).toBeTruthy();
    });

    it('should return Result.err if the message exists but the attribute does not', () => {
        const deletion = resources.deleteAttribute({ messageId: 'test', id: 'logigassagagsagsa', languageCode: 'en' });
        expect(deletion.isErr).toBeTruthy();
    });
}


() => {
        const create = resources.createAttribute({
            messageId: 'test',
            id: 'login',
            pattern: 'Welcome to this test, please login.',
            languageCode: 'en',
        });
        if (create.isErr) {
            fail(create.error);
        }
        const attribute = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(serializePattern(attribute?.value ?? new Pattern([]))).toEqual('Welcome to this test, please login.');
        const deletion = resources.deleteAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(deletion.isOk).toBeTruthy();
        const attributeAfterDeletion = resources.getAttribute({ messageId: 'test', id: 'login', languageCode: 'en' });
        expect(attributeAfterDeletion).toBeUndefined();
    }


() => {
        const deletion = resources.deleteAttribute({ messageId: 'tadsdasdasdadsest', id: 'login', languageCode: 'en' });
        expect(deletion.isErr).toBeTruthy();
    }


() => {
        const deletion = resources.deleteAttribute({ messageId: 'test', id: 'logigassagagsagsa', languageCode: 'en' });
        expect(deletion.isErr).toBeTruthy();
    }


() => {
    it('should return Result.ok when deleting existing attributes', () => {
        for (const languageCode of resources.containedLanguageCodes()) {
            const create = resources.createAttribute({
                messageId: 'test',
                id: 'login',
                pattern: 'Welcome to this test, please login.',
                languageCode: languageCode,
            });
            if (create.isErr) {
                fail(create.error);
            }
        }

        const deletion = resources.deleteAttributeForAllResources({ messageId: 'test', id: 'login' });
        expect(deletion.isOk).toBeTruthy();
    });
    // hmmm bad. Since resources is not an immutable class and all actions are mutable
    // sub deletions might succeed and are not reversed.
    it('should return Result.err when deleting an attribute that does not exist for one language', () => {
        for (const languageCode of resources.containedLanguageCodes().slice(1)) {
            const create = resources.createAttribute({
                messageId: 'test',
                id: 'login',
                pattern: 'Welcome to this test, please login.',
                languageCode: languageCode,
            });
            if (create.isErr) {
                fail(create.error);
            }
        }
        const deletion = resources.deleteAttributeForAllResources({ messageId: 'test', id: 'login' });
        expect(deletion.isErr).toBeTruthy();
    });
}