import { isDesktopMode } from "../env"
import { extension } from "../mime/mime"
import { getIndexedDBValue } from "./indexeddb"


export enum FileSystemType {
  OPFS = "opfs",
  NFS = "nfs",
}

export const getFsRootHandle = async (fsType: FileSystemType) => {
  let dirHandle: FileSystemDirectoryHandle

  switch (fsType) {
    case FileSystemType.NFS:
      // how it works https://developer.chrome.com/blog/persistent-permissions-for-the-file-system-access-api
      dirHandle = await getIndexedDBValue("kv", "localPath")
      break
    case FileSystemType.OPFS:
    default:
      dirHandle = await navigator.storage.getDirectory()
      break
  }
  return dirHandle
}

export const getExternalFolderHandle = async (name: string) => {
  const externalFolders = await getIndexedDBValue<FileSystemDirectoryHandle[]>(
    "kv",
    "externalFolders"
  )
  const dirHandle = externalFolders.find((dir) => dir.name === name)
  return dirHandle
}


const adapterSymbol = Symbol('adapter');

function hasAdapterSymbol(obj: any): boolean {
  const symbols = Object.getOwnPropertySymbols(obj);
  return symbols.some(symbol => symbol.toString() === adapterSymbol.toString());
}


/**
 * get DirHandle for a given path list
 * we read config from indexeddb to decide which file system to use
 * there are two file systems:
 * 1. opfs: origin private file system. store files in web.
 * 2. nfs: Native File System. store files in local file system.
 * @param _paths path list just like ["root", "dir1", "dir2"]
 * @param rootDirHandle we can pass rootDirHandle to avoid reading from indexeddb
 * @returns
 */
export const getDirHandle = async (
  _paths: string[],
  rootDirHandle?: FileSystemDirectoryHandle
) => {
  const paths = [..._paths]
  let dirHandle: FileSystemDirectoryHandle
  if (rootDirHandle) {
    dirHandle = rootDirHandle
  } else {
    const fsType: FileSystemType = await getIndexedDBValue("kv", "fs")
    dirHandle = await getFsRootHandle(fsType)
  }
  for (let path of paths) {
    dirHandle = await dirHandle.getDirectoryHandle(path, { create: true })
  }
  return dirHandle
}

/**
 * eidos fs structure:
 * - spaces
 *  - space1
 *    - db.sqlite3
 *    - files
 *      - 1234567890.png
 *      - 0987654321.png
 *  - space2
 *    - db.sqlite3
 *
 * spaces
 * - what is a space? a space is a folder that contains a sqlite3 database, default name is db.sqlite3.
 * - one space is one database.
 *
 * files
 * - files is a folder that contains all static files, such as images, videos, etc.
 * - when user upload a file, it will be saved in this folder. hash will be used as file name. e.g. 1234567890.png
 */

export class EidosFileSystemManager {
  rootDirHandle: FileSystemDirectoryHandle | undefined
  constructor(rootDirHandle?: FileSystemDirectoryHandle) {
    if (rootDirHandle) {
      this.rootDirHandle = rootDirHandle
    }
  }

  isSameEntry = async (dirHandle: FileSystemDirectoryHandle) => {
    return this.rootDirHandle?.isSameEntry(dirHandle)
  }

  getDirHandle = async (paths: string[]) => {
    return getDirHandle(paths, this.rootDirHandle)
  }

  walk = async (_paths: string[]): Promise<string[][]> => {
    const dirHandle = await getDirHandle(_paths, this.rootDirHandle)
    const paths = []
    const rootDirHandle = await getDirHandle([], this.rootDirHandle)
    for await (let entry of (dirHandle as any).values()) {
      if (entry.kind === "file") {
        const path = await (this.rootDirHandle || rootDirHandle).resolve(entry)
        paths.push(path)
      } else if (entry.kind === "directory") {
        const subPaths: any = await this.walk([..._paths, entry.name])
        paths.push(...subPaths)
      }
    }
    return paths
  }

  copyFile = async (_paths: string[], targetFs: EidosFileSystemManager) => {
    // copy file to target fs
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const file = await this.getFile(paths)
    const targetPaths = paths.slice(0, -1)
    await targetFs.addFile(targetPaths, file)
  }

  copyTo = async (
    targetFs: EidosFileSystemManager,
    options?: {
      ignoreSqlite?: boolean
    },
    cb?: (data: { current: number; total: number; msg: string }) => void
  ) => {
    const paths = await this.walk([])
    const targetPaths = await targetFs.walk([])
    const targetPathsSet = new Set(targetPaths.map((p) => p.join("/")))

    const total = paths.length
    for (let path of paths) {
      const current = paths.indexOf(path) + 1
      // ignore .opfs-sahpool
      if (path[0] === ".opfs-sahpool") {
        cb?.({
          current,
          total,
          msg: "ignore .opfs-sahpool",
        })
        continue
      }

      if (path[path.length - 1] === "db.sqlite3") {
        if (options?.ignoreSqlite) {
          cb?.({
            current,
            total,
            msg: `ignore db.sqlite3`,
          })
          continue
        } else {
          // if target fs is nfs, we need to copy sqlite file every time
          await this.copyFile(path, targetFs)
          cb?.({
            current,
            total,
            msg: `copying ${path.join("/")}`,
          })
        }
      }

      // check if file exists
      if (targetPathsSet.has(path.join("/"))) {
        cb?.({
          current,
          total,
          msg: `file exists ${path.join("/")}`,
        })
        continue
      }
      await this.copyFile(path, targetFs)
      cb?.({
        current,
        total,
        msg: `copying ${path.join("/")}`,
      })
    }
    console.log("copy done")
  }

  getFileUrlByPath = (path: string, replaceSpace?: string) => {
    const paths = path.split("/").slice(1)
    if (replaceSpace) {
      paths[0] = replaceSpace
    }
    return "/" + paths.join("/")
  }

  getFileByURL = async (url: string) => {
    const path = new URL(url).pathname
    const parentPaths = path.split("/").slice(0, -1).filter(Boolean)
    const parentDirHandle = await getDirHandle(
      ["spaces", ...parentPaths],
      this.rootDirHandle
    )
    const filename = path.split("/").pop()
    const realFilename = decodeURIComponent(filename!)
    const fileHandle = await parentDirHandle.getFileHandle(realFilename)
    return fileHandle.getFile()
  }

  getFileByPath = async (path: string) => {
    const paths = path.split("/")
    const file = await this.getFile(paths)
    return file
  }

  listDir = async (_paths: string[]) => {
    const dirHandle = await getDirHandle(_paths, this.rootDirHandle)
    const entries: FileSystemFileHandle[] = []
    for await (let entry of (dirHandle as any).values()) {
      entries.push(entry)
    }
    return entries
  }

  updateOrCreateDocFile = async (_paths: string[], content: string) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const filename = paths.pop()
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    const fileHandle = await dirHandle.getFileHandle(filename!, {
      create: true,
    })
    const writable = await (fileHandle as any).createWritable()
    await writable.write(content)
    await writable.close()
    console.log("update doc file", filename)
  }

  checkFileExists = async (_paths: string[]) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const filename = paths.pop()
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    try {
      await dirHandle.getFileHandle(filename!)
      return true
    } catch (e) {
      return false
    }
  }

  getFile = async (_paths: string[], options?: FileSystemGetFileOptions) => {
    const paths = [..._paths.filter(Boolean)]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const filename = paths.pop()
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    const fileHandle = await dirHandle.getFileHandle(filename!, options)
    const file = await fileHandle.getFile()
    return file
  }

  getFileText = async (_paths: string[]) => {
    const file = await this.getFile(_paths)
    return await file.text()
  }

  getDocContent = async (_paths: string[]) => {
    const file = await this.getFile(_paths)
    return await file.text()
  }

  addDir = async (_paths: string[], dirName: string) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    const r = await dirHandle.getDirectoryHandle(dirName, { create: true })

    // const path = await opfsRoot.resolve(r)
  }

  addFile = async (_paths: string[], file: File, fileId?: string) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    console.log("dirHandle", dirHandle)
    // if fileId is provided, use it as file name
    const fileExt = extension(file.type)
    const filename = fileId ? `${fileId}.${fileExt}` : file.name
    const fileHandle = await dirHandle.getFileHandle(filename, {
      create: true,
    })
    const writable = await (fileHandle as any).createWritable()
    await writable.write(file)
    await writable.close()
    // fileHandle get path
    const rootDirHandle = await getDirHandle([], this.rootDirHandle)
    const relativePath = await rootDirHandle.resolve(fileHandle)
    console.log("relativePath", { rootDirHandle, fileHandle, relativePath })
    return relativePath
  }

  deleteEntry = async (_paths: string[], isDir = false) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    if (isDir) {
      const dirHandle = await getDirHandle(paths, this.rootDirHandle)
      // The remove() method is currently only implemented in Chrome. You can feature-detect support via 'remove' in FileSystemFileHandle.prototype.
      await (dirHandle as any).remove({
        recursive: true,
      })
    } else {
      const filename = paths.pop()
      const dirHandle = await getDirHandle(paths, this.rootDirHandle)
      await dirHandle.removeEntry(filename!)
    }
  }

  renameFile = async (_paths: string[], newName: string) => {
    const paths = [..._paths]
    if (paths.length === 0) {
      throw new Error("paths can't be empty")
    }
    const filename = paths.pop()
    const dirHandle = await getDirHandle(paths, this.rootDirHandle)
    const fileHandle = (await dirHandle.getFileHandle(filename!, {
      create: true,
    })) as any
    await fileHandle.move(newName)
  }
}

// deprecated
export const efsManager = isDesktopMode ? new EidosFileSystemManager() : new EidosFileSystemManager()

export const getExternalFolderManager = async (name: string) => {
  const dirHandler = await getExternalFolderHandle(name)
  if (!dirHandler) {
    throw new Error("external folder not found")
  }
  const efsManager = new EidosFileSystemManager(dirHandler)
  return efsManager
}
