// This file processes data from https://developer.mozilla.org/en-US/docs/Web

import bcd, { BrowserName, SimpleSupportStatement, SupportBlock } from '@mdn/browser-compat-data'
import { CSSFeature, CSSPrefixMap, CSSProperty, Engine, JSFeature, PrefixData, Support, SupportMap } from './index'

const supportedEnvironments: Record<string, Engine> = {
  chrome: 'Chrome',
  deno: 'Deno',
  edge: 'Edge',
  firefox: 'Firefox',
  ie: 'IE',
  nodejs: 'Node',
  opera: 'Opera',
  safari: 'Safari',
  safari_ios: 'IOS',
}

const jsFeatures: Partial<Record<JSFeature, string>> = {
  ClassStaticBlocks: 'javascript.classes.static.initialization_blocks',
  ExportStarAs: 'javascript.statements.export.namespace',
  FromBase64: 'javascript.builtins.Uint8Array.fromBase64',
  ImportAssertions: 'javascript.statements.import.import_assertions',
  ImportAttributes: 'javascript.statements.import.import_attributes',
  ImportMeta: 'javascript.operators.import_meta',
  RegexpMatchIndices: 'javascript.builtins.RegExp.hasIndices',
  TopLevelAwait: 'javascript.operators.await.top_level',
}

const cssFeatures: Partial<Record<CSSFeature, string | string[]>> = {
  ColorFunctions: [
    'css.types.color.color',
    'css.types.color.lab',
    'css.types.color.lch',
    'css.types.color.oklab',
    'css.types.color.oklch',
  ],
  GradientDoublePosition: [
    'css.types.gradient.conic-gradient.doubleposition',
    'css.types.gradient.linear-gradient.doubleposition',
    'css.types.gradient.radial-gradient.doubleposition',
    'css.types.gradient.repeating-linear-gradient.doubleposition',
    'css.types.gradient.repeating-radial-gradient.doubleposition',
  ],
  GradientInterpolation: [
    'css.types.gradient.conic-gradient.hue_interpolation_method',
    'css.types.gradient.conic-gradient.interpolation_color_space',
    'css.types.gradient.linear-gradient.hue_interpolation_method',
    'css.types.gradient.linear-gradient.interpolation_color_space',
    'css.types.gradient.radial-gradient.hue_interpolation_method',
    'css.types.gradient.radial-gradient.interpolation_color_space',
    'css.types.gradient.repeating-conic-gradient.hue_interpolation_method',
    'css.types.gradient.repeating-conic-gradient.interpolation_color_space',
    'css.types.gradient.repeating-linear-gradient.hue_interpolation_method',
    'css.types.gradient.repeating-linear-gradient.interpolation_color_space',
    'css.types.gradient.repeating-radial-gradient.hue_interpolation_method',
    'css.types.gradient.repeating-radial-gradient.interpolation_color_space',
  ],
  GradientMidpoints: [
    'css.types.gradient.linear-gradient.interpolation_hints',
    'css.types.gradient.radial-gradient.interpolation_hints',
    'css.types.gradient.repeating-linear-gradient.interpolation_hints',
    'css.types.gradient.repeating-radial-gradient.interpolation_hints',
  ],
  HexRGBA: 'css.types.color.rgb_hexadecimal_notation.alpha_hexadecimal_notation',
  HWB: 'css.types.color.hwb',
  InsetProperty: 'css.properties.inset',
  Modern_RGB_HSL: [
    'css.types.color.hsl.alpha_parameter',
    'css.types.color.hsl.space_separated_parameters',
    'css.types.color.rgb.alpha_parameter',
    'css.types.color.rgb.float_values',
    'css.types.color.rgb.space_separated_parameters',
  ],
  Nesting: 'css.selectors.nesting',
  RebeccaPurple: 'css.types.color.named-color.rebeccapurple',
}

const similarPrefixedProperty: Record<string, { prefix: string, property: string }> = {
  'css.properties.mask-composite': {
    prefix: '-webkit-',
    property: 'css.properties.-webkit-mask-composite',
  },
}

const cssPrefixFeatures: Record<string, CSSProperty> = {
  'css.properties.height.stretch': 'DHeight',
  'css.properties.mask': 'DMask',
  'css.properties.mask-composite': 'DMaskComposite',
  'css.properties.mask-image': 'DMaskImage',
  'css.properties.mask-origin': 'DMaskOrigin',
  'css.properties.mask-position': 'DMaskPosition',
  'css.properties.mask-repeat': 'DMaskRepeat',
  'css.properties.mask-size': 'DMaskSize',
  'css.properties.max-height.stretch': 'DMaxHeight',
  'css.properties.max-width.stretch': 'DMaxWidth',
  'css.properties.min-height.stretch': 'DMinHeight',
  'css.properties.min-width.stretch': 'DMinWidth',
  'css.properties.text-decoration-color': 'DTextDecorationColor',
  'css.properties.text-decoration-line': 'DTextDecorationLine',
  'css.properties.text-decoration-skip': 'DTextDecorationSkip',
  'css.properties.text-emphasis-color': 'DTextEmphasisColor',
  'css.properties.text-emphasis-position': 'DTextEmphasisPosition',
  'css.properties.text-emphasis-style': 'DTextEmphasisStyle',
  'css.properties.user-select': 'DUserSelect',
  'css.properties.width.stretch': 'DWidth',
}

const alternativeNameToPrefix: Record<string, string> = {
  '-webkit-fill-available': '-webkit-',
  '-moz-available': '-moz-',
}

export const js: SupportMap<JSFeature> = {} as SupportMap<JSFeature>
export const css: SupportMap<CSSFeature> = {} as SupportMap<CSSFeature>
export const cssPrefix: CSSPrefixMap = {}

const isSemver = /^\d+(?:\.\d+(?:\.\d+)?)?$/

const compareVersions = (aStr: string, bStr: string): number => {
  const a = aStr.split('.')
  const b = bStr.split('.')
  let diff = +a[0] - +b[0]
  if (diff === 0) {
    diff = +(a[1] || '0') - +(b[1] || '0')
    if (diff === 0) {
      diff = +(a[2] || '0') - +(b[2] || '0')
    }
  }
  return diff
}

const extractProperty = (object: any, fullKey: string): any => {
  for (const key of fullKey.split('.')) {
    object = object[key]
  }
  if (!object) throw new Error(`Failed to find "${fullKey}"`)
  return object
}

const addFeatures = <F extends string>(map: SupportMap<F>, features: Partial<Record<F, string | string[]>>): void => {
  for (const feature in features) {
    const keys = features[feature]
    const maxVersions: Partial<Record<Engine, { version: string, isSupported: boolean }>> = {}

    for (const fullKey of Array.isArray(keys) ? keys : [keys]) {
      const support: SupportBlock = extractProperty(bcd, fullKey).__compat.support

      for (const env in support) {
        const engine = supportedEnvironments[env]

        if (engine) {
          const entries = support[env as BrowserName]!

          for (const { flags, version_added, version_removed, partial_implementation } of Array.isArray(entries) ? entries : [entries]) {
            if (typeof version_added === 'string' && isSemver.test(version_added)) {
              // The feature isn't considered to be supported if it was removed,
              // if it requires a flag, or if it's only partially-implemented
              const isSupported = (!version_removed || !flags) && !partial_implementation
              const maxVersion = maxVersions[engine]
              if (
                !maxVersion ||
                compareVersions(version_added, maxVersion.version) > 0 ||
                (compareVersions(version_added, maxVersion.version) === 0 && !isSupported)
              ) {
                maxVersions[engine] = { version: version_added, isSupported }
              }
            }
          }
        }
      }
    }

    const engines: Partial<Record<Engine, Record<string, Support>>> = {}
    for (const engine in maxVersions) {
      const { version, isSupported } = maxVersions[engine as Engine]!
      engines[engine as Engine] = { [version]: { force: isSupported } }
    }
    map[feature] = engines
  }
}

addFeatures(js, jsFeatures)
addFeatures(css, cssFeatures)

for (const fullKey in cssPrefixFeatures) {
  const prefixData: PrefixData[] = []
  const support: SupportBlock = extractProperty(bcd, fullKey).__compat.support

  for (const env in support) {
    const engine = supportedEnvironments[env]

    if (engine) {
      const initialEntries = support[env as BrowserName]!
      let entries: SimpleSupportStatement[] = Array.isArray(initialEntries) ? initialEntries : [initialEntries]

      // Figure out which version this property can be used unprefixed, if any.
      // This assumes that support for these CSS properties is never removed.
      // This assumption is wrong (Edge removed many features when it changed
      // its engine from EdgeHTML to Blink, basically becoming another browser)
      // but we ignore those cases for now.
      let version_unprefixed: string | undefined
      for (const { prefix, flags, version_added, version_removed, alternative_name } of entries) {
        if (!prefix && !alternative_name && !flags && typeof version_added === 'string' && !version_removed && isSemver.test(version_added)) {
          version_unprefixed = version_added
        }
      }

      type PrefixRange = { prefix: string, start: string, end?: string }
      const ranges: PrefixRange[] = []

      // The MDN dataset sometimes doesn't list prefixes if the values for the
      // prefixed property are sufficiently different. In that case, we may need
      // to search for the prefix information within another property instead.
      const similar = similarPrefixedProperty[fullKey]
      if (similar) {
        const similarSupport: SupportBlock = extractProperty(bcd, similar.property).__compat.support
        const similarEntries = similarSupport[env as BrowserName]
        if (!similarEntries) continue
        entries = Array.isArray(similarEntries) ? similarEntries : [similarEntries]
      }

      // Find all version ranges where a given prefix is supported
      for (let i = 0; i < entries.length; i++) {
        let { prefix, flags, version_added, version_removed, alternative_name } = entries[i]

        if (similar) {
          if (prefix) throw new Error(`Unexpected prefix "${prefix}" for similar property "${similar.property}"`)
          prefix = similar.prefix
        }

        if ((prefix || alternative_name) && !flags && typeof version_added === 'string' && isSemver.test(version_added)) {
          const range: PrefixRange = { prefix: prefix || alternativeNameToPrefix[alternative_name!], start: version_added }
          let withoutPrefix: string | undefined

          // The prefix is no longer needed if support for the feature was removed
          if (typeof version_removed === 'string' && isSemver.test(version_removed)) {
            withoutPrefix = version_removed
          }

          // The prefix is no longer needed if it can be used unprefixed
          if (version_unprefixed && (!withoutPrefix || compareVersions(version_unprefixed, withoutPrefix) < 0)) {
            withoutPrefix = version_unprefixed
          }

          if (withoutPrefix) {
            if (compareVersions(version_added, withoutPrefix) === 0) {
              // No prefix is needed if support for the property with and without the prefix was added simultaneously
              continue
            }
            range.end = withoutPrefix
          }

          ranges.push(range)
        }
      }

      // Sort earlier versions first, then sort prefixes for equal versions lexicographically
      ranges.sort((a, b) => compareVersions(a.start, b.start) || +(a.prefix > b.prefix) - +(a.prefix < b.prefix))

      for (let i = 0; i < ranges.length; i++) {
        const { prefix, start, end } = ranges[i]

        // Skip this prefix if it's entirely covered by the previous prefix.
        // Sometimes engines add support for multiple prefixes at a time. For
        // example, in version 12 Edge added support for both "-ms-user-select"
        // and "-webkit-user-select", so we don't need to generate both.
        if (i > 0) {
          const prev = ranges[i - 1]
          if (compareVersions(start, prev.start) >= 0 && (!prev.end || (end && compareVersions(end, prev.end) <= 0))) {
            continue
          }
        }

        const data: PrefixData = { engine, prefix: prefix.replace(/^-|-$/g, '') }
        if (end) {
          data.withoutPrefix = end.split('.').map((x: string) => +x)
        }
        prefixData.push(data)
      }
    }
  }

  cssPrefix[cssPrefixFeatures[fullKey]] = prefixData
}
