import {
  DataUri,
  NumberExt,
  FunctionExt,
  Vector,
  Rectangle,
  Size,
  KeyValue,
  Basecoat,
  Dom,
  Graph,
} from '@antv/x6'
import './api'

export class Export extends Basecoat<Export.EventArgs> implements Graph.Plugin {
  public name = 'export'
  private graph: Graph

  constructor() {
    super()
  }

  get view() {
    return this.graph.view
  }

  init(graph: Graph) {
    this.graph = graph
  }

  exportPNG(fileName = 'chart', options: Export.ToImageOptions = {}) {
    this.toPNG((dataUri) => {
      DataUri.downloadDataUri(dataUri, fileName)
    }, options)
  }

  exportJPEG(fileName = 'chart', options: Export.ToImageOptions = {}) {
    this.toJPEG((dataUri) => {
      DataUri.downloadDataUri(dataUri, fileName)
    }, options)
  }

  exportSVG(fileName = 'chart', options: Export.ToSVGOptions = {}) {
    this.toSVG((svg: string) => {
      DataUri.downloadDataUri(DataUri.svgToDataUrl(svg), fileName)
    }, options)
  }

  toSVG(callback: Export.ToSVGCallback, options: Export.ToSVGOptions = {}) {
    this.notify('before:export', options)

    // to keep pace with it's doc description witch, the default value should be true.
    // without Object.hasOwn method cause by ts config target.
    // without instance.hasOwnProperty method cause by ts rule.
    // the condition will be false if these properties have been set undefined in the target,
    // but will be true if these properties are not in the target, cause the doc.
    !Object.prototype.hasOwnProperty.call(options, 'copyStyle') &&
      (options.copyStyles = true)
    !Object.prototype.hasOwnProperty.call(options, 'serializeImages') &&
      (options.serializeImages = true)

    const rawSVG = this.view.svg
    const vSVG = Vector.create(rawSVG).clone()
    let clonedSVG = vSVG.node as SVGSVGElement
    const vStage = vSVG.findOne(
      `.${this.view.prefixClassName('graph-svg-stage')}`,
    )!

    const viewBox =
      options.viewBox || this.graph.graphToLocal(this.graph.getContentBBox())
    const dimension = options.preserveDimensions
    if (dimension) {
      const size = typeof dimension === 'boolean' ? viewBox : dimension
      vSVG.attr({
        width: size.width,
        height: size.height,
      })
    }

    vSVG
      .removeAttribute('style')
      .attr(
        'viewBox',
        [viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(' '),
      )

    vStage.removeAttribute('transform')

    // Stores all the CSS declarations from external stylesheets to the
    // `style` attribute of the SVG document nodes.

    // This is achieved in three steps.
    // -----------------------------------

    // 1. Disabling all the stylesheets in the page and therefore collecting
    //    only default style values. This, together with the step 2, makes it
    //    possible to discard default CSS property values and store only those
    //    that differ.
    //
    // 2. Enabling back all the stylesheets in the page and collecting styles
    //    that differ from the default values.
    //
    // 3. Applying the difference between default values and the ones set by
    //    custom stylesheets onto the `style` attribute of each of the nodes
    //    in SVG.

    if (options.copyStyles) {
      const document = rawSVG.ownerDocument!
      const raws = Array.from(rawSVG.querySelectorAll('*'))
      const clones = Array.from(clonedSVG.querySelectorAll('*'))

      const styleSheetCount = document.styleSheets.length
      const styleSheetsCopy = []
      for (let k = styleSheetCount - 1; k >= 0; k -= 1) {
        // There is a bug (bugSS) in Chrome 14 and Safari. When you set
        // `stylesheet.disable = true` it will also remove it from
        // `document.styleSheets`. So we need to store all stylesheets before
        // we disable them. Later on we put them back to `document.styleSheets`
        // if needed.

        // See the bug `https://code.google.com/p/chromium/issues/detail?id=88310`.
        styleSheetsCopy[k] = document.styleSheets[k]
        document.styleSheets[k].disabled = true
      }

      const defaultComputedStyles: KeyValue<KeyValue<string>> = {}
      raws.forEach((elem, index) => {
        const computedStyle = window.getComputedStyle(elem, null)
        // We're making a deep copy of the `computedStyle` so that it's not affected
        // by that next step when all the stylesheets are re-enabled again.
        const defaultComputedStyle: KeyValue<string> = {}
        Object.keys(computedStyle).forEach((property) => {
          defaultComputedStyle[property] =
            computedStyle.getPropertyValue(property)
        })

        defaultComputedStyles[index] = defaultComputedStyle
      })

      // Copy all stylesheets back
      if (styleSheetCount !== document.styleSheets.length) {
        styleSheetsCopy.forEach((copy, index) => {
          document.styleSheets[index] = copy
        })
      }

      for (let i = 0; i < styleSheetCount; i += 1) {
        document.styleSheets[i].disabled = false
      }

      const customStyles: KeyValue<KeyValue<string>> = {}
      raws.forEach((elem, index) => {
        const computedStyle = window.getComputedStyle(elem, null)
        const defaultComputedStyle = defaultComputedStyles[index]
        const customStyle: KeyValue<string> = {}

        Object.keys(computedStyle).forEach((property) => {
          if (
            !NumberExt.isNumber(property) &&
            computedStyle.getPropertyValue(property) !==
              defaultComputedStyle[property]
          ) {
            customStyle[property] = computedStyle.getPropertyValue(property)
          }
        })

        customStyles[index] = customStyle
      })

      clones.forEach((elem, index) => {
        Dom.css(elem, customStyles[index])
      })
    }

    const stylesheet = options.stylesheet
    if (typeof stylesheet === 'string') {
      const cDATASection = rawSVG
        .ownerDocument!.implementation.createDocument(null, 'xml', null)
        .createCDATASection(stylesheet)

      vSVG.prepend(
        Vector.create(
          'style',
          {
            type: 'text/css',
          },
          [cDATASection as any],
        ),
      )
    }

    const format = () => {
      const beforeSerialize = options.beforeSerialize
      if (typeof beforeSerialize === 'function') {
        const ret = FunctionExt.call(beforeSerialize, this.graph, clonedSVG)
        if (ret instanceof SVGSVGElement) {
          clonedSVG = ret
        }
      }

      const dataUri = new XMLSerializer()
        .serializeToString(clonedSVG)
        .replace(/&nbsp;/g, '\u00a0')

      this.notify('after:export', options)
      callback(dataUri)
    }

    if (options.serializeImages) {
      const deferrals = vSVG.find('image').map((vImage) => {
        return new Promise<void>((resolve) => {
          const url = vImage.attr('xlink:href') || vImage.attr('href')
          DataUri.imageToDataUri(url, (err, dataUri) => {
            if (!err && dataUri) {
              vImage.attr('xlink:href', dataUri)
            }
            resolve()
          })
        })
      })

      Promise.all(deferrals).then(format)
    } else {
      format()
    }
  }

  toDataURL(callback: Export.ToSVGCallback, options: Export.ToDataURLOptions) {
    let viewBox = options.viewBox || this.graph.getContentBBox()

    const padding = NumberExt.normalizeSides(options.padding)
    if (options.width && options.height) {
      if (padding.left + padding.right >= options.width) {
        padding.left = padding.right = 0
      }
      if (padding.top + padding.bottom >= options.height) {
        padding.top = padding.bottom = 0
      }
    }

    const expanding = new Rectangle(
      -padding.left,
      -padding.top,
      padding.left + padding.right,
      padding.top + padding.bottom,
    )

    if (options.width && options.height) {
      const width = viewBox.width + padding.left + padding.right
      const height = viewBox.height + padding.top + padding.bottom
      expanding.scale(width / options.width, height / options.height)
    }

    viewBox = Rectangle.create(viewBox).moveAndExpand(expanding)

    const rawSize =
      typeof options.width === 'number' && typeof options.height === 'number'
        ? { width: options.width, height: options.height }
        : viewBox

    let scale = options.ratio ? parseFloat(options.ratio) : 1
    if (!Number.isFinite(scale) || scale === 0) {
      scale = 1
    }

    const size = {
      width: Math.max(Math.round(rawSize.width * scale), 1),
      height: Math.max(Math.round(rawSize.height * scale), 1),
    }

    {
      const imgDataCanvas = document.createElement('canvas')
      const context2D = imgDataCanvas.getContext('2d')!
      imgDataCanvas.width = size.width
      imgDataCanvas.height = size.height
      const x = size.width - 1
      const y = size.height - 1
      context2D.fillStyle = 'rgb(1,1,1)'
      context2D.fillRect(x, y, 1, 1)
      const data = context2D.getImageData(x, y, 1, 1).data
      if (data[0] !== 1 || data[1] !== 1 || data[2] !== 1) {
        throw new Error('size exceeded')
      }
    }

    const img = new Image()
    img.onload = () => {
      const canvas = document.createElement('canvas')
      canvas.width = size.width
      canvas.height = size.height

      const context = canvas.getContext('2d')!
      context.fillStyle = options.backgroundColor || 'white'
      context.fillRect(0, 0, size.width, size.height)

      try {
        context.drawImage(img, 0, 0, size.width, size.height)
        const dataUri = canvas.toDataURL(options.type, options.quality)
        callback(dataUri)
      } catch (error) {
        // pass
      }
    }

    this.toSVG(
      (dataUri) => {
        img.src = `data:image/svg+xml,${encodeURIComponent(dataUri)}`
      },
      {
        ...options,
        viewBox,
        serializeImages: true,
        preserveDimensions: {
          ...size,
        },
      },
    )
  }

  toPNG(callback: Export.ToSVGCallback, options: Export.ToImageOptions = {}) {
    this.toDataURL(callback, {
      ...options,
      type: 'image/png',
    })
  }

  toJPEG(callback: Export.ToSVGCallback, options: Export.ToImageOptions = {}) {
    this.toDataURL(callback, {
      ...options,
      type: 'image/jpeg',
    })
  }

  protected notify<K extends keyof Export.EventArgs>(
    name: K,
    args: Export.EventArgs[K],
  ) {
    this.trigger(name, args)
    this.graph.trigger(name, args)
  }

  @Basecoat.dispose()
  dispose(): void {
    this.off()
  }
}

export namespace Export {
  export interface EventArgs {
    'before:export': Export.ToSVGOptions
    'after:export': Export.ToSVGOptions
  }

  export type ToSVGCallback = (dataUri: string) => any

  export interface ToSVGOptions {
    /**
     * By default, the resulting SVG has set width and height to `100%`.
     * If you'd like to have the dimensions to be set to the actual content
     * width and height, set `preserveDimensions` to `true`. An object with
     * `width` and `height` properties can be also used here if you need to
     * define the export size explicitely.
     */
    preserveDimensions?: boolean | Size

    viewBox?: Rectangle.RectangleLike

    /**
     * When set to `true` all the styles from external stylesheets are copied
     * to the resulting SVG export. Note this requires a lot of computations
     * and it might significantly affect the export time.
     */
    copyStyles?: boolean

    stylesheet?: string

    /**
     * Converts all contained images into Data URI format.
     */
    serializeImages?: boolean

    /**
     * A function called before the XML serialization. It may be used to
     * modify the exported SVG before it is converted to a string. The
     * function can also return a new SVGDocument.
     */
    beforeSerialize?: (this: Graph, svg: SVGSVGElement) => any
  }

  export interface ToImageOptions extends ToSVGOptions {
    /**
     * The width of the image in pixels.
     */
    width?: number
    /**
     * The height of the image in pixels.
     */
    height?: number
    ratio?: string
    backgroundColor?: string
    padding?: NumberExt.SideOptions
    quality?: number
  }

  export interface ToDataURLOptions extends ToImageOptions {
    type: 'image/png' | 'image/jpeg'
  }
}
