/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
const {Keyframes} = require("devtools/client/animationinspector/components/keyframes");

/**
 * UI component responsible for displaying detailed information for a given
 * animation.
 * This includes information about timing, easing, keyframes, animated
 * properties.
 *
 * @param {Object} serverTraits The list of server-side capabilities.
 */
function AnimationDetails(serverTraits) {
  EventEmitter.decorate(this);

  this.onFrameSelected = this.onFrameSelected.bind(this);

  this.keyframeComponents = [];
  this.serverTraits = serverTraits;
}

exports.AnimationDetails = AnimationDetails;

AnimationDetails.prototype = {
  // These are part of frame objects but are not animated properties. This
  // array is used to skip them.
  NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],

  init: function (containerEl) {
    this.containerEl = containerEl;
  },

  destroy: function () {
    this.unrender();
    this.containerEl = null;
    this.serverTraits = null;
  },

  unrender: function () {
    for (let component of this.keyframeComponents) {
      component.off("frame-selected", this.onFrameSelected);
      component.destroy();
    }
    this.keyframeComponents = [];

    while (this.containerEl.firstChild) {
      this.containerEl.firstChild.remove();
    }
  },

  getPerfDataForProperty: function (animation, propertyName) {
    let warning = "";
    let className = "";
    if (animation.state.propertyState) {
      let isRunningOnCompositor;
      for (let propState of animation.state.propertyState) {
        if (propState.property == propertyName) {
          isRunningOnCompositor = propState.runningOnCompositor;
          if (typeof propState.warning != "undefined") {
            warning = propState.warning;
          }
          break;
        }
      }
      if (isRunningOnCompositor && warning == "") {
        className = "oncompositor";
      } else if (!isRunningOnCompositor && warning != "") {
        className = "warning";
      }
    }
    return {className, warning};
  },

  /**
   * Get a list of the tracks of the animation actor
   * @return {Object} A list of tracks, one per animated property, each
   * with a list of keyframes
   */
  getTracks: Task.async(function* () {
    let tracks = {};

    /*
     * getFrames is a AnimationPlayorActor method that returns data about the
     * keyframes of the animation.
     * In FF48, the data it returns change, and will hold only longhand
     * properties ( e.g. borderLeftWidth ), which does not match what we
     * want to display in the animation detail.
     * A new AnimationPlayerActor function, getProperties, is introduced,
     * that returns the animated css properties of the animation and their
     * keyframes values.
     * If the animation actor has the getProperties function, we use it, and if
     * not, we fall back to getFrames, which then returns values we used to
     * handle.
     */
    if (this.serverTraits.hasGetProperties) {
      let properties = yield this.animation.getProperties();
      for (let {name, values} of properties) {
        if (!tracks[name]) {
          tracks[name] = [];
        }

        for (let {value, offset} of values) {
          tracks[name].push({value, offset});
        }
      }
    } else {
      let frames = yield this.animation.getFrames();
      for (let frame of frames) {
        for (let name in frame) {
          if (this.NON_PROPERTIES.indexOf(name) != -1) {
            continue;
          }

          if (!tracks[name]) {
            tracks[name] = [];
          }

          tracks[name].push({
            value: frame[name],
            offset: frame.computedOffset
          });
        }
      }
    }

    return tracks;
  }),

  render: Task.async(function* (animation) {
    this.unrender();

    if (!animation) {
      return;
    }
    this.animation = animation;

    // We might have been destroyed in the meantime, or the component might
    // have been re-rendered.
    if (!this.containerEl || this.animation !== animation) {
      return;
    }

    // Build an element for each animated property track.
    this.tracks = yield this.getTracks(animation, this.serverTraits);

    // Useful for tests to know when the keyframes have been retrieved.
    this.emit("keyframes-retrieved");

    for (let propertyName in this.tracks) {
      let line = createNode({
        parent: this.containerEl,
        attributes: {"class": "property"}
      });
      let {warning, className} =
        this.getPerfDataForProperty(animation, propertyName);
      createNode({
        // text-overflow doesn't work in flex items, so we need a second level
        // of container to actually have an ellipsis on the name.
        // See bug 972664.
        parent: createNode({
          parent: line,
          attributes: {"class": "name"}
        }),
        textContent: getCssPropertyName(propertyName),
        attributes: {"title": warning,
                     "class": className}
      });

      // Add the keyframes diagram for this property.
      let framesWrapperEl = createNode({
        parent: line,
        attributes: {"class": "track-container"}
      });

      let framesEl = createNode({
        parent: framesWrapperEl,
        attributes: {"class": "frames"}
      });

      // Scale the list of keyframes according to the current time scale.
      let {x, w} = TimeScale.getAnimationDimensions(animation);
      framesEl.style.left = `${x}%`;
      framesEl.style.width = `${w}%`;

      let keyframesComponent = new Keyframes();
      keyframesComponent.init(framesEl);
      keyframesComponent.render({
        keyframes: this.tracks[propertyName],
        propertyName: propertyName,
        animation: animation
      });
      keyframesComponent.on("frame-selected", this.onFrameSelected);

      this.keyframeComponents.push(keyframesComponent);
    }
  }),

  onFrameSelected: function (e, args) {
    // Relay the event up, it's needed in parents too.
    this.emit(e, args);
  }
};

/**
 * Turn propertyName into property-name.
 * @param {String} jsPropertyName A camelcased CSS property name. Typically
 * something that comes out of computed styles. E.g. borderBottomColor
 * @return {String} The corresponding CSS property name: border-bottom-color
 */
function getCssPropertyName(jsPropertyName) {
  return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
}
exports.getCssPropertyName = getCssPropertyName;
