// src/index.ts
import * as fs from "fs";
import { minimatch } from "minimatch";
import * as path from "path";
var defaultOptions = {
  appDir: "app",
  routeDir: "routes",
  basePath: "/",
  paramPrefixChar: "$",
  nestedDirectoryChar: "+",
  routeRegex: /((\${nestedDirectoryChar}[\/\\][^\/\\:?*]+)|[\/\\]((index|route|layout|page)|(_[^\/\\:?*]+)|([^\/\\:?*]+\.route)))\.(ts|tsx|js|jsx|md|mdx)$$/
};
var defaultDefineRoutes = void 0;
function flatRoutes(routeDir, defineRoutes, options = {}) {
  const routes = _flatRoutes(
    options.appDir ?? defaultOptions.appDir,
    options.ignoredRouteFiles ?? [],
    {
      ...defaultOptions,
      ...options,
      routeDir,
      defineRoutes
    }
  );
  Object.values(routes).forEach((route) => {
    if (route.parentId === void 0) {
      route.parentId = "root";
    }
  });
  return routes;
}
function _flatRoutes(appDir, ignoredFilePatternsOrOptions, options) {
  let ignoredFilePatterns = [];
  if (ignoredFilePatternsOrOptions && !Array.isArray(ignoredFilePatternsOrOptions)) {
    options = ignoredFilePatternsOrOptions;
  } else {
    ignoredFilePatterns = ignoredFilePatternsOrOptions ?? [];
  }
  if (!options) {
    options = defaultOptions;
  }
  let routeMap = /* @__PURE__ */ new Map();
  let nameMap = /* @__PURE__ */ new Map();
  let routeDirs = Array.isArray(options.routeDir) ? options.routeDir : [options.routeDir ?? "routes"];
  let defineRoutes = options.defineRoutes ?? defaultDefineRoutes;
  if (!defineRoutes) {
    throw new Error("You must provide a defineRoutes function");
  }
  let visitFiles = options.visitFiles ?? defaultVisitFiles;
  const routeRegex = getRouteRegex(
    options.routeRegex ?? defaultOptions.routeRegex,
    options.nestedDirectoryChar ?? defaultOptions.nestedDirectoryChar
  );
  for (let routeDir of routeDirs) {
    visitFiles(path.join(appDir, routeDir), (file) => {
      if (ignoredFilePatterns && ignoredFilePatterns.some(
        (pattern) => minimatch(file, pattern, { dot: true })
      )) {
        return;
      }
      if (isRouteModuleFile(file, routeRegex)) {
        let routeInfo = getRouteInfo(routeDir, file, options);
        routeMap.set(routeInfo.id, routeInfo);
        nameMap.set(routeInfo.name, routeInfo);
        return;
      }
    });
  }
  Array.from(routeMap.values()).forEach((routeInfo) => {
    let parentId = findParentRouteId(routeInfo, nameMap);
    routeInfo.parentId = parentId;
  });
  function defineNestedRoutes(defineRoute, parentId) {
    let childRoutes = Array.from(routeMap.values()).filter(
      (routeInfo) => routeInfo.parentId === parentId
    );
    let parentRoute = parentId ? routeMap.get(parentId) : void 0;
    let parentRoutePath = parentRoute?.path ?? "/";
    for (let childRoute of childRoutes) {
      let routePath = childRoute?.path?.slice(parentRoutePath.length) ?? "";
      if (routePath.startsWith("/")) {
        routePath = routePath.slice(1);
      }
      let index = childRoute.index;
      if (index) {
        let invalidChildRoutes = Object.values(routeMap).filter(
          (routeInfo) => routeInfo.parentId === childRoute.id
        );
        if (invalidChildRoutes.length > 0) {
          throw new Error(
            `Child routes are not allowed in index routes. Please remove child routes of ${childRoute.id}`
          );
        }
        defineRoute(routePath, routeMap.get(childRoute.id).file, {
          index: true
        });
      } else {
        defineRoute(routePath, routeMap.get(childRoute.id).file, () => {
          defineNestedRoutes(defineRoute, childRoute.id);
        });
      }
    }
  }
  let routes = defineRoutes(defineNestedRoutes);
  return routes;
}
var routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
var serverRegex = /\.server\.(ts|tsx|js|jsx|md|mdx)$/;
function isRouteModuleFile(filename, routeRegex) {
  let isFlatFile = !filename.includes(path.sep);
  if (isFlatFile) {
    return routeModuleExts.includes(path.extname(filename));
  }
  let isRoute = routeRegex.test(filename);
  if (isRoute) {
    let isServer = serverRegex.test(filename);
    return !isServer;
  }
  return false;
}
var memoizedRegex = /* @__PURE__ */ (() => {
  const cache = {};
  return (input) => {
    if (input in cache) {
      return cache[input];
    }
    const newRegex = new RegExp(input);
    cache[input] = newRegex;
    return newRegex;
  };
})();
function isIndexRoute(routeId, options) {
  const nestedDirectoryChar = options.nestedDirectoryChar.replace(
    /[.*+\-?^${}()|[\]\\]/g,
    "\\$&"
  );
  const indexRouteRegex = memoizedRegex(
    `((^|[.]|[${nestedDirectoryChar}]\\/)(index|_index))(\\/[^\\/]+)?$|(\\/_?index\\/)`
  );
  return indexRouteRegex.test(routeId);
}
function getRouteInfo(routeDir, file, options) {
  let filePath = normalizeSlashes(path.join(routeDir, file));
  let routeId = createRouteId(filePath);
  let routeIdWithoutRoutes = routeId.slice(routeDir.length + 1);
  let index = isIndexRoute(routeIdWithoutRoutes, options);
  let routeSegments = getRouteSegments(
    routeIdWithoutRoutes,
    index,
    options.paramPrefixChar,
    options.nestedDirectoryChar
  );
  let routePath = createRoutePath(routeSegments, index, options);
  let routeInfo = {
    id: routeId,
    path: routePath,
    file: filePath,
    name: routeSegments.join("/"),
    segments: routeSegments,
    index
  };
  return routeInfo;
}
function createRoutePath(routeSegments, index, options) {
  let result = "";
  let basePath = options.basePath ?? "/";
  let paramPrefixChar = options.paramPrefixChar ?? "$";
  if (index) {
    routeSegments[routeSegments.length - 1] = "";
  }
  for (let i = 0; i < routeSegments.length; i++) {
    let segment = routeSegments[i];
    if (segment.startsWith("_")) {
      continue;
    }
    if (segment.endsWith("_")) {
      segment = segment.slice(0, -1);
    }
    if (segment.includes("[") && segment.includes("]")) {
      let output = "";
      let depth = 0;
      for (const char of segment) {
        if (char === "[" && depth === 0) {
          depth++;
        } else if (char === "]" && depth > 0) {
          depth--;
        } else {
          output += char;
        }
      }
      segment = output;
    }
    if (segment.startsWith(paramPrefixChar)) {
      if (segment === paramPrefixChar) {
        result += `/*`;
      } else {
        result += `/:${segment.slice(1)}`;
      }
    } else if (segment.startsWith(`(${paramPrefixChar}`)) {
      result += `/:${segment.slice(2, segment.length - 1)}?`;
    } else if (segment.startsWith("(")) {
      result += `/${segment.slice(1, segment.length - 1)}?`;
    } else {
      result += `/${segment}`;
    }
  }
  if (basePath !== "/") {
    result = basePath + result;
  }
  if (result.endsWith("/")) {
    result = result.slice(0, -1);
  }
  return result || void 0;
}
function findParentRouteId(routeInfo, nameMap) {
  let parentName = routeInfo.segments.slice(0, -1).join("/");
  while (parentName) {
    if (nameMap.has(parentName)) {
      return nameMap.get(parentName).id;
    }
    parentName = parentName.substring(0, parentName.lastIndexOf("/"));
  }
  return void 0;
}
function getRouteSegments(name, index, paramPrefixChar = "$", nestedDirectoryChar = "+") {
  let routeSegments = [];
  let i = 0;
  let routeSegment = "";
  let state = "START";
  let subState = "NORMAL";
  let hasPlus = false;
  const escapedNestedDirectoryChar = nestedDirectoryChar.replace(
    /[.*+\-?^${}()|[\]\\]/g,
    "\\$&"
  );
  const combinedRegex = new RegExp(`${escapedNestedDirectoryChar}[/\\\\]`, "g");
  const testRegex = new RegExp(`${escapedNestedDirectoryChar}[/\\\\]`);
  const replacePattern = `${escapedNestedDirectoryChar}/_\\.`;
  const replaceRegex = new RegExp(replacePattern);
  if (replaceRegex.test(name)) {
    const replaceRegexGlobal = new RegExp(replacePattern, "g");
    name = name.replace(replaceRegexGlobal, `_${nestedDirectoryChar}/`);
  }
  if (testRegex.test(name)) {
    name = name.replace(combinedRegex, ".");
    hasPlus = true;
  }
  let hasFolder = /\//.test(name);
  if ((hasPlus && hasFolder || !hasPlus) && !name.endsWith(".route")) {
    let last = name.lastIndexOf("/");
    if (last >= 0) {
      name = name.substring(0, last);
    }
  }
  let pushRouteSegment = (routeSegment2) => {
    if (routeSegment2) {
      routeSegments.push(routeSegment2);
    }
  };
  while (i < name.length) {
    let char = name[i];
    switch (state) {
      case "START":
        if (routeSegment.includes(paramPrefixChar) && !(routeSegment.startsWith(paramPrefixChar) || routeSegment.startsWith(`(${paramPrefixChar}`))) {
          throw new Error(
            `Route params must start with prefix char ${paramPrefixChar}: ${routeSegment}`
          );
        }
        if (routeSegment.includes("(") && !routeSegment.startsWith("(") && !routeSegment.endsWith(")")) {
          throw new Error(
            `Optional routes must start and end with parentheses: ${routeSegment}`
          );
        }
        pushRouteSegment(routeSegment);
        routeSegment = "";
        state = "PATH";
        continue;
      // restart without advancing index
      case "PATH":
        if (isPathSeparator(char) && subState === "NORMAL") {
          state = "START";
          break;
        } else if (char === "[") {
          subState = "ESCAPE";
        } else if (char === "]") {
          subState = "NORMAL";
        }
        routeSegment += char;
        break;
    }
    i++;
  }
  pushRouteSegment(routeSegment);
  if (routeSegments.at(-1) === "route") {
    routeSegments = routeSegments.slice(0, -1);
  }
  if (!index && hasPlus && routeSegments.at(-1)?.startsWith("_")) {
    routeSegments = routeSegments.slice(0, -1);
  }
  return routeSegments;
}
var pathSeparatorRegex = /[\/\\.]/;
function isPathSeparator(char) {
  return pathSeparatorRegex.test(char);
}
function defaultVisitFiles(dir, visitor, baseDir = dir) {
  for (let filename of fs.readdirSync(dir)) {
    let file = path.resolve(dir, filename);
    let stat = fs.statSync(file);
    if (stat.isDirectory()) {
      defaultVisitFiles(file, visitor, baseDir);
    } else if (stat.isFile()) {
      visitor(path.relative(baseDir, file));
    }
  }
}
function createRouteId(file) {
  return normalizeSlashes(stripFileExtension(file));
}
function normalizeSlashes(file) {
  return file.split(path.win32.sep).join("/");
}
function stripFileExtension(file) {
  return file.replace(/\.[a-z0-9]+$/i, "");
}
var getRouteRegex = (RegexRequiresNestedDirReplacement, nestedDirectoryChar) => {
  nestedDirectoryChar = nestedDirectoryChar.replace(
    /[.*+\-?^${}()|[\]\\]/g,
    "\\$&"
  );
  return new RegExp(
    RegexRequiresNestedDirReplacement.source.replace("\\${nestedDirectoryChar}", `[${nestedDirectoryChar}]`)
  );
};
export {
  createRouteId,
  createRoutePath,
  flatRoutes as default,
  defaultVisitFiles,
  flatRoutes,
  getRouteInfo,
  getRouteSegments,
  isIndexRoute,
  isRouteModuleFile,
  normalizeSlashes
};
