import { Injectable } from "@angular/core";
import {
  NavigationCancel,
  NavigationEnd,
  NavigationError,
  NavigationExtras,
  Router,
} from "@angular/router";
import { Routes } from "@angular/router";

export type RouterResponse = {
  url: string;
  params: any; // Object with your routing parameters
  fragment: { [key: string]: string };
  rawFragment: string;
  query: { [key: string]: string };
  rawQuery: string;
};

type RouterParam = {
  param: string;
  pos: number;
};

type RouterPath = {
  wildcard: string;
  params: RouterParam[];
  subscriptions: Function[];
};

type ProcessResult = {
  routerResponse: RouterResponse;
  routerPath: RouterPath;
};

type UrlSlice = {
  /**
   * first part of the url
   */
  base: string;
  /**
   * part after #
   */
  fragment: string;
  /**
   * part after ?
   */
  query: string;
};

@Injectable({
  providedIn: "root",
})

/**
 * We are using advanced url handling. In some case we need params, which are not accessable outside the routing scope.
 * Moreover there are situations, where we have to use the Router and the ActivatedRoute in mixed.
 * It can make a spagetti subscription code. To avoid that, this service hide the routing layer
 * and gives you an api which can calculate the necessary parameters, and url parsing.
 */
export class RouterHandler {
  private routerResponse: RouterResponse = null;
  private currentRouterPath: RouterPath = null;
  private config: RouterPath[];

  public static FRAGMENT_SEPARATOR = ";";
  public static FRAGMENT_EQUAL = "!";

  public static FRAGMENT_KEY_RESERVED_RE = /[!;]/;
  public static FRAGMENT_DELIM_RE = /((?:[^;]|;;)+);?/g;

  constructor(private router: Router) {
    this.config = [];

    let flatConfig = this.parseRouteConfig(router.config);
    flatConfig.forEach((f) => {
      this.config.push(this.makeRouterPathFromFlatConfig(f));
    });

    this.router.events.subscribe((val) => {
      if (val instanceof NavigationEnd) {
        let res = this.processUrl(decodeURI(val.url.substr(1))); // we dont need the first "/"
        if (res) {
          this.routerResponse = res.routerResponse;
          this.currentRouterPath = res.routerPath;

          this.currentRouterPath.subscriptions.forEach((cb) => {
            cb(res.routerResponse);
          });
        }
      }
    });
  }

  public navigate(commands: any[], extras?: NavigationExtras) {
    return this.router.navigate(commands, extras);
  }

  /**
   * make flat list from routes, where it can be child routes
   * @param route Routes from @angular/router - this is what you directly pass to the router: RouterModule.forRoot(routes)
   * @returns list of routes. childs are merged in the first level
   *
   * from this:
   *      {path: 'search/:resourceId/:query', component: SearchViewComponent},
   *      {path: 'room/:id', component: ResourcePageComponent, children: [
   *          {path: 'chat', component: ChatLayoutComponent},
   *          {path: 'drive', component: DriveWindowComponent },
   *      ]},
   *
   *    it generates this:
   *
   *    [
   *        "search/:resourceId/:query",
   *        "room/:id",
   *        "room/:id/chat",
   *        "room/:id/drive"
   *    ]
   */
  private parseRouteConfig(route: Routes): string[] {
    let flatConfig = [];

    route.forEach((r) => {
      if (r.path != undefined) {
        flatConfig.push(r.path);

        if (r.children) {
          let childPath = this.parseRouteConfig(r.children);
          childPath.forEach((p) => {
            flatConfig.push(r.path + "/" + p);
          });
        }
      }
    });

    return flatConfig;
  }

  private makeRouterPathFromFlatConfig(flatConfig: string): RouterPath {
    return {
      wildcard: this.makePathWildcardFromFlatConfig(flatConfig),
      params: this.detectParamsFromFlatConfig(flatConfig),
      subscriptions: [],
    };
  }

  private makePathWildcardFromFlatConfig(flatConfig: string): string {
    return flatConfig.replace(/\:[a-zA-Z0-9_.-]+/g, "?");
  }

  private detectParamsFromFlatConfig(flatConfig: string): RouterParam[] {
    let slice = flatConfig.split("/");
    let params = [];

    for (let i = 0; i < slice.length; i++) {
      if (slice[i].startsWith(":")) {
        params.push({
          param: slice[i].substr(1),
          pos: i,
        });
      }
    }

    return params;
  }

  public processUrl(url): ProcessResult | null {
    let urlSlice = this.getUrlComponents(url);

    if (urlSlice.query && urlSlice.query.length > 0 && urlSlice.base != "simplepay-back") {
      let urlWithoutQueryParams =
        urlSlice.base +
        (urlSlice.fragment && urlSlice.fragment.length > 0 ? "#" + urlSlice.fragment : "");
      history.pushState({}, "Nano", urlWithoutQueryParams);
      console.log("cut unnecessary query url parts");
    }

    for (let i = 0; i < this.config.length; i++) {
      if (this.isUrlFit(urlSlice.base, this.config[i].wildcard)) {
        return {
          routerResponse: {
            url: urlSlice.base,
            rawFragment: urlSlice.fragment,
            rawQuery: urlSlice.query,
            params: this.getParameterUsingRouterPath(
              urlSlice.base,
              this.config[i]
            ),
            fragment: this.parseFragment(urlSlice.fragment),
            query: this.parseQueryParams(urlSlice.query),
          },
          routerPath: this.config[i],
        }; // we dont need to check the others
      }
    }

    return {
      routerResponse: {
        url: urlSlice.base,
        rawFragment: urlSlice.fragment,
        rawQuery: urlSlice.query,
        params: {},
        fragment: this.parseFragment(urlSlice.fragment),
        query: this.parseQueryParams(urlSlice.query),
      },
      routerPath: null,
    };
  }

  private getUrlComponents(url: string): UrlSlice {
    let [urlPart, ...fragments] = url.split("#");
    let fragmentPart = fragments.join("#"); // next # after the first is a value, not separator
    let [cleanUrl, queryParams] = urlPart.split("?");

    return {
      base: cleanUrl,
      fragment: fragmentPart,
      query: queryParams,
    };
  }

  public getRoute(): RouterResponse {
    return this.routerResponse;
  }

  /**
   *
   * @param url url without fragment and query params
   * @param wildcard
   * @returns
   */
  private isUrlFit(url: string, wildcard: string): boolean {
    let urlSlice = url.split("/");
    let wildcardSlice = wildcard.split("/");

    if (wildcardSlice.length !== urlSlice.length) return false;

    for (let i = 0; i < urlSlice.length; i++) {
      if (wildcardSlice[i] == "?") continue;

      if (urlSlice[i] != wildcardSlice[i]) return false;
    }

    return true;
  }

  /**
   *
   * @param url url without fragment and query
   * @param config
   */
  private getParameterUsingRouterPath(
    url: string,
    config: RouterPath
  ): { [key: string]: string } {
    let slice = url.split("/");

    let params = {};

    config.params.forEach((p) => {
      if (slice.length >= p.pos) {
        params[p.param] = slice[p.pos];
      } else {
        params[p.param] = undefined;
      }
    });

    return params;
  }

  public parseFragment(fragment: string): { [key: string]: string } {
    if (fragment && fragment.length > 0) {
      let parts = this.getMatches(fragment, RouterHandler.FRAGMENT_DELIM_RE);

      let output = {};

      parts.forEach((element) => {
        let [key, value] = this.splitByFirstDelimiter(
          element,
          RouterHandler.FRAGMENT_EQUAL
        );
        output[key ? this.decodeFragmentValue(key) : ""] = value
          ? this.decodeFragmentValue(value)
          : "";
        
      });
      return output;
    } else {
      return {};
    }
  }

  private getMatches(string, regex, index?) {
    index || (index = 1); // default to the first capturing group
    var matches = [];
    var match;
    while (match = regex.exec(string)) {
      matches.push(match[index]);
    }
    return matches;
  }

  /**
   * generate a fragment string form the parsed fragment object
   * @param {[key: string]: string} fragment
   * @returns string
   */
  public fragmentToRaw(fragment: { [key: string]: string }): string {
    let output = [];
    for (let key in fragment) {
      if (key.search(RouterHandler.FRAGMENT_KEY_RESERVED_RE) > -1) {
        throw new Error("Reserved character in fragment key: " + key);
      }

      output.push(
        this.encodeFragmentValue(key) +
          RouterHandler.FRAGMENT_EQUAL +
          this.encodeFragmentValue(fragment[key])
      );
    }

    return output.join(RouterHandler.FRAGMENT_SEPARATOR);
  }

  public parseQueryParams(query: string): { [key: string]: string } {
    if (query && query.length > 0) {
      let parts = query.split("&");

      let output = {};

      parts.forEach((element) => {
        let keyValue = element.split("=");
        output[keyValue[0]] = keyValue[1];
      });

      return output;
    } else {
      return {};
    }
  }

  private isSubscriptionPatternFit(
    urlPattern: string,
    wildcard: string
  ): boolean {
    let patternSlice = urlPattern.split("/");
    let wildcardSlice = wildcard.split("/");

    if (wildcardSlice.length < patternSlice.length) return false;

    for (let i = 0; i < patternSlice.length; i++) {
      if (wildcardSlice[i] == "?") continue;

      if (wildcardSlice[i] != patternSlice[i]) return false;
    }

    return true;
  }

  /**
   * When your component initalize after the router process, you can ask,
   * if you are on the specific page. Pattern works similar, like subscribe, unsubscibe
   * @param urlPattern
   * @returns true, if your pattern match with the current url
   */
  public isOnPage(urlPattern: string): boolean {
    if (this.currentRouterPath) {
      return this.isSubscriptionPatternFit(
        urlPattern,
        this.currentRouterPath.wildcard
      );
    } else {
      console.warn("Could not initalize router currentPath");
      return false;
    }
  }

  public isUrlOnPage(url: string, urlPattern: string): boolean {
    url = this.getUrlComponents(url).base;
    return this.isUrlFit(url, urlPattern);
  }

  /**
   *
   * @param urlPattern example: "page" or "page/subpage" or "page/?/subpage" or "page/?", where "?" means an existing param or string
   * @param handler
   */
  public subscribe(urlPattern: string, handler: Function) {
    this.config.forEach((routerPath) => {
      if (this.isSubscriptionPatternFit(urlPattern, routerPath.wildcard)) {
        routerPath.subscriptions.push(handler);
      }
    });
  }

  public unsubscribe(urlPattern, handler: Function) {
    this.config.forEach((routerPath) => {
      if (this.isSubscriptionPatternFit(urlPattern, routerPath.wildcard)) {
        if (routerPath.subscriptions.indexOf(handler) > -1) {
          routerPath.subscriptions.splice(
            routerPath.subscriptions.indexOf(handler),
            1
          );
        }
      }
    });
  }

  public subscribeAll(handler: Function) {
    this.config.forEach((routerPath) => {
      routerPath.subscriptions.push(handler);
    });
  }

  public unsubscribeAll(handler: Function) {
    this.config.forEach((routerPath) => {
      if (routerPath.subscriptions.indexOf(handler) > -1) {
        routerPath.subscriptions.splice(
          routerPath.subscriptions.indexOf(handler),
          1
        );
      }
    });
  }

  /**
   *
   * @param fragmentValue
   * @returns
   */
  public encodeFragmentValue(fragmentValue): string | null {
    try {
      return encodeURI(fragmentValue.replaceAll(";", ";;"));
    } catch (e) {
      return "";
    }
  }

  public decodeFragmentValue(fragmentValue): string | null {
    try {
      fragmentValue = decodeURI(fragmentValue /*.replace(/\+/g, " ")*/);
    } catch (e) {}
    return fragmentValue.replaceAll(";;", ";");
  }

  private splitByFirstDelimiter(
    str: string,
    delimiter: string
  ): [string, string] {
    let pos = str.search(delimiter);
    if (pos > -1) {
      return [str.substr(0, pos), str.substr(pos + delimiter.length)];
    } else {
      return [str, ""];
    }
  }
}
