/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import ts from 'typescript';

import {ChangeTracker, PendingChange} from '../../utils/change_tracker';

import {findClassDeclaration} from '../../utils/typescript/class_declaration';
import {findLiteralProperty} from '../../utils/typescript/property_name';
import {
  isAngularRoutesArray,
  isProvideRoutesCallExpression,
  isRouterCallExpression,
  isRouterModuleCallExpression,
  isRouterProviderCallExpression,
  isStandaloneComponent,
} from './util';

interface RouteData {
  routeFilePath: string;
  routeFileImports: ts.ImportDeclaration[];
  array: ts.ArrayLiteralExpression;
}

export interface RouteMigrationData {
  path: string;
  file: string;
}

/**
 * Converts all application routes that are using standalone components to be lazy loaded.
 * @param sourceFile File that should be migrated.
 * @param program
 */
export function migrateFileToLazyRoutes(
  sourceFile: ts.SourceFile,
  program: ts.Program,
): {
  pendingChanges: PendingChange[];
  migratedRoutes: RouteMigrationData[];
  skippedRoutes: RouteMigrationData[];
} {
  const typeChecker = program.getTypeChecker();
  const printer = ts.createPrinter();
  const tracker = new ChangeTracker(printer);

  const routeArraysToMigrate = findRoutesArrayToMigrate(sourceFile, typeChecker);

  if (routeArraysToMigrate.length === 0) {
    return {pendingChanges: [], skippedRoutes: [], migratedRoutes: []};
  }

  const {skippedRoutes, migratedRoutes} = migrateRoutesArray(
    routeArraysToMigrate,
    typeChecker,
    tracker,
  );

  return {
    pendingChanges: tracker.recordChanges().get(sourceFile) || [],
    skippedRoutes,
    migratedRoutes,
  };
}

/** Finds route object that can be migrated */
function findRoutesArrayToMigrate(sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) {
  const routesArrays: RouteData[] = [];

  sourceFile.forEachChild(function walk(node) {
    if (ts.isCallExpression(node)) {
      if (
        isRouterModuleCallExpression(node, typeChecker) ||
        isRouterProviderCallExpression(node, typeChecker) ||
        isRouterCallExpression(node, typeChecker) ||
        isProvideRoutesCallExpression(node, typeChecker)
      ) {
        const arg = node.arguments[0]; // ex: RouterModule.forRoot(routes) or provideRouter(routes)
        const routeFileImports = sourceFile.statements.filter(ts.isImportDeclaration);

        if (ts.isArrayLiteralExpression(arg) && arg.elements.length > 0) {
          // ex: inline routes array: RouterModule.forRoot([{ path: 'test', component: TestComponent }])
          routesArrays.push({
            routeFilePath: sourceFile.fileName,
            array: arg as ts.ArrayLiteralExpression,
            routeFileImports,
          });
        } else if (ts.isIdentifier(arg)) {
          // ex: reference to routes array: RouterModule.forRoot(routes)
          // RouterModule.forRoot(routes), provideRouter(routes), provideRoutes(routes)
          const symbol = typeChecker.getSymbolAtLocation(arg);
          if (!symbol?.declarations) return;

          for (const declaration of symbol.declarations) {
            if (ts.isVariableDeclaration(declaration)) {
              const initializer = declaration.initializer;
              if (initializer && ts.isArrayLiteralExpression(initializer)) {
                // ex: const routes = [{ path: 'test', component: TestComponent }];
                routesArrays.push({
                  routeFilePath: sourceFile.fileName,
                  array: initializer,
                  routeFileImports,
                });
              }
            }
          }
        }
      }
    }

    if (ts.isVariableDeclaration(node)) {
      if (isAngularRoutesArray(node, typeChecker)) {
        const initializer = node.initializer;
        if (
          initializer &&
          ts.isArrayLiteralExpression(initializer) &&
          initializer.elements.length > 0
        ) {
          // ex: const routes: Routes = [{ path: 'test', component: TestComponent }];
          if (routesArrays.find((x) => x.array === initializer)) {
            // already exists
            return;
          }

          routesArrays.push({
            routeFilePath: sourceFile.fileName,
            array: initializer,
            routeFileImports: sourceFile.statements.filter(ts.isImportDeclaration),
          });
        }
      }
    }

    node.forEachChild(walk);
  });

  return routesArrays;
}

/** Migrate a routes object standalone components to be lazy loaded. */
function migrateRoutesArray(
  routesArray: RouteData[],
  typeChecker: ts.TypeChecker,
  tracker: ChangeTracker,
): {migratedRoutes: RouteMigrationData[]; skippedRoutes: RouteMigrationData[]} {
  const migratedRoutes: RouteMigrationData[] = [];
  const skippedRoutes: RouteMigrationData[] = [];
  const importsToRemove: ts.ImportDeclaration[] = [];

  for (const route of routesArray) {
    route.array.elements.forEach((element) => {
      if (ts.isObjectLiteralExpression(element)) {
        const {
          migratedRoutes: migrated,
          skippedRoutes: toBeSkipped,
          importsToRemove: toBeRemoved,
        } = migrateRoute(element, route, typeChecker, tracker);
        migratedRoutes.push(...migrated);
        skippedRoutes.push(...toBeSkipped);
        importsToRemove.push(...toBeRemoved);
      }
    });
  }

  for (const importToRemove of importsToRemove) {
    tracker.removeNode(importToRemove);
  }

  return {migratedRoutes, skippedRoutes};
}

/**
 * Migrates a single route object and returns the results of the migration
 * It recursively migrates the children routes if they exist
 */
function migrateRoute(
  element: ts.ObjectLiteralExpression,
  route: RouteData,
  typeChecker: ts.TypeChecker,
  tracker: ChangeTracker,
): {
  migratedRoutes: RouteMigrationData[];
  skippedRoutes: RouteMigrationData[];
  importsToRemove: ts.ImportDeclaration[];
} {
  const skippedRoutes: RouteMigrationData[] = [];
  const migratedRoutes: RouteMigrationData[] = [];
  const importsToRemove: ts.ImportDeclaration[] = [];

  const component = findLiteralProperty(element, 'component');

  // this can be empty string or a variable that is not a string, or not present at all
  const routePath = findLiteralProperty(element, 'path')?.getText() ?? '';

  const children = findLiteralProperty(element, 'children') as ts.PropertyAssignment | undefined;

  // recursively migrate children routes first if they exist
  if (children && ts.isArrayLiteralExpression(children.initializer)) {
    for (const childRoute of children.initializer.elements) {
      if (ts.isObjectLiteralExpression(childRoute)) {
        const {
          migratedRoutes: migrated,
          skippedRoutes: toBeSkipped,
          importsToRemove: toBeRemoved,
        } = migrateRoute(childRoute, route, typeChecker, tracker);
        migratedRoutes.push(...migrated);
        skippedRoutes.push(...toBeSkipped);
        importsToRemove.push(...toBeRemoved);
      }
    }
  }

  const routeMigrationResults = {migratedRoutes, skippedRoutes, importsToRemove};

  if (!component) {
    return routeMigrationResults;
  }

  const componentDeclaration = findClassDeclaration(component, typeChecker);

  if (!componentDeclaration) {
    return routeMigrationResults;
  }

  // if component is not a standalone component, skip it
  if (!isStandaloneComponent(componentDeclaration)) {
    skippedRoutes.push({path: routePath, file: route.routeFilePath});
    return routeMigrationResults;
  }

  const componentClassName =
    componentDeclaration.name && ts.isIdentifier(componentDeclaration.name)
      ? componentDeclaration.name.text
      : null;
  if (!componentClassName) {
    return routeMigrationResults;
  }

  // if component is in the same file as the routes array, skip it
  if (componentDeclaration.getSourceFile().fileName === route.routeFilePath) {
    return routeMigrationResults;
  }

  const componentImport = route.routeFileImports.find((importDecl) =>
    importDecl.importClause?.getText().includes(componentClassName),
  )!;

  // remove single and double quotes from the import path
  let componentImportPath = ts.isStringLiteral(componentImport?.moduleSpecifier)
    ? componentImport.moduleSpecifier.text
    : null;

  // if the import path is not a string literal, skip it
  if (!componentImportPath) {
    skippedRoutes.push({path: routePath, file: route.routeFilePath});
    return routeMigrationResults;
  }

  const isDefaultExport =
    componentDeclaration.modifiers?.some((x) => x.kind === ts.SyntaxKind.DefaultKeyword) ?? false;

  const loadComponent = createLoadComponentPropertyAssignment(
    componentImportPath,
    componentClassName,
    isDefaultExport,
  );

  tracker.replaceNode(component, loadComponent);
  // Add the import statement for the standalone component
  if (!importsToRemove.includes(componentImport)) {
    importsToRemove.push(componentImport);
  }
  migratedRoutes.push({path: routePath, file: route.routeFilePath});

  // the component was migrated, so we return the results
  return routeMigrationResults;
}


# Route lazy loading migration
This schematic helps developers to convert eagerly loaded component routes to lazy loaded routes.
By lazy loading components we can split the production bundle into smaller chunks,
to avoid big JS bundle that includes all routes, which negatively affects initial page load of an application.

## How to run this migration?
The migration can be run using the following command:

```bash
ng generate @angular/core:route-lazy-loading
```

By default, migration will go over the entire application. If you want to apply this migration to a subset of the files, you can pass the path argument as shown below:

```bash
ng generate @angular/core:route-lazy-loading --path src/app/sub-component
```

The value of the path parameter is a relative path within the project.

### How does it work?
The schematic will attempt to find all the places where the application routes as defined:
- `RouterModule.forRoot` and `RouterModule.forChild`
- `Router.resetConfig`
- `provideRouter`
- `provideRoutes`
- variables of type `Routes` or `Route[]` (e.g. `const routes: Routes = [{...}]`)

The migration will check all the components in the routes, check if they are standalone and eagerly loaded, and if so, it will convert them to lazy loaded routes.

**Before:**
```typescript
// app.module.ts
import { HomeComponent } from './home/home.component';

@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: 'home',
        component: HomeComponent, // HomeComponent is standalone and eagerly loaded
      },
    ]),
  ],
})
export class AppModule {}
```
**After:**
```typescript
// app.module.ts
@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: 'home',
        // ↓ HomeComponent is now lazy loaded
        loadComponent: () => import('./home/home.component').then(m => m.HomeComponent),
      },
    ]),
  ],
})
export class AppModule {}
```

> This migration will also collect information about all the components declared in NgModules
and output the list of routes that use them (including corresponding location of the file).
Consider making those components standalone and run this migration again.
You can use an existing migration (see https://angular.dev/reference/migrations/standalone)
to convert those components to standalone.


## Inject migration
Automated migration that converts classes using constructor-based injection to the `inject`
function. It can be run using:

```bash
ng generate @angular/core:inject-migration
```

**Before:**
```typescript
import { Component, Inject, Optional } from '@angular/core';
import { MyService } from './service';
import { DI_TOKEN } from './token';

@Component({})
export class MyComp {
  constructor(private service: MyService, @Inject(TOKEN) @Optional() readonly token: string) {}
}
```

**After:**
```typescript
import { Component, inject } from '@angular/core';
import { MyService } from './service';
import { DI_TOKEN } from './token';

@Component({})
export class MyComp {
  private service = inject(MyService);
  readonly token = inject(DI_TOKEN, { optional: true });
}
```

### Options
The migration includes several options to customize its output.

#### `path`
Determines which sub-path in your project should be migrated. Pass in `.` or leave it blank to
migrate the entire directory.

#### `migrateAbstractClasses`
Angular doesn't validate that parameters of abstract classes are injectable. This means that the
migration can't reliably migrate them to `inject` without risking breakages which is why they're
disabled by default. Enable this option if you want abstract classes to be migrated, but note
that you may have to **fix some breakages manually**.

#### `backwardsCompatibleConstructors`
By default the migration tries to clean up the code as much as it can, which includes deleting
parameters from the constructor, or even the entire constructor if it doesn't include any code.
In some cases this can lead to compilation errors when classes with Angular decorators inherit from
other classes with Angular decorators. If you enable this option, the migration will generate an
additional constructor signature to keep it backwards compatible, at the expense of more code.

**Before:**
```typescript
import { Component } from '@angular/core';
import { MyService } from './service';

@Component({})
export class MyComp {
  constructor(private service: MyService) {}
}
```

**After:**
```typescript
import { Component } from '@angular/core';
import { MyService } from './service';

@Component({})
export class MyComp {
  private service = inject(MyService);

  /** Inserted by Angular inject() migration for backwards compatibility */
  constructor(...args: unknown[]);

  constructor() {}
}
```

#### `nonNullableOptional`
If injection fails for a parameter with the `@Optional` decorator, Angular returns `null` which
means that the real type of any `@Optional` parameter will be `| null`. However, because decorators
cannot influence their types, there is a lot of existing code whose type is incorrect. The type is
fixed in `inject()` which can cause new compilation errors to show up. If you enable this option,
the migration will produce a non-null assertion after the `inject()` call to match the old type,
at the expense of potentially hiding type errors.

**Note:** non-null assertions won't be added to parameters that are already typed to be nullable,
because the code that depends on them likely already accounts for their nullability.

**Before:**
```typescript
import { Component, Inject, Optional } from '@angular/core';
import { TOKEN_ONE, TOKEN_TWO } from './token';

@Component({})
export class MyComp {
  constructor(
    @Inject(TOKEN_ONE) @Optional() private tokenOne: number,
    @Inject(TOKEN_TWO) @Optional() private tokenTwo: string | null) {}
}
```

**After:**
```typescript
import { Component, inject } from '@angular/core';
import { TOKEN_ONE, TOKEN_TWO } from './token';

@Component({})
export class MyComp {
  // Note the `!` at the end.
  private tokenOne = inject(TOKEN_ONE, { optional: true })!;

  // Does not have `!` at the end, because the type was already nullable.
  private tokenTwo = inject(TOKEN_TWO, { optional: true });
}
```


# Standalone migration
`ng generate` schematic that helps users to convert an application to `standalone` components,
directives and pipes. The migration can be run with `ng generate @angular/core:standalone` and it
has the following options:

* `mode` - Configures the mode that migration should run in. The different modes are clarified
further down in this document.
* `path` - Relative path within the project that the migration should apply to. Can be used to
migrate specific sub-directories individually. Defaults to the project root.

## Migration flow
The standalone migration involves multiple distinct operations, and as such has to be run multiple
times. Authors should verify that the app still works between each of the steps. If the application
is large, it can be easier to use the `path` option to migrate specific sub-sections of the app
individually.

**Note:** The schematic often needs to generate new code or copy existing code to different places.
This means that likely the formatting won't match your app anymore and there may be some lint
failures. The application should compile, but it's expected that the author will fix up any
formatting and linting failures.

An example migration could look as follows:
1. `ng generate @angular/core:standalone`.
2. Select the "Convert all components, directives and pipes to standalone" option.
3. Verify that the app works and commit the changes.
4. `ng generate @angular/core:standalone`.
5. Select the "Remove unnecessary NgModule classes" option.
6. Verify that the app works and commit the changes.
7. `ng generate @angular/core:standalone`.
8. Select the "Bootstrap the application using standalone APIs" option.
9. Verify that the app works and commit the changes.
10. Run your linting and formatting checks, and fix any failures. Commit the result.


## Migration modes
The migration is made up the following modes that are intended to be run in the order they are
listed in:
1. Convert declarations to standalone.
2. Remove unnecessary NgModules.
3. Switch to standalone bootstrapping API.

### Convert declarations to standalone
In this mode, the migration will find all of the components, directives and pipes, and convert them
to standalone by removing `standalone: false` and adding any dependencies to the `imports` array.

**Note:** NgModules which bootstrap a component are explicitly ignored in this step, because they
are likely to be root modules and they would have to be bootstrapped using `bootstrapApplication`
instead of `bootstrapModule`. Their declarations will be converted automatically as a part of the
"Switch to standalone bootstrapping API" step.

**Before:**
```typescript
// app.module.ts
@NgModule({
  imports: [CommonModule],
  declarations: [MyComp, MyDir, MyPipe]
})
export class AppModule {}
```

```typescript
// my-comp.ts
@Component({
  selector: 'my-comp',
  template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>',
  standalone: false,
})
export class MyComp {
  public showGreeting = true;
}
```

```typescript
// my-dir.ts
@Directive({selector: '[my-dir]', standalone: false})
export class MyDir {}
```

```typescript
// my-pipe.ts
@Pipe({name: 'myPipe', pure: true, standalone: false})
export class MyPipe {}
```

**After:**
```typescript
// app.module.ts
@NgModule({
  imports: [CommonModule, MyComp, MyDir, MyPipe]
})
export class AppModule {}
```

```typescript
// my-comp.ts
@Component({
  selector: 'my-comp',
  template: '<div my-dir *ngIf="showGreeting">{{ "Hello" | myPipe }}</div>',
  imports: [NgIf, MyDir, MyPipe]
})
export class MyComp {
  public showGreeting = true;
}
```

```typescript
// my-dir.ts
@Directive({selector: '[my-dir]'})
export class MyDir {}
```

```typescript
// my-pipe.ts
@Pipe({name: 'myPipe', pure: true})
export class MyPipe {}
```

### Remove unnecessary NgModules
After converting all declarations to standalone, a lot of NgModules won't be necessary anymore!
This step identifies such modules and deletes them, including as many references to them, as
possible. If a module reference can't be deleted automatically, the migration will leave a TODO
comment saying `TODO(standalone-migration): clean up removed NgModule reference manually` so that
the author can delete it themselves.

A module is considered "safe to remove" if it:
* Has no `declarations`.
* Has no `providers`.
* Has no `bootstrap` components.
* Has no `imports` that reference a `ModuleWithProviders` symbol or a module that can't be removed.
* Has no class members. Empty constructors are ignored.

**Before:**
```typescript
// importer.module.ts

@NgModule({
  imports: [FooComp, BarPipe],
  exports: [FooComp, BarPipe]
})
export class ImporterModule {}
```

```typescript
// configurer.module.ts
import {ImporterModule} from './importer.module';

console.log(ImporterModule);

@NgModule({
  imports: [ImporterModule],
  exports: [ImporterModule],
  providers: [{provide: FOO, useValue: 123}]
})
export class ConfigurerModule {}
```

```typescript
// index.ts
export {ImporterModule, ConfigurerModule} from './modules/index';
```

**After:**
```typescript
// importer.module.ts
// Deleted!
```

```typescript
// configurer.module.ts
console.log(/* TODO(standalone-migration): clean up removed NgModule reference manually */ ImporterModule);

@NgModule({
  imports: [],
  exports: [],
  providers: [{provide: FOO, useValue: 123}]
})
export class ConfigurerModule {}
```

```typescript
// index.ts
export {ConfigurerModule} from './modules/index';
```

### Switch to standalone bootstrapping API
Converts any usages of the old `bootstrapModule` API to the new `bootstrapApplication`. To do this
in a safe way, the migration has to make the following changes to the application's code:
1. Generate the `bootstrapApplication` call to replace the `bootstrapModule` one.
2. Convert the `declarations` of the module that is being bootstrapped to `standalone`. These
modules were skipped explicitly in the first step of the migration.
3. Copy any `providers` from the bootstrapped module into the `providers` option of
`bootstrapApplication`.
4. Copy any classes from the `imports` array of the rootModule to the `providers` option of
`bootstrapApplication` and wrap them in an `importsProvidersFrom` function call.
5. Adjust any dynamic import paths so that they're correct when they're copied over.
6. If an API with a standalone equivalent is detected, it may be converted automatically as well.
E.g. `RouterModule.forRoot` will become `provideRouter`.
7. Remove the root module.

If the migration detects that the `providers` or `imports` of the root module are referencing code
outside of the class declaration, it will attempt to carry over as much of it as it can to the new
location. If some of that code is exported, it will be imported in the new location, otherwise it
will be copied over.

**Before:**
```typescript
// ./app/app.module.ts
import {NgModule, InjectionToken} from '@angular/core';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppComponent} from './app.component.ts';
import {SharedModule} from './shared.module';
import {ImportedInterface} from './some-interface';
import {CONFIG} from './config';

interface NonImportedInterface {
  foo: any;
  baz: ImportedInterface;
}

const token = new InjectionToken<NonImportedInterface>('token');

export class ExportedConfigClass {}

@NgModule({
  imports: [
    SharedModule,
    BrowserAnimationsModule,
    RouterModule.forRoot([{
      path: 'shop',
      loadComponent: () => import('./shop/shop.component').then(m => m.ShopComponent)
    }], {
      initialNavigation: 'enabledBlocking'
    })
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [
    {provide: token, useValue: {foo: true, bar: {baz: false}}},
    {provide: CONFIG, useClass: ExportedConfigClass}
  ]
})
export class AppModule {}
```

```typescript
// ./app/app.component.ts
@Component({selector: 'app', template: 'hello', standalone: false})
export class AppComponent {}
```

```typescript
// ./main.ts
import {platformBrowser} from '@angular/platform-browser';
import {AppModule} from './app/app.module';

platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
```

**After:**
```typescript
// ./app/app.module.ts
import {NgModule, InjectionToken} from '@angular/core';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppComponent} from './app.component.ts';
import {SharedModule} from '../shared/shared.module';
import {ImportedInterface} from './some-interface';
import {CONFIG} from './config';

interface NonImportedInterface {
  foo: any;
  bar: ImportedInterface;
}

const token = new InjectionToken<NonImportedInterface>('token');

export class ExportedConfigClass {}
```

```typescript
// ./app/app.component.ts
@Component({selector: 'app', template: 'hello'})
export class AppComponent {}
```

```typescript
// ./main.ts
import {platformBrowser, bootstrapApplication} from '@angular/platform-browser';
import {InjectionToken, importProvidersFrom} from '@angular/core';
import {withEnabledBlockingInitialNavigation, provideRouter} from '@angular/router';
import {provideAnimations} from '@angular/platform-browser/animations';
import {AppModule, ExportedConfigClass} from './app/app.module';
import {AppComponent} from './app/app.component';
import {CONFIG} from './app/config';
import {SharedModule} from './shared/shared.module';
import {ImportedInterface} from './app/some-interface';

interface NonImportedInterface {
  foo: any;
  bar: ImportedInterface;
}

const token = new InjectionToken<NonImportedInterface>('token');

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(SharedModule),
    {provide: token, useValue: {foo: true, bar: {baz: false}}},
    {provide: CONFIG, useClass: ExportedConfigClass},
    provideAnimations(),
    provideRouter([{
      path: 'shop',
      loadComponent: () => import('./app/shop/shop.component').then(m => m.ShopComponent)
    }], withEnabledBlockingInitialNavigation())
  ]
}).catch(e => console.error(e));
```


## Control Flow Syntax migration

Angular v17 introduces a new control flow syntax. This migration replaces the
existing usages of `*ngIf`, `*ngFor`, and `*ngSwitch` to their equivalent block
syntax. Existing ng-templates are preserved in case they are used elsewhere in
the template. It has the following option:

* `path` - Relative path within the project that the migration should apply to. Can be used to
migrate specific sub-directories individually. Defaults to the project root.

#### Before
```ts
import {Component} from '@angular/core';

@Component({
  template: `<div><span *ngIf="show">Content here</span></div>`
})
export class MyComp {
  show = false;
}
```

#### After
```ts
import {Component} from '@angular/core';

@Component({
  template: `<div>@if (show) {<span>Content here</span>}</div>`
})
export class MyComp {
  show = false
}
```


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {join, relative} from 'path';

import {normalizePath} from '../../utils/change_tracker';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';

import {migrateTemplate} from './migration';
import {AnalyzedFile, MigrateError} from './types';
import {analyze} from './util';

interface Options {
  path: string;
  format: boolean;
}

export function migrate(options: Options): Rule {
  return async (tree: Tree, context: SchematicContext) => {
    const basePath = process.cwd();
    const pathToMigrate = normalizePath(join(basePath, options.path));
    let allPaths = [];
    if (pathToMigrate.trim() !== '') {
      allPaths.push(pathToMigrate);
    }

    if (!allPaths.length) {
      throw new SchematicsException(
        'Could not find any tsconfig file. Cannot run the control flow migration.',
      );
    }

    let errors: string[] = [];

    for (const tsconfigPath of allPaths) {
      const migrateErrors = runControlFlowMigration(
        tree,
        tsconfigPath,
        basePath,
        pathToMigrate,
        options,
      );
      errors = [...errors, ...migrateErrors];
    }

    if (errors.length > 0) {
      context.logger.warn(`WARNING: ${errors.length} errors occurred during your migration:\n`);
      errors.forEach((err: string) => {
        context.logger.warn(err);
      });
    }
  };
}

function runControlFlowMigration(
  tree: Tree,
  tsconfigPath: string,
  basePath: string,
  pathToMigrate: string,
  schematicOptions: Options,
): string[] {
  if (schematicOptions.path.startsWith('..')) {
    throw new SchematicsException(
      'Cannot run control flow migration outside of the current project.',
    );
  }

  const program = createMigrationProgram(tree, tsconfigPath, basePath);
  const sourceFiles = program
    .getSourceFiles()
    .filter(
      (sourceFile) =>
        sourceFile.fileName.startsWith(pathToMigrate) &&
        canMigrateFile(basePath, sourceFile, program),
    );

  if (sourceFiles.length === 0) {
    throw new SchematicsException(
      `Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`,
    );
  }

  const analysis = new Map<string, AnalyzedFile>();
  const migrateErrors = new Map<string, MigrateError[]>();

  for (const sourceFile of sourceFiles) {
    analyze(sourceFile, analysis);
  }

  // sort files with .html files first
  // this ensures class files know if it's safe to remove CommonModule
  const paths = sortFilePaths([...analysis.keys()]);

  for (const path of paths) {
    const file = analysis.get(path)!;
    const ranges = file.getSortedRanges();
    const relativePath = relative(basePath, path);
    const content = tree.readText(relativePath);
    const update = tree.beginUpdate(relativePath);

    for (const {start, end, node, type} of ranges) {
      const template = content.slice(start, end);
      const length = (end ?? content.length) - start;

      const {migrated, errors} = migrateTemplate(
        template,
        type,
        node,
        file,
        schematicOptions.format,
        analysis,
      );

      if (migrated !== null) {
        update.remove(start, length);
        update.insertLeft(start, migrated);
      }

      if (errors.length > 0) {
        migrateErrors.set(path, errors);
      }
    }

    tree.commitUpdate(update);
  }

  const errorList: string[] = [];

  for (let [template, errors] of migrateErrors) {
    errorList.push(generateErrorMessage(template, errors));
  }

  return errorList;
}

function sortFilePaths(names: string[]): string[] {
  names.sort((a, _) => (a.endsWith('.html') ? -1 : 0));
  return names;
}

function generateErrorMessage(path: string, errors: MigrateError[]): string {
  let errorMessage = `Template "${path}" encountered ${errors.length} errors during migration:\n`;
  errorMessage += errors.map((e) => ` - ${e.type}: ${e.error}\n`);
  return errorMessage;
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

export {SIGNAL as ɵSIGNAL} from '@angular/core/primitives/signals';

export {isSignal, Signal, ValueEqualityFn} from './render3/reactivity/api';
export {computed, CreateComputedOptions} from './render3/reactivity/computed';
export {
  CreateSignalOptions,
  signal,
  WritableSignal,
  ɵunwrapWritableSignal,
} from './render3/reactivity/signal';
export {linkedSignal} from './render3/reactivity/linked_signal';
export {untracked} from './render3/reactivity/untracked';
export {
  CreateEffectOptions,
  effect,
  EffectRef,
  EffectCleanupFn,
  EffectCleanupRegisterFn,
} from './render3/reactivity/effect';
export {
  MicrotaskEffectScheduler as ɵMicrotaskEffectScheduler,
  microtaskEffect as ɵmicrotaskEffect,
} from './render3/reactivity/microtask_effect';
export {EffectScheduler as ɵEffectScheduler} from './render3/reactivity/root_effect_scheduler';
export {afterRenderEffect, ɵFirstAvailableSignal} from './render3/reactivity/after_render_effect';
export {assertNotInReactiveContext} from './render3/reactivity/asserts';


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {APP_ID, PLATFORM_ID} from './application/application_tokens';
import {inject} from './di/injector_compatibility';
import {ɵɵdefineInjectable} from './di/interface/defs';
import {getDocument} from './render3/interfaces/document';

/**
 * A type-safe key to use with `TransferState`.
 *
 * Example:
 *
 * ```
 * const COUNTER_KEY = makeStateKey<number>('counter');
 * let value = 10;
 *
 * transferState.set(COUNTER_KEY, value);
 * ```
 *
 * @publicApi
 */
export type StateKey<T> = string & {
  __not_a_string: never;
  __value_type?: T;
};

/**
 * Create a `StateKey<T>` that can be used to store value of type T with `TransferState`.
 *
 * Example:
 *
 * ```
 * const COUNTER_KEY = makeStateKey<number>('counter');
 * let value = 10;
 *
 * transferState.set(COUNTER_KEY, value);
 * ```
 *
 * @publicApi
 */
export function makeStateKey<T = void>(key: string): StateKey<T> {
  return key as StateKey<T>;
}

function initTransferState(): TransferState {
  const transferState = new TransferState();
  if (inject(PLATFORM_ID) === 'browser') {
    transferState.store = retrieveTransferredState(getDocument(), inject(APP_ID));
  }

  return transferState;
}

/**
 * A key value store that is transferred from the application on the server side to the application
 * on the client side.
 *
 * The `TransferState` is available as an injectable token.
 * On the client, just inject this token using DI and use it, it will be lazily initialized.
 * On the server it's already included if `renderApplication` function is used. Otherwise, import
 * the `ServerTransferStateModule` module to make the `TransferState` available.
 *
 * The values in the store are serialized/deserialized using JSON.stringify/JSON.parse. So only
 * boolean, number, string, null and non-class objects will be serialized and deserialized in a
 * non-lossy manner.
 *
 * @publicApi
 */
export class TransferState {
  /** @nocollapse */
  static ɵprov = /** @pureOrBreakMyCode */ /* @__PURE__ */ ɵɵdefineInjectable({
    token: TransferState,
    providedIn: 'root',
    factory: initTransferState,
  });

  /** @internal */
  store: Record<string, unknown | undefined> = {};

  private onSerializeCallbacks: {[k: string]: () => unknown | undefined} = {};

  /**
   * Get the value corresponding to a key. Return `defaultValue` if key is not found.
   */
  get<T>(key: StateKey<T>, defaultValue: T): T {
    return this.store[key] !== undefined ? (this.store[key] as T) : defaultValue;
  }

  /**
   * Set the value corresponding to a key.
   */
  set<T>(key: StateKey<T>, value: T): void {
    this.store[key] = value;
  }

  /**
   * Remove a key from the store.
   */
  remove<T>(key: StateKey<T>): void {
    delete this.store[key];
  }

  /**
   * Test whether a key exists in the store.
   */
  hasKey<T>(key: StateKey<T>): boolean {
    return this.store.hasOwnProperty(key);
  }

  /**
   * Indicates whether the state is empty.
   */
  get isEmpty(): boolean {
    return Object.keys(this.store).length === 0;
  }

  /**
   * Register a callback to provide the value for a key when `toJson` is called.
   */
  onSerialize<T>(key: StateKey<T>, callback: () => T): void {
    this.onSerializeCallbacks[key] = callback;
  }

  /**
   * Serialize the current state of the store to JSON.
   */
  toJson(): string {
    // Call the onSerialize callbacks and put those values into the store.
    for (const key in this.onSerializeCallbacks) {
      if (this.onSerializeCallbacks.hasOwnProperty(key)) {
        try {
          this.store[key] = this.onSerializeCallbacks[key]();
        } catch (e) {
          console.warn('Exception in onSerialize callback: ', e);
        }
      }
    }

    // Escape script tag to avoid break out of <script> tag in serialized output.
    // Encoding of `<` is the same behaviour as G3 script_builders.
    return JSON.stringify(this.store).replace(/</g, '\\u003C');
  }
}

function retrieveTransferredState(
  doc: Document,
  appId: string,
): Record<string, unknown | undefined> {
  // Locate the script tag with the JSON data transferred from the server.
  // The id of the script tag is set to the Angular appId + 'state'.
  const script = doc.getElementById(appId + '-state');
  if (script?.textContent) {
    try {
      // Avoid using any here as it triggers lint errors in google3 (any is not allowed).
      // Decoding of `<` is done of the box by browsers and node.js, same behaviour as G3
      // script_builders.
      return JSON.parse(script.textContent) as {};
    } catch (e) {
      console.warn('Exception while restoring TransferState for app ' + appId, e);
    }
  }

  return {};
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */
import {SimpleChanges} from './simple_change';

/**
 * @description
 * A lifecycle hook that is called when any data-bound property of a directive changes.
 * Define an `ngOnChanges()` method to handle the changes.
 *
 * @see {@link DoCheck}
 * @see {@link OnInit}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define an on-changes handler for an input property.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnChanges'}
 *
 * @publicApi
 */
export interface OnChanges {
  /**
   * A callback method that is invoked immediately after the
   * default change detector has checked data-bound properties
   * if at least one has changed, and before the view and content
   * children are checked.
   * @param changes The changed properties.
   */
  ngOnChanges(changes: SimpleChanges): void;
}

/**
 * @description
 * A lifecycle hook that is called after Angular has initialized
 * all data-bound properties of a directive.
 * Define an `ngOnInit()` method to handle any additional initialization tasks.
 *
 * @see {@link AfterContentInit}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define its own initialization method.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnInit'}
 *
 * @publicApi
 */
export interface OnInit {
  /**
   * A callback method that is invoked immediately after the
   * default change detector has checked the directive's
   * data-bound properties for the first time,
   * and before any of the view or content children have been checked.
   * It is invoked only once when the directive is instantiated.
   */
  ngOnInit(): void;
}

/**
 * A lifecycle hook that invokes a custom change-detection function for a directive,
 * in addition to the check performed by the default change-detector.
 *
 * The default change-detection algorithm looks for differences by comparing
 * bound-property values by reference across change detection runs. You can use this
 * hook to check for and respond to changes by some other means.
 *
 * When the default change detector detects changes, it invokes `ngOnChanges()` if supplied,
 * regardless of whether you perform additional change detection.
 * Typically, you should not use both `DoCheck` and `OnChanges` to respond to
 * changes on the same input.
 *
 * @see {@link OnChanges}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface
 * to invoke it own change-detection cycle.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='DoCheck'}
 *
 * For a more complete example and discussion, see
 * [Defining custom change detection](guide/components/lifecycle#defining-custom-change-detection).
 *
 * @publicApi
 */
export interface DoCheck {
  /**
   * A callback method that performs change-detection, invoked
   * after the default change-detector runs.
   * See `KeyValueDiffers` and `IterableDiffers` for implementing
   * custom change checking for collections.
   *
   */
  ngDoCheck(): void;
}

/**
 * A lifecycle hook that is called when a directive, pipe, or service is destroyed.
 * Use for any custom cleanup that needs to occur when the
 * instance is destroyed.
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface
 * to define its own custom clean-up method.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='OnDestroy'}
 *
 * @publicApi
 */
export interface OnDestroy {
  /**
   * A callback method that performs custom clean-up, invoked immediately
   * before a directive, pipe, or service instance is destroyed.
   */
  ngOnDestroy(): void;
}

/**
 * @description
 * A lifecycle hook that is called after Angular has fully initialized
 * all content of a directive. It will run only once when the projected content is initialized.
 * Define an `ngAfterContentInit()` method to handle any additional initialization tasks.
 *
 * @see {@link OnInit}
 * @see {@link AfterViewInit}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define its own content initialization method.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterContentInit'}
 *
 * @publicApi
 */
export interface AfterContentInit {
  /**
   * A callback method that is invoked immediately after
   * Angular has completed initialization of all of the directive's
   * content.
   * It is invoked only once when the directive is instantiated.
   */
  ngAfterContentInit(): void;
}

/**
 * @description
 * A lifecycle hook that is called after the default change detector has
 * completed checking all content of a directive. It will run after the content
 * has been checked and most of the time it's during a change detection cycle.
 *
 * @see {@link AfterViewChecked}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define its own after-check functionality.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterContentChecked'}
 *
 * @publicApi
 */
export interface AfterContentChecked {
  /**
   * A callback method that is invoked immediately after the
   * default change detector has completed checking all of the directive's
   * content.
   */
  ngAfterContentChecked(): void;
}

/**
 * @description
 * A lifecycle hook that is called after Angular has fully initialized
 * a component's view.
 * Define an `ngAfterViewInit()` method to handle any additional initialization tasks.
 *
 * @see {@link OnInit}
 * @see {@link AfterContentInit}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define its own view initialization method.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterViewInit'}
 *
 * @publicApi
 */
export interface AfterViewInit {
  /**
   * A callback method that is invoked immediately after
   * Angular has completed initialization of a component's view.
   * It is invoked only once when the view is instantiated.
   *
   */
  ngAfterViewInit(): void;
}

/**
 * @description
 * A lifecycle hook that is called after the default change detector has
 * completed checking a component's view for changes.
 *
 * @see {@link AfterContentChecked}
 * @see [Lifecycle hooks guide](guide/components/lifecycle)
 *
 * @usageNotes
 * The following snippet shows how a component can implement this interface to
 * define its own after-check functionality.
 *
 * {@example core/ts/metadata/lifecycle_hooks_spec.ts region='AfterViewChecked'}
 *
 * @publicApi
 */
export interface AfterViewChecked {
  /**
   * A callback method that is invoked immediately after the
   * default change detector has completed one change-check cycle
   * for a component's view.
   */
  ngAfterViewChecked(): void;
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {Type} from '../interface/type';
import {getClosureSafeProperty} from '../util/property';
import {stringify} from '../util/stringify';

/**
 * An interface that a function passed into `forwardRef` has to implement.
 *
 * @usageNotes
 * ### Example
 *
 * {@example core/di/ts/forward_ref/forward_ref_spec.ts region='forward_ref_fn'}
 * @publicApi
 */
export interface ForwardRefFn {
  (): any;
}

const __forward_ref__ = getClosureSafeProperty({__forward_ref__: getClosureSafeProperty});

/**
 * Allows to refer to references which are not yet defined.
 *
 * For instance, `forwardRef` is used when the `token` which we need to refer to for the purposes of
 * DI is declared, but not yet defined. It is also used when the `token` which we use when creating
 * a query is not yet defined.
 *
 * `forwardRef` is also used to break circularities in standalone components imports.
 *
 * @usageNotes
 * ### Circular dependency example
 * {@example core/di/ts/forward_ref/forward_ref_spec.ts region='forward_ref'}
 *
 * ### Circular standalone reference import example
 * ```ts
 * @Component({
 *   standalone: true,
 *   imports: [ChildComponent],
 *   selector: 'app-parent',
 *   template: `<app-child [hideParent]="hideParent"></app-child>`,
 * })
 * export class ParentComponent {
 *   @Input() hideParent: boolean;
 * }
 *
 *
 * @Component({
 *   standalone: true,
 *   imports: [CommonModule, forwardRef(() => ParentComponent)],
 *   selector: 'app-child',
 *   template: `<app-parent *ngIf="!hideParent"></app-parent>`,
 * })
 * export class ChildComponent {
 *   @Input() hideParent: boolean;
 * }
 * ```
 *
 * @publicApi
 */
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
  (<any>forwardRefFn).__forward_ref__ = forwardRef;
  (<any>forwardRefFn).toString = function () {
    return stringify(this());
  };
  return <Type<any>>(<any>forwardRefFn);
}

/**
 * Lazily retrieves the reference value from a forwardRef.
 *
 * Acts as the identity function when given a non-forward-ref value.
 *
 * @usageNotes
 * ### Example
 *
 * {@example core/di/ts/forward_ref/forward_ref_spec.ts region='resolve_forward_ref'}
 *
 * @see {@link forwardRef}
 * @publicApi
 */
export function resolveForwardRef<T>(type: T): T {
  return isForwardRef(type) ? type() : type;
}

/** Checks whether a function is wrapped by a `forwardRef`. */
export function isForwardRef(fn: any): fn is () => any {
  return (
    typeof fn === 'function' &&
    fn.hasOwnProperty(__forward_ref__) &&
    fn.__forward_ref__ === forwardRef
  );
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {RuntimeError, RuntimeErrorCode} from '../errors';
import {
  InjectorProfilerContext,
  setInjectorProfilerContext,
} from '../render3/debug/injector_profiler';

import {getInjectImplementation, setInjectImplementation} from './inject_switch';
import type {Injector} from './injector';
import {getCurrentInjector, setCurrentInjector} from './injector_compatibility';
import {assertNotDestroyed, R3Injector} from './r3_injector';

/**
 * Runs the given function in the [context](guide/di/dependency-injection-context) of the given
 * `Injector`.
 *
 * Within the function's stack frame, [`inject`](api/core/inject) can be used to inject dependencies
 * from the given `Injector`. Note that `inject` is only usable synchronously, and cannot be used in
 * any asynchronous callbacks or after any `await` points.
 *
 * @param injector the injector which will satisfy calls to [`inject`](api/core/inject) while `fn`
 *     is executing
 * @param fn the closure to be run in the context of `injector`
 * @returns the return value of the function, if any
 * @publicApi
 */
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT {
  if (injector instanceof R3Injector) {
    assertNotDestroyed(injector);
  }

  let prevInjectorProfilerContext: InjectorProfilerContext;
  if (ngDevMode) {
    prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null});
  }
  const prevInjector = setCurrentInjector(injector);
  const previousInjectImplementation = setInjectImplementation(undefined);
  try {
    return fn();
  } finally {
    setCurrentInjector(prevInjector);
    ngDevMode && setInjectorProfilerContext(prevInjectorProfilerContext!);
    setInjectImplementation(previousInjectImplementation);
  }
}

/**
 * Whether the current stack frame is inside an injection context.
 */
export function isInInjectionContext(): boolean {
  return getInjectImplementation() !== undefined || getCurrentInjector() != null;
}
/**
 * Asserts that the current stack frame is within an [injection
 * context](guide/di/dependency-injection-context) and has access to `inject`.
 *
 * @param debugFn a reference to the function making the assertion (used for the error message).
 *
 * @publicApi
 */
export function assertInInjectionContext(debugFn: Function): void {
  // Taking a `Function` instead of a string name here prevents the unminified name of the function
  // from being retained in the bundle regardless of minification.
  if (!isInInjectionContext()) {
    throw new RuntimeError(
      RuntimeErrorCode.MISSING_INJECTION_CONTEXT,
      ngDevMode &&
        debugFn.name +
          '() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`',
    );
  }
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {Subscription} from 'rxjs';

import {ApplicationRef} from '../../application/application_ref';
import {
  ENVIRONMENT_INITIALIZER,
  EnvironmentProviders,
  inject,
  Injectable,
  InjectionToken,
  makeEnvironmentProviders,
  StaticProvider,
} from '../../di';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasksInternal} from '../../pending_tasks';
import {performanceMarkFeature} from '../../util/performance';
import {NgZone} from '../../zone';
import {InternalNgZoneOptions} from '../../zone/ng_zone';

import {
  ChangeDetectionScheduler,
  ZONELESS_SCHEDULER_DISABLED,
  ZONELESS_ENABLED,
  SCHEDULE_IN_ROOT_ZONE,
} from './zoneless_scheduling';
import {SCHEDULE_IN_ROOT_ZONE_DEFAULT} from './flags';

@Injectable({providedIn: 'root'})
export class NgZoneChangeDetectionScheduler {
  private readonly zone = inject(NgZone);
  private readonly changeDetectionScheduler = inject(ChangeDetectionScheduler);
  private readonly applicationRef = inject(ApplicationRef);

  private _onMicrotaskEmptySubscription?: Subscription;

  initialize(): void {
    if (this._onMicrotaskEmptySubscription) {
      return;
    }

    this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({
      next: () => {
        // `onMicroTaskEmpty` can happen _during_ the zoneless scheduler change detection because
        // zone.run(() => {}) will result in `checkStable` at the end of the `zone.run` closure
        // and emit `onMicrotaskEmpty` synchronously if run coalsecing is false.
        if (this.changeDetectionScheduler.runningTick) {
          return;
        }
        this.zone.run(() => {
          this.applicationRef.tick();
        });
      },
    });
  }

  ngOnDestroy() {
    this._onMicrotaskEmptySubscription?.unsubscribe();
  }
}

/**
 * Internal token used to verify that `provideZoneChangeDetection` is not used
 * with the bootstrapModule API.
 */
export const PROVIDED_NG_ZONE = new InjectionToken<boolean>(
  typeof ngDevMode === 'undefined' || ngDevMode ? 'provideZoneChangeDetection token' : '',
  {factory: () => false},
);

export function internalProvideZoneChangeDetection({
  ngZoneFactory,
  ignoreChangesOutsideZone,
  scheduleInRootZone,
}: {
  ngZoneFactory?: () => NgZone;
  ignoreChangesOutsideZone?: boolean;
  scheduleInRootZone?: boolean;
}): StaticProvider[] {
  ngZoneFactory ??= () =>
    new NgZone({...getNgZoneOptions(), scheduleInRootZone} as InternalNgZoneOptions);
  return [
    {provide: NgZone, useFactory: ngZoneFactory},
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useFactory: () => {
        const ngZoneChangeDetectionScheduler = inject(NgZoneChangeDetectionScheduler, {
          optional: true,
        });
        if (
          (typeof ngDevMode === 'undefined' || ngDevMode) &&
          ngZoneChangeDetectionScheduler === null
        ) {
          throw new RuntimeError(
            RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
            `A required Injectable was not found in the dependency injection tree. ` +
              'If you are bootstrapping an NgModule, make sure that the `BrowserModule` is imported.',
          );
        }
        return () => ngZoneChangeDetectionScheduler!.initialize();
      },
    },
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useFactory: () => {
        const service = inject(ZoneStablePendingTask);
        return () => {
          service.initialize();
        };
      },
    },
    // Always disable scheduler whenever explicitly disabled, even if another place called
    // `provideZoneChangeDetection` without the 'ignore' option.
    ignoreChangesOutsideZone === true ? {provide: ZONELESS_SCHEDULER_DISABLED, useValue: true} : [],
    {
      provide: SCHEDULE_IN_ROOT_ZONE,
      useValue: scheduleInRootZone ?? SCHEDULE_IN_ROOT_ZONE_DEFAULT,
    },
  ];
}

/**
 * Provides `NgZone`-based change detection for the application bootstrapped using
 * `bootstrapApplication`.
 *
 * `NgZone` is already provided in applications by default. This provider allows you to configure
 * options like `eventCoalescing` in the `NgZone`.
 * This provider is not available for `platformBrowser().bootstrapModule`, which uses
 * `BootstrapOptions` instead.
 *
 * @usageNotes
 * ```typescript
 * bootstrapApplication(MyApp, {providers: [
 *   provideZoneChangeDetection({eventCoalescing: true}),
 * ]});
 * ```
 *
 * @publicApi
 * @see {@link bootstrapApplication}
 * @see {@link NgZoneOptions}
 */
export function provideZoneChangeDetection(options?: NgZoneOptions): EnvironmentProviders {
  const ignoreChangesOutsideZone = options?.ignoreChangesOutsideZone;
  const scheduleInRootZone = (options as any)?.scheduleInRootZone;
  const zoneProviders = internalProvideZoneChangeDetection({
    ngZoneFactory: () => {
      const ngZoneOptions = getNgZoneOptions(options);
      ngZoneOptions.scheduleInRootZone = scheduleInRootZone;
      if (ngZoneOptions.shouldCoalesceEventChangeDetection) {
        performanceMarkFeature('NgZone_CoalesceEvent');
      }
      return new NgZone(ngZoneOptions);
    },
    ignoreChangesOutsideZone,
    scheduleInRootZone,
  });
  return makeEnvironmentProviders([
    {provide: PROVIDED_NG_ZONE, useValue: true},
    {provide: ZONELESS_ENABLED, useValue: false},
    zoneProviders,
  ]);
}

/**
 * Used to configure event and run coalescing with `provideZoneChangeDetection`.
 *
 * @publicApi
 *
 * @see {@link provideZoneChangeDetection}
 */
export interface NgZoneOptions {
  /**
   * Optionally specify coalescing event change detections or not.
   * Consider the following case.
   *
   * ```
   * <div (click)="doSomething()">
   *   <button (click)="doSomethingElse()"></button>
   * </div>
   * ```
   *
   * When button is clicked, because of the event bubbling, both
   * event handlers will be called and 2 change detections will be
   * triggered. We can coalesce such kind of events to only trigger
   * change detection only once.
   *
   * By default, this option will be false. So the events will not be
   * coalesced and the change detection will be triggered multiple times.
   * And if this option be set to true, the change detection will be
   * triggered async by scheduling a animation frame. So in the case above,
   * the change detection will only be triggered once.
   */
  eventCoalescing?: boolean;

  /**
   * Optionally specify if `NgZone#run()` method invocations should be coalesced
   * into a single change detection.
   *
   * Consider the following case.
   * ```
   * for (let i = 0; i < 10; i ++) {
   *   ngZone.run(() => {
   *     // do something
   *   });
   * }
   * ```
   *
   * This case triggers the change detection multiple times.
   * With ngZoneRunCoalescing options, all change detections in an event loop trigger only once.
   * In addition, the change detection executes in requestAnimation.
   *
   */
  runCoalescing?: boolean;

  /**
   * When false, change detection is scheduled when Angular receives
   * a clear indication that templates need to be refreshed. This includes:
   *
   * - calling `ChangeDetectorRef.markForCheck`
   * - calling `ComponentRef.setInput`
   * - updating a signal that is read in a template
   * - attaching a view that is marked dirty
   * - removing a view
   * - registering a render hook (templates are only refreshed if render hooks do one of the above)
   *
   * @deprecated This option was introduced out of caution as a way for developers to opt out of the
   *    new behavior in v18 which schedule change detection for the above events when they occur
   *    outside the Zone. After monitoring the results post-release, we have determined that this
   *    feature is working as desired and do not believe it should ever be disabled by setting
   *    this option to `true`.
   */
  ignoreChangesOutsideZone?: boolean;
}

// Transforms a set of `BootstrapOptions` (supported by the NgModule-based bootstrap APIs) ->
// `NgZoneOptions` that are recognized by the NgZone constructor. Passing no options will result in
// a set of default options returned.
export function getNgZoneOptions(options?: NgZoneOptions): InternalNgZoneOptions {
  return {
    enableLongStackTrace: typeof ngDevMode === 'undefined' ? false : !!ngDevMode,
    shouldCoalesceEventChangeDetection: options?.eventCoalescing ?? false,
    shouldCoalesceRunChangeDetection: options?.runCoalescing ?? false,
  };
}


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {InjectionToken} from '../../di/injection_token';

export const enum NotificationSource {
  // Change detection needs to run in order to synchronize application state
  // with the DOM when the following notifications are received:
  // This operation indicates that a subtree needs to be traversed during change detection.
  MarkAncestorsForTraversal,
  // A component/directive gets a new input.
  SetInput,
  // Defer block state updates need change detection to fully render the state.
  DeferBlockStateUpdate,
  // Debugging tools updated state and have requested change detection.
  DebugApplyChanges,
  // ChangeDetectorRef.markForCheck indicates the component is dirty/needs to refresh.
  MarkForCheck,

  // Bound listener callbacks execute and can update state without causing other notifications from
  // above.
  Listener,

  // Custom elements do sometimes require checking directly.
  CustomElement,

  // The following notifications do not require views to be refreshed
  // but we should execute render hooks:
  // Render hooks are guaranteed to execute with the schedulers timing.
  RenderHook,
  DeferredRenderHook,
  // Views might be created outside and manipulated in ways that
  // we cannot be aware of. When a view is attached, Angular now "knows"
  // about it and we now know that DOM might have changed (and we should
  // run render hooks). If the attached view is dirty, the `MarkAncestorsForTraversal`
  // notification should also be received.
  ViewAttached,
  // When DOM removal happens, render hooks may be interested in the new
  // DOM state but we do not need to refresh any views unless. If change
  // detection is required after DOM removal, another notification should
  // be received (i.e. `markForCheck`).
  ViewDetachedFromDOM,
  // Applying animations might result in new DOM state and should rerun render hooks
  AsyncAnimationsLoaded,
  // The scheduler is notified when a pending task is removed via the public API.
  // This allows us to make stability async, delayed until the next application tick.
  PendingTaskRemoved,
  // An `effect()` outside of the view tree became dirty and might need to run.
  RootEffect,
  // An `effect()` within the view tree became dirty.
  ViewEffect,
}

/**
 * Injectable that is notified when an `LView` is made aware of changes to application state.
 */
export abstract class ChangeDetectionScheduler {
  abstract notify(source: NotificationSource): void;
  abstract runningTick: boolean;
}

/** Token used to indicate if zoneless was enabled via provideZonelessChangeDetection(). */
export const ZONELESS_ENABLED = new InjectionToken<boolean>(
  typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless enabled' : '',
  {providedIn: 'root', factory: () => false},
);

/** Token used to indicate `provideExperimentalZonelessChangeDetection` was used. */
export const PROVIDED_ZONELESS = new InjectionToken<boolean>(
  typeof ngDevMode === 'undefined' || ngDevMode ? 'Zoneless provided' : '',
  {providedIn: 'root', factory: () => false},
);

export const ZONELESS_SCHEDULER_DISABLED = new InjectionToken<boolean>(
  typeof ngDevMode === 'undefined' || ngDevMode ? 'scheduler disabled' : '',
);

// TODO(atscott): Remove in v19. Scheduler should be done with runOutsideAngular.
export const SCHEDULE_IN_ROOT_ZONE = new InjectionToken<boolean>(
  typeof ngDevMode === 'undefined' || ngDevMode ? 'run changes outside zone in root' : '',
);


@Injectable({providedIn: 'root'})
export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
  private readonly appRef = inject(ApplicationRef);
  private readonly taskService = inject(PendingTasksInternal);
  private readonly ngZone = inject(NgZone);
  private readonly zonelessEnabled = inject(ZONELESS_ENABLED);
  private readonly disableScheduling =
    inject(ZONELESS_SCHEDULER_DISABLED, {optional: true}) ?? false;
  private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.run;
  private readonly schedulerTickApplyArgs = [{data: {'__scheduler_tick__': true}}];
  private readonly subscriptions = new Subscription();
  private readonly angularZoneId = this.zoneIsDefined
    ? (this.ngZone as NgZonePrivate)._inner?.get(angularZoneInstanceIdProperty)
    : null;
  private readonly scheduleInRootZone =
    !this.zonelessEnabled &&
    this.zoneIsDefined &&
    (inject(SCHEDULE_IN_ROOT_ZONE, {optional: true}) ?? false);

  private cancelScheduledCallback: null | (() => void) = null;
  private useMicrotaskScheduler = false;
  runningTick = false;
  pendingRenderTaskId: number | null = null;

  constructor() {
    this.subscriptions.add(
      this.appRef.afterTick.subscribe(() => {
        // If the scheduler isn't running a tick but the application ticked, that means
        // someone called ApplicationRef.tick manually. In this case, we should cancel
        // any change detections that had been scheduled so we don't run an extra one.
        if (!this.runningTick) {
          this.cleanup();
        }
      }),
    );
    this.subscriptions.add(
      this.ngZone.onUnstable.subscribe(() => {
        // If the zone becomes unstable when we're not running tick (this happens from the zone.run),
        // we should cancel any scheduled change detection here because at this point we
        // know that the zone will stabilize at some point and run change detection itself.
        if (!this.runningTick) {
          this.cleanup();
        }
      }),
    );

    // TODO(atscott): These conditions will need to change when zoneless is the default
    // Instead, they should flip to checking if ZoneJS scheduling is provided
    this.disableScheduling ||=
      !this.zonelessEnabled &&
      // NoopNgZone without enabling zoneless means no scheduling whatsoever
      (this.ngZone instanceof NoopNgZone ||
        // The same goes for the lack of Zone without enabling zoneless scheduling
        !this.zoneIsDefined);
  }

  notify(source: NotificationSource): void {
    if (!this.zonelessEnabled && source === NotificationSource.Listener) {
      // When the notification comes from a listener, we skip the notification unless the
      // application has enabled zoneless. Ideally, listeners wouldn't notify the scheduler at all
      // automatically. We do not know that a developer made a change in the listener callback that
      // requires an `ApplicationRef.tick` (synchronize templates / run render hooks). We do this
      // only for an easier migration from OnPush components to zoneless. Because listeners are
      // usually executed inside the Angular zone and listeners automatically call `markViewDirty`,
      // developers never needed to manually use `ChangeDetectorRef.markForCheck` or some other API
      // to make listener callbacks work correctly with `OnPush` components.
      return;
    }

    let force = false;

    switch (source) {
      case NotificationSource.MarkAncestorsForTraversal: {
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeTraversal;
        break;
      }
      case NotificationSource.DebugApplyChanges:
      case NotificationSource.DeferBlockStateUpdate:
      case NotificationSource.MarkForCheck:
      case NotificationSource.Listener:
      case NotificationSource.SetInput: {
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeCheck;
        break;
      }
      case NotificationSource.DeferredRenderHook: {
        // Render hooks are "deferred" when they're triggered from other render hooks. Using the
        // deferred dirty flags ensures that adding new hooks doesn't automatically trigger a loop
        // inside tick().
        this.appRef.deferredDirtyFlags |= ApplicationRefDirtyFlags.AfterRender;
        break;
      }
      case NotificationSource.CustomElement: {
        // We use `ViewTreeTraversal` to ensure we refresh the element even if this is triggered
        // during CD. In practice this is a no-op since the elements code also calls via a
        // `markForRefresh()` API which sends `NotificationSource.MarkAncestorsForTraversal` anyway.
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeTraversal;
        force = true;
        break;
      }
      case NotificationSource.RootEffect: {
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.RootEffects;
        // Root effects still force a CD, even if the scheduler is disabled. This ensures that
        // effects always run, even when triggered from outside the zone when the scheduler is
        // otherwise disabled.
        force = true;
        break;
      }
      case NotificationSource.ViewEffect: {
        // This is technically a no-op, since view effects will also send a
        // `MarkAncestorsForTraversal` notification. Still, we set this for logical consistency.
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeTraversal;
        // View effects still force a CD, even if the scheduler is disabled. This ensures that
        // effects always run, even when triggered from outside the zone when the scheduler is
        // otherwise disabled.
        force = true;
        break;
      }
      case NotificationSource.PendingTaskRemoved: {
        // Removing a pending task via the public API forces a scheduled tick, ensuring that
        // stability is async and delayed until there was at least an opportunity to run
        // application synchronization. This prevents some footguns when working with the
        // public API for pending tasks where developers attempt to update application state
        // immediately after removing the last task.
        force = true;
        break;
      }
      case NotificationSource.ViewDetachedFromDOM:
      case NotificationSource.ViewAttached:
      case NotificationSource.RenderHook:
      case NotificationSource.AsyncAnimationsLoaded:
      default: {
        // These notifications only schedule a tick but do not change whether we should refresh
        // views. Instead, we only need to run render hooks unless another notification from the
        // other set is also received before `tick` happens.
        this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.AfterRender;
      }
    }

    if (!this.shouldScheduleTick(force)) {
      return;
    }

    if (typeof ngDevMode === 'undefined' || ngDevMode) {
      if (this.useMicrotaskScheduler) {
        trackMicrotaskNotificationForDebugging();
      } else {
        consecutiveMicrotaskNotifications = 0;
        stackFromLastFewNotifications.length = 0;
      }
    }

    const scheduleCallback = this.useMicrotaskScheduler
      ? scheduleCallbackWithMicrotask
      : scheduleCallbackWithRafRace;
    this.pendingRenderTaskId = this.taskService.add();
    if (this.scheduleInRootZone) {
      this.cancelScheduledCallback = Zone.root.run(() => scheduleCallback(() => this.tick()));
    } else {
      this.cancelScheduledCallback = this.ngZone.runOutsideAngular(() =>
        scheduleCallback(() => this.tick()),
      );
    }
  }

  private shouldScheduleTick(force: boolean): boolean {
    if ((this.disableScheduling && !force) || this.appRef.destroyed) {
      return false;
    }
    // already scheduled or running
    if (this.pendingRenderTaskId !== null || this.runningTick || this.appRef._runningTick) {
      return false;
    }
    // If we're inside the zone don't bother with scheduler. Zone will stabilize
    // eventually and run change detection.
    if (
      !this.zonelessEnabled &&
      this.zoneIsDefined &&
      Zone.current.get(angularZoneInstanceIdProperty + this.angularZoneId)
    ) {
      return false;
    }

    return true;
  }

  /**
   * Calls ApplicationRef._tick inside the `NgZone`.
   *
   * Calling `tick` directly runs change detection and cancels any change detection that had been
   * scheduled previously.
   *
   * @param shouldRefreshViews Passed directly to `ApplicationRef._tick` and skips straight to
   *     render hooks when `false`.
   */


private tick(): void {
    // When ngZone.run below exits, onMicrotaskEmpty may emit if the zone is
    // stable. We want to prevent double ticking so we track whether the tick is
    // already running and skip it if so.
    if (this.runningTick || this.appRef.destroyed) {
      return;
    }

    // If we reach the tick and there is no work to be done in ApplicationRef.tick,
    // skip it altogether and clean up. There may be no work if, for example, the only
    // event that notified the scheduler was the removal of a pending task.
    if (this.appRef.dirtyFlags === ApplicationRefDirtyFlags.None) {
      this.cleanup();
      return;
    }

    // The scheduler used to pass "whether to check views" as a boolean flag instead of setting
    // fine-grained dirtiness flags, and global checking was always used on the first pass. This
    // created an interesting edge case: if a notification made a view dirty and then ticked via the
    // scheduler (and not the zone) a global check was still performed.
    //
    // Ideally, this would not be the case, and only zone-based ticks would do global passes.
    // However this is a breaking change and requires fixes in g3. Until this cleanup can be done,
    // we add the `ViewTreeGlobal` flag to request a global check if any views are dirty in a
    // scheduled tick (unless zoneless is enabled, in which case global checks aren't really a
    // thing).
    //
    // TODO(alxhub): clean up and remove this workaround as a breaking change.
    if (!this.zonelessEnabled && this.appRef.dirtyFlags & ApplicationRefDirtyFlags.ViewTreeAny) {
      this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeGlobal;
    }

    const task = this.taskService.add();
    try {
      this.ngZone.run(
        () => {
          this.runningTick = true;
          this.appRef._tick();
        },
        undefined,
        this.schedulerTickApplyArgs,
      );
    } catch (e: unknown) {
      this.taskService.remove(task);
      throw e;
    } finally {
      this.cleanup();
    }
    // If we're notified of a change within 1 microtask of running change
    // detection, run another round in the same event loop. This allows code
    // which uses Promise.resolve (see NgModel) to avoid
    // ExpressionChanged...Error to still be reflected in a single browser
    // paint, even if that spans multiple rounds of change detection.
    this.useMicrotaskScheduler = true;
    scheduleCallbackWithMicrotask(() => {
      this.useMicrotaskScheduler = false;
      this.taskService.remove(task);
    });
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
    this.cleanup();
  }

  private cleanup() {
    this.runningTick = false;
    this.cancelScheduledCallback?.();
    this.cancelScheduledCallback = null;
    // If this is the last task, the service will synchronously emit a stable
    // notification. If there is a subscriber that then acts in a way that
    // tries to notify the scheduler again, we need to be able to respond to
    // schedule a new change detection. Therefore, we should clear the task ID
    // before removing it from the pending tasks (or the tasks service should
    // not synchronously emit stable, similar to how Zone stableness only
    // happens if it's still stable after a microtask).
    if (this.pendingRenderTaskId !== null) {
      const taskId = this.pendingRenderTaskId;
      this.pendingRenderTaskId = null;
      this.taskService.remove(taskId);
    }
  }
}

/**
 * Provides change detection without ZoneJS for the application bootstrapped using
 * `bootstrapApplication`.
 *
 * This function allows you to configure the application to not use the state/state changes of
 * ZoneJS to schedule change detection in the application. This will work when ZoneJS is not present
 * on the page at all or if it exists because something else is using it (either another Angular
 * application which uses ZoneJS for scheduling or some other library that relies on ZoneJS).
 *
 * This can also be added to the `TestBed` providers to configure the test environment to more
 * closely match production behavior. This will help give higher confidence that components are
 * compatible with zoneless change detection.
 *
 * ZoneJS uses browser events to trigger change detection. When using this provider, Angular will
 * instead use Angular APIs to schedule change detection. These APIs include:
 *
 * - `ChangeDetectorRef.markForCheck`
 * - `ComponentRef.setInput`
 * - updating a signal that is read in a template
 * - when bound host or template listeners are triggered
 * - attaching a view that was marked dirty by one of the above
 * - removing a view
 * - registering a render hook (templates are only refreshed if render hooks do one of the above)
 *
 * @usageNotes
 * ```typescript
 * bootstrapApplication(MyApp, {providers: [
 *   provideExperimentalZonelessChangeDetection(),
 * ]});
 * ```
 *
 * This API is experimental. Neither the shape, nor the underlying behavior is stable and can change
 * in patch versions. There are known feature gaps and API ergonomic considerations. We will iterate
 * on the exact API based on the feedback and our understanding of the problem and solution space.
 *
 * @publicApi
 * @experimental
 * @see [bootstrapApplication](/api/platform-browser/bootstrapApplication)
 */
export function provideExperimentalZonelessChangeDetection(): EnvironmentProviders {
  performanceMarkFeature('NgZoneless');

  if ((typeof ngDevMode === 'undefined' || ngDevMode) && typeof Zone !== 'undefined' && Zone) {
    const message = formatRuntimeError(
      RuntimeErrorCode.UNEXPECTED_ZONEJS_PRESENT_IN_ZONELESS_MODE,
      `The application is using zoneless change detection, but is still loading Zone.js. ` +
        `Consider removing Zone.js to get the full benefits of zoneless. ` +
        `In applications using the Angular CLI, Zone.js is typically included in the "polyfills" section of the angular.json file.`,
    );
    console.warn(message);
  }

  return makeEnvironmentProviders([
    {provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
    {provide: NgZone, useClass: NoopNgZone},
    {provide: ZONELESS_ENABLED, useValue: true},
    {provide: SCHEDULE_IN_ROOT_ZONE, useValue: false},
    typeof ngDevMode === 'undefined' || ngDevMode
      ? [{provide: PROVIDED_ZONELESS, useValue: true}]
      : [],
  ]);
}


export abstract class ViewContainerRef {
  /**
   * Anchor element that specifies the location of this container in the containing view.
   * Each view container can have only one anchor element, and each anchor element
   * can have only a single view container.
   *
   * Root elements of views attached to this container become siblings of the anchor element in
   * the rendered view.
   *
   * Access the `ViewContainerRef` of an element by placing a `Directive` injected
   * with `ViewContainerRef` on the element, or use a `ViewChild` query.
   *
   * <!-- TODO: rename to anchorElement -->
   */
  abstract get element(): ElementRef;

  /**
   * The dependency injector for this view container.
   */
  abstract get injector(): Injector;

  /** @deprecated No replacement */
  abstract get parentInjector(): Injector;

  /**
   * Destroys all views in this container.
   */
  abstract clear(): void;

  /**
   * Retrieves a view from this container.
   * @param index The 0-based index of the view to retrieve.
   * @returns The `ViewRef` instance, or null if the index is out of range.
   */
  abstract get(index: number): ViewRef | null;

  /**
   * Reports how many views are currently attached to this container.
   * @returns The number of views.
   */
  abstract get length(): number;

  /**
   * Instantiates an embedded view and inserts it
   * into this container.
   * @param templateRef The HTML template that defines the view.
   * @param context The data-binding context of the embedded view, as declared
   * in the `<ng-template>` usage.
   * @param options Extra configuration for the created view. Includes:
   *  * index: The 0-based index at which to insert the new view into this container.
   *           If not specified, appends the new view as the last entry.
   *  * injector: Injector to be used within the embedded view.
   *
   * @returns The `ViewRef` instance for the newly created view.
   */
  abstract createEmbeddedView<C>(
    templateRef: TemplateRef<C>,
    context?: C,
    options?: {
      index?: number;
      injector?: Injector;
    },
  ): EmbeddedViewRef<C>;

  /**
   * Instantiates an embedded view and inserts it
   * into this container.
   * @param templateRef The HTML template that defines the view.
   * @param context The data-binding context of the embedded view, as declared
   * in the `<ng-template>` usage.
   * @param index The 0-based index at which to insert the new view into this container.
   * If not specified, appends the new view as the last entry.
   *
   * @returns The `ViewRef` instance for the newly created view.
   */
  abstract createEmbeddedView<C>(
    templateRef: TemplateRef<C>,
    context?: C,
    index?: number,
  ): EmbeddedViewRef<C>;

  /**
   * Instantiates a single component and inserts its host view into this container.
   *
   * @param componentType Component Type to use.
   * @param options An object that contains extra parameters:
   *  * index: the index at which to insert the new component's host view into this container.
   *           If not specified, appends the new view as the last entry.
   *  * injector: the injector to use as the parent for the new component.
   *  * ngModuleRef: an NgModuleRef of the component's NgModule, you should almost always provide
   *                 this to ensure that all expected providers are available for the component
   *                 instantiation.
   *  * environmentInjector: an EnvironmentInjector which will provide the component's environment.
   *                 you should almost always provide this to ensure that all expected providers
   *                 are available for the component instantiation. This option is intended to
   *                 replace the `ngModuleRef` parameter.
   *  * projectableNodes: list of DOM nodes that should be projected through
   *                      [`<ng-content>`](api/core/ng-content) of the new component instance.
   *
   * @returns The new `ComponentRef` which contains the component instance and the host view.
   */
  abstract createComponent<C>(
    componentType: Type<C>,
    options?: {
      index?: number;
      injector?: Injector;
      ngModuleRef?: NgModuleRef<unknown>;
      environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
      projectableNodes?: Node[][];
    },
  ): ComponentRef<C>;

  /**
   * Instantiates a single component and inserts its host view into this container.
   *
   * @param componentFactory Component factory to use.
   * @param index The index at which to insert the new component's host view into this container.
   * If not specified, appends the new view as the last entry.
   * @param injector The injector to use as the parent for the new component.
   * @param projectableNodes List of DOM nodes that should be projected through
   *     [`<ng-content>`](api/core/ng-content) of the new component instance.
   * @param ngModuleRef An instance of the NgModuleRef that represent an NgModule.
   * This information is used to retrieve corresponding NgModule injector.
   *
   * @returns The new `ComponentRef` which contains the component instance and the host view.
   *
   * @deprecated Angular no longer requires component factories to dynamically create components.
   *     Use different signature of the `createComponent` method, which allows passing
   *     Component class directly.
   */
  abstract createComponent<C>(
    componentFactory: ComponentFactory<C>,
    index?: number,
    injector?: Injector,
    projectableNodes?: any[][],
    environmentInjector?: EnvironmentInjector | NgModuleRef<any>,
  ): ComponentRef<C>;

  /**
   * Inserts a view into this container.
   * @param viewRef The view to insert.
   * @param index The 0-based index at which to insert the view.
   * If not specified, appends the new view as the last entry.
   * @returns The inserted `ViewRef` instance.
   *
   */
  abstract insert(viewRef: ViewRef, index?: number): ViewRef;

  /**
   * Moves a view to a new location in this container.
   * @param viewRef The view to move.
   * @param index The 0-based index of the new location.
   * @returns The moved `ViewRef` instance.
   */
  abstract move(viewRef: ViewRef, currentIndex: number): ViewRef;

  /**
   * Returns the index of a view within the current container.
   * @param viewRef The view to query.
   * @returns The 0-based index of the view's position in this container,
   * or `-1` if this container doesn't contain the view.
   */
  abstract indexOf(viewRef: ViewRef): number;

  /**
   * Destroys a view attached to this container
   * @param index The 0-based index of the view to destroy.
   * If not specified, the last view in the container is removed.
   */
  abstract remove(index?: number): void;

  /**
   * Detaches a view from this container without destroying it.
   * Use along with `insert()` to move a view within the current container.
   * @param index The 0-based index of the view to detach.
   * If not specified, the last view in the container is detached.
   */
  abstract detach(index?: number): ViewRef | null;

  /**
   * @internal
   * @nocollapse
   */
  static __NG_ELEMENT_ID__: () => ViewContainerRef = injectViewContainerRef;
}

/**
 * Creates a ViewContainerRef and stores it on the injector. Or, if the ViewContainerRef
 * already exists, retrieves the existing ViewContainerRef.
 *
 * @returns The ViewContainerRef instance to use
 */
export function injectViewContainerRef(): ViewContainerRef {
  const previousTNode = getCurrentTNode() as TElementNode | TElementContainerNode | TContainerNode;
  return createContainerRef(previousTNode, getLView());
}

const VE_ViewContainerRef = ViewContainerRef;

// TODO(alxhub): cleaning up this indirection triggers a subtle bug in Closure in g3. Once the fix
// for that lands, this can be cleaned up.


export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterface {
  private _appRef: ApplicationRef | null = null;
  private _attachedToViewContainer = false;

  get rootNodes(): any[] {
    const lView = this._lView;
    const tView = lView[TVIEW];
    return collectNativeNodes(tView, lView, tView.firstChild, []);
  }

  constructor(
    /**
     * This represents `LView` associated with the component when ViewRef is a ChangeDetectorRef.
     *
     * When ViewRef is created for a dynamic component, this also represents the `LView` for the
     * component.
     *
     * For a "regular" ViewRef created for an embedded view, this is the `LView` for the embedded
     * view.
     *
     * @internal
     */
    public _lView: LView,

    /**
     * This represents the `LView` associated with the point where `ChangeDetectorRef` was
     * requested.
     *
     * This may be different from `_lView` if the `_cdRefInjectingView` is an embedded view.
     */
    private _cdRefInjectingView?: LView,
    readonly notifyErrorHandler = true,
  ) {}

  get context(): T {
    return this._lView[CONTEXT] as unknown as T;
  }

  /**
   * Reports whether the given view is considered dirty according to the different marking mechanisms.
   */
  get dirty(): boolean {
    return (
      !!(
        this._lView[FLAGS] &
        (LViewFlags.Dirty | LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh)
      ) || !!this._lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty
    );
  }

  /**
   * @deprecated Replacing the full context object is not supported. Modify the context
   *   directly, or consider using a `Proxy` if you need to replace the full object.
   * // TODO(devversion): Remove this.
   */
  set context(value: T) {
    if (ngDevMode) {
      // Note: We have a warning message here because the `@deprecated` JSDoc will not be picked
      // up for assignments on the setter. We want to let users know about the deprecated usage.
      console.warn(
        'Angular: Replacing the `context` object of an `EmbeddedViewRef` is deprecated.',
      );
    }

    this._lView[CONTEXT] = value as unknown as {};
  }

  get destroyed(): boolean {
    return (this._lView[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed;
  }

  destroy(): void {
    if (this._appRef) {
      this._appRef.detachView(this);
    } else if (this._attachedToViewContainer) {
      const parent = this._lView[PARENT];
      if (isLContainer(parent)) {
        const viewRefs = parent[VIEW_REFS] as ViewRef<unknown>[] | null;
        const index = viewRefs ? viewRefs.indexOf(this) : -1;
        if (index > -1) {
          ngDevMode &&
            assertEqual(
              index,
              parent.indexOf(this._lView) - CONTAINER_HEADER_OFFSET,
              'An attached view should be in the same position within its container as its ViewRef in the VIEW_REFS array.',
            );
          detachView(parent, index);
          removeFromArray(viewRefs!, index);
        }
      }
      this._attachedToViewContainer = false;
    }
    destroyLView(this._lView[TVIEW], this._lView);
  }

  onDestroy(callback: Function) {
    storeLViewOnDestroy(this._lView, callback as () => void);
  }

  /**
   * Marks a view and all of its ancestors dirty.
   *
   * This can be used to ensure an {@link ChangeDetectionStrategy#OnPush} component is
   * checked when it needs to be re-rendered but the two normal triggers haven't marked it
   * dirty (i.e. inputs haven't changed and events haven't fired in the view).
   *
   * <!-- TODO: Add a link to a chapter on OnPush components -->
   *
   * @usageNotes
   * ### Example
   *
   * ```typescript
   * @Component({
   *   selector: 'app-root',
   *   template: `Number of ticks: {{numberOfTicks}}`
   *   changeDetection: ChangeDetectionStrategy.OnPush,
   * })
   * class AppComponent {
   *   numberOfTicks = 0;
   *
   *   constructor(private ref: ChangeDetectorRef) {
   *     setInterval(() => {
   *       this.numberOfTicks++;
   *       // the following is required, otherwise the view will not be updated
   *       this.ref.markForCheck();
   *     }, 1000);
   *   }
   * }
   * ```
   */
  markForCheck(): void {
    markViewDirty(this._cdRefInjectingView || this._lView, NotificationSource.MarkForCheck);
  }

  markForRefresh(): void {
    markViewForRefresh(this._cdRefInjectingView || this._lView);
  }

  /**
   * Detaches the view from the change detection tree.
   *
   * Detached views will not be checked during change detection runs until they are
   * re-attached, even if they are dirty. `detach` can be used in combination with
   * {@link ChangeDetectorRef#detectChanges} to implement local change
   * detection checks.
   *
   * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->
   * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->
   *
   * @usageNotes
   * ### Example
   *
   * The following example defines a component with a large list of readonly data.
   * Imagine the data changes constantly, many times per second. For performance reasons,
   * we want to check and update the list every five seconds. We can do that by detaching
   * the component's change detector and doing a local check every five seconds.
   *
   * ```typescript
   * class DataProvider {
   *   // in a real application the returned data will be different every time
   *   get data() {
   *     return [1,2,3,4,5];
   *   }
   * }
   *
   * @Component({
   *   selector: 'giant-list',
   *   template: `
   *     <li *ngFor="let d of dataProvider.data">Data {{d}}</li>
   *   `,
   * })
   * class GiantList {
   *   constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) {
   *     ref.detach();
   *     setInterval(() => {
   *       this.ref.detectChanges();
   *     }, 5000);
   *   }
   * }
   *
   * @Component({
   *   selector: 'app',
   *   providers: [DataProvider],
   *   template: `
   *     <giant-list><giant-list>
   *   `,
   * })
   * class App {
   * }
   * ```
   */
  detach(): void {
    this._lView[FLAGS] &= ~LViewFlags.Attached;
  }

  /**
   * Re-attaches a view to the change detection tree.
   *
   * This can be used to re-attach views that were previously detached from the tree
   * using {@link ChangeDetectorRef#detach}. Views are attached to the tree by default.
   *
   * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->
   *
   * @usageNotes
   * ### Example
   *
   * The following example creates a component displaying `live` data. The component will detach
   * its change detector from the main change detector tree when the component's live property
   * is set to false.
   *
   * ```typescript
   * class DataProvider {
   *   data = 1;
   *
   *   constructor() {
   *     setInterval(() => {
   *       this.data = this.data * 2;
   *     }, 500);
   *   }
   * }
   *
   * @Component({
   *   selector: 'live-data',
   *   inputs: ['live'],
   *   template: 'Data: {{dataProvider.data}}'
   * })
   * class LiveData {
   *   constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) {}
   *
   *   set live(value) {
   *     if (value) {
   *       this.ref.reattach();
   *     } else {
   *       this.ref.detach();
   *     }
   *   }
   * }
   *
   * @Component({
   *   selector: 'app-root',
   *   providers: [DataProvider],
   *   template: `
   *     Live Update: <input type="checkbox" [(ngModel)]="live">
   *     <live-data [live]="live"><live-data>
   *   `,
   * })
   * class AppComponent {
   *   live = true;
   * }
   * ```
   */


rt function handleUnknownPropertyError(
  propName: string,
  tagName: string | null,
  nodeType: TNodeType,
  lView: LView,
): void {
  // Special-case a situation when a structural directive is applied to
  // an `<ng-template>` element, for example: `<ng-template *ngIf="true">`.
  // In this case the compiler generates the `ɵɵtemplate` instruction with
  // the `null` as the tagName. The directive matching logic at runtime relies
  // on this effect (see `isInlineTemplate`), thus using the 'ng-template' as
  // a default value of the `tNode.value` is not feasible at this moment.
  if (!tagName && nodeType === TNodeType.Container) {
    tagName = 'ng-template';
  }

  const isHostStandalone = isHostComponentStandalone(lView);
  const templateLocation = getTemplateLocationDetails(lView);

  let message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'${templateLocation}.`;

  const schemas = `'${isHostStandalone ? '@Component' : '@NgModule'}.schemas'`;
  const importLocation = isHostStandalone
    ? "included in the '@Component.imports' of this component"
    : 'a part of an @NgModule where this component is declared';
  if (KNOWN_CONTROL_FLOW_DIRECTIVES.has(propName)) {
    // Most likely this is a control flow directive (such as `*ngIf`) used in
    // a template, but the directive or the `CommonModule` is not imported.
    const correspondingImport = KNOWN_CONTROL_FLOW_DIRECTIVES.get(propName);
    message +=
      `\nIf the '${propName}' is an Angular control flow directive, ` +
      `please make sure that either the '${correspondingImport}' directive or the 'CommonModule' is ${importLocation}.`;
  } else {
    // May be an Angular component, which is not imported/declared?
    message +=
      `\n1. If '${tagName}' is an Angular component and it has the ` +
      `'${propName}' input, then verify that it is ${importLocation}.`;
    // May be a Web Component?
    if (tagName && tagName.indexOf('-') > -1) {
      message +=
        `\n2. If '${tagName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' ` +
        `to the ${schemas} of this component to suppress this message.`;
      message +=
        `\n3. To allow any property add 'NO_ERRORS_SCHEMA' to ` +
        `the ${schemas} of this component.`;
    } else {
      // If it's expected, the error can be suppressed by the `NO_ERRORS_SCHEMA` schema.
      message +=
        `\n2. To allow any property add 'NO_ERRORS_SCHEMA' to ` +
        `the ${schemas} of this component.`;
    }
  }

  reportUnknownPropertyError(message);
}

export function reportUnknownPropertyError(message: string) {
  if (shouldThrowErrorOnUnknownProperty) {
    throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
  } else {
    console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
  }
}

/**
 * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`)
 * and must **not** be used in production bundles. The function makes megamorphic reads, which might
 * be too slow for production mode and also it relies on the constructor function being available.
 *
 * Gets a reference to the host component def (where a current component is declared).
 *
 * @param lView An `LView` that represents a current component that is being rendered.
 */
export function getDeclarationComponentDef(lView: LView): ComponentDef<unknown> | null {
  !ngDevMode && throwError('Must never be called in production mode');

  const declarationLView = lView[DECLARATION_COMPONENT_VIEW] as LView<Type<unknown>>;
  const context = declarationLView[CONTEXT];

  // Unable to obtain a context.
  if (!context) return null;

  return context.constructor ? getComponentDef(context.constructor) : null;
}

/**
 * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`)
 * and must **not** be used in production bundles. The function makes megamorphic reads, which might
 * be too slow for production mode.
 *
 * Checks if the current component is declared inside of a standalone component template.
 *
 * @param lView An `LView` that represents a current component that is being rendered.
 */
export function isHostComponentStandalone(lView: LView): boolean {
  !ngDevMode && throwError('Must never be called in production mode');

  const componentDef = getDeclarationComponentDef(lView);
  // Treat host component as non-standalone if we can't obtain the def.
  return !!componentDef?.standalone;
}

/**
 * WARNING: this is a **dev-mode only** function (thus should always be guarded by the `ngDevMode`)
 * and must **not** be used in production bundles. The function makes megamorphic reads, which might
 * be too slow for production mode.
 *
 * Constructs a string describing the location of the host component template. The function is used
 * in dev mode to produce error messages.
 *
 * @param lView An `LView` that represents a current component that is being rendered.
 */
export function getTemplateLocationDetails(lView: LView): string {
  !ngDevMode && throwError('Must never be called in production mode');

  const hostComponentDef = getDeclarationComponentDef(lView);
  const componentClassName = hostComponentDef?.type?.name;
  return componentClassName ? ` (used in the '${componentClassName}' component template)` : '';
}

/**
 * The set of known control flow directives and their corresponding imports.
 * We use this set to produce a more precises error message with a note
 * that the `CommonModule` should also be included.
 */
export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Map([
  ['ngIf', 'NgIf'],
  ['ngFor', 'NgFor'],
  ['ngSwitchCase', 'NgSwitchCase'],
  ['ngSwitchDefault', 'NgSwitchDefault'],
]);
/**
 * Returns true if the tag name is allowed by specified schemas.
 * @param schemas Array of schemas
 * @param tagName Name of the tag
 */
export function matchingSchemas(schemas: SchemaMetadata[] | null, tagName: string | null): boolean {
  if (schemas !== null) {
    for (let i = 0; i < schemas.length; i++) {
      const schema = schemas[i];
      if (
        schema === NO_ERRORS_SCHEMA ||
        (schema === CUSTOM_ELEMENTS_SCHEMA && tagName && tagName.indexOf('-') > -1)
      ) {
        return true;
      }
    }
  }

  return false;
}