import React from "react";
/* -------------------------------------------------------------------------- */
/*                                  Libraries                                 */
/* -------------------------------------------------------------------------- */
import { useTranslation } from "react-i18next";
import { matchPath, PathMatch, useLocation } from "react-router-dom";
import { useRoutesDefinition } from "./definition";
/* -------------------------------------------------------------------------- */
/*                                    Types                                   */
/* -------------------------------------------------------------------------- */
import {
  ActiveRoutePath,
  ActiveRoutePathTitleCallbackParams,
  RoutePathDefinition,
  WithRouteProps,
} from "./types";
/* -------------------------------------------------------------------------- */
/*                                    Misc                                    */
/* -------------------------------------------------------------------------- */

/* -------------------------------------------------------------------------- */
/*                                   Helpers                                  */
/* -------------------------------------------------------------------------- */
function joinPaths(paths: string[]): string {
  return paths.join("/").replace(/\/\/+/g, "/");
}

function concatPaths(parent: string, current: string) {
  const jointPaths = joinPaths([parent, current]);
  return jointPaths;
}

function addActiveRoutePathIfPossible(
  activeRoutePaths: ActiveRoutePath[],
  activePath: ActiveRoutePath
) {
  if (canBeAddedToActiveRoutes(activeRoutePaths, activePath.match)) {
    activeRoutePaths.push(activePath);
  }
}

function isResolvedAsActive(
  toPathname: string,
  locationPathname: string,
  definition: RoutePathDefinition,
) {
  return (
    isPathActiveForLocation(toPathname, locationPathname) &&
    isNotCatchAll(definition.path || "")
  );
}

function canBeAddedToActiveRoutes(
  activeRoutePaths: ActiveRoutePath[],
  match: PathMatch<string>
) {
  return (
    isNotSameAsPreviousMatch(activeRoutePaths, match) &&
    isMoreSpecificThanPreviousMatch(activeRoutePaths, match.pathname)
  );
}

function getPreviousMatch(
  previousMatches: ActiveRoutePath[]
): ActiveRoutePath | undefined {
  return previousMatches[previousMatches.length - 1];
}

function isNotSameAsPreviousMatch(
  previousMatches: ActiveRoutePath[],
  match: PathMatch<string>
): boolean {
  const previousMatchedPathname =
    getPreviousMatch(previousMatches)?.match.pattern ?? "";
  return previousMatchedPathname !== match.pattern;
}

function isMoreSpecificThanPreviousMatch(
  previousMatches: ActiveRoutePath[],
  toPathname: string
): boolean {
  const previousMatchedPathname =
    getPreviousMatch(previousMatches)?.match.pathname ?? "";
  return toPathname.length > previousMatchedPathname.length;
}

function isNotCatchAll(path: string) {
  return path !== "*";
}

function isPathActiveForLocation(
  pathName: string,
  locationPathname: string,
) {
  return (
    locationPathname === pathName ||
    (locationPathname.startsWith(pathName) &&
      locationPathname.charAt(pathName.length) === "/")
  );
}

function matchPatternInPath(
  pathPattern: string,
  locationPathname: string,
  requireExactMatch: boolean = false
): PathMatch<string> | null {
  return matchPath(
    { path: pathPattern, end: requireExactMatch },
    locationPathname
  );
}

export function mapActivePathBranch(
  siblings: RoutePathDefinition[],
  locationPathname: string,
  translation: ActiveRoutePathTitleCallbackParams["translation"],
  parentPath: string = ""
) {
  if (siblings.length === 0) {
    return [];
  }
  const activeBranch: ActiveRoutePath[] = [];

  siblings.forEach((definition) => {
    const pathPatternWithParent = concatPaths(parentPath, definition.path);
    
    if (pathPatternWithParent === "/") return;

    const match = matchPatternInPath(pathPatternWithParent, locationPathname);
    if (!match) return;
    const activeRoutePath: ActiveRoutePath = mapRouteDefinitionToActivePath(
      definition,
      [],
      match,
      locationPathname,
      parentPath,
      translation
    );
    addActiveRoutePathIfPossible(activeBranch, activeRoutePath);
  });

  return activeBranch;
}

export function mapRouteDefinitionToActivePath(
  definition: RoutePathDefinition,
  siblings: RoutePathDefinition[],
  match: PathMatch<string>,
  locationPathname: string,
  parentPath: string = "",
  transition: ActiveRoutePathTitleCallbackParams["translation"]
): ActiveRoutePath {
  return {
    definition: definition,
    title:
      typeof definition.title === "function"
        ? definition.title({
            definition,
            match,
            locationPathname: locationPathname,
            translation: transition,
          })
        : definition.title,
    match: match,
    siblings: mapActivePathBranch(
      siblings,
      locationPathname,
      transition,
      parentPath
    ),
  };
}

export function mapDefinitionToActivePath(
  definitions: RoutePathDefinition[],
  locationPathname: string,
  translation: ActiveRoutePathTitleCallbackParams["translation"],
  parentPath: string = ""
): ActiveRoutePath[] {
  const activeRoutePaths: ActiveRoutePath[] = [];

  definitions.forEach((definition, index) => {
    const pathPatternWithParent = concatPaths(parentPath, definition.path);
    const match = matchPatternInPath(pathPatternWithParent, locationPathname);
    if (!match) {
      return;
    }

    if (isResolvedAsActive(match.pathname, locationPathname, definition)) {
      const activeRoutePath: ActiveRoutePath = mapRouteDefinitionToActivePath(
        definition,
        definitions,
        match,
        locationPathname,
        parentPath,
        translation
      );
      addActiveRoutePathIfPossible(activeRoutePaths, activeRoutePath);

      if (definition.children) {
        const nestedMatches = mapDefinitionToActivePath(
          definition.children,
          locationPathname,
          translation,
          pathPatternWithParent
        );
        nestedMatches.forEach((activePath) => {
          addActiveRoutePathIfPossible(activeRoutePaths, activePath);
        });
      }
    }
  });
  return activeRoutePaths;
}

/* -------------------------------------------------------------------------- */
/*                            HOC with route params                           */
/* -------------------------------------------------------------------------- */
const withRouteParams = <T extends WithRouteProps = WithRouteProps>(
  Component: React.ComponentType<T>,
  componentName = Component.displayName ?? Component.name
): {
  (props: Omit<T, Exclude<keyof WithRouteProps, "path" | "name">>): JSX.Element;
  displayName: string;
} => {
  function WithRouteParams(
    props: Omit<T, Exclude<keyof WithRouteProps, "path" | "name" | "routes">>
  ) {
    const { t } = useTranslation();
    const withRouteProps: WithRouteProps = {
      activePath: mapDefinitionToActivePath(
        useRoutesDefinition(),
        useLocation().pathname,
        t
      ),
      path: props.path,
      name: props.name,
    };
    return <Component {...withRouteProps} {...(props as T)} />;
  }
  WithRouteParams.displayName = `withRouteParams(${componentName})`;

  return WithRouteParams;
};

export { withRouteParams };
