﻿/**
 * Generates Touch Tone noises for your input in AE!
 *
 * Uses markers & expressions-- move markers & change text to change audio!
 *
 * @author Zack Lovatt <zack@lova.tt>
 * @version 0.1.0
 */
(function phoneAFriend () {
  // # Frames for the tone to last
  var toneDuration = 8.5;

  var number = getUserInput();

  if (!number) {
    return;
  }

  var comp = app.project.activeItem;

  if (!(comp && comp instanceof CompItem)) {
    alert("Open a comp!");
    return;
  }

  app.beginUndoGroup("Phone A Friend");

  try {
    var toneNull = comp.layers.addNull();
    toneNull.name = "Phone a Friend!";

    // Add the effect & expressions
    var toneEffect = toneNull
      .property("ADBE Effect Parade")
      .addProperty("ADBE Aud Tone");
    var freq1 = toneEffect.property("ADBE Aud Tone-0002");
    addPhoneAFriendExpression(freq1, toneDuration, 0);
    var freq2 = toneEffect.property("ADBE Aud Tone-0003");
    addPhoneAFriendExpression(freq2, toneDuration, 1);
    toneEffect.property("ADBE Aud Tone-0004").setValue(0);
    toneEffect.property("ADBE Aud Tone-0005").setValue(0);
    toneEffect.property("ADBE Aud Tone-0006").setValue(0);

    // Add muter. Annoying but needed to prevent warpy audio.
    var audioLevels = toneNull.property("ADBE Audio Group").property("ADBE Audio Levels");
    audioLevels.expression = [
      "// # frames for tone to last",
      "var toneLength = " + toneDuration + ";",
      "",
      "var priorMarkerIndex = 0;",
      "if (thisLayer.marker.numKeys > 0) {",
      "  var nearestMarker = thisLayer.marker.nearestKey(time);",
      "  priorMarkerIndex = nearestMarker.index;",
      "",
      "  if (nearestMarker.time > time) {",
      "    priorMarkerIndex = nearestMarker.index - 1;",
      "  }",
      "}",
      "",
      "var result = [-192, -192];",
      "if (priorMarkerIndex !== 0) {",
      "  var priorMarker = thisLayer.marker.key(priorMarkerIndex);",
      "  var inputMin = priorMarker.time + framesToTime(1);",
      "  var inputMax = inputMin + framesToTime(toneLength - 2);",
      "",
      "  if (time >= inputMin && time < inputMax) {",
      "    result = [0, 0];",
      "  }",
      "}",
      "",
      "result;",
    ].join("\n");

    // Add the markers
    addMarkersToLayer(toneNull, number, 15);
  } catch (e) {
    alert(e);
  } finally {
    app.endUndoGroup();
  }

  /**
   * Gets user input
   *
   * @returns {string} User input phone number
   */
  function getUserInput() {
    var valid = "1234567890*#";
    var number = prompt("Enter phone number!", "967-1111", "Phone A Friend");

    if (!number) {
      return;
    }

    number = number.toUpperCase();
    number = number.split("-").join("");
    number = number.split(" ").join("");

    var invalid = [];
    var result = [];
    for (var ii = 0, il = number.length; ii < il; ii++) {
      var key = number.charAt(ii);

      if (valid.indexOf(key) > -1) {
        result.push(key);
        continue;
      }

      switch (key) {
        case "A":
        case "B":
        case "C":
          result.push("2");
          break;

        case "D":
        case "E":
        case "F":
          result.push("3");
          break;

        case "G":
        case "H":
        case "I":
          result.push("4");
          break;

        case "J":
        case "K":
        case "L":
          result.push("5");
          break;

        case "M":
        case "N":
        case "O":
          result.push("6");
          break;

        case "P":
        case "Q":
        case "R":
        case "S":
          result.push("7");
          break;

        case "T":
        case "U":
        case "V":
          result.push("8");
          break;

        case "W":
        case "X":
        case "Y":
        case "Z":
          result.push("9");
          break;

        case "+":
          result.push("0");
          break;

        default:
          if (invalid.join("").indexOf(key) === -1) {
            invalid.push(key);
          }
          break;
      }
    }

    if (invalid.length > 0) {
      throw "Invalid characters: '" + invalid.join(", ") + "'";
    }

    return result;
  }

  /**
   * Adds expression to a given prop
   *
   * @param {Property} prop      Property to add exp to
   * @param {number} toneLength  # Frames to have the tone last for
   * @param {number} matrixIndex Matrix index to use
   */
  function addPhoneAFriendExpression(prop, toneLength, matrixIndex) {
    if (!prop.canSetExpression) {
      throw "Can't add expression!";
    }

    prop.expression = [
      "// # frames for tone to last",
      "var toneLength = " + toneLength + ";",
      "",
      "function characterToTone(character, matrixIndex) {",
      "  var toneMap = [",
      '    { "123": 697, "456": 770, "789": 852, "*0#": 941 },',
      '    { "147*": 1209, "2580": 1336, "369#": 1477 }',
      "  ];",
      "",
      "  var matrixSet = toneMap[matrixIndex];",
      "",
      "  for (var button in matrixSet) {",
      "    if (!matrixSet.hasOwnProperty(button)) { continue; }",
      "    if (button.indexOf(character) === -1) { continue; }",
      "    return matrixSet[button];",
      "  }",
      "}",
      "",
      "var priorMarkerIndex = 0;",
      "if (thisLayer.marker.numKeys > 0) {",
      "  var nearestMarker = thisLayer.marker.nearestKey(time);",
      "  priorMarkerIndex = nearestMarker.index;",
      "",
      "  if (nearestMarker.time > time) {",
      "    priorMarkerIndex = nearestMarker.index - 1;",
      "  }",
      "}",
      "",
      "var result = 0;",
      'var valid = "1234567890*#ABCD";',
      "if (priorMarkerIndex !== 0) {",
      "  var priorMarker = thisLayer.marker.key(priorMarkerIndex);",
      "",
      "  var button = priorMarker.comment;",
      "",
      '  if (button !== "" && valid.indexOf(button) > -1) {',
      "    var inputMin = priorMarker.time;",
      "    var inputMax = inputMin + framesToTime(toneLength);",
      "",
      "    if (time >= inputMin && time < inputMax) {",
      "      result = characterToTone(button, " + matrixIndex + ");",
      "    }",
      "  }",
      "}",
      "",
      "result;"
    ].join("\n");
  }

  /**
   * Adds markers to a layer at a given interval
   *
   * @param {Layer} layer         Layer to add markers to
   * @param {string[]} markerText Marker text array
   * @param {nubmer} interval     Interval in frames
   */
  function addMarkersToLayer(layer, markerText, interval) {
    var markers = layer.property("ADBE Marker");
    var intervalInSeconds = interval * layer.containingComp.frameDuration;

    for (var ii = 0, il = markerText.length; ii < il; ii++) {
      var marker = new MarkerValue(markerText[ii]);
      markers.setValueAtTime(intervalInSeconds * ii, marker);
    }
  }
})();
