import {
  Component,
  SkipSelf,
  Optional,
  OnInit,
  OnDestroy,
} from '@angular/core';
import { combineLatest, Subject, BehaviorSubject } from 'rxjs';
import {
  tap,
  takeUntil,
  distinctUntilChanged,
  debounceTime,
  map,
} from 'rxjs/operators';
import { Route, ActiveRoute } from './route';
import { Router } from './router.service';
import { compareParams } from './route-params.service';
import { compareRoutes } from './utils/compare-routes';
import { matchRoute, parsePath } from './utils/path-parser';

interface State {
  activeRoute: ActiveRoute | null;
  routes: Route[];
}

@Component({
  // tslint:disable-next-line:component-selector
  selector: 'router',
  standalone: true,
  template: '<ng-content></ng-content>',
})
export class RouterComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject();
  private readonly state$ = new BehaviorSubject<State>({
    activeRoute: null,
    routes: [],
  });

  readonly activeRoute$ = this.state$.pipe(
    map((state) => state.activeRoute),
    distinctUntilChanged(this.compareActiveRoutes),
    takeUntil(this.destroy$)
  );
  readonly routes$ = this.state$.pipe(
    map((state) => state.routes),
    distinctUntilChanged(this.compareRoutes),
    takeUntil(this.destroy$)
  );

  public basePath = '';

  // support multiple "routers"
  // router (base /)
  // blog(.*?)
  // router (base /blog)
  // post1(blog/post1/(.*?)
  // post2
  // post3

  constructor(
    private router: Router,
    @SkipSelf() @Optional() public parentRouterComponent: RouterComponent
  ) {}

  ngOnInit() {
    combineLatest([this.routes$.pipe(debounceTime(1)), this.router.url$])
      .pipe(
        distinctUntilChanged(),
        tap(([routes, url]: [Route[], string]) => {
          let routeToRender = null;
          for (const route of routes) {
            routeToRender = this.isRouteMatch(url, route);

            if (routeToRender) {
              this.setRoute(url, route);
              break;
            }
          }

          if (!routeToRender) {
            this.setActiveRoute({ route: null, params: {}, path: '' });
          }
        }),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  setRoute(url: string, route: Route) {
    this.basePath = route.path;
    const match = matchRoute(url, route);
    this.setActiveRoute({
      route,
      params: match?.params || {},
      path: match?.path || '',
    });
  }

  registerRoute(route: Route) {
    route.matcher = route.matcher || parsePath(route);
    this.updateRoutes(route);

    return route;
  }

  setActiveRoute(activeRoute: ActiveRoute) {
    this.updateState({ activeRoute });
  }

  unregisterRoute(route: Route) {
    this.updateRoutes(route);
  }

  normalizePath(path: string) {
    return this.router.normalizePath(path);
  }

  ngOnDestroy() {
    this.destroy$.next(true);
  }

  private isRouteMatch(url: string, route: Route) {
    return route.matcher?.exec(url);
  }

  private compareActiveRoutes(
    previous: ActiveRoute,
    current: ActiveRoute
  ): boolean {
    if (previous === current) {
      return true;
    }
    if (!previous) {
      return false;
    }
    return (
      previous.path === current.path &&
      compareParams(previous.params, current.params) &&
      previous.route === current.route
    );
  }

  private compareRoutes(previous: Route[], current: Route[]): boolean {
    if (previous === current) {
      return true;
    }
    if (!previous) {
      return false;
    }
    return (
      previous.length === current.length &&
      previous.every((route, i) => route === current[i])
    );
  }

  private updateState(newState: Partial<State>) {
    this.state$.next({ ...this.state$.value, ...newState });
  }

  private updateRoutes(route: Route) {
    const routes = this.state$.value.routes;
    const index = routes.indexOf(route);
    if (index > -1) {
      this.updateState({
        routes: [...routes.slice(0, index), ...routes.slice(index + 1)],
      });
    } else {
      this.updateState({ routes: routes.concat(route).sort(compareRoutes) });
    }
  }
}