let next_property_id = 1;

// Generate a unique property name on the form --prop-N.
function generate_name() {
  return `--prop-${next_property_id++}`;
}

// Produce a compatible initial value for the specified syntax.
function any_initial_value(syntax) {
  let components = syntax.split('|').map(x => x.trim())
  let first_component = components[0];

  if (first_component.endsWith('+') || first_component.endsWith('#'))
    first_component = first_component.slice(0, -1);

  switch (first_component) {
    case '*':
    case '<custom-ident>':
      return 'NULL';
    case '<angle>':
      return '0deg';
    case '<color>':
      return 'rgb(0, 0, 0)';
    case '<image>':
    case '<url>':
      return 'url(0)';
    case '<integer>':
    case '<length-percentage>':
    case '<length>':
    case '<number>':
      return '0';
    case '<percentage>':
      return '0%';
    case '<resolution>':
      return '0dpi';
    case '<time>':
      return '0s';
    case '<transform-function>':
    case '<transform-list>':
      return 'matrix(0, 0, 0, 0, 0, 0)';
    default:
      // We assume syntax is a specific custom ident.
      return first_component;
  }
}

// Registers a unique property on the form '--prop-N' and returns the name.
// Any value except 'syntax' may be omitted, in which case the property will
// not inherit, and some undefined (but compatible) initial value will be
// generated. If a single string is used as the argument, it is assumed to be
// the syntax.
function generate_property(reg) {
  // Verify that only valid keys are specified. This prevents the caller from
  // accidentally supplying 'inherited' instead of 'inherits', for example.
  if (typeof(reg) === 'object') {
    const permitted = new Set(['name', 'syntax', 'initialValue', 'inherits']);
    if (!Object.keys(reg).every(k => permitted.has(k)))
      throw new Error('generate_property: invalid parameter');
  }

  let syntax = typeof(reg) === 'string' ? reg : reg.syntax;
  let initial = typeof(reg.initialValue) === 'undefined' ? any_initial_value(syntax)
                                                         : reg.initialValue;
  let inherits = typeof(reg.inherits) === 'undefined' ? false : reg.inherits;

  let name = generate_name();
  CSS.registerProperty({
    name: name,
    syntax: syntax,
    initialValue: initial,
    inherits: inherits
  });
  return name;
}

function all_syntaxes() {
  return [
    '*',
    '<angle>',
    '<color>',
    '<custom-ident>',
    '<image>',
    '<integer>',
    '<length-percentage>',
    '<length>',
    '<number>',
    '<percentage>',
    '<resolution>',
    '<time>',
    '<transform-function>',
    '<transform-list>',
    '<url>'
  ]
}

function with_style_node(text, fn) {
  let node = document.createElement('style');
  node.textContent = text;
  try {
    document.body.append(node);
    fn(node);
  } finally {
    node.remove();
  }
}

function with_at_property(desc, fn) {
  let name = typeof(desc.name) === 'undefined' ? generate_name() : desc.name;
  let text = `@property ${name} {`;
  if (typeof(desc.syntax) !== 'undefined')
    text += `syntax:${desc.syntax};`;
  if (typeof(desc.initialValue) !== 'undefined')
    text += `initial-value:${desc.initialValue};`;
  if (typeof(desc.inherits) !== 'undefined')
    text += `inherits:${desc.inherits};`;
  text += '}';
  with_style_node(text, (node) => fn(name, node.sheet.rules[0]));
}

function test_with_at_property(desc, fn, description) {
  test(() => with_at_property(desc, fn), description);
}

function test_with_style_node(text, fn, description) {
  test(() => with_style_node(text, fn), description);
}

function animation_test(property, values, description) {
  const name = generate_name();
  property.name = name;
  CSS.registerProperty(property);

  test(() => {
    const duration = 1000;
    const keyframes = {};
    keyframes[name] = values.keyframes;

    const iterations = 3;
    const composite = values.composite || "replace";
    const iterationComposite = values.iterationComposite || "replace";
    const animation = target.animate(keyframes, { composite, iterationComposite, iterations, duration });
    animation.pause();
    // We seek to the middle of the third iteration which will allow to test cases where
    // iterationComposite is set to something other than "replace".
    animation.currentTime = duration * 2.5;

    assert_equals(getComputedStyle(target).getPropertyValue(name), values.expected);
  }, description);
};

function discrete_animation_test(syntax, fromValue, toValue, description) {
  test(() => {
    const name = generate_name();

    CSS.registerProperty({
      name,
      syntax,
      inherits: false,
      initialValue: fromValue
    });

    const duration = 1000;
    const keyframes = [];
    keyframes[name] = toValue;
    const animation = target.animate(keyframes, duration);
    animation.pause();

    const checkAtProgress = (progress, expected) => {
      animation.currentTime = duration * 0.25;
      assert_equals(getComputedStyle(target).getPropertyValue(name), fromValue, `The correct value is used at progress = ${progress}`);
    };

    checkAtProgress(0, fromValue);
    checkAtProgress(0.25, fromValue);
    checkAtProgress(0.49, fromValue);
    checkAtProgress(0.5, toValue);
    checkAtProgress(0.75, toValue);
    checkAtProgress(1, toValue);
  }, description || `Animating a custom property of type ${syntax} is discrete`);
}

function transition_test(options, description) {
  promise_test(async () => {
    const customProperty = generate_name();

    options.transitionProperty ??= customProperty;

    CSS.registerProperty({
      name: customProperty,
      syntax: options.syntax,
      inherits: false,
      initialValue: options.from
    });

    assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value");

    const transitionEventPromise = new Promise(resolve => {
      let listener = event => {
          target.removeEventListener("transitionrun", listener);
          assert_equals(event.propertyName, customProperty, "TransitionEvent has the expected property name");
          resolve();
      };
      target.addEventListener("transitionrun", listener);
    });

    target.style.transition = `${options.transitionProperty} 1s -500ms linear`;
    target.style.setProperty(customProperty, options.to);

    const animations = target.getAnimations();
    assert_equals(animations.length, 1, "A single animation is running");

    const transition = animations[0];
    assert_class_string(transition, "CSSTransition", "A CSSTransition is running");

    transition.pause();
    assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.expected, "Element has the expected animated value");

    await transitionEventPromise;
  }, description);
}

function no_transition_test(options, description) {
  test(() => {
    const customProperty = generate_name();

    CSS.registerProperty({
      name: customProperty,
      syntax: options.syntax,
      inherits: false,
      initialValue: options.from
    });

    assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.from, "Element has the expected initial value");

    target.style.transition = `${customProperty} 1s -500ms linear`;
    target.style.setProperty(customProperty, options.to);

    assert_equals(target.getAnimations().length, 0, "No animation was created");
    assert_equals(getComputedStyle(target).getPropertyValue(customProperty), options.to, "Element has the expected final value");
  }, description);
};
