import olFeature from 'ol/Feature'
import olGeomLineString from 'ol/geom/LineString'
import olGeomMultiPoint from 'ol/geom/MultiPoint'
import olStyleFill from 'ol/style/Fill'
import olStyleStroke from 'ol/style/Stroke'
import olStyleStyle from 'ol/style/Style'
import olStyleCircle from 'ol/style/Circle'
import olStyleText from 'ol/style/Text'
import olPoint from 'ol/geom/Point'
import centroid from '@turf/centroid'

import { pointsFromVertices, olKitTurf, pairCoordinates, getCoordinates, getText, calculateScale } from './utils'

function resolveStyleFunctionArgs (args) {
  // Using function.prototype.bind with additional arguments injects those arguments at the zeroth index of the arguements object and since opts is optional we need to handle a variable arguement object
  const argLength = args.length
  const feature = args[argLength - 2] || args
  const resolution = args[argLength - 1]
  const opts = argLength >= 3 ? args[0] : {}

  return { feature, resolution, opts }
}

/**
 * Style ol/Features
 * @function
 * @param {object} opts - The config object
 * @param {ol/Feature} feature - The feature you want to style
 * @param {number} resolution - the resolution of the map
 * @returns {object} The style object for the passed feature
 */
function styleText (...args) {
  const { feature, resolution, opts } = resolveStyleFunctionArgs(args)
  const label = feature.get('_vmf_label')
  const isMeasurement = feature.get('_vmf_type') === '_vmf_measurement'
  const isCentroidLabel = feature.get('_ol_kit_needs_centroid_label')
  const offsetY = isCentroidLabel ? 20 : isMeasurement ? 0 : (label.fontSize || 16) / 2 // eslint-disable-line
  const styleOpts = {
    placement: opts.placement || 'point',
    textAlign: opts.textAlign,
    textBaseline: opts.textBaseLine || 'top',
    maxAngle: opts.maxAngle || Infinity,
    // These weird calculations provide the most accurate placement relative to the textarea where people edit their text
    offsetX: isMeasurement ? 0 : (label.fontSize || 16) / 2,
    offsetY: offsetY,
    rotation: -feature.get('_vmf_rotation') || 0,
    font: `bold ${label.fontSize || 16}px sans-serif`,
    stroke: new olStyleStroke({
      // show a black outline unless the text color is black; then show a white outline
      color: label.color === '#000000' ? '#ffffff' : '#000000',
      width: 3
    }),
    text: getText(label, resolution, opts),
    scale: calculateScale(opts.store.map, feature),
    fill: new olStyleFill({
      color: label.color || '#ffffff'
    }),
    image: new olStyleCircle({
      radius: 7,
      fill: new olStyleFill({
        color: '#ffcc33'
      })
    }),
    rotateWithView: false,
    overflow: true
  }

  return new olStyleText(styleOpts)
}

function coordinateLabels (multiPoint, resolution, opts) {
  return multiPoint.getPoints().map(point => coordinateLabel(point, resolution, opts))
}

function coordinateLabel (pointGeometry, resolution, opts) {
  const geom = pointGeometry.clone()
  const pointFeature = new olFeature({
    geometry: geom,
    _vmf_label: {
      fontSize: 16
    }
  })

  return new olStyleStyle({
    text: styleText({
      store: opts,
      placement: 'point',
      maxAngle: Math.PI / 4,
      textAlign: undefined,
      textBaseline: 'hanging'
    }, pointFeature, resolution),
    geometry: geom
  })
}

function lengthLabel (lineGeometry, resolution, opts) {
  const geom = lineGeometry.clone()
  const perimeterFeature = new olFeature({
    geometry: geom,
    _vmf_type: '_vmf_measurement',
    _vmf_label: {
      fontSize: 16
    }
  })

  return new olStyleStyle({
    text: styleText({
      store: opts,
      placement: 'line',
      maxAngle: Math.PI / 4,
      textAlign: undefined,
      textBaseline: 'hanging'
    }, perimeterFeature, resolution),
    geometry: geom
  })
}

function perimeterSegmentLabels (polygonGeometry, resolution, opts) {
  const labelStyles = []
  const clonedGeom = polygonGeometry.clone()
  const perimeterCoords = clonedGeom.getLinearRing(0).getCoordinates()

  for (let i = 0; i < perimeterCoords.length - 1;) {
    const segment = [perimeterCoords[i], perimeterCoords[i += 1]]

    if (segment.flat(Infinity).includes(undefined)) break // exit the loop if we get any undefined values.  It's better to not label a segment than to break draw entirely.
    const segmentGeom = new olGeomLineString(segment)
    const segmentFeature = new olFeature({
      geometry: segmentGeom,
      _vmf_type: '_vmf_measurement',
      _vmf_label: {
        fontSize: 16
      }
    })

    labelStyles.push(new olStyleStyle({
      text: styleText({
        store: opts,
        placement: 'line',
        textBaseline: 'hanging'
      }, segmentFeature, resolution),
      geometry: segmentGeom
    }))
  }

  return labelStyles
}

function areaLabel (polygonGeometry, resolution, opts) {
  const areaGeometry = polygonGeometry.clone()
  const areaFeature = new olFeature({
    geometry: areaGeometry,
    _vmf_type: '_vmf_measurement',
    _vmf_label: {
      fontSize: 16
    }
  })

  return new olStyleStyle({
    text: styleText({
      store: opts
    }, areaFeature, resolution),
    geometry: areaGeometry
  })
}

function centroidLabel (geometry, resolution, opts) {
  const point = geometry.getType() === 'Circle ' ? new olPoint(geometry.clone().getCenter()) : olKitTurf(centroid, [geometry]).getGeometry()
  const pointFeature = new olFeature({
    geometry: point,
    _vmf_type: '_vmf_measurement', // styleText determines the type of label to render based on the feature's type so we need this temporary feature to be a 'measurement' feature
    _vmf_label: {
      fontSize: 16
    },
    _ol_kit_needs_centroid_label: true
  })

  return new olStyleStyle({
    text: styleText({
      store: opts,
      placement: 'point',
      maxAngle: Math.PI / 4,
      textAlign: undefined,
      textBaseline: 'hanging'
    }, pointFeature, resolution),
    geometry: point
  })
}

function getVertices (args) {
  const { feature } = resolveStyleFunctionArgs(args)
  const geometry = feature.getGeometry()
  const layout = geometry.getLayout()

  switch (geometry.getType()) {
    case 'MultiPolygon': {
      const coordinates = geometry.getCoordinates()
      const flatCoords = coordinates.flat(Infinity)
      const pairedCoords = pairCoordinates(flatCoords, layout.length)

      return new olGeomMultiPoint(pairedCoords, layout)
    }
    case 'GeometryCollection': {
      const deepCoords = getCoordinates(geometry)
      const flatCoords = deepCoords.flat(Infinity)
      const pairedCoords = pairCoordinates(flatCoords, layout.length)

      return new olGeomMultiPoint(pairedCoords)
    }
    default:
      return new olGeomMultiPoint(pointsFromVertices(geometry))
  }
}

/**
 * Style an ol/Feature with orange circle vertices, a blue outline, an area label, and a perimeter length label. Can be used with individual features as a style function or call it directly to get a style object for use with `vectorContext.drawFeature`
 * @function
 * @param {object} opts - The config object
 * @param {ol/Feature} feature - The feature you want to style
 * @param {number} resolution - the resolution of the map
 * @returns {object} The style object for the passed feature
 */
export function immediateEditStyle (...args) {
  const { feature, resolution, opts = {} } = resolveStyleFunctionArgs(args)
  const fill = new olStyleFill({
    color: 'rgba(0, 0, 255, 0.2)'
  })
  const stroke = new olStyleStroke({
    color: 'blue',
    width: 3
  })
  const image = new olStyleCircle({
    radius: 7,
    fill: new olStyleFill({
      color: '#ffcc33'
    })
  })
  const vertexGeometry = getVertices(args)
  const vertices = [new olStyleStyle({
    image: new olStyleCircle({
      radius: 5,
      fill: new olStyleFill({
        color: 'orange'
      })
    }),
    geometry: vertexGeometry
  })]

  // checking to see if opts.geometry is defined allows us to call this function recursively for geometry collections
  const geometry = opts.geometry || feature.clone().getGeometry()
  const areaLabelsFlag = feature.get('_ol_kit_area_labels')
  const distanceLabelsFlag = feature.get('_ol_kit_distance_labels')
  const needsVertexLabels = feature.get('_ol_kit_coordinate_labels') !== undefined
  const needsCentroidLabels = feature.get('_ol_kit_needs_centroid_label') !== undefined
  const needsAreaLabels = opts.showMeasurements && areaLabelsFlag
  const needsDistanceLabels = opts.showMeasurements && distanceLabelsFlag
  const isNotCircle = feature.get('_ol_kit_draw_mode') !== 'circle'
  const vertexLabels = (needsVertexLabels && opts.showMeasurements && isNotCircle) ? coordinateLabels(getVertices(feature), resolution, opts) : [] // eslint-disable-line
  const type = geometry.getType()

  switch (type) {
    case 'Point':
      return [new olStyleStyle({
        image
      }), ...vertexLabels]
    case 'LineString': {
      const lengthLabels = needsDistanceLabels ? [lengthLabel(geometry, resolution, opts)] : []

      return [new olStyleStyle({
        stroke,
        image
      }), ...vertices, ...lengthLabels, ...vertexLabels]
    }
    case 'MultiLineString': {
      const lineStrings = geometry.getLineStrings()
      const lengthLabels = needsDistanceLabels
        ? lineStrings.map(lineString => lengthLabel(lineString, resolution, opts)) : []

      return [new olStyleStyle({
        stroke,
        image
      }), ...vertices, ...lengthLabels, ...vertexLabels]
    }
    case 'MultiPolygon': {
      const polygons = geometry.getPolygons()
      // create a label for each polygon
      const perimeterLabels = needsDistanceLabels
        ? polygons.map(polygon => perimeterSegmentLabels(polygon, resolution, opts)) : []
      const areaLabels = needsAreaLabels
        ? polygons.map(polygon => areaLabel(polygon, resolution, opts)) : []

      return [new olStyleStyle({
        stroke,
        fill,
        image
      }), ...vertices, ...areaLabels, ...perimeterLabels.flat(Infinity), ...vertexLabels]
    }
    case 'Polygon': {
      let labels = vertexLabels // eslint-disable-line

      if (needsAreaLabels) labels.push(areaLabel(geometry, resolution, opts))
      if (needsDistanceLabels && isNotCircle) labels = [...labels, ...perimeterSegmentLabels(geometry, resolution, opts)] //eslint-disable-line
      if (needsCentroidLabels) labels.push(centroidLabel(geometry, resolution, opts))

      return [new olStyleStyle({
        stroke,
        fill,
        image
      }), ...vertices, ...labels]
    }
    case 'GeometryCollection': {
      // Recursive.  Since feature stores our metadata it needs to be preserved.  Therefore we pass the geometry we want to use through the opts object.
      const componentStyles = geometry.getGeometries().map(geom => {
        return immediateEditStyle.apply(this, [Object.assign(opts, { geometry: geom }), feature, resolution])
      })
      const flatStyles = componentStyles.flat(Infinity)

      return flatStyles
    }
    case 'Circle': {
      const labels = vertexLabels

      if (needsAreaLabels) labels.push(areaLabel(geometry, resolution, opts))
      if (needsCentroidLabels) labels.push(centroidLabel(geometry, resolution, opts))

      return [new olStyleStyle({
        stroke,
        fill
      }), ...labels]
    }
    default:
      return [new olStyleStyle({
        stroke,
        fill,
        image
      }), ...vertices]
  }
}