<?php

/**
 * Identity function, returns its argument unmodified.
 *
 * This is useful almost exclusively as a workaround to an oddity in the PHP
 * grammar -- this is a syntax error:
 *
 *    COUNTEREXAMPLE
 *    new Thing()->doStuff();
 *
 * ...but this works fine:
 *
 *    id(new Thing())->doStuff();
 *
 * @param   wild Anything.
 * @return  wild Unmodified argument.
 */
function id($x) {
  return $x;
}


/**
 * Access an array index, retrieving the value stored there if it exists or
 * a default if it does not. This function allows you to concisely access an
 * index which may or may not exist without raising a warning.
 *
 * @param   array   Array to access.
 * @param   scalar  Index to access in the array.
 * @param   wild    Default value to return if the key is not present in the
 *                  array.
 * @return  wild    If `$array[$key]` exists, that value is returned. If not,
 *                  $default is returned without raising a warning.
 */
function idx(array $array, $key, $default = null) {
  // isset() is a micro-optimization - it is fast but fails for null values.
  if (isset($array[$key])) {
    return $array[$key];
  }

  // Comparing $default is also a micro-optimization.
  if ($default === null || array_key_exists($key, $array)) {
    return null;
  }

  return $default;
}


/**
 * Call a method on a list of objects. Short for "method pull", this function
 * works just like @{function:ipull}, except that it operates on a list of
 * objects instead of a list of arrays. This function simplifies a common type
 * of mapping operation:
 *
 *    COUNTEREXAMPLE
 *    $names = array();
 *    foreach ($objects as $key => $object) {
 *      $names[$key] = $object->getName();
 *    }
 *
 * You can express this more concisely with mpull():
 *
 *    $names = mpull($objects, 'getName');
 *
 * mpull() takes a third argument, which allows you to do the same but for
 * the array's keys:
 *
 *    COUNTEREXAMPLE
 *    $names = array();
 *    foreach ($objects as $object) {
 *      $names[$object->getID()] = $object->getName();
 *    }
 *
 * This is the mpull version():
 *
 *    $names = mpull($objects, 'getName', 'getID');
 *
 * If you pass ##null## as the second argument, the objects will be preserved:
 *
 *    COUNTEREXAMPLE
 *    $id_map = array();
 *    foreach ($objects as $object) {
 *      $id_map[$object->getID()] = $object;
 *    }
 *
 * With mpull():
 *
 *    $id_map = mpull($objects, null, 'getID');
 *
 * See also @{function:ipull}, which works similarly but accesses array indexes
 * instead of calling methods.
 *
 * @param   list          Some list of objects.
 * @param   string|null   Determines which **values** will appear in the result
 *                        array. Use a string like 'getName' to store the
 *                        value of calling the named method in each value, or
 *                        ##null## to preserve the original objects.
 * @param   string|null   Determines how **keys** will be assigned in the result
 *                        array. Use a string like 'getID' to use the result
 *                        of calling the named method as each object's key, or
 *                        `null` to preserve the original keys.
 * @return  dict          A dictionary with keys and values derived according
 *                        to whatever you passed as `$method` and `$key_method`.
 */
function mpull(array $list, $method, $key_method = null) {
  $result = array();
  foreach ($list as $key => $object) {
    if ($key_method !== null) {
      $key = $object->$key_method();
    }
    if ($method !== null) {
      $value = $object->$method();
    } else {
      $value = $object;
    }
    $result[$key] = $value;
  }
  return $result;
}


/**
 * Access a property on a list of objects. Short for "property pull", this
 * function works just like @{function:mpull}, except that it accesses object
 * properties instead of methods. This function simplifies a common type of
 * mapping operation:
 *
 *    COUNTEREXAMPLE
 *    $names = array();
 *    foreach ($objects as $key => $object) {
 *      $names[$key] = $object->name;
 *    }
 *
 * You can express this more concisely with ppull():
 *
 *    $names = ppull($objects, 'name');
 *
 * ppull() takes a third argument, which allows you to do the same but for
 * the array's keys:
 *
 *    COUNTEREXAMPLE
 *    $names = array();
 *    foreach ($objects as $object) {
 *      $names[$object->id] = $object->name;
 *    }
 *
 * This is the ppull version():
 *
 *    $names = ppull($objects, 'name', 'id');
 *
 * If you pass ##null## as the second argument, the objects will be preserved:
 *
 *    COUNTEREXAMPLE
 *    $id_map = array();
 *    foreach ($objects as $object) {
 *      $id_map[$object->id] = $object;
 *    }
 *
 * With ppull():
 *
 *    $id_map = ppull($objects, null, 'id');
 *
 * See also @{function:mpull}, which works similarly but calls object methods
 * instead of accessing object properties.
 *
 * @param   list          Some list of objects.
 * @param   string|null   Determines which **values** will appear in the result
 *                        array. Use a string like 'name' to store the value of
 *                        accessing the named property in each value, or
 *                        `null` to preserve the original objects.
 * @param   string|null   Determines how **keys** will be assigned in the result
 *                        array. Use a string like 'id' to use the result of
 *                        accessing the named property as each object's key, or
 *                        `null` to preserve the original keys.
 * @return  dict          A dictionary with keys and values derived according
 *                        to whatever you passed as `$property` and
 *                        `$key_property`.
 */
function ppull(array $list, $property, $key_property = null) {
  $result = array();
  foreach ($list as $key => $object) {
    if ($key_property !== null) {
      $key = $object->$key_property;
    }
    if ($property !== null) {
      $value = $object->$property;
    } else {
      $value = $object;
    }
    $result[$key] = $value;
  }
  return $result;
}


/**
 * Choose an index from a list of arrays. Short for "index pull", this function
 * works just like @{function:mpull}, except that it operates on a list of
 * arrays and selects an index from them instead of operating on a list of
 * objects and calling a method on them.
 *
 * This function simplifies a common type of mapping operation:
 *
 *    COUNTEREXAMPLE
 *    $names = array();
 *    foreach ($list as $key => $dict) {
 *      $names[$key] = $dict['name'];
 *    }
 *
 * With ipull():
 *
 *    $names = ipull($list, 'name');
 *
 * See @{function:mpull} for more usage examples.
 *
 * @param   list          Some list of arrays.
 * @param   scalar|null   Determines which **values** will appear in the result
 *                        array. Use a scalar to select that index from each
 *                        array, or null to preserve the arrays unmodified as
 *                        values.
 * @param   scalar|null   Determines which **keys** will appear in the result
 *                        array. Use a scalar to select that index from each
 *                        array, or null to preserve the array keys.
 * @return  dict          A dictionary with keys and values derived according
 *                        to whatever you passed for `$index` and `$key_index`.
 */
function ipull(array $list, $index, $key_index = null) {
  $result = array();
  foreach ($list as $key => $array) {
    if ($key_index !== null) {
      $key = $array[$key_index];
    }
    if ($index !== null) {
      $value = $array[$index];
    } else {
      $value = $array;
    }
    $result[$key] = $value;
  }
  return $result;
}


/**
 * Group a list of objects by the result of some method, similar to how
 * GROUP BY works in an SQL query. This function simplifies grouping objects
 * by some property:
 *
 *    COUNTEREXAMPLE
 *    $animals_by_species = array();
 *    foreach ($animals as $animal) {
 *      $animals_by_species[$animal->getSpecies()][] = $animal;
 *    }
 *
 * This can be expressed more tersely with mgroup():
 *
 *    $animals_by_species = mgroup($animals, 'getSpecies');
 *
 * In either case, the result is a dictionary which maps species (e.g., like
 * "dog") to lists of animals with that property, so all the dogs are grouped
 * together and all the cats are grouped together, or whatever super
 * businessesey thing is actually happening in your problem domain.
 *
 * See also @{function:igroup}, which works the same way but operates on
 * array indexes.
 *
 * @param   list    List of objects to group by some property.
 * @param   string  Name of a method, like 'getType', to call on each object
 *                  in order to determine which group it should be placed into.
 * @param   ...     Zero or more additional method names, to subgroup the
 *                  groups.
 * @return  dict    Dictionary mapping distinct method returns to lists of
 *                  all objects which returned that value.
 */
function mgroup(array $list, $by /* , ... */) {
  $map = mpull($list, $by);

  $groups = array();
  foreach ($map as $group) {
    // Can't array_fill_keys() here because 'false' gets encoded wrong.
    $groups[$group] = array();
  }

  foreach ($map as $key => $group) {
    $groups[$group][$key] = $list[$key];
  }

  $args = func_get_args();
  $args = array_slice($args, 2);
  if ($args) {
    array_unshift($args, null);
    foreach ($groups as $group_key => $grouped) {
      $args[0] = $grouped;
      $groups[$group_key] = call_user_func_array('mgroup', $args);
    }
  }

  return $groups;
}


/**
 * Group a list of arrays by the value of some index. This function is the same
 * as @{function:mgroup}, except it operates on the values of array indexes
 * rather than the return values of method calls.
 *
 * @param   list    List of arrays to group by some index value.
 * @param   string  Name of an index to select from each array in order to
 *                  determine which group it should be placed into.
 * @param   ...     Zero or more additional indexes names, to subgroup the
 *                  groups.
 * @return  dict    Dictionary mapping distinct index values to lists of
 *                  all objects which had that value at the index.
 */
function igroup(array $list, $by /* , ... */) {
  $map = ipull($list, $by);

  $groups = array();
  foreach ($map as $group) {
    $groups[$group] = array();
  }

  foreach ($map as $key => $group) {
    $groups[$group][$key] = $list[$key];
  }

  $args = func_get_args();
  $args = array_slice($args, 2);
  if ($args) {
    array_unshift($args, null);
    foreach ($groups as $group_key => $grouped) {
      $args[0] = $grouped;
      $groups[$group_key] = call_user_func_array('igroup', $args);
    }
  }

  return $groups;
}


/**
 * Sort a list of objects by the return value of some method. In PHP, this is
 * often vastly more efficient than `usort()` and similar.
 *
 *    // Sort a list of Duck objects by name.
 *    $sorted = msort($ducks, 'getName');
 *
 * It is usually significantly more efficient to define an ordering method
 * on objects and call `msort()` than to write a comparator. It is often more
 * convenient, as well.
 *
 * NOTE: This method does not take the list by reference; it returns a new list.
 *
 * @param   list    List of objects to sort by some property.
 * @param   string  Name of a method to call on each object; the return values
 *                  will be used to sort the list.
 * @return  list    Objects ordered by the return values of the method calls.
 */
function msort(array $list, $method) {
  $surrogate = mpull($list, $method);

  asort($surrogate);

  $result = array();
  foreach ($surrogate as $key => $value) {
    $result[$key] = $list[$key];
  }

  return $result;
}


/**
 * Sort a list of arrays by the value of some index. This method is identical to
 * @{function:msort}, but operates on a list of arrays instead of a list of
 * objects.
 *
 * @param   list    List of arrays to sort by some index value.
 * @param   string  Index to access on each object; the return values
 *                  will be used to sort the list.
 * @return  list    Arrays ordered by the index values.
 */
function isort(array $list, $index) {
  $surrogate = ipull($list, $index);

  asort($surrogate);

  $result = array();
  foreach ($surrogate as $key => $value) {
    $result[$key] = $list[$key];
  }

  return $result;
}


/**
 * Filter a list of objects by executing a method across all the objects and
 * filter out the ones with empty() results. this function works just like
 * @{function:ifilter}, except that it operates on a list of objects instead
 * of a list of arrays.
 *
 * For example, to remove all objects with no children from a list, where
 * 'hasChildren' is a method name, do this:
 *
 *   mfilter($list, 'hasChildren');
 *
 * The optional third parameter allows you to negate the operation and filter
 * out nonempty objects. To remove all objects that DO have children, do this:
 *
 *   mfilter($list, 'hasChildren', true);
 *
 * @param  array        List of objects to filter.
 * @param  string       A method name.
 * @param  bool         Optionally, pass true to drop objects which pass the
 *                      filter instead of keeping them.
 * @return array        List of objects which pass the filter.
 */
function mfilter(array $list, $method, $negate = false) {
  if (!is_string($method)) {
    throw new InvalidArgumentException(pht('Argument method is not a string.'));
  }

  $result = array();
  foreach ($list as $key => $object) {
    $value = $object->$method();

    if (!$negate) {
      if (!empty($value)) {
        $result[$key] = $object;
      }
    } else {
      if (empty($value)) {
        $result[$key] = $object;
      }
    }
  }

  return $result;
}


/**
 * Filter a list of arrays by removing the ones with an empty() value for some
 * index. This function works just like @{function:mfilter}, except that it
 * operates on a list of arrays instead of a list of objects.
 *
 * For example, to remove all arrays without value for key 'username', do this:
 *
 *   ifilter($list, 'username');
 *
 * The optional third parameter allows you to negate the operation and filter
 * out nonempty arrays. To remove all arrays that DO have value for key
 * 'username', do this:
 *
 *   ifilter($list, 'username', true);
 *
 * @param  array        List of arrays to filter.
 * @param  scalar       The index.
 * @param  bool         Optionally, pass true to drop arrays which pass the
 *                      filter instead of keeping them.
 * @return array        List of arrays which pass the filter.
 */
function ifilter(array $list, $index, $negate = false) {
  if (!is_scalar($index)) {
    throw new InvalidArgumentException(pht('Argument index is not a scalar.'));
  }

  $result = array();
  if (!$negate) {
    foreach ($list as $key => $array) {
      if (!empty($array[$index])) {
        $result[$key] = $array;
      }
    }
  } else {
    foreach ($list as $key => $array) {
      if (empty($array[$index])) {
        $result[$key] = $array;
      }
    }
  }

  return $result;
}


/**
 * Selects a list of keys from an array, returning a new array with only the
 * key-value pairs identified by the selected keys, in the specified order.
 *
 * Note that since this function orders keys in the result according to the
 * order they appear in the list of keys, there are effectively two common
 * uses: either reducing a large dictionary to a smaller one, or changing the
 * key order on an existing dictionary.
 *
 * @param  dict    Dictionary of key-value pairs to select from.
 * @param  list    List of keys to select.
 * @return dict    Dictionary of only those key-value pairs where the key was
 *                 present in the list of keys to select. Ordering is
 *                 determined by the list order.
 */
function array_select_keys(array $dict, array $keys) {
  $result = array();
  foreach ($keys as $key) {
    if (array_key_exists($key, $dict)) {
      $result[$key] = $dict[$key];
    }
  }
  return $result;
}


/**
 * Checks if all values of array are instances of the passed class. Throws
 * `InvalidArgumentException` if it isn't true for any value.
 *
 * @param  array
 * @param  string  Name of the class or 'array' to check arrays.
 * @return array   Returns passed array.
 */
function assert_instances_of(array $arr, $class) {
  $is_array = !strcasecmp($class, 'array');

  foreach ($arr as $key => $object) {
    if ($is_array) {
      if (!is_array($object)) {
        $given = gettype($object);
        throw new InvalidArgumentException(
          pht(
            "Array item with key '%s' must be of type array, %s given.",
            $key,
            $given));
      }

    } else if (!($object instanceof $class)) {
      $given = gettype($object);
      if (is_object($object)) {
        $given = pht('instance of %s', get_class($object));
      }
      throw new InvalidArgumentException(
        pht(
          "Array item with key '%s' must be an instance of %s, %s given.",
          $key,
          $class,
          $given));
    }
  }

  return $arr;
}

/**
 * Assert that passed data can be converted to string.
 *
 * @param  string    Assert that this data is valid.
 * @return void
 *
 * @task   assert
 */
function assert_stringlike($parameter) {
  switch (gettype($parameter)) {
    case 'string':
    case 'NULL':
    case 'boolean':
    case 'double':
    case 'integer':
      return;
    case 'object':
      if (method_exists($parameter, '__toString')) {
        return;
      }
      break;
    case 'array':
    case 'resource':
    case 'unknown type':
    default:
      break;
  }

  throw new InvalidArgumentException(
    pht(
      'Argument must be scalar or object which implements %s!',
      '__toString()'));
}

/**
 * Returns the first argument which is not strictly null, or `null` if there
 * are no such arguments. Identical to the MySQL function of the same name.
 *
 * @param  ...         Zero or more arguments of any type.
 * @return mixed       First non-`null` arg, or null if no such arg exists.
 */
function coalesce(/* ... */) {
  $args = func_get_args();
  foreach ($args as $arg) {
    if ($arg !== null) {
      return $arg;
    }
  }
  return null;
}


/**
 * Similar to @{function:coalesce}, but less strict: returns the first
 * non-`empty()` argument, instead of the first argument that is strictly
 * non-`null`. If no argument is nonempty, it returns the last argument. This
 * is useful idiomatically for setting defaults:
 *
 *   $display_name = nonempty($user_name, $full_name, "Anonymous");
 *
 * @param  ...         Zero or more arguments of any type.
 * @return mixed       First non-`empty()` arg, or last arg if no such arg
 *                     exists, or null if you passed in zero args.
 */
function nonempty(/* ... */) {
  $args = func_get_args();
  $result = null;
  foreach ($args as $arg) {
    $result = $arg;
    if ($arg) {
      break;
    }
  }
  return $result;
}


/**
 * Invokes the "new" operator with a vector of arguments. There is no way to
 * `call_user_func_array()` on a class constructor, so you can instead use this
 * function:
 *
 *   $obj = newv($class_name, $argv);
 *
 * That is, these two statements are equivalent:
 *
 *   $pancake = new Pancake('Blueberry', 'Maple Syrup', true);
 *   $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true));
 *
 * DO NOT solve this problem in other, more creative ways! Three popular
 * alternatives are:
 *
 *   - Build a fake serialized object and unserialize it.
 *   - Invoke the constructor twice.
 *   - just use `eval()` lol
 *
 * These are really bad solutions to the problem because they can have side
 * effects (e.g., __wakeup()) and give you an object in an otherwise impossible
 * state. Please endeavor to keep your objects in possible states.
 *
 * If you own the classes you're doing this for, you should consider whether
 * or not restructuring your code (for instance, by creating static
 * construction methods) might make it cleaner before using `newv()`. Static
 * constructors can be invoked with `call_user_func_array()`, and may give your
 * class a cleaner and more descriptive API.
 *
 * @param  string  The name of a class.
 * @param  list    Array of arguments to pass to its constructor.
 * @return obj     A new object of the specified class, constructed by passing
 *                 the argument vector to its constructor.
 */
function newv($class_name, array $argv) {
  $reflector = new ReflectionClass($class_name);
  if ($argv) {
    return $reflector->newInstanceArgs($argv);
  } else {
    return $reflector->newInstance();
  }
}


/**
 * Returns the first element of an array. Exactly like reset(), but doesn't
 * choke if you pass it some non-referenceable value like the return value of
 * a function.
 *
 * @param    array Array to retrieve the first element from.
 * @return   wild  The first value of the array.
 */
function head(array $arr) {
  return reset($arr);
}

/**
 * Returns the last element of an array. This is exactly like `end()` except
 * that it won't warn you if you pass some non-referencable array to
 * it -- e.g., the result of some other array operation.
 *
 * @param    array Array to retrieve the last element from.
 * @return   wild  The last value of the array.
 */
function last(array $arr) {
  return end($arr);
}

/**
 * Returns the first key of an array.
 *
 * @param    array       Array to retrieve the first key from.
 * @return   int|string  The first key of the array.
 */
function head_key(array $arr) {
  reset($arr);
  return key($arr);
}

/**
 * Returns the last key of an array.
 *
 * @param    array       Array to retrieve the last key from.
 * @return   int|string  The last key of the array.
 */
function last_key(array $arr) {
  end($arr);
  return key($arr);
}

/**
 * Merge a vector of arrays performantly. This has the same semantics as
 * array_merge(), so these calls are equivalent:
 *
 *   array_merge($a, $b, $c);
 *   array_mergev(array($a, $b, $c));
 *
 * However, when you have a vector of arrays, it is vastly more performant to
 * merge them with this function than by calling array_merge() in a loop,
 * because using a loop generates an intermediary array on each iteration.
 *
 * @param list Vector of arrays to merge.
 * @return list Arrays, merged with array_merge() semantics.
 */
function array_mergev(array $arrayv) {
  if (!$arrayv) {
    return array();
  }

  foreach ($arrayv as $key => $item) {
    if (!is_array($item)) {
      throw new InvalidArgumentException(
        pht(
          'Expected all items passed to %s to be arrays, but '.
          'argument with key "%s" has type "%s".',
          __FUNCTION__.'()',
          $key,
          gettype($item)));
    }
  }

  return call_user_func_array('array_merge', $arrayv);
}


/**
 * Split a corpus of text into lines. This function splits on "\n", "\r\n", or
 * a mixture of any of them.
 *
 * NOTE: This function does not treat "\r" on its own as a newline because none
 * of SVN, Git or Mercurial do on any OS.
 *
 * @param string Block of text to be split into lines.
 * @param bool If true, retain line endings in result strings.
 * @return list List of lines.
 */
function phutil_split_lines($corpus, $retain_endings = true) {
  if (!strlen($corpus)) {
    return array('');
  }

  // Split on "\r\n" or "\n".
  if ($retain_endings) {
    $lines = preg_split('/(?<=\n)/', $corpus);
  } else {
    $lines = preg_split('/\r?\n/', $corpus);
  }

  // If the text ends with "\n" or similar, we'll end up with an empty string
  // at the end; discard it.
  if (end($lines) == '') {
    array_pop($lines);
  }

  if ($corpus instanceof PhutilSafeHTML) {
    return array_map('phutil_safe_html', $lines);
  }

  return $lines;
}


/**
 * Simplifies a common use of `array_combine()`. Specifically, this:
 *
 *   COUNTEREXAMPLE:
 *   if ($list) {
 *     $result = array_combine($list, $list);
 *   } else {
 *     // Prior to PHP 5.4, array_combine() failed if given empty arrays.
 *     $result = array();
 *   }
 *
 * ...is equivalent to this:
 *
 *   $result = array_fuse($list);
 *
 * @param   list  List of scalars.
 * @return  dict  Dictionary with inputs mapped to themselves.
 */
function array_fuse(array $list) {
  if ($list) {
    return array_combine($list, $list);
  }
  return array();
}


/**
 * Add an element between every two elements of some array. That is, given a
 * list `A, B, C, D`, and some element to interleave, `x`, this function returns
 * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate
 * the list into a string. In particular:
 *
 *   implode('', array_interleave($x, $list));
 *
 * ...is equivalent to:
 *
 *   implode($x, $list);
 *
 * This function does not preserve keys.
 *
 * @param wild  Element to interleave.
 * @param list  List of elements to be interleaved.
 * @return list Original list with the new element interleaved.
 */
function array_interleave($interleave, array $array) {
  $result = array();
  foreach ($array as $item) {
    $result[] = $item;
    $result[] = $interleave;
  }
  array_pop($result);
  return $result;
}

function phutil_is_windows() {
  // We can also use PHP_OS, but that's kind of sketchy because it returns
  // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for
  // DIRECTORY_SEPARATOR is more straightforward.
  return (DIRECTORY_SEPARATOR != '/');
}

function phutil_is_hiphop_runtime() {
  return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1);
}

/**
 * Converts a string to a loggable one, with unprintables and newlines escaped.
 *
 * @param string  Any string.
 * @return string String with control and newline characters escaped, suitable
 *                for printing on a single log line.
 */
function phutil_loggable_string($string) {
  if (preg_match('/^[\x20-\x7E]+$/', $string)) {
    return $string;
  }

  $result = '';

  static $c_map = array(
    '\\' => '\\\\',
    "\n" => '\\n',
    "\r" => '\\r',
    "\t" => '\\t',
  );

  $len = strlen($string);
  for ($ii = 0; $ii < $len; $ii++) {
    $c = $string[$ii];
    if (isset($c_map[$c])) {
      $result .= $c_map[$c];
    } else {
      $o = ord($c);
      if ($o < 0x20 || $o == 0x7F) {
        $result .= '\\x'.sprintf('%02X', $o);
      } else {
        $result .= $c;
      }
    }
  }

  return $result;
}


/**
 * Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE.
 *
 * PHP's `fwrite()` is broken, and never returns `false` for writes to broken
 * nonblocking pipes: it always returns 0, and provides no straightforward
 * mechanism for distinguishing between EAGAIN (buffer is full, can't write any
 * more right now) and EPIPE or similar (no write will ever succeed).
 *
 * See: https://bugs.php.net/bug.php?id=39598
 *
 * If you call this method instead of `fwrite()`, it will attempt to detect
 * when a zero-length write is caused by EAGAIN and return `0` only if the
 * write really should be retried.
 *
 * @param resource  Socket or pipe stream.
 * @param string    Bytes to write.
 * @return bool|int Number of bytes written, or `false` on any error (including
 *                  errors which `fpipe()` can not detect, like a broken pipe).
 */
function phutil_fwrite_nonblocking_stream($stream, $bytes) {
  if (!strlen($bytes)) {
    return 0;
  }

  $result = @fwrite($stream, $bytes);
  if ($result !== 0) {
    // In cases where some bytes are witten (`$result > 0`) or
    // an error occurs (`$result === false`), the behavior of fwrite() is
    // correct. We can return the value as-is.
    return $result;
  }

  // If we make it here, we performed a 0-length write. Try to distinguish
  // between EAGAIN and EPIPE. To do this, we're going to `stream_select()`
  // the stream, write to it again if PHP claims that it's writable, and
  // consider the pipe broken if the write fails.

  $read = array();
  $write = array($stream);
  $except = array();

  @stream_select($read, $write, $except, 0);

  if (!$write) {
    // The stream isn't writable, so we conclude that it probably really is
    // blocked and the underlying error was EAGAIN. Return 0 to indicate that
    // no data could be written yet.
    return 0;
  }

  // If we make it here, PHP **just** claimed that this stream is writable, so
  // perform a write. If the write also fails, conclude that these failures are
  // EPIPE or some other permanent failure.
  $result = @fwrite($stream, $bytes);
  if ($result !== 0) {
    // The write worked or failed explicitly. This value is fine to return.
    return $result;
  }

  // We performed a 0-length write, were told that the stream was writable, and
  // then immediately performed another 0-length write. Conclude that the pipe
  // is broken and return `false`.
  return false;
}


/**
 * Convert a human-readable unit description into a numeric one. This function
 * allows you to replace this:
 *
 *   COUNTEREXAMPLE
 *   $ttl = (60 * 60 * 24 * 30); // 30 days
 *
 * ...with this:
 *
 *   $ttl = phutil_units('30 days in seconds');
 *
 * ...which is self-documenting and difficult to make a mistake with.
 *
 * @param   string  Human readable description of a unit quantity.
 * @return  int     Quantity of specified unit.
 */
function phutil_units($description) {
  $matches = null;
  if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) {
    throw new InvalidArgumentException(
      pht(
        'Unable to parse unit specification (expected a specification in the '.
        'form "%s"): %s',
        '5 days in seconds',
        $description));
  }

  $quantity = (int)$matches[1];
  $src_unit = $matches[2];
  $dst_unit = $matches[3];

  switch ($dst_unit) {
    case 'seconds':
      switch ($src_unit) {
        case 'second':
        case 'seconds':
          $factor = 1;
          break;
        case 'minute':
        case 'minutes':
          $factor = 60;
          break;
        case 'hour':
        case 'hours':
          $factor = 60 * 60;
          break;
        case 'day':
        case 'days':
          $factor = 60 * 60 * 24;
          break;
        default:
          throw new InvalidArgumentException(
            pht(
              'This function can not convert from the unit "%s".',
              $src_unit));
      }
      break;
    default:
      throw new InvalidArgumentException(
        pht(
          'This function can not convert into the unit "%s".',
          $dst_unit));
  }

  return $quantity * $factor;
}


/**
 * Decode a JSON dictionary.
 *
 * @param   string    A string which ostensibly contains a JSON-encoded list or
 *                    dictionary.
 * @return  mixed     Decoded list/dictionary.
 */
function phutil_json_decode($string) {
  $result = @json_decode($string, true);

  if (!is_array($result)) {
    // Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead.
    // This will probably fail, but will throw a useful exception.
    $parser = new PhutilJSONParser();
    $result = $parser->parse($string);
  }

  return $result;
}


/**
 * Encode a value in JSON, raising an exception if it can not be encoded.
 *
 * @param wild A value to encode.
 * @return string JSON representation of the value.
 */
function phutil_json_encode($value) {
  $result = @json_encode($value);
  if ($result === false) {
    $reason = phutil_validate_json($value);
    if (function_exists('json_last_error')) {
      $err = json_last_error();
      if (function_exists('json_last_error_msg')) {
        $msg = json_last_error_msg();
        $extra = pht('#%d: %s', $err, $msg);
      } else {
        $extra = pht('#%d', $err);
      }
    } else {
      $extra = null;
    }

    if ($extra) {
      $message = pht(
        'Failed to JSON encode value (%s): %s.',
        $extra,
        $reason);
    } else {
      $message = pht(
        'Failed to JSON encode value: %s.',
        $reason);
    }

    throw new Exception($message);
  }

  return $result;
}


/**
 * Produce a human-readable explanation why a value can not be JSON-encoded.
 *
 * @param wild Value to validate.
 * @param string Path within the object to provide context.
 * @return string|null Explanation of why it can't be encoded, or null.
 */
function phutil_validate_json($value, $path = '') {
  if ($value === null) {
    return;
  }

  if ($value === true) {
    return;
  }

  if ($value === false) {
    return;
  }

  if (is_int($value)) {
    return;
  }

  if (is_float($value)) {
    return;
  }

  if (is_array($value)) {
    foreach ($value as $key => $subvalue) {
      if (strlen($path)) {
        $full_key = $path.' > ';
      } else {
        $full_key = '';
      }

      if (!phutil_is_utf8($key)) {
        $full_key = $full_key.phutil_utf8ize($key);
        return pht(
          'Dictionary key "%s" is not valid UTF8, and can not be JSON encoded.',
          $full_key);
      }

      $full_key .= $key;
      $result = phutil_validate_json($subvalue, $full_key);
      if ($result !== null) {
        return $result;
      }
    }
  }

  if (is_string($value)) {
    if (!phutil_is_utf8($value)) {
      $display = substr($value, 0, 256);
      $display = phutil_utf8ize($display);
      if (!strlen($path)) {
        return pht(
          'String value is not valid UTF8, and can not be JSON encoded: %s',
          $display);
      } else {
        return pht(
          'Dictionary value at key "%s" is not valid UTF8, and can not be '.
          'JSON encoded: %s',
          $path,
          $display);
      }
    }
  }

  return;
}


/**
 * Decode an INI string.
 *
 * @param  string
 * @return mixed
 */
function phutil_ini_decode($string) {
  $results = null;
  $trap = new PhutilErrorTrap();

  try {
    if (!function_exists('parse_ini_string')) {
      throw new PhutilMethodNotImplementedException(
        pht(
          '%s is not compatible with your version of PHP (%s). This function '.
          'is only supported on PHP versions newer than 5.3.0.',
          __FUNCTION__,
          phpversion()));
    }

    $results = @parse_ini_string($string, true, INI_SCANNER_RAW);

    if ($results === false) {
      throw new PhutilINIParserException(trim($trap->getErrorsAsString()));
    }

    foreach ($results as $section => $result) {
      if (!is_array($result)) {
        // We JSON decode the value in ordering to perform the following
        // conversions:
        //
        //   - `'true'` => `true`
        //   - `'false'` => `false`
        //   - `'123'` => `123`
        //   - `'1.234'` => `1.234`
        //
        $result = json_decode($result, true);

        if ($result !== null && !is_array($result)) {
          $results[$section] = $result;
        }

        continue;
      }

      foreach ($result as $key => $value) {
        $value = json_decode($value, true);

        if ($value !== null && !is_array($value)) {
          $results[$section][$key] = $value;
        }
      }
    }
  } catch (Exception $ex) {
    $trap->destroy();
    throw $ex;
  }

  $trap->destroy();
  return $results;
}


/**
 * Attempt to censor any plaintext credentials from a string.
 *
 * The major use case here is to censor usernames and passwords from command
 * output. For example, when `git fetch` fails, the output includes credentials
 * for authenticated HTTP remotes.
 *
 * @param   string  Some block of text.
 * @return  string  A similar block of text, but with credentials that could
 *                  be identified censored.
 */
function phutil_censor_credentials($string) {
  return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', 'xxxxx', $string);
}


/**
 * Returns a parsable string representation of a variable.
 *
 * This function is intended to behave similarly to PHP's `var_export` function,
 * but the output is intended to follow our style conventions.
 *
 * @param  wild    The variable you want to export.
 * @return string
 */
function phutil_var_export($var) {
  // `var_export(null, true)` returns `"NULL"` (in uppercase).
  if ($var === null) {
    return 'null';
  }

  // PHP's `var_export` doesn't format arrays very nicely. In particular:
  //
  //   - An empty array is split over two lines (`"array (\n)"`).
  //   - A space separates "array" and the first opening brace.
  //   - Non-associative arrays are returned as associative arrays with an
  //     integer key.
  //
  if (is_array($var)) {
    if (count($var) === 0) {
      return 'array()';
    }

    // Don't show keys for non-associative arrays.
    $show_keys = (array_keys($var) !== range(0, count($var) - 1));

    $output = array();
    $output[] = 'array(';

    foreach ($var as $key => $value) {
      // Adjust the indentation of the value.
      $value = str_replace("\n", "\n  ", phutil_var_export($value));
      $output[] = '  '.
        ($show_keys ? var_export($key, true).' => ' : '').
        $value.',';
    }

    $output[] = ')';
    return implode("\n", $output);
  }

  // Let PHP handle everything else.
  return var_export($var, true);
}


/**
 * An improved version of `fnmatch`.
 *
 * @param  string  A glob pattern.
 * @param  string  A path.
 * @return bool
 */
function phutil_fnmatch($glob, $path) {
  // Modify the glob to allow `**/` to match files in the root directory.
  $glob = preg_replace('@(?:(?<!\\\\)\\*){2}/@', '{,*/,**/}', $glob);

  $escaping = false;
  $in_curlies = 0;
  $regex = '';

  for ($i = 0; $i < strlen($glob); $i++) {
    $char = $glob[$i];
    $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null;

    $escape = array('$', '(', ')', '+', '.', '^', '|');
    $mapping = array();

    if ($escaping) {
      $escape[] = '*';
      $escape[] = '?';
      $escape[] = '{';
    } else {
      $mapping['*'] = $next_char === '*' ? '.*' : '[^/]*';
      $mapping['?'] = '[^/]';
      $mapping['{'] = '(';

      if ($in_curlies) {
        $mapping[','] = '|';
        $mapping['}'] = ')';
      }
    }

    if (in_array($char, $escape)) {
      $regex .= "\\{$char}";
    } else if ($replacement = idx($mapping, $char)) {
      $regex .= $replacement;
    } else if ($char === '\\') {
      if ($escaping) {
        $regex .= '\\\\';
      }
      $escaping = !$escaping;
      continue;
    } else {
      $regex .= $char;
    }

    if ($char === '{' && !$escaping) {
      $in_curlies++;
    } else if ($char === '}' && $in_curlies && !$escaping) {
      $in_curlies--;
    }

    $escaping = false;
  }

  if ($in_curlies || $escaping) {
    throw new InvalidArgumentException(pht('Invalid glob pattern.'));
  }

  $regex = '(\A'.$regex.'\z)';
  return (bool)preg_match($regex, $path);
}
