import type { Dom, Nilable } from '../common'
import { ObjectExt } from '../common'
import { Config } from '../config'
import type { RectangleLike } from '../geometry'
import type { Graph } from '../graph'
import type {
  Cell,
  Edge,
  EdgeLabel,
  Model,
  Node,
  TerminalData,
  TerminalType,
} from '../model'
import type { Port } from '../model/port'
import type {
  ConnectionPointManualItem,
  ConnectionPointNativeItem,
  ConnectorManualItem,
  ConnectorNativeItem,
  EdgeAnchorManualItem,
  EdgeAnchorNativeItem,
  NodeAnchorManualItem,
  NodeAnchorNativeItem,
  RouterManualItem,
  RouterNativeItem,
} from '../registry'
import { Edge as StandardEdge } from '../shape'
import type { CellView, EdgeView, NodeView } from '../view'
import type { CellViewInteracting } from '../view/cell/type'
import type { MarkupSelectors } from '../view/markup'
import type { BackgroundManagerOptions } from './background'
import type { GridCommonOptions, GridDrawOptions, GridOptions } from './grid'
import type { HighlightManagerOptions } from './highlight'
import type { MouseWheelOptions } from './mousewheel'
import type { PanningOptions } from './panning'

export interface VirtualOptions {
  enabled?: boolean
  margin?: number
}

interface Common {
  container: HTMLElement
  model?: Model

  x: number
  y: number
  width: number
  height: number
  autoResize?: boolean | Element | Document

  background?: false | BackgroundManagerOptions

  scaling: {
    min?: number
    max?: number
  }

  moveThreshold: number
  clickThreshold: number
  magnetThreshold: number | 'onleave'
  preventDefaultDblClick: boolean
  preventDefaultContextMenu:
    | boolean
    | ((this: Graph, { view }: { view: CellView | null }) => boolean)
  preventDefaultMouseDown: boolean
  preventDefaultBlankAction: boolean
  interacting: CellViewInteracting

  async?: boolean
  virtual?: boolean | VirtualOptions

  guard: (e: Dom.EventObject, view?: CellView | null) => boolean

  onPortRendered?: (args: OnPortRenderedArgs) => void
  onEdgeLabelRendered?: (
    args: OnEdgeLabelRenderedArgs,
  ) => void | ((args: OnEdgeLabelRenderedArgs) => void)

  createCellView?: (
    this: Graph,
    cell: Cell,
  ) => typeof CellView | (new (...args: any[]) => CellView) | null | undefined
}

export interface ManualBooleans {
  panning: boolean | Partial<PanningOptions>
  mousewheel: boolean | Partial<MouseWheelOptions>
  embedding: boolean | Partial<Embedding>
}

export interface GraphManual extends Partial<Common>, Partial<ManualBooleans> {
  grid?: boolean | number | (Partial<GridCommonOptions> & GridDrawOptions)
  connecting?: Partial<Connecting>
  translating?: Partial<Translating>
  highlighting?: Partial<Highlighting>
}

export interface GraphDefinition extends Common {
  grid: GridOptions
  panning: PanningOptions
  mousewheel: MouseWheelOptions
  embedding: Embedding
  connecting: Connecting
  translating: Translating
  highlighting: Highlighting
}

type OptionItem<T, S> = S | ((this: Graph, arg: T) => S)

type NodeAnchorOptions = string | NodeAnchorNativeItem | NodeAnchorManualItem
type EdgeAnchorOptions = string | EdgeAnchorNativeItem | EdgeAnchorManualItem
type ConnectionPointOptions =
  | string
  | ConnectionPointNativeItem
  | ConnectionPointManualItem

export interface Connecting {
  /**
   * Snap edge to the closest node/port in the given radius on dragging.
   */
  snap: boolean | { radius: number; anchor?: 'center' | 'bbox' }

  /**
   * Specify whether connect to point on the graph is allowed.
   */
  allowBlank: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * When set to `false`, edges can not be connected to the same node,
   * meaning the source and target of the edge can not be the same node.
   */
  allowLoop: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * Specify whether connect to node(not the port on the node) is allowed.
   */
  allowNode: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * Specify whether connect to edge is allowed.
   */
  allowEdge: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * Specify whether connect to port is allowed.
   */
  allowPort: boolean | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * Specify whether more than one edge connected to the same source and
   * target node is allowed.
   */
  allowMulti:
    | boolean
    | 'withPort'
    | ((this: Graph, args: ValidateConnectionArgs) => boolean)

  /**
   * Highlights all the available magnets or nodes when a edge is
   * dragging(reconnecting). This gives a hint to the user to what
   * other nodes/ports this edge can be connected. What magnets/cells
   * are available is determined by the `validateConnection` function.
   */
  highlight: boolean

  anchor: NodeAnchorOptions
  sourceAnchor?: NodeAnchorOptions
  targetAnchor?: NodeAnchorOptions
  edgeAnchor: EdgeAnchorOptions
  sourceEdgeAnchor?: EdgeAnchorOptions
  targetEdgeAnchor?: EdgeAnchorOptions

  connectionPoint: ConnectionPointOptions
  sourceConnectionPoint?: ConnectionPointOptions
  targetConnectionPoint?: ConnectionPointOptions

  router: string | RouterNativeItem | RouterManualItem
  connector: string | ConnectorNativeItem | ConnectorManualItem

  createEdge?: (
    this: Graph,
    args: {
      sourceCell: Cell
      sourceView: CellView
      sourceMagnet: Element
    },
  ) => Nilable<Edge> | void

  /**
   * Check whether to add a new edge to the graph when user clicks
   * on an a magnet.
   */
  validateMagnet?: (
    this: Graph,
    args: {
      cell: Cell
      view: CellView
      magnet: Element
      e: Dom.MouseDownEvent | Dom.MouseEnterEvent
    },
  ) => boolean

  /**
   * Custom validation on stop draggin the edge arrowhead(source/target).
   * If the function returns `false`, the edge is either removed(edges
   * which are created during the interaction) or reverted to the state
   * before the interaction.
   */
  validateEdge?: (
    this: Graph,
    args: {
      edge: Edge
      type: TerminalType
      previous: TerminalData
    },
  ) => boolean

  /**
   * Check whether to allow or disallow the edge connection while an
   * arrowhead end (source/target) being changed.
   */
  validateConnection: (this: Graph, args: ValidateConnectionArgs) => boolean
}

export interface ValidateConnectionArgs {
  type?: TerminalType | null
  edge?: Edge | null
  edgeView?: EdgeView | null
  sourceCell?: Cell | null
  targetCell?: Cell | null
  sourceView?: CellView | null
  targetView?: CellView | null
  sourcePort?: string | null
  targetPort?: string | null
  sourceMagnet?: Element | null
  targetMagnet?: Element | null
}

export interface Translating {
  /**
   * Restrict the translation (movement) of nodes by a given bounding box.
   * If set to `true`, the user will not be able to move nodes outside the
   * boundary of the graph area.
   */
  restrict: boolean | OptionItem<CellView | null, RectangleLike | number | null>
  /**
   * After a node is moved, if it overlaps with other nodes, it will be
   * automatically offset (by default, no offset occurs).
   */
  autoOffset?: boolean
}

export interface Embedding {
  enabled?: boolean

  /**
   * Determines the way how a cell finds a suitable parent when it's dragged
   * over the graph. The cell with the highest z-index (visually on the top)
   * will be chosen.
   */
  findParent?:
    | 'bbox'
    | 'center'
    | 'topLeft'
    | 'topRight'
    | 'bottomLeft'
    | 'bottomRight'
    | ((this: Graph, args: { node: Node; view: NodeView }) => Cell[])

  /**
   * If enabled only the node on the very front is taken into account for the
   * embedding. If disabled the nodes under the dragged view are tested one by
   * one (from front to back) until a valid parent found.
   */
  frontOnly?: boolean

  /**
   * Check whether to allow or disallow the node embedding while it's being
   * translated. By default, all nodes can be embedded into all other nodes.
   */
  validate: (
    this: Graph,
    args: {
      child: Node
      parent: Node
      childView: CellView
      parentView: CellView
    },
  ) => boolean
}

/**
 * Configure which highlighter to use (and with which options) for
 * each type of interaction.
 */
export interface Highlighting {
  /**
   * The default highlighter to use (and options) when none is specified
   */
  default: HighlightManagerOptions
  /**
   * When a cell is dragged over another cell in embedding mode.
   */
  embedding?: HighlightManagerOptions | null
  /**
   * When showing all nodes to which a valid connection can be made.
   */
  nodeAvailable?: HighlightManagerOptions | null
  /**
   * When showing all magnets to which a valid connection can be made.
   */
  magnetAvailable?: HighlightManagerOptions | null
  /**
   * When a valid edge connection can be made to an node.
   */
  magnetAdsorbed?: HighlightManagerOptions | null
}

export function getOptions(options: Partial<GraphManual>) {
  const { grid, panning, mousewheel, embedding, ...others } = options

  // size
  // ----
  const container = options.container
  if (container != null) {
    if (others.width == null) {
      others.width = container.clientWidth
    }

    if (others.height == null) {
      others.height = container.clientHeight
    }
  } else {
    throw new Error(`Ensure the container of the graph is specified and valid`)
  }

  const result = ObjectExt.merge({}, defaults, others) as GraphDefinition

  // grid
  // ----
  const defaultGrid: GridCommonOptions = { size: 10, visible: false }
  if (typeof grid === 'number') {
    result.grid = { size: grid, visible: false }
  } else if (typeof grid === 'boolean') {
    result.grid = { ...defaultGrid, visible: grid }
  } else {
    result.grid = { ...defaultGrid, ...grid }
  }

  // booleas
  // -------
  const booleas: (keyof ManualBooleans)[] = [
    'panning',
    'mousewheel',
    'embedding',
  ]

  booleas.forEach((key) => {
    const val = options[key]
    if (typeof val === 'boolean') {
      result[key].enabled = val
    } else if (val != null) {
      result[key] = {
        ...result[key],
        ...(val as any),
      }
    }
  })

  return result
}

export interface OnPortRenderedArgs {
  node: Node
  port: Port
  container: Element
  selectors?: MarkupSelectors
  labelContainer?: Element
  labelSelectors?: MarkupSelectors | null
  contentContainer: Element
  contentSelectors?: MarkupSelectors
}

export interface OnEdgeLabelRenderedArgs {
  edge: Edge
  label: EdgeLabel
  container: Element
  selectors: MarkupSelectors
}

export const defaults: Partial<GraphDefinition> = {
  x: 0,
  y: 0,
  scaling: {
    min: 0.01,
    max: 16,
  },
  grid: {
    size: 10,
    visible: false,
  },
  background: false,

  panning: {
    enabled: true,
    eventTypes: ['leftMouseDown'],
  },
  mousewheel: {
    enabled: false,
    factor: 1.2,
    zoomAtMousePosition: true,
  },

  highlighting: {
    default: {
      name: 'stroke',
      args: {
        padding: 3,
      },
    },
    nodeAvailable: {
      name: 'className',
      args: {
        className: Config.prefix('available-node'),
      },
    },
    magnetAvailable: {
      name: 'className',
      args: {
        className: Config.prefix('available-magnet'),
      },
    },
  },
  connecting: {
    snap: false,
    allowLoop: true,
    allowNode: true,
    allowEdge: false,
    allowPort: true,
    allowBlank: true,
    allowMulti: true,
    highlight: false,

    anchor: 'center',
    edgeAnchor: 'ratio',
    connectionPoint: 'boundary',
    router: 'normal',
    connector: 'normal',

    validateConnection(this: Graph, { type, sourceView, targetView }) {
      const view = type === 'target' ? targetView : sourceView
      return view != null
    },

    createEdge() {
      return new StandardEdge()
    },
  },
  translating: {
    restrict: false,
  },
  embedding: {
    enabled: false,
    findParent: 'bbox',
    frontOnly: true,
    validate: () => true,
  },

  moveThreshold: 0,
  clickThreshold: 0,
  magnetThreshold: 0,
  preventDefaultDblClick: true,
  preventDefaultMouseDown: false,
  preventDefaultContextMenu: true,
  preventDefaultBlankAction: true,
  interacting: {
    edgeLabelMovable: false,
  },

  async: true,
  virtual: false,
  guard: () => false,
}
