/// <reference types="@vuemap/amap-jsapi-types" />
import Event from '../event'
import utils from './utils'
import BoundsItem from './BoundsItem'
import type { DistrictCluster } from './index'
/*declare global {
  interface Window {
    Loca: any
  }
}*/
export interface StyleOption {
  strokeColor?: string
  strokeOpacity?: number
  strokeWeight?: number
  fillColor?: string
  fillOpacity?: number
}
export interface FeatureStyleByLevelOption {
  country?: StyleOption // 国家
  province?: StyleOption // 省份
  city?: StyleOption // 市
  district?: StyleOption // 区县
}
export interface RenderOptions {
  // engine?: 'default' | 'loca'
  minHeightToShowSubFeatures?: number
  minSiblingAvgHeightToShowSubFeatures?: number
  minSubAvgHeightToShowSubFeatures?: number
  featureStyleByLevel?: FeatureStyleByLevelOption
  zIndex?: number // 层级
  visible?: boolean // 是否显示
  areaNodeCacheLimit?: number
  getFeatureStyle?: (feature: any, dataItems: any[]) => StyleOption
  zooms?: [number, number]
  renderPolygon?: (feature: any, dataItems: any[]) => AMap.Polygon // 自定义绘制多边形
  renderClusterMarker?: (feature: any, dataItems: any[]) => AMap.Marker // 自定义绘制标号
  clusterMarkerEventSupport?: boolean // 聚合标注是否开启事件支持，默认true。
  clusterMarkerClickToShowSub?: boolean // 点击聚合标注是否触发展示子级区划（即调用 zoomToShowSubFeatures 方法），默认true
  featureEventSupport?: boolean // 区划面是否开启事件支持，默认true
  featureClickToShowSub?: boolean // 点击区划面是否触发进入子级区划，默认false
}
interface CustomRenderOptions extends RenderOptions {
  map: AMap.Map
}
type _OptOptions = Required<CustomRenderOptions>
export class BaseRender extends Event {
  baseId = 1
  _ins: DistrictCluster
  _currentZoom = 2
  _currentScaleFactor?: number
  _currentViewBounds?: BoundsItem
  _currentViewBoundsInLngLat?: AMap.Bounds
  _currentPixelRatio?: number

  _currentFeatures: { feature: any; dataItems: any }[] = []
  _currentRenderId?: number
  _loadLeft = 0
  _isRendering?: boolean
  _opts: _OptOptions
  _renderLaterId: any
  _map: AMap.Map
  _polygonCache: AMap.Polygon[] = []
  _markerCache: AMap.Marker[] = []
  layer: any
  loca: any
  _locaPolygonLayer: any
  markerGroup?: AMap.OverlayGroup
  constructor(districtCluster: DistrictCluster, options: CustomRenderOptions) {
    super()
    this._opts = utils.extend(
      {
        engine: 'default',
        areaNodeCacheLimit: -1,
        minHeightToShowSubFeatures: 630,
        minSiblingAvgHeightToShowSubFeatures: 600,
        minSubAvgHeightToShowSubFeatures: 300,
        zooms: [2, 30],
        clusterMarkerEventSupport: true, // 聚合标注是否开启事件支持，默认true。
        clusterMarkerClickToShowSub: true, // 点击聚合标注是否触发展示子级区划（即调用 zoomToShowSubFeatures 方法），默认true
        featureEventSupport: true, // 区划面是否开启事件支持，默认true
        featureClickToShowSub: false,
        featureStyleByLevel: {
          country: {
            strokeColor: 'rgb(31, 119, 180)',
            strokeOpacity: 0.9,
            strokeWeight: 2,
            fillColor: 'rgb(49, 163, 84)',
            fillOpacity: 0.8
          },
          province: {
            strokeColor: 'rgb(31, 119, 180)',
            strokeOpacity: 0.9,
            strokeWeight: 2,
            fillColor: 'rgb(116, 196, 118)',
            fillOpacity: 0.7
          },
          city: {
            strokeColor: 'rgb(31, 119, 180)',
            strokeOpacity: 0.9,
            strokeWeight: 2,
            fillColor: 'rgb(161, 217, 155)',
            fillOpacity: 0.6
          },
          district: {
            strokeColor: 'rgb(31, 119, 180)',
            strokeOpacity: 0.9,
            strokeWeight: 2,
            fillColor: 'rgb(199, 233, 192)',
            fillOpacity: 0.5
          }
        }
      },
      options
    )
    this._map = this._opts.map
    this._createLayer()
    this._ins = districtCluster
    this._isRendering = !1
    this._loadLeft = 0
    this._currentFeatures = []
  }

  _createLayer() {
    this.markerGroup = new AMap.OverlayGroup()
    this._map.add(this.markerGroup as any)
    /*if(this._opts.engine === 'loca'){
      this.loca = new window.Loca.Container({
        map: this._map
      });
      this._locaPolygonLayer = new window.Loca.PolygonLayer({
        zIndex: this._opts.zIndex,
        visible: this._opts.visible
      })
      this._locaPolygonLayer.setSource(new window.Loca.GeoJSONSource({
        data: {
          "type": "FeatureCollection",
          "features": []
        }
      }))
      this._locaPolygonLayer.setStyle({
        topColor: (index, feature) => {
          return this._getFeatureStyleOptions(feature, feature.properties.dataItems).fillColor
        }
      })
      this.loca.add(this._locaPolygonLayer)
    }else{*/
    this.layer = new (AMap as any).VectorLayer({
      zIndex: this._opts.zIndex || 10,
      visible: this._opts.visible || true
    })
    this._map.addLayer(this.layer)
    // }
  }

  zoomToShowSubFeatures(adcode, center?: any) {
    const minZoomToShowSub = this.getMinZoomToShowSub(adcode)
    if (minZoomToShowSub >= 3) {
      const map = this._ins.getMap()
      if (map) {
        if (!center) {
          const treeNode = this._ins._distMgr.getNodeByAdcode(adcode)
          center = treeNode.center
        }
        map.setZoomAndCenter(minZoomToShowSub, center)
      }
    }
  }
  getPixelRatio() {
    return Math.min(2, Math.round(window.devicePixelRatio || 1))
  }
  refreshViewState() {
    if (!this._ins._distMgr.isReady()) return !1
    const simpIns = this._ins
    if (!simpIns.isReady()) return !1
    const map = simpIns.getMap() as AMap.Map,
      mapViewBounds = map.getBounds(),
      viewSize = map.getSize(),
      currZoom = map.getZoom(3),
      maxZoom = this._opts.zooms[1],
      scaleFactor = Math.pow(2, maxZoom - currZoom),
      northWest = mapViewBounds.getNorthWest(),
      topLeft = map.lngLatToCoords([northWest.getLng(), northWest.getLat()]),
      bounds = new BoundsItem(topLeft[0], topLeft[1], viewSize.width * scaleFactor, viewSize.height * scaleFactor)
    this._currentZoom = currZoom
    this._currentScaleFactor = scaleFactor
    this._currentViewBounds = bounds
    this._currentViewBoundsInLngLat = mapViewBounds
    this._currentPixelRatio = this.getPixelRatio()
  }
  renderViewport() {
    this.refreshViewState()
    if (!this._currentViewBounds) return !1
    // this.markerGroup.clearOverlays()
    // this.layer.clear()
    // this._polygonCache = []
    // this._markerCache = []
    this._currentRenderId = this.baseId++
    this._loadLeft = 0
    this._currentFeatures = []
    this._renderViewDist(this._currentRenderId)
    this._isRendering = false
  }
  getCurrentRenderId() {
    return this._currentRenderId
  }
  isRenderIdStillValid(renderId) {
    return renderId === this._currentRenderId
  }
  _renderViewDist(renderId) {
    const adcodes2Render: any[] = []
    if (this._currentZoom < this._opts.zooms[0] || this._currentZoom > this._opts.zooms[1]) {
      this.isRenderIdStillValid(renderId) && this._prepareFeatures(renderId, adcodes2Render)
      return
    }
    this._ins.getDistMgr().traverseTopNodes(
      this._currentViewBoundsInLngLat as AMap.Bounds,
      this._currentZoom,
      (node) => {
        adcodes2Render.push(node.adcode)
      },
      () => {
        // console.log('adcodes2Render: ', adcodes2Render)
        this.isRenderIdStillValid(renderId) && this._prepareFeatures(renderId, adcodes2Render)
      },
      this
    )
  }
  getMinZoomToShowSub(adcode) {
    const treeNode = this._ins._distMgr.getNodeByAdcode(adcode)
    if (!treeNode || !treeNode.idealZoom) return -1
    if (!treeNode._minZoomToShowSub) {
      const zooms = this._ins.getZooms() as [number, number]
      for (let i = zooms[0]; i <= zooms[1]; i++)
        if (this.shouldShowSubOnZoom(treeNode, i)) {
          treeNode._minZoomToShowSub = i
          break
        }
    }
    return treeNode._minZoomToShowSub || -1
  }
  shouldShowSubOnZoom(treeNode, zoom) {
    if (!treeNode.idealZoom) return !1
    if (treeNode._minZoomToShowSub && zoom >= treeNode._minZoomToShowSub) return !0
    let boundsSize = this._ins._distMgr.getNodeBoundsSize(treeNode, zoom)
    if (1e5 === treeNode.adcode && boundsSize[1] > 400) return !0
    if (boundsSize[1] < this._opts.minHeightToShowSubFeatures) return !1
    let i, len, heightSum
    if (treeNode.children) {
      const children = treeNode.children
      heightSum = 0
      len = children.length
      if (len) {
        for (i = 0; i < len; i++) {
          boundsSize = this._ins._distMgr.getNodeBoundsSize(children[i], zoom)
          heightSum += boundsSize[1]
        }
        if (heightSum / len < this._opts.minSubAvgHeightToShowSubFeatures) return !1
      }
    }
    const parentAdcode = this._ins._distMgr.getParentAdcode(treeNode.adcode, treeNode.acroutes)
    if (parentAdcode) {
      const parentNode = this._ins._distMgr.getNodeByAdcode(parentAdcode),
        siblings = parentNode.children
      siblings || console.error('No children bound', treeNode, parentNode)
      len = siblings.length
      if (len > 1) {
        heightSum = 0
        for (i = 0; i < len; i++)
          if (siblings[i].adcode !== treeNode.adcode) {
            boundsSize = this._ins._distMgr.getNodeBoundsSize(siblings[i], zoom)
            heightSum += boundsSize[1]
          }
        if (heightSum / (len - 1) < this._opts.minSiblingAvgHeightToShowSubFeatures) return !1
      }
    }
    return !0
  }
  _shouldShowSub(treeNode) {
    return !(!treeNode.children || !treeNode.children.length) && this.shouldShowSubOnZoom(treeNode, this._currentZoom)
  }
  _prepareFeatures(renderId, adcodes) {
    const justSelfList: any[] = [],
      showSubList: any[] = []
    for (let i = 0, len = adcodes.length; i < len; i++) {
      const treeNode = this._ins._distMgr.getNodeByAdcode(adcodes[i])
      if (!treeNode) throw new Error(`Can not find node: ${adcodes[i]}`)
      this._shouldShowSub(treeNode) ? showSubList.push(adcodes[i]) : justSelfList.push(adcodes[i])
    }
    this._prepareSelfFeatures(renderId, justSelfList)
    this._prepareSubFeatures(renderId, showSubList)
    this._checkLoadFinish(renderId)
  }
  _prepareSelfFeatures(renderId, adcodes) {
    let toLoadAdcode
    const currZoom = this._currentZoom as number
    for (let i = 0, len = adcodes.length; i < len; i++) {
      const treeNode = this._ins._distMgr.getNodeByAdcode(adcodes[i])
      toLoadAdcode = null
      if (treeNode.acroutes) {
        const parentNode = this._ins._distMgr.getNodeByAdcode(treeNode.acroutes[treeNode.acroutes.length - 1])
        ;(!treeNode.idealZoom ||
          currZoom < treeNode.idealZoom - 1 ||
          Math.abs(currZoom - parentNode.idealZoom) <= Math.abs(treeNode.idealZoom - currZoom)) &&
          (toLoadAdcode = parentNode.adcode)
      }
      this._loadAndRenderSelf(renderId, toLoadAdcode ? toLoadAdcode : adcodes[i], adcodes[i])
    }
  }
  _prepareSubFeatures(renderId, adcodes) {
    let i, len
    for (i = 0, len = adcodes.length; i < len; i++) {
      this._loadAndRenderSub(renderId, adcodes[i])
    }
  }
  _renderSelf(renderId, adcode, areaNode) {
    let feature
    if (adcode === areaNode.getAdcode()) {
      feature = areaNode.getParentFeature()
    } else {
      const subFeatures = areaNode.getSubFeatures(),
        subIdx = this._ins._distMgr.getSubIdx(adcode)
      feature = subFeatures[subIdx]
      if (!feature) {
        console.warn('Werid, can not find sub feature', areaNode.getAdcode(), adcode)
        return
      }
      if (feature.properties.adcode !== adcode) {
        console.warn('Sub adcode not match!!', subFeatures, subIdx)
        return
      }
    }
    this._ins.getDistCounter().calcDistGroup(
      adcode,
      !1,
      () => {
        this.isRenderIdStillValid(renderId) && this._prepRenderFeatureInPixel(renderId, feature)
      },
      this
    )
  }
  _checkLoadFinish(renderId) {
    if (0 === this._loadLeft) {
      const self = this
      setTimeout(function () {
        self.isRenderIdStillValid(renderId) && self._handleRenderFinish()
      }, 0)
    }
  }
  _renderSub(renderId, areaNode) {
    const subFeatures = areaNode.getSubFeatures()
    this._ins.getDistCounter().calcDistGroup(
      areaNode.getAdcode(),
      !0,
      () => {
        if (this.isRenderIdStillValid(renderId))
          for (let i = 0, len = subFeatures.length; i < len; i++)
            this._prepRenderFeatureInPixel(renderId, subFeatures[i])
      },
      this
    )
  }
  _handleRenderFinish() {
    this._tryFreeMemery()
    this._renderAllFeature()
  }
  _renderAllFeature() {
    // if (this._opts.engine === 'loca') {
    //   this._renderAllFeatureByLoca()
    // } else {
    this._renderAllFeatureByDefault()
    // }
  }
  /*_renderAllFeatureByLoca() {
    const featureData = {
      type: 'FeatureCollection',
      features: []
    } as any
    this._currentFeatures.forEach((item) => {
      const feature = item.feature
      feature.properties.dataItems = item.dataItems
      featureData.features.push(feature)
    })
    this._locaPolygonLayer.setSource(
      new window.Loca.GeoJSONSource({
        data: featureData
      })
    )
  }*/
  _renderAllFeatureByDefault() {
    // console.log('this._currentFeatures: ', this._currentFeatures)
    // 存储需要新增的面区划
    const needRenderPolygons: AMap.Polygon[] = []
    // 存储需要移除的面区划
    const needRemovePolygon: AMap.Polygon[] = []
    // 存储需要新增的聚合点
    const needRenderMarker: AMap.Marker[] = []
    // 存储需要移除的聚合点
    const needRemoveMarker: AMap.Marker[] = []
    for (let i = 0; i < this._polygonCache.length; i++) {
      const prePolygon = this._polygonCache[i]
      const adcode = prePolygon.getExtData()._data.adcode
      let isInCache = false
      for (let j = 0; j < this._currentFeatures.length; j++) {
        const item = this._currentFeatures[j]
        const feature = item.feature
        const props = feature.properties
        if (adcode === props.adcode) {
          isInCache = true
          this._currentFeatures.splice(j, 1)
          break
        }
      }
      if (!isInCache) {
        needRemovePolygon.push(prePolygon)
        this._polygonCache.splice(i, 1)
        needRemoveMarker.push(this._markerCache[i])
        this._markerCache.splice(i, 1)
        i--
      }
    }
    this._currentFeatures.forEach((item) => {
      const polygon = this._createPolygonFeature(item.feature, item.dataItems)
      if (this._opts.featureEventSupport) {
        polygon.on(
          'click',
          utils.bind((e) => {
            this.emit('featureClick', e, item.feature)
            if (this._opts.featureClickToShowSub) {
              this._ins.zoomToShowSubFeatures(item.feature.properties.adcode)
            }
          }, this)
        )
        polygon.on(
          'mouseover',
          utils.bind((e) => {
            this.emit('featureMouseover', e, item.feature)
          }, this)
        )
        polygon.on(
          'mouseout',
          utils.bind((e) => {
            this.emit('featureMouseout', e, item.feature)
          }, this)
        )
      }
      const marker = this._createClusterMarker(item.feature, item.dataItems)
      if (this._opts.clusterMarkerEventSupport) {
        marker.on(
          'click',
          utils.bind((e) => {
            this.emit('clusterMarkerClick', e, {
              adcode: item.feature.properties.adcode,
              ...item
            })
            if (this._opts.clusterMarkerClickToShowSub) {
              this._ins.zoomToShowSubFeatures(item.feature.properties.adcode)
            }
          }, this)
        )
      }
      needRenderPolygons.push(polygon)
      needRenderMarker.push(marker)
    })
    this.layer.remove(needRemovePolygon)
    this.markerGroup?.removeOverlays(needRemoveMarker)
    this.layer.add(needRenderPolygons)
    this._polygonCache.push(...needRenderPolygons)
    needRenderPolygons.length = 0
    this.markerGroup?.addOverlays(needRenderMarker)
    this._markerCache.push(...needRenderMarker)
    needRenderMarker.length = 0
  }
  _tryFreeMemery() {
    this._ins.getDistMgr().tryClearCache(this._currentRenderId, this._opts.areaNodeCacheLimit)
  }
  _increaseLoadLeft() {
    this._loadLeft++
  }
  _decreaseLoadLeft(renderId) {
    this._loadLeft--
    0 === this._loadLeft && this._checkLoadFinish(renderId)
  }
  _loadAndRenderSelf(renderId, loadAdcode, adcode) {
    this._ins.getDistMgr().touchAdcode(loadAdcode, renderId)
    const distExplorer = this._ins._distMgr.getExplorer(),
      areaNode = distExplorer.getLocalAreaNode(loadAdcode)
    if (areaNode) this._renderSelf(renderId, adcode, areaNode)
    else {
      this._increaseLoadLeft()
      distExplorer.loadAreaNode(
        loadAdcode,
        (error, areaNode) => {
          if (this.isRenderIdStillValid(renderId)) {
            error ? console.error(error) : this._renderSelf(renderId, adcode, areaNode)
            this._decreaseLoadLeft(renderId)
          }
        },
        this
      )
    }
  }
  _loadAndRenderSub(renderId, adcode) {
    this._ins.getDistMgr().touchAdcode(adcode, renderId)
    const distExplorer = this._ins._distMgr.getExplorer(),
      areaNode = distExplorer.getLocalAreaNode(adcode)
    if (areaNode) {
      this._renderSub(renderId, areaNode)
    } else {
      this._increaseLoadLeft()
      distExplorer.loadAreaNode(
        adcode,
        (error, areaNode) => {
          if (this.isRenderIdStillValid(renderId)) {
            error ? console.error(error) : this._renderSub(renderId, areaNode)
            this._decreaseLoadLeft(renderId)
          }
        },
        this
      )
    }
  }
  _prepRenderFeatureInPixel(renderId, feature) {
    if (!this._ins.getDistMgr().isExcludedAdcode(feature.properties.adcode)) {
      const dataItems = this._ins.getDistCounter().getPackItemsByAdcode(feature.properties.adcode)
      this._currentFeatures.push({
        feature,
        dataItems
      })
      // this.renderClusterMarker(renderId, feature, dataItems)
      // this.renderFeature(renderId, feature, null, dataItems)
    }
  }
  _createPolygonFeature(feature, dataItems) {
    const props = Object.assign({}, feature.properties)
    props.dataItems = dataItems
    if (this._opts.renderPolygon) {
      const polygon = this._opts.renderPolygon(feature, dataItems)
      const extData = polygon.getExtData() || {}
      extData._data = props
      polygon.setExtData(extData)
      return polygon
    }
    const styleOptions = this._getFeatureStyleOptions(feature, dataItems) || {}
    return new (AMap as any).Polygon({
      path: feature.geometry.coordinates,
      ...styleOptions,
      extData: {
        _data: props
      }
    })
  }
  _createClusterMarker(feature, dataItems) {
    const props = feature.properties
    props.dataItems = dataItems
    if (this._opts.renderClusterMarker) {
      const marker = this._opts.renderClusterMarker(feature, dataItems)
      const extData = marker.getExtData() || {}
      extData._data = props
      marker.setExtData(extData)
      return marker
    }
    const nodeClassNames = {
      title: 'amap-ui-district-cluster-marker-title',
      body: 'amap-ui-district-cluster-marker-body',
      container: 'amap-ui-district-cluster-marker'
    }
    const container = document.createElement('div')
    const title = document.createElement('span')
    title.className = nodeClassNames.title
    const body = document.createElement('span')
    body.className = nodeClassNames.body
    container.appendChild(title)
    container.appendChild(body)
    const routeNames: any[] = [],
      classNameList = [nodeClassNames.container, `level_${props.level}`, `adcode_${props.adcode}`]
    if (props.acroutes)
      for (let acroutes = props.acroutes, i = 0, len = acroutes.length; i < len; i++) {
        classNameList.push(`descendant_of_${acroutes[i]}`)
        i === len - 1 && classNameList.push(`child_of_${acroutes[i]}`)
        i > 0 && routeNames.push(this._ins._distMgr.getNodeByAdcode(acroutes[i]).name)
      }
    container.className = classNameList.join(' ')
    if (routeNames.length > 0) {
      routeNames.push(props.name)
      container.setAttribute('title', routeNames.join('>'))
    } else container.removeAttribute('title')
    title.innerHTML = utils.escapeHtml(props.name)
    body.innerHTML = dataItems.length

    return new AMap.Marker({
      topWhenClick: !0,
      offset: new AMap.Pixel(-20, -30),
      content: container,
      position: props.center,
      extData: {
        _data: props
      }
    })
  }
  _getFeatureStyleOptions(feature, dataItems): StyleOption {
    const styleGetter = this._opts.getFeatureStyle
    const defaultStyle = this._opts.featureStyleByLevel[feature.properties.level]
    if (!styleGetter) {
      return defaultStyle
    }
    const styleOptions = styleGetter.call(null, feature, dataItems)
    return !styleOptions
      ? defaultStyle
      : utils.extend({}, this._opts.featureStyleByLevel[feature.properties.level], styleOptions)
  }
  renderLater(delay?: number) {
    if (!this._renderLaterId) {
      this._renderLaterId = setTimeout(() => {
        this.render()
      }, delay || 100)
    }
  }
  isRendering() {
    return this._isRendering
  }
  render() {
    if (this._renderLaterId) {
      clearTimeout(this._renderLaterId)
      this._renderLaterId = null
    }
    this._isRendering = true
    this._ins._distMgr.onReady(this.renderViewport, this, !0)
  }
  forceRender() {
    if (this._renderLaterId) {
      clearTimeout(this._renderLaterId)
      this._renderLaterId = null
    }
    this._isRendering = true
    this.clear()
    this._ins._distMgr.onReady(this.renderViewport, this, !0)
  }
  getOption(k) {
    return this._opts[k]
  }
  getOptions() {
    return this._opts
  }
  show() {
    this.layer.show()
    this.markerGroup?.show()
  }
  hide() {
    this.layer.hide()
    this.markerGroup?.hide()
  }
  clear() {
    this.layer.clear()
    this.markerGroup?.clearOverlays()
    this._polygonCache = []
    this._markerCache = []
  }
  setzIndex(zIndex: number) {
    this.layer.setzIndex(zIndex)
  }
  getZooms() {
    return this._opts.zooms
  }
  destroy() {
    this._map.removeLayer(this.layer)
    this._map.remove(this.markerGroup as any)
    this._currentFeatures = []
    this.clear()
    this.layer = null
    this._map = null as any
    this._ins = null as any
  }
}
