import * as CSS from 'csstype'

import EventEmitter from 'eventemitter3'
import { LSPluginCaller } from './LSPlugin.caller'
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
import { LSPluginRequest } from './modules/LSPlugin.Request'

export type WithOptional<T, K extends keyof T> = Omit<T, K> &
  Partial<Pick<T, K>>

export type PluginLocalIdentity = string

export type ThemeMode = 'light' | 'dark'

export interface LegacyTheme {
  name: string
  url: string
  description?: string
  mode?: ThemeMode
  pid: PluginLocalIdentity
}

export interface Theme extends LegacyTheme {
  mode: ThemeMode
}

export type StyleString = string
export type StyleOptions = {
  key?: string
  style: StyleString
}

export type UIContainerAttrs = {
  draggable: boolean
  resizable: boolean
}

export type UIBaseOptions = {
  key?: string
  replace?: boolean
  template: string | null
  style?: CSS.Properties
  attrs?: Record<string, string>
  close?: 'outside' | string
  reset?: boolean // reset slot content or not
}

export type UIPathIdentity = {
  /**
   * DOM selector
   */
  path: string
}

export type UISlotIdentity = {
  /**
   * Slot key
   */
  slot: string
}

export type UISlotOptions = UIBaseOptions & UISlotIdentity

export type UIPathOptions = UIBaseOptions & UIPathIdentity

export type UIOptions = UIBaseOptions | UIPathOptions | UISlotOptions

export interface LSPluginPkgConfig {
  id: PluginLocalIdentity
  main: string
  entry: string // alias of main
  title: string
  mode: 'shadow' | 'iframe'
  themes: Theme[]
  icon: string
  /**
   * Alternative entrypoint for development.
   */
  devEntry: unknown
  /**
   * For legacy themes, do not use.
   */
  theme: unknown
}

export interface LSPluginBaseInfo {
  /**
   * Must be unique.
   */
  id: string
  mode: 'shadow' | 'iframe'
  settings: {
    disabled: boolean
  } & Record<string, unknown>
  effect: boolean
  /**
   * For internal use only. Indicates if plugin is installed in dot root.
   */
  iir: boolean
  /**
   * For internal use only.
   */
  lsr: string
}

export type IHookEvent = {
  [key: string]: any
}

export type IUserOffHook = () => void
export type IUserHook<E = any, R = IUserOffHook> = (
  callback: (e: IHookEvent & E) => void
) => IUserOffHook
export type IUserSlotHook<E = any> = (
  callback: (e: IHookEvent & UISlotIdentity & E) => void
) => void
export type IUserConditionSlotHook<C = any, E = any> = (
  condition: C,
  callback: (e: IHookEvent & UISlotIdentity & E) => void
) => void

export type EntityID = number
export type BlockUUID = string
export type BlockUUIDTuple = ['uuid', BlockUUID]

export type IEntityID = { id: EntityID; [key: string]: any }
export type IBatchBlock = {
  content: string

  /**
   * @NOTE: not supported for DB graph
   */
  properties?: Record<string, any>

  children?: Array<IBatchBlock>
}
export type IDatom = [e: number, a: string, v: any, t: number, added: boolean]

export type IGitResult = { stdout: string; stderr: string; exitCode: number }

export interface AppUserInfo {
  [key: string]: any
}

export interface AppInfo {
  version: string

  [key: string]: unknown
}

/**
 * User's app configurations
 */
export interface AppUserConfigs {
  preferredThemeMode: ThemeMode
  preferredFormat: 'markdown' | 'org'
  preferredDateFormat: string
  preferredStartOfWeek: string
  preferredLanguage: string
  preferredWorkflow: string

  currentGraph: string
  showBracket: boolean
  enabledFlashcards: boolean
  enabledJournals: boolean

  [key: string]: unknown
}

/**
 * In Logseq, a graph represents a repository of connected pages and blocks
 */
export interface AppGraphInfo {
  name: string
  url: string
  path: string

  [key: string]: unknown
}

/**
 * Block - Logseq's fundamental data structure.
 */
export interface BlockEntity {
  id: EntityID // db id
  uuid: BlockUUID
  order: string
  format: 'markdown' | 'org'
  parent: IEntityID
  title: string
  content?: string // @deprecated. Use :title instead!
  page: IEntityID
  createdAt: number
  updatedAt: number
  properties?: Record<string, any>
  'collapsed?': boolean

  // optional fields in dummy page
  left?: IEntityID
  anchor?: string
  body?: any
  children?: Array<BlockEntity | BlockUUIDTuple>
  container?: string
  file?: IEntityID
  level?: number
  meta?: { timestamps: any; properties: any; startPos: number; endPos: number }
  marker?: string

  [key: string]: unknown
}

/**
 * Page is just a block with some specific properties.
 */
export interface PageEntity {
  id: EntityID
  uuid: BlockUUID
  name: string
  format: 'markdown' | 'org'
  type: 'page' | 'journal' | 'whiteboard' | 'class' | 'property' | 'hidden'
  updatedAt: number
  createdAt: number
  'journal?': boolean

  title?: string
  file?: IEntityID
  originalName?: string
  namespace?: IEntityID
  children?: Array<PageEntity>
  properties?: Record<string, any>
  journalDay?: number

  [key: string]: unknown
}

export type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
export type BlockPageName = string
export type PageIdentity = BlockPageName | BlockIdentity
export type SlashCommandActionCmd =
  | 'editor/input'
  | 'editor/hook'
  | 'editor/clear-current-slash'
  | 'editor/restore-saved-cursor'
export type SlashCommandAction = [cmd: SlashCommandActionCmd, ...args: any]
export type SimpleCommandCallback<E = any> = (e: IHookEvent & E) => void
export type BlockCommandCallback = (
  e: IHookEvent & { uuid: BlockUUID }
) => Promise<void>
export type BlockCursorPosition = {
  left: number
  top: number
  height: number
  pos: number
  rect: DOMRect
}

export type Keybinding = string | Array<string>
export type SimpleCommandKeybinding = {
  mode?: 'global' | 'non-editing' | 'editing'
  binding: Keybinding
  mac?: string // special for Mac OS
}

export type SettingSchemaDesc = {
  key: string
  type: 'string' | 'number' | 'boolean' | 'enum' | 'object' | 'heading'
  default: string | number | boolean | Array<any> | object | null
  title: string
  description: string // support markdown
  inputAs?: 'color' | 'date' | 'datetime-local' | 'range' | 'textarea'
  enumChoices?: Array<string>
  enumPicker?: 'select' | 'radio' | 'checkbox' // default: select
}

export type ExternalCommandType =
  | 'logseq.command/run'
  | 'logseq.editor/cycle-todo'
  | 'logseq.editor/down'
  | 'logseq.editor/up'
  | 'logseq.editor/expand-block-children'
  | 'logseq.editor/collapse-block-children'
  | 'logseq.editor/open-file-in-default-app'
  | 'logseq.editor/open-file-in-directory'
  | 'logseq.editor/select-all-blocks'
  | 'logseq.editor/toggle-open-blocks'
  | 'logseq.editor/zoom-in'
  | 'logseq.editor/zoom-out'
  | 'logseq.editor/indent'
  | 'logseq.editor/outdent'
  | 'logseq.editor/copy'
  | 'logseq.editor/cut'
  | 'logseq.go/home'
  | 'logseq.go/journals'
  | 'logseq.go/keyboard-shortcuts'
  | 'logseq.go/next-journal'
  | 'logseq.go/prev-journal'
  | 'logseq.go/search'
  | 'logseq.go/tomorrow'
  | 'logseq.go/backward'
  | 'logseq.go/forward'
  | 'logseq.search/re-index'
  | 'logseq.sidebar/clear'
  | 'logseq.sidebar/open-today-page'
  | 'logseq.ui/goto-plugins'
  | 'logseq.ui/select-theme-color'
  | 'logseq.ui/toggle-brackets'
  | 'logseq.ui/toggle-contents'
  | 'logseq.ui/toggle-document-mode'
  | 'logseq.ui/toggle-help'
  | 'logseq.ui/toggle-left-sidebar'
  | 'logseq.ui/toggle-right-sidebar'
  | 'logseq.ui/toggle-settings'
  | 'logseq.ui/toggle-theme'
  | 'logseq.ui/toggle-wide-mode'

export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'

export type SearchIndiceInitStatus = boolean
export type SearchBlockItem = {
  id: EntityID
  uuid: BlockIdentity
  content: string
  page: EntityID
}
export type SearchPageItem = string
export type SearchFileItem = string

export interface IPluginSearchServiceHooks {
  name: string
  options?: Record<string, any>

  onQuery: (
    graph: string,
    key: string,
    opts: Partial<{ limit: number }>
  ) => Promise<{
    graph: string
    key: string
    blocks?: Array<Partial<SearchBlockItem>>
    pages?: Array<SearchPageItem>
    files?: Array<SearchFileItem>
  }>

  onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
  onIndiceReset: (graph: string) => Promise<void>
  onBlocksChanged: (
    graph: string,
    changes: {
      added: Array<SearchBlockItem>
      removed: Array<EntityID>
    }
  ) => Promise<void>
  onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
}

/**
 * App level APIs
 */
export interface IAppProxy {
  /**
   * @added 0.0.4
   * @param key
   */
  getInfo: (key?: keyof AppInfo) => Promise<AppInfo | any>

  getUserInfo: () => Promise<AppUserInfo | null>
  getUserConfigs: () => Promise<AppUserConfigs>

  // services
  registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void

  // commands
  registerCommand: (
    type: string,
    opts: {
      key: string
      label: string
      desc?: string
      palette?: boolean
      keybinding?: SimpleCommandKeybinding
    },
    action: SimpleCommandCallback
  ) => void

  registerCommandPalette: (
    opts: {
      key: string
      label: string
      keybinding?: SimpleCommandKeybinding
    },
    action: SimpleCommandCallback
  ) => void

  /**
   * Supported key names
   * @link https://gist.github.com/xyhp915/d1a6d151a99f31647a95e59cdfbf4ddc
   * @param keybinding
   * @param action
   */
  registerCommandShortcut: (
    keybinding: SimpleCommandKeybinding | string,
    action: SimpleCommandCallback,
    opts?: Partial<{
      key: string
      label: string
      desc: string
      extras: Record<string, any>
    }>
  ) => void

  /**
   * Supported all registered palette commands
   * @param type
   * @param args
   */
  invokeExternalCommand: (
    type: ExternalCommandType,
    ...args: Array<any>
  ) => Promise<void>

  /**
   * Call external plugin command provided by models or registered commands
   * @added 0.0.13
   * @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key`
   * @param args
   */
  invokeExternalPlugin: (type: string, ...args: Array<any>) => Promise<unknown>

  /**
   * @added 0.0.13
   * @param pid
   */
  getExternalPlugin: (pid: string) => Promise<{} | null>

  /**
   * Get state from app store
   * valid state is here
   * https://github.com/logseq/logseq/blob/master/src/main/frontend/state.cljs#L27
   *
   * @example
   * ```ts
   * const isDocMode = await logseq.App.getStateFromStore('document/mode?')
   * ```
   * @param path
   */
  getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
  setStateFromStore: (path: string | Array<string>, value: any) => Promise<void>

  // native
  relaunch: () => Promise<void>
  quit: () => Promise<void>
  openExternalLink: (url: string) => Promise<void>

  /**
   * @deprecated Using `logseq.Git.execCommand`
   * @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md
   * @param args
   */
  execGitCommand: (args: string[]) => Promise<string>

  // graph
  getCurrentGraph: () => Promise<AppGraphInfo | null>
  checkCurrentIsDbGraph: () => Promise<Boolean>
  getCurrentGraphConfigs: (...keys: string[]) => Promise<any>
  setCurrentGraphConfigs: (configs: {}) => Promise<void>
  getCurrentGraphFavorites: () => Promise<Array<string | PageEntity> | null>
  getCurrentGraphRecent: () => Promise<Array<string | PageEntity> | null>
  getCurrentGraphTemplates: () => Promise<Record<string, BlockEntity> | null>

  // router
  pushState: (
    k: string,
    params?: Record<string, any>,
    query?: Record<string, any>
  ) => void
  replaceState: (
    k: string,
    params?: Record<string, any>,
    query?: Record<string, any>
  ) => void

  // templates
  getTemplate: (name: string) => Promise<BlockEntity | null>
  existTemplate: (name: string) => Promise<Boolean>
  createTemplate: (
    target: BlockUUID,
    name: string,
    opts?: { overwrite: boolean }
  ) => Promise<any>
  removeTemplate: (name: string) => Promise<any>
  insertTemplate: (target: BlockUUID, name: string) => Promise<any>

  setZoomFactor: (factor: number) => void
  setFullScreen: (flag: boolean | 'toggle') => void
  setLeftSidebarVisible: (flag: boolean | 'toggle') => void
  setRightSidebarVisible: (flag: boolean | 'toggle') => void
  clearRightSidebarBlocks: (opts?: { close: boolean }) => void

  registerUIItem: (
    type: 'toolbar' | 'pagebar',
    opts: { key: string; template: string }
  ) => void

  registerPageMenuItem: (
    tag: string,
    action: (e: IHookEvent & { page: string }) => void
  ) => void

  // hook events
  onCurrentGraphChanged: IUserHook
  onGraphAfterIndexed: IUserHook<{ repo: string }>
  onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
  onThemeChanged: IUserHook<
    Partial<{ name: string; mode: string; pid: string; url: string }>>
  onTodayJournalCreated: IUserHook<{ title: string }>
  onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook
  onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook

  /**
   * provide ui slot to specific block with UUID
   *
   * @added 0.0.13
   */
  onBlockRendererSlotted: IUserConditionSlotHook<
    BlockUUID,
    Omit<BlockEntity, 'children' | 'page'>
  >

  /**
   * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
   *
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-pomodoro-timer
   * @example
   * ```ts
   * // e.g. {{renderer :h1, hello world, green}}
   *
   * logseq.App.onMacroRendererSlotted(({ slot, payload: { arguments } }) => {
   *   let [type, text, color] = arguments
   *   if (type !== ':h1') return
   *    logseq.provideUI({
   *      key: 'h1-playground',
   *      slot, template: `
   *       <h2 style="color: ${color || 'red'}">${text}</h2>
   *      `,
   *   })
   * })
   * ```
   */
  onMacroRendererSlotted: IUserSlotHook<{
    payload: { arguments: Array<string>; uuid: string; [key: string]: any }
  }>

  onPageHeadActionsSlotted: IUserSlotHook
  onRouteChanged: IUserHook<{ path: string; template: string }>
  onSidebarVisibleChanged: IUserHook<{ visible: boolean }>

  // internal
  _installPluginHook: (pid: string, hook: string, opts?: any) => void
  _uninstallPluginHook: (pid: string, hookOrAll: string | boolean) => void
}

/**
 * Editor related APIs
 */
export interface IEditorProxy extends Record<string, any> {
  /**
   * register a custom command which will be added to the Logseq slash command list
   * @param tag - displayed name of command
   * @param action - can be a single callback function to run when the command is called, or an array of fixed commands with arguments
   *
   *
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-slash-commands
   *
   * @example
   * ```ts
   * logseq.Editor.registerSlashCommand("Say Hi", () => {
   *   console.log('Hi!')
   * })
   * ```
   *
   * @example
   * ```ts
   * logseq.Editor.registerSlashCommand("💥 Big Bang", [
   *   ["editor/hook", "customCallback"],
   *   ["editor/clear-current-slash"],
   * ]);
   * ```
   */
  registerSlashCommand: (
    tag: string,
    action: BlockCommandCallback | Array<SlashCommandAction>
  ) => unknown

  /**
   * register a custom command in the block context menu (triggered by right-clicking the block dot)
   * @param label - displayed name of command
   * @param action - can be a single callback function to run when the command is called
   */
  registerBlockContextMenuItem: (
    label: string,
    action: BlockCommandCallback
  ) => unknown

  /**
   * Current it's only available for pdf viewer
   * @param label - displayed name of command
   * @param action - callback for the clickable item
   * @param opts - clearSelection: clear highlight selection when callback invoked
   */
  registerHighlightContextMenuItem: (
    label: string,
    action: SimpleCommandCallback,
    opts?: {
      clearSelection: boolean
    }
  ) => unknown

  // block related APIs

  checkEditing: () => Promise<BlockUUID | boolean>

  insertAtEditingCursor: (content: string) => Promise<void>

  restoreEditingCursor: () => Promise<void>

  exitEditingMode: (selectBlock?: boolean) => Promise<void>

  getEditingCursorPosition: () => Promise<BlockCursorPosition | null>

  getEditingBlockContent: () => Promise<string>

  getCurrentPage: () => Promise<PageEntity | BlockEntity | null>

  getCurrentBlock: () => Promise<BlockEntity | null>

  getSelectedBlocks: () => Promise<Array<BlockEntity> | null>

  clearSelectedBlocks: () => Promise<void>

  /**
   * get all blocks of the current page as a tree structure
   *
   * @example
   * ```ts
   * const blocks = await logseq.Editor.getCurrentPageBlocksTree()
   * initMindMap(blocks)
   * ```
   */
  getCurrentPageBlocksTree: () => Promise<Array<BlockEntity>>

  /**
   * get all blocks for the specified page
   *
   * @param srcPage - the page name or uuid
   */
  getPageBlocksTree: (srcPage: PageIdentity) => Promise<Array<BlockEntity>>

  /**
   * get all page/block linked references
   * @param srcPage
   */
  getPageLinkedReferences: (
    srcPage: PageIdentity
  ) => Promise<Array<[page: PageEntity, blocks: Array<BlockEntity>]> | null>

  /**
   * get flatten pages from top namespace
   * @param namespace
   */
  getPagesFromNamespace: (
    namespace: BlockPageName
  ) => Promise<Array<PageEntity> | null>

  /**
   * construct pages tree from namespace pages
   * @param namespace
   */
  getPagesTreeFromNamespace: (
    namespace: BlockPageName
  ) => Promise<Array<PageEntity> | null>

  /**
   * Create a unique UUID string which can then be assigned to a block.
   * @added 0.0.8
   */
  newBlockUUID: () => Promise<string>

  /**
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
   *
   * @param srcBlock
   * @param content
   * @param opts
   */
  insertBlock: (
    srcBlock: BlockIdentity,
    content: string,
    opts?: Partial<{
      before: boolean
      sibling: boolean
      isPageBlock: boolean
      focus: boolean
      customUUID: string
      properties: {}
    }>
  ) => Promise<BlockEntity | null>

  /**
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
   *
   * `keepUUID` will allow you to set a custom UUID for blocks by setting their properties.id
   */
  insertBatchBlock: (
    srcBlock: BlockIdentity,
    batch: IBatchBlock | Array<IBatchBlock>,
    opts?: Partial<{ before: boolean; sibling: boolean; keepUUID: boolean }>
  ) => Promise<Array<BlockEntity> | null>

  updateBlock: (
    srcBlock: BlockIdentity,
    content: string,
    opts?: Partial<{ properties: {} }>
  ) => Promise<void>

  removeBlock: (srcBlock: BlockIdentity) => Promise<void>

  getBlock: (
    srcBlock: BlockIdentity | EntityID,
    opts?: Partial<{ includeChildren: boolean }>
  ) => Promise<BlockEntity | null>

  /**
   * @example
   *
   * ```ts
   *  logseq.Editor.setBlockCollapsed('uuid', true)
   *  logseq.Editor.setBlockCollapsed('uuid', 'toggle')
   * ```
   * @param uuid
   * @param opts
   */
  setBlockCollapsed: (
    uuid: BlockUUID,
    opts: { flag: boolean | 'toggle' } | boolean | 'toggle'
  ) => Promise<void>

  getPage: (
    srcPage: PageIdentity | EntityID,
    opts?: Partial<{ includeChildren: boolean }>
  ) => Promise<PageEntity | null>

  createPage: (
    pageName: BlockPageName,
    properties?: {},
    opts?: Partial<{
      redirect: boolean
      createFirstBlock: boolean
      format: BlockEntity['format']
      journal: boolean
    }>
  ) => Promise<PageEntity | null>

  deletePage: (pageName: BlockPageName) => Promise<void>

  renamePage: (oldName: string, newName: string) => Promise<void>

  getAllPages: (repo?: string) => Promise<PageEntity[] | null>

  prependBlockInPage: (
    page: PageIdentity,
    content: string,
    opts?: Partial<{ properties: {} }>
  ) => Promise<BlockEntity | null>

  appendBlockInPage: (
    page: PageIdentity,
    content: string,
    opts?: Partial<{ properties: {} }>
  ) => Promise<BlockEntity | null>

  getPreviousSiblingBlock: (
    srcBlock: BlockIdentity
  ) => Promise<BlockEntity | null>

  getNextSiblingBlock: (
    srcBlock: BlockIdentity
  ) => Promise<BlockEntity | null>

  moveBlock: (
    srcBlock: BlockIdentity,
    targetBlock: BlockIdentity,
    opts?: Partial<{ before: boolean; children: boolean }>
  ) => Promise<void>

  editBlock: (srcBlock: BlockIdentity, opts?: { pos: number }) => Promise<void>
  selectBlock: (srcBlock: BlockIdentity) => Promise<void>

  saveFocusedCodeEditorContent: () => Promise<void>

  // property entity related APIs (DB only)
  getProperty: (key: string) => Promise<BlockEntity | null>

  /**
   * insert or update property entity
   * @param key
   * @param schema
   * @param opts
   */
  upsertProperty: (
    key: string,
    schema?: Partial<{
      type: 'default' | 'map' | 'number' | 'keyword' | 'node' | 'date' | 'checkbox' | string,
      cardinality: 'many' | 'one',
      hide: boolean
      public: boolean
    }>,
    opts?: {}) => Promise<IEntityID>

  // block property related APIs
  upsertBlockProperty: (
    block: BlockIdentity,
    key: string,
    value: any
  ) => Promise<void>

  removeBlockProperty: (block: BlockIdentity, key: string) => Promise<void>

  getBlockProperty: (block: BlockIdentity, key: string) => Promise<BlockEntity | string | null>

  getBlockProperties: (block: BlockIdentity) => Promise<Record<string, any> | null>

  scrollToBlockInPage: (
    pageName: BlockPageName,
    blockId: BlockIdentity,
    opts?: { replaceState: boolean }
  ) => void

  openInRightSidebar: (id: BlockUUID | EntityID) => void

  /**
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator
   */
  onInputSelectionEnd: IUserHook<{
    caret: any
    point: { x: number; y: number }
    start: number
    end: number
    text: string
  }>
}

/**
 * Datascript related APIs
 */
export interface IDBProxy {
  /**
   * Run a DSL query
   * @link https://docs.logseq.com/#/page/queries
   * @param dsl
   */
  q: <T = any>(dsl: string) => Promise<Array<T> | null>

  /**
   * Run a datascript query
   */
  datascriptQuery: <T = any>(query: string, ...inputs: Array<any>) => Promise<T>

  /**
   * Hook all transaction data of DB
   *
   * @added 0.0.2
   */
  onChanged: IUserHook<{
    blocks: Array<BlockEntity>
    txData: Array<IDatom>
    txMeta?: { outlinerOp: string; [key: string]: any }
  }>

  /**
   * Subscribe a specific block changed event
   *
   * @added 0.0.2
   */
  onBlockChanged(
    uuid: BlockUUID,
    callback: (
      block: BlockEntity,
      txData: Array<IDatom>,
      txMeta?: { outlinerOp: string; [key: string]: any }
    ) => void
  ): IUserOffHook
}

/**
 * Git related APIS
 */
export interface IGitProxy {
  /**
   * @added 0.0.2
   * @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md
   * @param args
   */
  execCommand: (args: string[]) => Promise<IGitResult>

  loadIgnoreFile: () => Promise<string>
  saveIgnoreFile: (content: string) => Promise<void>
}

/**
 * UI related APIs
 */
export type UIMsgOptions = {
  key: string
  timeout: number // milliseconds. `0` indicate that keep showing
}

export type UIMsgKey = UIMsgOptions['key']

export interface IUIProxy {
  showMsg: (
    content: string,
    status?: 'success' | 'warning' | 'error' | string,
    opts?: Partial<UIMsgOptions>
  ) => Promise<UIMsgKey>
  closeMsg: (key: UIMsgKey) => void
  queryElementRect: (selector: string) => Promise<DOMRectReadOnly | null>
  queryElementById: (id: string) => Promise<string | boolean>
  checkSlotValid: (slot: UISlotIdentity['slot']) => Promise<boolean>
  resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<Record<string, string | undefined> | null>
}

/**
 * Assets related APIs
 */
export interface IAssetsProxy {
  /**
   * @added 0.0.2
   * @param exts
   */
  listFilesOfCurrentGraph(exts?: string | string[]): Promise<
    Array<{
      path: string
      size: number
      accessTime: number
      modifiedTime: number
      changeTime: number
      birthTime: number
    }>
  >

  /**
   * @example https://github.com/logseq/logseq/pull/6488
   * @added 0.0.10
   */
  makeSandboxStorage(): IAsyncStorage

  /**
   * make assets scheme url based on current graph
   * @added 0.0.15
   * @param path
   */
  makeUrl(path: string): Promise<string>

  /**
   * try to open asset type file in Logseq app
   * @added 0.0.16
   * @param path
   */
  builtInOpen(path: string): Promise<boolean | undefined>
}

export interface ILSPluginThemeManager {
  get themes(): Map<PluginLocalIdentity, Theme[]>

  registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void>

  unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise<void>

  selectTheme(
    opt: Theme | LegacyTheme,
    options: { effect?: boolean; emit?: boolean }
  ): Promise<void>
}

export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'

export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
  /**
   * Connection status with the main app
   */
  connected: boolean

  /**
   * Duplex message caller
   */
  caller: LSPluginCaller

  /**
   * The plugin configurations from package.json
   */
  baseInfo: LSPluginBaseInfo

  /**
   * The plugin user settings
   */
  settings?: LSPluginBaseInfo['settings']

  /**
   * The main Logseq app is ready to run the plugin
   *
   * @param model - same as the model in `provideModel`
   */
  ready(model?: Record<string, any>): Promise<any>

  /**
   * @param callback - a function to run when the main Logseq app is ready
   */
  ready(callback?: (e: any) => void | {}): Promise<any>

  ready(
    model?: Record<string, any>,
    callback?: (e: any) => void | {}
  ): Promise<any>

  beforeunload: (callback: () => Promise<void>) => void

  /**
   * Create a object to hold the methods referenced in `provideUI`
   *
   * @example
   * ```ts
   * logseq.provideModel({
   *  openCalendar () {
   *    console.log('Open the calendar!')
   *  }
   * })
   * ```
   */
  provideModel(model: Record<string, any>): this

  /**
   * Set the theme for the main Logseq app
   */
  provideTheme(theme: Theme): this

  /**
   * Inject custom css for the main Logseq app
   *
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
   * @example
   * ```ts
   *   logseq.provideStyle(`
   *    @import url("https://at.alicdn.com/t/font_2409735_r7em724douf.css");
   *  )
   * ```
   */
  provideStyle(style: StyleString | StyleOptions): this

  /**
   * Inject custom UI at specific DOM node.
   * Event handlers can not be passed by string, so you need to create them in `provideModel`
   *
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator
   * @example
   * ```ts
   * logseq.provideUI({
   * key: 'open-calendar',
   * path: '#search',
   * template: `
   *  <a data-on-click="openCalendar" onclick="alert('abc')' style="opacity: .6; display: inline-flex; padding-left: 3px;'>
   *    <i class="iconfont icon-Calendaralt2"></i>
   *  </a>
   * `
   * })
   * ```
   */
  provideUI(ui: UIOptions): this

  /**
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
   *
   * @param schemas
   */
  useSettingsSchema(schemas: Array<SettingSchemaDesc>): this

  /**
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
   *
   * @param attrs
   */
  updateSettings(attrs: Record<string, any>): void

  onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook

  showSettingsUI(): void

  hideSettingsUI(): void

  setMainUIAttrs(attrs: Record<string, any>): void

  /**
   * Set the style for the plugin's UI
   *
   * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts
   * @example
   * ```ts
   * logseq.setMainUIInlineStyle({
   *  position: 'fixed',
   *  zIndex: 11,
   * })
   * ```
   */
  setMainUIInlineStyle(style: CSS.Properties): void

  /**
   * show the plugin's UI
   */
  showMainUI(opts?: { autoFocus: boolean }): void

  /**
   * hide the plugin's UI
   */
  hideMainUI(opts?: { restoreEditingCursor: boolean }): void

  /**
   * toggle the plugin's UI
   */
  toggleMainUI(): void

  isMainUIVisible: boolean

  resolveResourceFullUrl(filePath: string): string

  App: IAppProxy
  Editor: IEditorProxy
  DB: IDBProxy
  Git: IGitProxy
  UI: IUIProxy
  Assets: IAssetsProxy

  Request: LSPluginRequest
  FileStorage: LSPluginFileStorage
  Experiments: LSPluginExperiments
}
