import SnowflakeId from 'snowflake-id'
const snowflake = new SnowflakeId() // 生成雪花id方法 snowflake.generate()

/**
 * 雪花id
 * @returns {string}
 */
export function getSnowflake(): string {
  return String(snowflake.generate())
}

/**
 * uuid
 * @returns {string}
 */
export function uuid(): string {
  const s: string[] = []
  const hexDigits = '0123456789abcdef'
  for (let i = 0; i < 36; i++) {
    s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
  }
  s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
  s[19] = hexDigits.substr((Number(s[19]) & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
  s[8] = s[13] = s[18] = s[23] = ''

  const uuid = s.join('')
  return uuid
}

/**
 * 字符串坐标转数组
 * @param {String|Array} point '100,100' 字符串坐标
 * @return {Array} [100,200]
 */
export function pointToArray(point) {
  return Array.isArray(point) ? point.map(Number) : point.split(',').map(Number)
}

/**
 * 转换网格
 * @author sunidea
 * @param {String} point '123,325' 或 '321'
 * @returns {String} '120,330' 或 '320
 */
export function pointToGrid(point) {
  if (Array.isArray(point)) {
    return point.map(pointToGrid)
  }
  return Math.round(point / 10) * 10
}

// 点到点的距离
export function dist2d(p1, p2) {
  const { 0: x1, 1: y1 } = p1
  const { 0: x2, 1: y2 } = p2
  const dx = x1 - x2
  const dy = y1 - y2
  return Math.sqrt(dx * dx + dy * dy)
}

/**
* 获取两点间距离
* @param {Array<number, number>} p1 第一个点坐标
* @param {Array<number, number>} p1 第二个点坐标
* @returns {number}
*/
export function getDistance(p1, p2) {
  const x = p1[0] - p2[0]
  const y = p1[1] - p2[1]
  return Math.hypot(x, y) // Math.sqrt(x * x + y * y);
}

/**
* 获取中点坐标
* @param {Array<number, number>} p1 第一个点坐标
* @param {Array<number, number>} p1 第二个点坐标
* @returns {Array<number, number>}
*/
export function getCenterByPoints(p1, p2) {
  const x = (p1[0] + p2[0]) / 2
  const y = (p1[1] + p2[1]) / 2
  return [x, y]
}

/**
 * 判断两个点集是否相等
 * @param {Array} coord1
 * @param {Array} coord2
 * @returns {Boolean}
 */
export function equals(coord1: number[], coord2: number[]): boolean {
  let equals = true
  for (let i = coord1.length - 1; i >= 0; --i) {
    if (coord1[i] !== coord2[i]) {
      equals = false
      break
    }
  }
  return equals
}

// 计算两个点之间的方向/与x轴的角度
export function angle(p1, p2) {
  const { 0: x1, 1: y1 } = p1
  const { 0: x2, 1: y2 } = p2
  const radian = Math.atan((y2 - y1) / (x2 - x1))
  const angle = radian * 180 / Math.PI
  return angle
}

/**
 * 线段上距离点P最近的一个点
 * getNearestCoord([2.2, 3.1], [[1, 1], [2, 3], [3, 3], [4, 2], [2, 0]])
 * @param {Array} point 点P [2.2, 3.1]
 * @param {Array} lines 线段 [[1, 1], [2, 3], [3, 3], [4, 2], [2, 0]]
 * @returns {Object} {dist: 0.5099019513592785, index: 1, point: [2, 3]}
 */
export function getNearestCoord(point: number[], lines: number[] | number[][]) {
  let d
  const res: {dist: number, index?: number, point?: number[]} = { dist: Infinity, index: undefined, point: undefined }
  if (Array.isArray(lines) && !lines.some((line: number | number[]) => Array.isArray(line))) {
    lines = [lines] as number[][]
  }
  lines.forEach(function (coords) {
    for (let i = 0; i < coords.length; i++) {
      d = dist2d(point, coords[i])
      if (d < res.dist) {
        res.dist = d
        res.index = i
        res.point = coords[i]
      }
    }
  })
  return res.point ? res : null
}

/**
 * 点P到线段AB的最短距离
 * 使用矢量算法，计算线AP在线段AB方向上的投影，当需要计算的数据量很大时，这种方式优势明显
 * 特殊情况如点在线段上、点在端点、点在线段延长线上等等的情况全部适用于此公式，只是作为特殊情况出现，无需另作讨论。这也是矢量算法思想的优势所在。
 * pointToSegmentDist([1, 3], [0, 1], [3, 1])
 * @param {Array} point 点P
 * @param {Array} point1 线段点A
 * @param {Array} point2 线段点B
 * @returns {Object} { dist: 2, point: [1, 1], type: 0 } point 投影坐标  dist 点P到投影距离  type 垂足位置，不为0表示垂足在线段外
 */
export function pointToSegmentDist(point, point1, point2) {
  const x = point[0], x1 = point1[0], x2 = point2[0]
  const y = point[1], y1 = point1[1], y2 = point2[1]

  // 线段AB 为一个点
  if (x1 === x2 && y1 === y2) {
    return {
      type: 0,
      point: point1,
      dist: 0
    }
  }

  const cross = (x2 - x1) * (x - x1) + (y2 - y1) * (y - y1)
  // let r = cross / d2
  // r < 0 点P的垂足在线段AB外，且点P距离线段AB最近的点为A
  // r = 0 点P的垂足和点P距离线段AB最近的点为A
  if (cross <= 0) {
    return {
      type: 1,
      point: point1,
      dist: Math.sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1))
    }
  }

  const d2 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
  // r > 1 点P的垂足在线段AB外，且点P距离线段AB最近的点为B
  // r = 1 点P的垂足和点P距离线段AB最近的点为B
  if (cross >= d2) {
    return {
      type: 2,
      point: point2,
      dist: Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2))
    }
  }

  const r = cross / d2
  const px = x1 + (x2 - x1) * r
  const py = y1 + (y2 - y1) * r
  return {
    type: 0,
    point: [px, py],
    dist: Math.sqrt((x - px) * (x - px) + (py - y) * (py - y))
  }
}

/**
 * 计算点到直线垂足坐标
 * @param {Array} point [200,200] 线段外点坐标
 * @param {Array} p1 [100,100] 线段坐标起始坐标
 * @param {Array} p2 [150,150] 线段坐标结束坐标
 * @returns {
 *   type: Boolean, // 垂足在线段内或外 true内 false外
 *   point: Array, // 垂足点坐标[100,100]
 *   dist: Number, // 点P到垂足距离
 * }
 */
export function pointToFootDist(point, p1, p2) {
  const { 0: x, 1: y } = point
  const { 0: x1, 1: y1 } = p1
  const { 0: x2, 1: y2 } = p2

  // 线段AB 为一个点
  if (x1 === x2 && y1 === y2) {
    return {
      type: true,
      point: p1,
      dist: 0
    }
  }

  const dx = x2 - x1
  const dy = y2 - y1
  // r < 0 点P的垂足在线段AB外，且点P距离线段AB最近的点为A
  // r > 1 点P的垂足在线段AB外，且点P距离线段AB最近的点为B
  // r = 0 点P的垂足和点P距离线段AB最近的点为A
  // r = 1 点P的垂足和点P距离线段AB最近的点为B
  const r = (dx * (x - x1) + dy * (y - y1)) / (dx * dx + dy * dy || 0)

  const px = x1 + dx * r
  const py = y1 + dy * r
  return {
    type: r >= 0 && r <= 1, // true  垂足在线段内   false 垂足在线段外
    point: [px, py], // 垂足
    dist: Math.sqrt((x - px) * (x - px) + (py - y) * (py - y)) // 点P到垂足距离
  }
}

/**
 * 得到目标相邻的2个点
 * @author sunidea
 * @param {Array<number>} point 目标点
 * @param {Array<Array<number>>} segments 连线中的点集合[[1,2],[5,10],[100,100]]
 * @returns {Object} 返回目标相邻的2个点，未找到返回null
 */
export const pointInSegments = (point: number[], segments: number[][]) => {
  let obj = {
    type: true,
    point: [],
    segment: [] as number[][],
    dist: 1000,
    index: null as number | null
  }
  for (let i = 1; i < segments.length; i++) {
    const p1 = segments[i - 1]
    const p2 = segments[i]
    const result = pointToFootDist(point, p1, p2)
    if (result.type && result.dist < obj.dist) {
      obj = {
        ...result,
        index: Number(i),
        segment: [p1, p2]
      }
    }
  }
  return obj
}
// export const pointInSegments = (p, segments) => {
//   for (let i = 1; i < segments.length; i++) {
//     let pi = segments[i - 1]
//     let pj = segments[i]
//     const minX = Math.min(pi[0], pj[0])
//     const maxX = Math.max(pi[0], pj[0])
//     const minY = Math.min(pi[1], pj[1])
//     const maxY = Math.max(pi[1], pj[1])
//     const x = p[0], y = p[1]
//     // 拖拽点必须在，2个点形成的矩形范围内
//     if (x <= minX || x >= maxX || y <= minY || y >= maxY) continue
//     const result = getSlope(pi, p, pj)
//     // 判断3点共线，误差10像素，k<=2600
//     if (Math.abs(result) <= 2600) {
//       return [pi, pj]
//     }
//   }
//   return null
// }

/**
 * 删除三点共线中间所有的点
 * @author sunidea
 * @param {*} segments 连线中的所有点集合
 */
export const filterPointInSegments = (segments) => {
  if (segments.length < 3) return
  for (let i = 1; i < segments.length; i++) {
    if (!segments[i + 1]) return
    const p1 = segments[i - 1]
    const p2 = segments[i + 1]
    const point = segments[i]
    const result = pointToFootDist(point, p1, p2)
    if (result.dist < 5) {
      segments.splice(i, 1)
      --i
    }
  }
}

/**
 * 斜率算法公式
 * @author sunidea
 * @param {Array} a [100,100]
 * @param {Array} b [150,150]
 * @param {Array} c [200,200]
 * @returns k === 0 则三点共线
 */
export const getSlope = (a, b, c) => {
  // 斜率算法公式，改为乘法，避免除数为0
  const { 0: x1, 1: y1 } = a
  const { 0: x2, 1: y2 } = b
  const { 0: x3, 1: y3 } = c
  return (y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)
}
// /**
//  * 斜率算法公式
//  * @author sunidea
//  * @param {Array} a [100,100]
//  * @param {Array} b [150,150]
//  * @param {Array} c [200,200]
//  * @returns x2,y2
//  */
// export function getSlopeXY2 (a, b, c) {
//   // 斜率算法公式，改为乘法，避免除数为0
//   const {0: x1, 1: y1} = a
//   const {0: x2, 1: y2} = b
//   const {0: x3, 1: y3} = c
//   const y3y1 = y3 - y1
//   const x3x1 = x3 - x1
//   const flag = y3y1 - y2 > x3x1 - x2 // 是否用x2求y2，否则用y2求x2
//   let getX2, getY2
//   if (flag) {
//     getY2 = (y3y1 * (x2 - x1) + (y1 * x3x1)) / x3x1
//     getX2 = (y3y1 * x1 + x3x1 * (y2 - y1)) / y3y1
//   }
//   return `${getX2},${getY2}`
// }


/**
 * 三角形面积求三点共线
 * @param {Array} a [100,100]
 * @param {Array} b [150,150]
 * @param {Array} c [200,200]
 * @returns s === 0 则三点共线
 */
export const collinear3Points = (a, b, c) => {
  const distance = (a, b) => {
    const x1 = a.x - b.x
    const y1 = a.y - b.y
    const z1 = a.z - b.z
    return Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1)
  }
  // 是否共线 海伦公式 S=sqrt(p(p-a)(p-b)(p-c)) p=(a+b+c)/2
  const getCollinear3Points = (a, b, c) => {
    const edgeA = distance(a, b)
    const edgeB = distance(b, c)
    const edgeC = distance(a, c)
    const p = 0.5 * (edgeA + edgeB + edgeC)
    if (p * (p - edgeA) * (p - edgeB) * (p - edgeC)) return false // 面积大于零 就是一个三角形 三点不共线
    return true
  }

  a = { x: a[0], y: a[1], z: 0 }
  b = { x: b[0], y: b[1], z: 0 }
  c = { x: c[0], y: c[1], z: 0 }
  return getCollinear3Points(a, b, c)
}


/**
 * 线段的长度
 * formatLength([[1, 0], [2, 1], [3, 0], [4, 2]])
 * @param {Array} coords 线段
 * @returns 5.06449510224598
 */
export function formatLength(coords) {
  coords = coords || []
  let length = 0
  // 通过遍历坐标计算两点之前距离，进而得到整条线的长度
  for (let i = 0, leng = coords.length - 1; i < leng; i++) {
    length += dist2d(coords[i], coords[i + 1])
  }
  return length
}

/**
 * 根据偏移量偏移一条线段
 * lineOffset([[1, 1], [2, 3], [3, 3], [4, 2], [2, 0]], 2, 3)
 * @param {Array} coords
 * @param {Number} deltaX
 * @param {Number} deltaY
 * @returns [[3, 4], [4, 6], [5, 6], [6, 5], [4, 3]]
 */
export function lineOffset(coords, deltaX, deltaY) {
  deltaX = deltaX || 0
  deltaX = isNaN(deltaX) ? 0 : deltaX
  deltaY = deltaY || 0
  deltaY = isNaN(deltaY) ? 0 : deltaY

  if (deltaX === 0 && deltaY === 0) return coords

  coords.forEach(coord => {
    coord[0] += deltaX
    coord[1] += deltaY
  })
  return coords
}

/**
 * 根据距离和角度偏移一条线段
 * offsetLine([[1, 1], [4, 2], [5, 5], [7, 6]], 5, 30)
 * @param {Array} line
 * @param {Number} d
 * @param {Number} angle
 * @returns [[5.330127018922194,3.4999999999999996],[8.330127018922195,4.5],[9.330127018922195,7.5],[11.330127018922195,8.5]]
 */
export function offsetLine(line, d, angle) {
  // let line = [[1, 1], [4, 2], [5, 5], [7, 6]]
  // let d = 1
  // let angle = 30

  // 斜边长度d已知，角度angle已知
  // 对边长度就是y的偏移量 就是 d * sin(angle) ==> d * Math.sin(angle * Math.PI / 180)
  // 邻边长度就是x的偏移量 就是 d * cos(angle) ==> d * Math.cos(angle * Math.PI / 180)
  const ox = d * Math.cos(angle * Math.PI / 180)
  const oy = d * Math.sin(angle * Math.PI / 180)
  return line.map(coords => [coords[0] + ox, coords[1] + oy])
}

/**
 * 根据一个点切割线段
 * 点不必落在线段上
 * 常用于计算鼠标位置距离线段起点或终点的距离
 * 简单版的是使用 getNearestCoord，而不是 pointToSegmentDist
 * getNearestCoord 寻找的是线上真实存在的一点
 * pointToSegmentDist 寻找的点位于线上，但不一定真实存在与线上
 * getClosestPoint([[0, 1], [2, 3], [5, 6], [3, 4], [4, 4]], [3, 5])
 * [
    [[0,1],[2,3],[3.5,4.5]],
    [[3.5,4.5],[5,6],[3,4],[4,4]]
   ]
 * @param {Array} coords
 * @param {Array} point
 * @returns
 */
export function getClosestPoint(coords, point) {
  if (!coords || coords.length < 2) return [[], []]

  let squaredDistance: { dist: number, point: number[] } = { dist: Infinity, point: [] }, index
  for (let i = 1; i < coords.length; i++) {
    const d = pointToSegmentDist(point, coords[i - 1], coords[i])
    if (d.dist < squaredDistance.dist) {
      squaredDistance = d
      index = i
    }
  }

  if (index === undefined) {
    return [coords, []]
  }

  const prearr = coords.slice(0, index)
  if (prearr.length && !equals(squaredDistance.point, prearr[prearr.length - 1])) {
    prearr.push(squaredDistance.point)
  }

  const nextarr = coords.slice(index)
  if (nextarr.length && !equals(squaredDistance.point, nextarr[0])) {
    nextarr.unshift(squaredDistance.point)
  }

  return [prearr, nextarr]
}

/**
 * 线段与线段的交点
 * 解线性方程组, 求线段AB与线段CD的交点坐标，如果没有交点，返回null
 * intersects([[0,0],[1,1]], [[3,0],[2,1]])  //null
 * intersects([[0,0],[1,1]], [[3,0],[0,1]])  //[0.75, 0.75]
 * @param {Array} coords1 线段1
 * @param {Array} coords2 线段2
 * @returns {Array|null}
 */
export function intersects(coords1: number[][], coords2: number[][]): number[] | null {
  const x1 = coords1[0][0]
  const y1 = coords1[0][1]
  const x2 = coords1[1][0]
  const y2 = coords1[1][1]
  const x3 = coords2[0][0]
  const y3 = coords2[0][1]
  const x4 = coords2[1][0]
  const y4 = coords2[1][1]
  // 斜率交叉相乘 k1 = (y4 - y3) / (x4 - x3)    k2 = (y2 - y1) / (x2 - x1)
  // k1 k2 同乘 (x4 - x3) * (x2 - x1) 并相减得到denom
  const denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
  // 如果分母为0 则平行或共线, 不相交
  if (denom === 0) {
    return null
  }

  const numeA = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
  const numeB = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))
  const uA = numeA / denom
  const uB = numeB / denom

  // 交点在线段1上，且交点也在线段2上
  if (uA >= 0 && uA <= 1 && uB >= 0 && uB <= 1) {
    const x = x1 + (uA * (x2 - x1))
    const y = y1 + (uA * (y2 - y1))
    return [x, y]
  }
  return null
}

/**
 * 线与线的所有交点
 * 接收两条线的点集合，求取line2 和 line1的交点
 * 如果规定count参数，代表返回前多少个交点，否则全部返回
 * 返回交点集合，集合中的元素属性包括 index1：交点在line1的位置，index2：交点在line2的位置，coords：交点坐标
 * 代码参考truf.lineIntersect：http://turfjs.org/docs/#lineIntersect
 * 如果交点为线的点，则会重复返回，包括返回的数据结构，都是为下面切割面函数splitPolygon服务的
 * lineIntersect([[0, 0], [0, 1], [1, 3], [3, 2], [5, 0]], [[-1, 0], [4, 3]])
 * // [{"index1":1,"index2":1,"coords":[0,0.6]},{"index1":3,"index2":1,"coords":[2.6363636363636367,2.1818181818181817]}]
 * @param {Array} line1 线1
 * @param {Array} line2 线2
 * @param {Array} count 交点坐标 不必传
 * @returns {Array<{index: number; index2: number; coodes: number[]}>}
 */
export function lineIntersect(line1: number[][], line2: number[][], count?: number) {
  const result: Array<{ index1: number, index2: number, coords: number[] }> = []
  for (let i = 1; i < line1.length; i++) {
    const coords1 = [line1[i - 1], line1[i]]
    // 求取数据的边界范围，函数放在了下面的多边形中
    const bbox1 = bbox(coords1)
    for (let j = 1; j < line2.length; j++) {
      const coords2 = [line2[j - 1], line2[j]]
      const bbox2 = bbox(coords2)
      // 判断两个边界范围的关系： bbox1 是否包含 bbox2，函数放在了下面的多边形中
      if (isIntersects(bbox1, bbox2)) {
        const p = intersects(coords1, coords2)
        if (p) {
          result.push({ index1: i, index2: j, coords: p })
          if (count && (result.length >= count)) {
            return result
          }
        }
      }
    }
  }
  return result
}

/**
 * 获取多边形边界范围
 * bbox([[1, 1], [2, 3], [3, 3], [4, 2], [2, 0]])
 * @param {Array} coords 多边形
 * @returns {Array} [1, 0, 4, 3]
 */
function bbox(coords) {
  // x/经度最小值 y/纬度最小值 x/经度最大值 y/纬度最大值
  const res = [Infinity, Infinity, -Infinity, -Infinity]
  coords.forEach(coord => {
    if (res[0] > coord[0]) res[0] = coord[0]
    if (res[2] < coord[0]) res[2] = coord[0]
    if (res[1] > coord[1]) res[1] = coord[1]
    if (res[3] < coord[1]) res[3] = coord[1]
  })
  return res
}

/**
 * 判断两个边界范围的关系： a 与 b 是否有交集
 * isIntersects([1, 0, 4, 3], [2, 2, 5, 5])  //true
 * isIntersects([1, 0, 4, 3], [5, 2, 5, 5])  //false
 * @param {Array} a 边界a
 * @param {Array} b 边界b
 * @returns {Boolean}
 */
function isIntersects(a, b) {
  return b[0] <= a[2] &&
    b[1] <= a[3] &&
    b[2] >= a[0] &&
    b[3] >= a[1]
}