import { type JSONObject, type KeyValue, ObjectExt, type Size } from '../common'
import { Point, type Rectangle } from '../geometry'
import {
  type CellAttrs,
  type PortLabelLayoutManualItem,
  type PortLabelLayoutNativeItem,
  type PortLabelLayoutNativeNames,
  type PortLabelLayoutResult,
  type PortLayoutDefinition,
  type PortLayoutManualItem,
  type PortLayoutNativeItem,
  type PortLayoutNativeNames,
  type PortLayoutResult,
  portLabelLayoutPresets,
  portLabelLayoutRegistry,
  portLayoutPresets,
  portLayoutRegistry,
} from '../registry'
import type { MarkupType } from '../view/markup'
import type { PointData } from '../types'

export interface Metadata {
  groups?: { [name: string]: GroupMetadata }
  items: PortMetadata[]
}

export type PortPosition =
  | Partial<PortLayoutNativeItem>
  | Partial<PortLayoutManualItem>

export type PortPositionMetadata =
  | PortLayoutNativeNames
  | Exclude<string, PortLayoutNativeNames>
  | PointData // absolute layout
  | PortPosition

export type PortLabelPosition =
  | Partial<PortLabelLayoutNativeItem>
  | Partial<PortLabelLayoutManualItem>

export type PortLabelPositionMetadata =
  | PortLabelLayoutNativeNames
  | Exclude<string, PortLabelLayoutNativeNames>
  | PortLabelPosition

export interface LabelMetadata {
  markup?: MarkupType
  size?: Size
  position?: PortLabelPositionMetadata
}

export interface Label {
  markup: string
  size?: Size
  position: PortLabelPosition
}

interface Common {
  markup: MarkupType
  attrs: CellAttrs
  zIndex: number | 'auto'
  size?: Size
}

export interface GroupMetadata extends Partial<Common>, KeyValue {
  label?: LabelMetadata
  position?: PortPositionMetadata
}

export interface Group extends Partial<Common> {
  label: Label
  position: PortPosition
}

interface PortBase {
  group?: string
  /**
   * Arguments for the port layout function.
   */
  args?: JSONObject
}

export interface PortMetadata extends Partial<Common>, PortBase, KeyValue {
  id?: string
  label?: LabelMetadata
}

export interface Port extends Group, PortBase {
  id: string
}

export interface LayoutResult {
  portId: string
  portAttrs?: CellAttrs
  portSize?: Size
  portLayout: PortLayoutResult
  labelSize?: Size
  labelLayout: PortLabelLayoutResult | null
}

export class PortManager {
  ports: Port[]
  groups: { [name: string]: Group }

  constructor(data: Metadata) {
    this.ports = []
    this.groups = {}
    this.init(ObjectExt.cloneDeep(data))
  }

  getPorts() {
    return this.ports
  }

  getGroup(groupName?: string | null) {
    return groupName != null ? this.groups[groupName] : null
  }

  getPortsByGroup(groupName?: string): Port[] {
    return this.ports.filter(
      (p) => p.group === groupName || (p.group == null && groupName == null),
    )
  }

  getPortsLayoutByGroup(groupName: string | undefined, elemBBox: Rectangle) {
    const ports = this.getPortsByGroup(groupName)
    const group = groupName ? this.getGroup(groupName) : null
    const groupPosition = group ? group.position : null
    const groupPositionName = groupPosition ? groupPosition.name : null

    let layoutFn: PortLayoutDefinition<any>

    if (groupPositionName != null) {
      const fn = portLayoutRegistry.get(groupPositionName)
      if (fn == null) {
        return portLayoutRegistry.onNotFound(groupPositionName)
      }
      layoutFn = fn
    } else {
      layoutFn = portLayoutPresets.left
    }

    const portsArgs = ports.map(
      (port) => (port && port.position && port.position.args) || {},
    )
    const groupArgs = (groupPosition && groupPosition.args) || {}
    const layouts = layoutFn(portsArgs, elemBBox, groupArgs)
    return layouts.map<LayoutResult>((portLayout, index) => {
      const port = ports[index]
      return {
        portLayout,
        portId: port.id!,
        portSize: port.size,
        portAttrs: port.attrs,
        labelSize: port.label.size,
        labelLayout: this.getPortLabelLayout(
          port,
          Point.create(portLayout.position),
          elemBBox,
        ),
      }
    })
  }

  protected init(data: Metadata) {
    const { groups, items } = data

    if (groups != null) {
      Object.keys(groups).forEach((key) => {
        this.groups[key] = this.parseGroup(groups[key])
      })
    }

    if (Array.isArray(items)) {
      items.forEach((item) => {
        this.ports.push(this.parsePort(item) as unknown as Port)
      })
    }
  }

  protected parseGroup(group: GroupMetadata) {
    return {
      ...group,
      label: this.getLabel(group, true),
      position: this.getPortPosition(group.position, true),
    } as Group
  }

  protected parsePort(port: PortMetadata) {
    const result = { ...port }
    const group = this.getGroup(port.group) || ({} as Group)

    result.markup = result.markup || group.markup
    result.attrs = ObjectExt.merge({}, group.attrs, result.attrs)
    result.position = this.createPosition(group, result)
    result.label = ObjectExt.merge({}, group.label, this.getLabel(result))
    result.zIndex = this.getZIndex(group, result)
    result.size = { ...group.size, ...result.size } as Size

    return result
  }

  protected getZIndex(group: Group, port: PortMetadata) {
    if (typeof port.zIndex === 'number') {
      return port.zIndex
    }

    if (typeof group.zIndex === 'number' || group.zIndex === 'auto') {
      return group.zIndex
    }

    return 'auto'
  }

  protected createPosition(group: Group, port: PortMetadata) {
    return ObjectExt.merge(
      {
        name: 'left',
        args: {},
      },
      group.position,
      { args: port.args },
    ) as PortPosition
  }

  protected getPortPosition(
    position?: PortPositionMetadata,
    setDefault = false,
  ): PortPosition {
    if (position == null) {
      if (setDefault) {
        return { name: 'left', args: {} }
      }
    } else {
      if (typeof position === 'string') {
        return {
          name: position,
          args: {},
        }
      }

      if (Array.isArray(position)) {
        return {
          name: 'absolute',
          args: { x: position[0], y: position[1] },
        }
      }

      if (typeof position === 'object') {
        return position
      }
    }

    return { args: {} }
  }

  protected getPortLabelPosition(
    position?: PortLabelPositionMetadata,
    setDefault = false,
  ): PortLabelPosition {
    if (position == null) {
      if (setDefault) {
        return { name: 'left', args: {} }
      }
    } else {
      if (typeof position === 'string') {
        return {
          name: position,
          args: {},
        }
      }

      if (typeof position === 'object') {
        return position
      }
    }

    return { args: {} }
  }

  protected getLabel(item: GroupMetadata, setDefaults = false) {
    const label = item.label || {}
    label.position = this.getPortLabelPosition(label.position, setDefaults)
    return label as Label
  }

  protected getPortLabelLayout(
    port: Port,
    portPosition: Point,
    elemBBox: Rectangle,
  ) {
    const name = port.label.position.name || 'left'
    const args = port.label.position.args || {}
    const layoutFn =
      portLabelLayoutRegistry.get(name) || portLabelLayoutPresets.left
    if (layoutFn) {
      return layoutFn(portPosition, elemBBox, args)
    }

    return null
  }
}
