const { execSync } = require('child_process');
const prefixes = require('autoprefixer/data/prefixes');
const browsers = require('caniuse-lite').agents;
const unpack = require('caniuse-lite').feature;
const features = require('caniuse-lite').features;
const mdn = require('@mdn/browser-compat-data');
const fs = require('fs');

const BROWSER_MAPPING = {
  and_chr: 'chrome',
  and_ff: 'firefox',
  ie_mob: 'ie',
  op_mob: 'opera',
  and_qq: null,
  and_uc: null,
  baidu: null,
  bb: null,
  kaios: null,
  op_mini: null,
  oculus: null,
};

const MDN_BROWSER_MAPPING = {
  chrome_android: 'chrome',
  firefox_android: 'firefox',
  opera_android: 'opera',
  safari_ios: 'ios_saf',
  webview_ios: 'ios_saf',
  samsunginternet_android: 'samsung',
  webview_android: 'android',
  oculus: null,
};

const latestBrowserVersions = {};
for (let b in browsers) {
  let versions = browsers[b].versions.slice(-10);
  for (let i = versions.length - 1; i >= 0; i--) {
    if (versions[i] != null && versions[i] != "all" && versions[i] != "TP") {
      latestBrowserVersions[b] = versions[i];
      break;
    }
  }
}

// Caniuse data for clip-path is incorrect.
// https://github.com/Fyrd/caniuse/issues/6209
prefixes['clip-path'].browsers = prefixes['clip-path'].browsers.filter(b => {
  let [name, version] = b.split(' ');
  return !(
    (name === 'safari' && parseVersion(version) >= (9 << 16 | 1 << 8)) ||
    (name === 'ios_saf' && parseVersion(version) >= (9 << 16 | 3 << 8))
  );
});

prefixes['any-pseudo'] = {
  browsers: Object.entries(mdn.css.selectors.is.__compat.support)
    .flatMap(([key, value]) => {
      if (Array.isArray(value)) {
        key = MDN_BROWSER_MAPPING[key] || key;
        let any = value.find(v => v.alternative_name?.includes('-any'))?.version_added;
        let supported = value.find(x => x.version_added && !x.alternative_name)?.version_added;
        if (any && supported) {
          let parts = supported.split('.');
          parts[0]--;
          supported = parts.join('.');
          return [`${key} ${any}}`, `${key} ${supported}`];
        }
      }

      return [];
    })
}

// Safari 4-13 supports background-clip: text with a prefix.
prefixes['background-clip'].browsers.push('safari 13');
prefixes['background-clip'].browsers.push('ios_saf 4', 'ios_saf 13');

let flexSpec = {};
let oldGradient = {};
let p = new Map();
for (let prop in prefixes) {
  let browserMap = {};
  for (let b of prefixes[prop].browsers) {
    let [name, version, variant] = b.split(' ');
    if (BROWSER_MAPPING[name] === null) {
      continue;
    }
    let prefix = browsers[name].prefix_exceptions?.[version] || browsers[name].prefix;

    // https://github.com/postcss/autoprefixer/blob/main/lib/hacks/backdrop-filter.js#L11
    if (prefix === 'ms' && prop === 'backdrop-filter') {
      prefix = 'webkit';
    }

    let origName = name;
    let isCurrentVersion = version === latestBrowserVersions[name];
    name = BROWSER_MAPPING[name] || name;
    let v = parseVersion(version);
    if (v == null) {
      console.log('BAD VERSION', prop, name, version);
      continue;
    }
    if (browserMap[name]?.[prefix] == null) {
      browserMap[name] = browserMap[name] || {};
      browserMap[name][prefix] = prefixes[prop].browsers.filter(b => b.startsWith(origName) || b.startsWith(name)).length === 1
        ? isCurrentVersion ? [null, null] : [null, v]
        : isCurrentVersion ? [v, null] : [v, v];
    } else {
      if (v < browserMap[name][prefix][0]) {
        browserMap[name][prefix][0] = v;
      }

      if (isCurrentVersion && browserMap[name][prefix][0] != null) {
        browserMap[name][prefix][1] = null;
      } else if (v > browserMap[name][prefix][1] && browserMap[name][prefix][1] != null) {
        browserMap[name][prefix][1] = v;
      }
    }

    if (variant === '2009') {
      if (flexSpec[name] == null) {
        flexSpec[name] = [v, v];
      } else {
        if (v < flexSpec[name][0]) {
          flexSpec[name][0] = v;
        }

        if (v > flexSpec[name][1]) {
          flexSpec[name][1] = v;
        }
      }
    } else if (variant === 'old' && prop.includes('gradient')) {
      if (oldGradient[name] == null) {
        oldGradient[name] = [v, v];
      } else {
        if (v < oldGradient[name][0]) {
          oldGradient[name][0] = v;
        }

        if (v > oldGradient[name][1]) {
          oldGradient[name][1] = v;
        }
      }
    }
  }
  addValue(p, browserMap, prop);
}


function addValue(map, value, prop) {
  let s = JSON.stringify(value);
  let found = false;
  for (let [key, val] of map) {
    if (JSON.stringify(val) === s) {
      key.push(prop);
      found = true;
      break;
    }
  }
  if (!found) {
    map.set([prop], value);
  }
}

let cssFeatures = [
  'css-sel2',
  'css-sel3',
  'css-gencontent',
  'css-first-letter',
  'css-first-line',
  'css-in-out-of-range',
  'form-validation',
  'css-any-link',
  'css-default-pseudo',
  'css-dir-pseudo',
  'css-focus-within',
  'css-focus-visible',
  'css-indeterminate-pseudo',
  'css-matches-pseudo',
  'css-optional-pseudo',
  'css-placeholder-shown',
  'dialog',
  'fullscreen',
  'css-marker-pseudo',
  'css-placeholder',
  'css-selection',
  'css-case-insensitive',
  'css-read-only-write',
  'css-autofill',
  'css-namespaces',
  'shadowdomv1',
  'css-rrggbbaa',
  'css-nesting',
  'css-not-sel-list',
  'css-has',
  'font-family-system-ui',
  'extended-system-fonts',
  'calc'
];

let cssFeatureMappings = {
  'css-dir-pseudo': 'DirSelector',
  'css-rrggbbaa': 'HexAlphaColors',
  'css-not-sel-list': 'NotSelectorList',
  'css-has': 'HasSelector',
  'css-matches-pseudo': 'IsSelector',
  'css-sel2': 'Selectors2',
  'css-sel3': 'Selectors3',
  'calc': 'CalcFunction'
};

let cssFeatureOverrides = {
  // Safari supports the ::marker pseudo element, but only supports styling some properties.
  // However this does not break using the selector itself, so ignore for our purposes.
  // https://bugs.webkit.org/show_bug.cgi?id=204163
  // https://github.com/parcel-bundler/lightningcss/issues/508
  'css-marker-pseudo': {
    safari: {
      'y #1': 'y'
    }
  }
};

let compat = new Map();
for (let feature of cssFeatures) {
  let data = unpack(features[feature]);
  let overrides = cssFeatureOverrides[feature];
  let browserMap = {};
  for (let name in data.stats) {
    if (BROWSER_MAPPING[name] === null) {
      continue;
    }

    name = BROWSER_MAPPING[name] || name;
    let browserOverrides = overrides?.[name];
    for (let version in data.stats[name]) {
      let value = data.stats[name][version];
      value = browserOverrides?.[value] || value;
      if (value === 'y') {
        let v = parseVersion(version);
        if (v == null) {
          console.log('BAD VERSION', feature, name, version);
          continue;
        }

        if (browserMap[name] == null || v < browserMap[name]) {
          browserMap[name] = v;
        }
      }
    }
  }

  let name = (cssFeatureMappings[feature] || feature).replace(/^css-/, '');
  addValue(compat, browserMap, name);
}

// No browser supports custom media queries yet.
addValue(compat, {}, 'custom-media-queries');

let mdnFeatures = {
  doublePositionGradients: mdn.css.types.gradient['radial-gradient'].doubleposition.__compat.support,
  clampFunction: mdn.css.types.clamp.__compat.support,
  placeSelf: mdn.css.properties['place-self'].__compat.support,
  placeContent: mdn.css.properties['place-content'].__compat.support,
  placeItems: mdn.css.properties['place-items'].__compat.support,
  overflowShorthand: mdn.css.properties['overflow'].multiple_keywords.__compat.support,
  mediaRangeSyntax: mdn.css['at-rules'].media.range_syntax.__compat.support,
  mediaIntervalSyntax: Object.fromEntries(
    Object.entries(mdn.css['at-rules'].media.range_syntax.__compat.support)
      .map(([browser, value]) => {
        // Firefox supported only ranges and not intervals for a while.
        if (Array.isArray(value)) {
          value = value.filter(v => !v.partial_implementation)
        } else if (value.partial_implementation) {
          value = undefined;
        }

        return [browser, value];
      })
  ),
  logicalBorders: mdn.css.properties['border-inline-start'].__compat.support,
  logicalBorderShorthand: mdn.css.properties['border-inline'].__compat.support,
  logicalBorderRadius: mdn.css.properties['border-start-start-radius'].__compat.support,
  logicalMargin: mdn.css.properties['margin-inline-start'].__compat.support,
  logicalMarginShorthand: mdn.css.properties['margin-inline'].__compat.support,
  logicalPadding: mdn.css.properties['padding-inline-start'].__compat.support,
  logicalPaddingShorthand: mdn.css.properties['padding-inline'].__compat.support,
  logicalInset: mdn.css.properties['inset-inline-start'].__compat.support,
  logicalSize: mdn.css.properties['inline-size'].__compat.support,
  logicalTextAlign: mdn.css.properties['text-align'].start.__compat.support,
  labColors: mdn.css.types.color.lab.__compat.support,
  oklabColors: mdn.css.types.color.oklab.__compat.support,
  colorFunction: mdn.css.types.color.color.__compat.support,
  spaceSeparatedColorNotation: mdn.css.types.color.rgb.space_separated_parameters.__compat.support,
  textDecorationThicknessPercent: mdn.css.properties['text-decoration-thickness'].percentage.__compat.support,
  textDecorationThicknessShorthand: mdn.css.properties['text-decoration'].includes_thickness.__compat.support,
  cue: mdn.css.selectors.cue.__compat.support,
  cueFunction: mdn.css.selectors.cue.selector_argument.__compat.support,
  anyPseudo: Object.fromEntries(
    Object.entries(mdn.css.selectors.is.__compat.support)
      .map(([key, value]) => {
        if (Array.isArray(value)) {
          value = value
            .filter(v => v.alternative_name?.includes('-any'))
            .map(({ alternative_name, ...other }) => other);
        }

        if (value && value.length) {
          return [key, value];
        } else {
          return [key, { version_added: false }];
        }
      })
  ),
  partPseudo: mdn.css.selectors.part.__compat.support,
  imageSet: mdn.css.types.image['image-set'].__compat.support,
  xResolutionUnit: mdn.css.types.resolution.x.__compat.support,
  nthChildOf: mdn.css.selectors['nth-child'].of_syntax.__compat.support,
  minFunction: mdn.css.types.min.__compat.support,
  maxFunction: mdn.css.types.max.__compat.support,
  roundFunction: mdn.css.types.round.__compat.support,
  remFunction: mdn.css.types.rem.__compat.support,
  modFunction: mdn.css.types.mod.__compat.support,
  absFunction: mdn.css.types.abs.__compat.support,
  signFunction: mdn.css.types.sign.__compat.support,
  hypotFunction: mdn.css.types.hypot.__compat.support,
  gradientInterpolationHints: mdn.css.types.gradient['linear-gradient'].interpolation_hints.__compat.support,
  borderImageRepeatRound: mdn.css.properties['border-image-repeat'].round.__compat.support,
  borderImageRepeatSpace: mdn.css.properties['border-image-repeat'].space.__compat.support,
  fontSizeRem: mdn.css.properties['font-size'].rem_values.__compat.support,
  fontSizeXXXLarge: mdn.css.properties['font-size']['xxx-large'].__compat.support,
  fontStyleObliqueAngle: mdn.css.properties['font-style']['oblique-angle'].__compat.support,
  fontWeightNumber: mdn.css.properties['font-weight'].number.__compat.support,
  fontStretchPercentage: mdn.css.properties['font-stretch'].percentage.__compat.support,
  lightDark: mdn.css.types.color['light-dark'].__compat.support,
  accentSystemColor: mdn.css.types.color['system-color'].accentcolor_accentcolortext.__compat.support,
  animationTimelineShorthand: mdn.css.properties.animation['animation-timeline_included'].__compat.support,
  viewTransition: mdn.css.selectors['view-transition'].__compat.support,
  detailsContent: mdn.css.selectors['details-content'].__compat.support,
  targetText: mdn.css.selectors['target-text'].__compat.support,
  picker: mdn.css.selectors.picker.__compat.support,
  pickerIcon: mdn.css.selectors['picker-icon'].__compat.support,
  checkmark: mdn.css.selectors.checkmark.__compat.support,
  grammarError: mdn.css.selectors['grammar-error'].__compat.support,
  spellingError: mdn.css.selectors['spelling-error'].__compat.support,
};

for (let key in mdn.css.types.length) {
  if (key === '__compat') {
    continue;
  }

  let feat = key.includes('_')
    ? key.replace(/_([a-z])/g, (_, l) => l.toUpperCase())
    : key + 'Unit';

  mdnFeatures[feat] = mdn.css.types.length[key].__compat.support;
}

for (let key in mdn.css.types.gradient) {
  if (key === '__compat') {
    continue;
  }

  let feat = key.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
  mdnFeatures[feat] = mdn.css.types.gradient[key].__compat.support;
}

const nonStandardListStyleType = new Set([
  // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type#non-standard_extensions
  'ethiopic-halehame',
  'ethiopic-halehame-am',
  'ethiopic-halehame-ti-er',
  'ethiopic-halehame-ti-et',
  'hangul',
  'hangul-consonant',
  'urdu',
  'cjk-ideographic',
  // https://github.com/w3c/csswg-drafts/issues/135
  'upper-greek'
]);

for (let key in mdn.css.properties['list-style-type']) {
  if (key === '__compat' || nonStandardListStyleType.has(key) || mdn.css.properties['list-style-type'][key].__compat.support.chrome.version_removed) {
    continue;
  }

  let feat = key[0].toUpperCase() + key.slice(1).replace(/-([a-z])/g, (_, l) => l.toUpperCase()) + 'ListStyleType';
  mdnFeatures[feat] = mdn.css.properties['list-style-type'][key].__compat.support;
}

for (let key in mdn.css.properties['width']) {
  if (key === '__compat' || key === 'animatable') {
    continue;
  }

  let feat = key[0].toUpperCase() + key.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + 'Size';
  mdnFeatures[feat] = mdn.css.properties['width'][key].__compat.support;
}

Object.entries(mdn.css.properties.width.stretch.__compat.support)
  .filter(([, v]) => v.alternative_name)
  .forEach(([k, v]) => {
    let name = v.alternative_name.slice(1).replace(/[-_]([a-z])/g, (_, l) => l.toUpperCase()) + 'Size';
    mdnFeatures[name] ??= {};
    mdnFeatures[name][k] = {version_added: v.version_added};
  });

for (let feature in mdnFeatures) {
  let browserMap = {};
  for (let name in mdnFeatures[feature]) {
    if (MDN_BROWSER_MAPPING[name] === null) {
      continue;
    }

    let feat = mdnFeatures[feature][name];
    let version;
    if (Array.isArray(feat)) {
      version = feat.filter(x => x.version_added && !x.alternative_name && !x.flags).sort((a, b) => parseVersion(a.version_added) < parseVersion(b.version_added) ? -1 : 1)[0].version_added;
    } else if (!feat.alternative_name && !feat.flags) {
      version = feat.version_added;
    }

    if (!version) {
      continue;
    }

    let v = parseVersion(version);
    if (v == null) {
      console.log('BAD VERSION', feature, name, version);
      continue;
    }

    name = MDN_BROWSER_MAPPING[name] || name;
    browserMap[name] = v;
  }

  addValue(compat, browserMap, feature);
}

addValue(compat, {
  safari: parseVersion('10.1'),
  ios_saf: parseVersion('10.3')
}, 'p3Colors');

addValue(compat, {
  // https://github.com/WebKit/WebKit/commit/baed0d8b0abf366e1d9a6105dc378c59a5f21575
  safari: parseVersion('10.1'),
  ios_saf: parseVersion('10.3')
}, 'LangSelectorList');

let prefixMapping = {
  webkit: 'WebKit',
  moz: 'Moz',
  ms: 'Ms',
  o: 'O'
};

let flags = [
  'Nesting',
  'NotSelectorList',
  'DirSelector',
  'LangSelectorList',
  'IsSelector',
  'TextDecorationThicknessPercent',
  'MediaIntervalSyntax',
  'MediaRangeSyntax',
  'CustomMediaQueries',
  'ClampFunction',
  'ColorFunction',
  'OklabColors',
  'LabColors',
  'P3Colors',
  'HexAlphaColors',
  'SpaceSeparatedColorNotation',
  'FontFamilySystemUi',
  'DoublePositionGradients',
  'VendorPrefixes',
  'LogicalProperties',
  'LightDark',
  ['Selectors', ['Nesting', 'NotSelectorList', 'DirSelector', 'LangSelectorList', 'IsSelector']],
  ['MediaQueries', ['MediaIntervalSyntax', 'MediaRangeSyntax', 'CustomMediaQueries']],
  ['Colors', ['ColorFunction', 'OklabColors', 'LabColors', 'P3Colors', 'HexAlphaColors', 'SpaceSeparatedColorNotation', 'LightDark']],
];

let enumify = (f) => f.replace(/^@([a-z])/, (_, x) => 'At' + x.toUpperCase()).replace(/^::([a-z])/, (_, x) => 'PseudoElement' + x.toUpperCase()).replace(/^:([a-z])/, (_, x) => 'PseudoClass' + x.toUpperCase()).replace(/(^|-)([a-z])/g, (_, a, x) => x.toUpperCase())

let allBrowsers = Object.keys(browsers).filter(b => !(b in BROWSER_MAPPING)).sort();
let browsersRs = `pub struct Browsers {
  pub ${allBrowsers.join(': Option<u32>,\n  pub ')}: Option<u32>
}`;
let flagsRs = `pub struct Features: u32 {
    ${flags.map((flag, i) => {
      if (Array.isArray(flag)) {
        return `const ${flag[0]} = ${flag[1].map(f => `Self::${f}.bits()`).join(' | ')};`
      } else {
        return `const ${flag} = 1 << ${i};`;
      }
    }).join('\n    ')}
  }`;
let targets = fs.readFileSync('src/targets.rs', 'utf8')
  .replace(/pub struct Browsers \{((?:.|\n)+?)\}/, browsersRs)
  .replace(/pub struct Features: u32 \{((?:.|\n)+?)\}/, flagsRs);

fs.writeFileSync('src/targets.rs', targets);
execSync('rustfmt src/targets.rs');

let targets_dts = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!

export interface Targets {
  ${allBrowsers.join('?: number,\n  ')}?: number
}

export const Features: {
  ${flags.map((flag, i) => {
    if (Array.isArray(flag)) {
      return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`
    } else {
      return `${flag}: ${1 << i},`;
    }
  }).join('\n  ')}
};
`;

fs.writeFileSync('node/targets.d.ts', targets_dts);

let flagsJs = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!

exports.Features = {
  ${flags.map((flag, i) => {
    if (Array.isArray(flag)) {
      return `${flag[0]}: ${flag[1].reduce((p, f) => p | (1 << flags.indexOf(f)), 0)},`
    } else {
      return `${flag}: ${1 << i},`;
    }
  }).join('\n  ')}
};
`;

fs.writeFileSync('node/flags.js', flagsJs);

let s = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!

use crate::vendor_prefix::VendorPrefix;
use crate::targets::Browsers;

#[allow(dead_code)]
pub enum Feature {
  ${[...p.keys()].flat().map(enumify).sort().join(',\n  ')}
}

impl Feature {
  pub fn prefixes_for(&self, browsers: Browsers) -> VendorPrefix {
    let mut prefixes = VendorPrefix::None;
    match self {
      ${[...p].map(([features, versions]) => {
  return `${features.map(name => `Feature::${enumify(name)}`).join(' |\n      ')} => {
        ${Object.entries(versions).map(([name, prefixes]) => {
          let needsVersion = !Object.values(prefixes).every(([min, max]) => min == null && max == null);
    return `if ${needsVersion ? `let Some(version) = browsers.${name}` : `browsers.${name}.is_some()`} {
          ${Object.entries(prefixes).map(([prefix, [min, max]]) => {
      if (!prefixMapping[prefix]) {
        throw new Error('Missing prefix ' + prefix);
      }
      let addPrefix = `prefixes |= VendorPrefix::${prefixMapping[prefix]};`;
      let condition;
      if (min == null && max == null) {
        return addPrefix;
      } else if (min == null) {
        condition = `version <= ${max}`;
      } else if (max == null) {
        condition = `version >= ${min}`;
      } else if (min == max) {
        condition = `version == ${min}`;
      } else {
        condition = `version >= ${min} && version <= ${max}`;
      }

      return `if ${condition} {
            ${addPrefix}
          }`
    }).join('\n          ')}
        }`;
  }).join('\n        ')}
      }`
}).join(',\n      ')}
    }
    prefixes
  }
}

pub fn is_flex_2009(browsers: Browsers) -> bool {
  ${Object.entries(flexSpec).map(([name, [min, max]]) => {
  return `if let Some(version) = browsers.${name} {
    if version >= ${min} && version <= ${max} {
      return true;
    }
  }`;
}).join('\n  ')}
  false
}

pub fn is_webkit_gradient(browsers: Browsers) -> bool {
  ${Object.entries(oldGradient).map(([name, [min, max]]) => {
  return `if let Some(version) = browsers.${name} {
    if version >= ${min} && version <= ${max} {
      return true;
    }
  }`;
}).join('\n  ')}
  false
}
`;

fs.writeFileSync('src/prefixes.rs', s);
execSync('rustfmt src/prefixes.rs');

let c = `// This file is autogenerated by build-prefixes.js. DO NOT EDIT!

use crate::targets::Browsers;

#[allow(dead_code)]
#[derive(Clone, Copy, PartialEq)]
pub enum Feature {
  ${[...compat.keys()].flat().map(enumify).sort().join(',\n  ')}
}

impl Feature {
  pub fn is_compatible(&self, browsers: Browsers) -> bool {
    match self {
      ${[...compat].map(([features, supportedBrowsers]) =>
  `${features.map(name => `Feature::${enumify(name)}`).join(' |\n      ')} => {` + (Object.entries(supportedBrowsers).length === 0 ? '\n        return false\n      }' : `
        ${Object.entries(supportedBrowsers).map(([browser, min]) =>
    `if let Some(version) = browsers.${browser} {
          if version < ${min} {
            return false
          }
        }`).join('\n        ')}${Object.keys(supportedBrowsers).length === allBrowsers.length ? '' : `\n        if ${allBrowsers.filter(b => !supportedBrowsers[b]).map(browser => `browsers.${browser}.is_some()`).join(' || ')} {
          return false
        }`}
      }`
  )).join('\n      ')}
    }
    true
  }

  pub fn is_partially_compatible(&self, targets: Browsers) -> bool {
    let mut browsers = Browsers::default();
    ${allBrowsers.map(browser => `if targets.${browser}.is_some() {
      browsers.${browser} = targets.${browser};
      if self.is_compatible(browsers) {
        return true
      }
      browsers.${browser} = None;
    }\n`).join('    ')}
    false
  }
}
`;

fs.writeFileSync('src/compat.rs', c);
execSync('rustfmt src/compat.rs');


function parseVersion(version) {
  version = version.replace('≤', '');
  let [major, minor = '0', patch = '0'] = version
    .split('-')[0]
    .split('.')
    .map(v => parseInt(v, 10));

  if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
    return null;
  }

  return major << 16 | minor << 8 | patch;
}
