import { HandlerContext, Handlers, PageProps, RouteConfig } from "$fresh/server.ts";
import { asset, Head } from "$fresh/runtime.ts";
import {
  decodePath,
  encodePath,
  removePrefixFromPathname,
} from "../../util/util.ts";
import { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx";
import RenderView from "../../islands/ContentRenderer.tsx";
import { serveFile } from "http/file_server.ts";
import { Status } from "http/http_status.ts";

type DirProps = {
  type: "dir";
  path: string;
  stat: Deno.FileInfo;
  files: EntryInfo[];
};
type FileProps = {
  type: "file";
  path: string;
  stat: Deno.FileInfo;
};

export type DirOrFileProps = DirProps | FileProps;

type RenderOption = {
  fileInfo?: Deno.FileInfo;
};

async function renderFile(
  req: Request,
  path: string,
  { fileInfo }: RenderOption = {},
) {
  try {
    if (!fileInfo) {
      fileInfo = await Deno.stat(path);
    }
    if (fileInfo.isDirectory) {
      // if index.html exists, serve it.
      // otherwise, serve a directory listing.
      const indexPath = join(path, "/index.html");
      try {
        await Deno.stat(indexPath);
        const res = await serveFile(req, indexPath);
        return res;
      } catch (e) {
        if (e instanceof Deno.errors.NotFound) {
          const list: Deno.DirEntry[] = [];
          for await (const entry of Deno.readDir(path)) {
            list.push(entry);
          }
          return new Response(
            JSON.stringify(
              list,
            ),
            {
              headers: {
                "content-type": "application/json",
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Methods":
                  "GET,HEAD,PUT,PATCH,POST,DELETE",
                "Access-Control-Allow-Headers":
                  "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
              },
              status: 200,
            },
          );
        }
      }
    }
    const res = await serveFile(req, path, {
      fileInfo,
    });
    return res;
  } catch (e) {
    if (e instanceof Deno.errors.NotFound) {
      return new Response("Not Found", {
        status: 404,
      });
    }
    throw e;
  }
}

async function renderPage(
  _req: Request,
  path: string,
  ctx: HandlerContext,
  { fileInfo }: RenderOption = {},
) {
  try {
    if (!fileInfo) {
      fileInfo = await Deno.stat(path);
    }

    if (fileInfo.isDirectory) {
      const filesIter = await Deno.readDir(path);
      const files: EntryInfo[] = [];
      for await (const file of filesIter) {
        const fileStat = await Deno.stat(join(path, file.name));
        files.push({
          ...file,
          lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined,
          size: fileStat.size,
        });
      }
      return await ctx.render({
        type: "dir",
        fileInfo,
        files,
        path,
      });
    } else {
      return await ctx.render({
        type: "file",
        fileInfo,
        path,
      });
    }
  } catch (e) {
    if (e instanceof Deno.errors.NotFound) {
      return await ctx.renderNotFound();
    }
    throw e;
  }
}

async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
  const url = new URL(req.url);
  const path = removePrefixFromPathname(decodePath(url.pathname), "/dir");
  const fileInfo = await Deno.stat(path);
  if (fileInfo.isFile && url.pathname.endsWith("/")) {
    url.pathname = url.pathname.slice(0, -1);
    return Response.redirect(url, Status.TemporaryRedirect);
  }
  if ((!fileInfo.isFile) && (!url.pathname.endsWith("/"))) {
    url.pathname += "/";
    return Response.redirect(url, Status.TemporaryRedirect);
  }

  if (url.searchParams.has("pretty")) {
    return await renderPage(req, path, ctx, { fileInfo });
  } else {
    return await renderFile(req, path, { fileInfo });
  }
}

export const handler: Handlers = {
  GET,
};

function isImageFile(path: string) {
  return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff)$/i.test(path);
}

function searchFiles(
  path: EntryInfo[],
  fn: (path: EntryInfo) => boolean,
  opt: { order?: (a: EntryInfo, b: EntryInfo) => number } = {},
) {
  const candiate = path.filter(fn);
  if (candiate.length > 0) {
    if (opt.order) {
      candiate.sort(opt.order);
    }
    return candiate[0];
  }
  return null;
}

export default function DirLists(props: PageProps<DirOrFileProps>) {
  const data = props.data;
  let cover = null, index = null, content = null;
  if (data.type === "dir") {
    cover = searchFiles(data.files, (f) => isImageFile(f.name));
    index = searchFiles(data.files, (f) => f.name === "index.html");
    const contentFilenameCandidate = [
      "README.md",
      "readme.md",
      "README.txt",
      "readme.txt",
    ];
    const contentFilenameCandidateSet = new Set(contentFilenameCandidate);
    content = searchFiles(
      data.files,
      (f) => contentFilenameCandidateSet.has(f.name),
      {
        order: (a, b) => {
          const aIndex = contentFilenameCandidate.indexOf(a.name);
          const bIndex = contentFilenameCandidate.indexOf(b.name);
          return (aIndex - bIndex);
        },
      },
    );
  }
  return (
    <>
      <Head>
        <title>Simple file server : {data.path}</title>
        <link rel="stylesheet" href={asset("/github-markdown.css")} />
        <link rel="stylesheet" href={asset("/base.css")} />
      </Head>
      <div class="p-4 mx-auto max-w-screen-md">
        {data.type === "dir"
          ? <DirList path={data.path} files={data.files}></DirList>
          : <FileViewer path={data.path}></FileViewer>}
        {index
          ? (
            <a
              href={`/dir/${encodePath(join(data.path, index.name))}`}
            >
              {cover
                ? (
                  <img
                    src={`/dir/${encodePath(join(data.path, cover.name))}`}
                  />
                )
                : (
                  <span class="border-2 border-gray-300 rounded-md p-2 block mt-2">
                    Index
                  </span>
                )}
            </a>
          )
          : null}
        {content
          ? (
            <div
              class="border-2 border-gray-300 rounded-md p-2 mt-2"
              id="README"
            >
              <RenderView
                src={`/dir/${encodePath(join(data.path, content.name))}`}
              />
            </div>
          )
          : null}
      </div>
    </>
  );
}

export const config : RouteConfig = {
  routeOverride: "/dir/**{/}?"
}