<?php

/**
 *
 * String
 *
 * A set of handy string methods
 *
 * @package   Kirby Toolkit
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      http://getkirby.com
 * @copyright Bastian Allgeier
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */
class Str {

  static public $ascii = array(
    '/Ä/' => 'Ae',
    '/æ|ǽ|ä/' => 'ae',
    '/œ|ö/' => 'oe',
    '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|А/' => 'A',
    '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|а/' => 'a',
    '/Б/' => 'B',
    '/б/' => 'b',
    '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C',
    '/ç|ć|ĉ|ċ|č|ц/' => 'c',
    '/Ð|Ď|Đ/' => 'Dj',
    '/ð|ď|đ/' => 'dj',
    '/Д/' => 'D',
    '/д/' => 'd',
    '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E',
    '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e',
    '/Ф/' => 'F',
    '/ƒ|ф/' => 'f',
    '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G',
    '/ĝ|ğ|ġ|ģ|г/' => 'g',
    '/Ĥ|Ħ|Х/' => 'H',
    '/ĥ|ħ|х/' => 'h',
    '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I',
    '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и/' => 'i',
    '/Ĵ|Й/' => 'J',
    '/ĵ|й/' => 'j',
    '/Ķ|К/' => 'K',
    '/ķ|к/' => 'k',
    '/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L',
    '/ĺ|ļ|ľ|ŀ|ł|л/' => 'l',
    '/М/' => 'M',
    '/м/' => 'm',
    '/Ñ|Ń|Ņ|Ň|Н/' => 'N',
    '/ñ|ń|ņ|ň|ŉ|н/' => 'n',
    '/Ö/' => 'Oe',
    '/ö/' => 'oe',
    '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|О/' => 'O',
    '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|о/' => 'o',
    '/П/' => 'P',
    '/п/' => 'p',
    '/Ŕ|Ŗ|Ř|Р/' => 'R',
    '/ŕ|ŗ|ř|р/' => 'r',
    '/Ś|Ŝ|Ş|Ș|Š|С/' => 'S',
    '/ś|ŝ|ş|ș|š|ſ|с/' => 's',
    '/Ţ|Ț|Ť|Ŧ|Т/' => 'T',
    '/ţ|ț|ť|ŧ|т/' => 't',
    '/Ü/' => 'Ue',
    '/ü/' => 'ue',
    '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У/' => 'U',
    '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у/' => 'u',
    '/В/' => 'V',
    '/в/' => 'v',
    '/Ý|Ÿ|Ŷ|Ы/' => 'Y',
    '/ý|ÿ|ŷ|ы/' => 'y',
    '/Ŵ/' => 'W',
    '/ŵ/' => 'w',
    '/Ź|Ż|Ž|З/' => 'Z',
    '/ź|ż|ž|з/' => 'z',
    '/Æ|Ǽ/' => 'AE',
    '/ß/'=> 'ss',
    '/Ĳ/' => 'IJ',
    '/ĳ/' => 'ij',
    '/Œ/' => 'OE',
    '/Ч/' => 'Ch',
    '/ч/' => 'ch',
    '/Ю/' => 'Ju',
    '/ю/' => 'ju',
    '/Я/' => 'Ja',
    '/я/' => 'ja',
    '/Ш/' => 'Sh',
    '/ш/' => 'sh',
    '/Щ/' => 'Shch',
    '/щ/' => 'shch',
    '/Ж/' => 'Zh',
    '/ж/' => 'zh',
  );

  /**
   * Converts a string to a html-safe string
   *
   * <code>
   *
   * echo str::html('some <em>über crazy</em> stuff');
   * // output: some <em>&uuml;ber crazy</em> stuff
   *
   * echo str::html('some <em>über crazy</em> stuff', false);
   * // output: some &lt;em&gt;&uuml;ber crazy&lt;/em&gt; stuff
   *
   * </code>
   *
   * @param  string  $string
   * @param  boolean $keepTags True: lets stuff inside html tags untouched.
   * @return string  The html string
   */
  static public function html($string, $keepTags = true) {
    return html::encode($string, $keepTags);
  }

  /**
   * Removes all html tags and encoded chars from a string
   *
   * <code>
   *
   * echo str::unhtml('some <em>crazy</em> stuff');
   * // output: some uber crazy stuff
   *
   * </code>
   *
   * @param  string  $string
   * @return string  The html string
   */
  static public function unhtml($string) {
    return html::decode($string);
  }

  /**
   * Converts a string to a xml-safe string
   * Converts it to html-safe first and then it
   * will replace html entities to xml entities
   *
   * <code>
   *
   * echo str::xml('some über crazy stuff');
   * // output: some &#252;ber crazy stuff
   *
   * </code>
   *
   * @param  string  $text
   * @param  boolean $html True: convert to html first
   * @return string
   */
  static public function xml($text, $html = true) {
    return xml::encode($text, $html);
  }

  /**
   * Removes all xml entities from a string
   * and convert them to html entities first
   * and remove all html entities afterwards.
   *
   * <code>
   *
   * echo str::unxml('some <em>&#252;ber</em> crazy stuff');
   * // output: some &uuml;ber crazy stuff
   *
   * </code>
   *
   * @param  string  $string
   * @return string
   */
  static public function unxml($string) {
    return xml::decode($string);
  }

  /**
   * Parses a string by a set of available methods
   *
   * Available methods:
   * - json
   * - xml
   * - url
   * - query
   * - php
   *
   * <code>
   *
   * str::parse('{"test":"cool","super":"genious"}');
   * // output: array(
   * //  'test' => 'cool',
   * //  'super' => 'genious'
   * // );
   *
   * str::parse('<xml><entries><cool>nice</cool></entries></xml>', 'xml');
   * // output: array(
   * //    'entries' => array(
   * //        'cool' => 'nice'
   * //    )
   * // );
   *
   * </code>
   *
   * @param  string  $string
   * @param  string  $mode
   * @return string
   */
  static public function parse($string, $mode = 'json') {

    if(is_array($string) or is_object($string)) return $string;

    switch($mode) {
      case 'json':
        return (array)@json_decode($string, true);
        break;
      case 'xml':
        return xml::parse($string);
        break;
      case 'url':
        return (array)@parse_url($string);
        break;
      case 'php':
        return @unserialize($string);
        break;
      default:
        return $string;
        break;
    }

  }

  /**
   * Encode a string (used for email addresses)
   *
   * @param  string  $string
   * @return string
   */
  static public function encode($string) {
    $string  = (string)$string;
    $encoded = '';
    for($i=0; $i < static::length($string); $i++) {
      $encoded .= rand(1, 2) == 1 ? '&#' . ord($string[$i]) . ';' : '&#x' . dechex(ord($string[$i])) . ';';
    }
    return $encoded;
  }

  /**
   * Generates an "a mailto" tag
   *
   * <code>
   *
   * echo str::email('bastian@getkirby.com');
   * echo str::email('bastian@getkirby.com', 'mail me');
   *
   * </code>
   *
   * @param string $href The url for the a tag
   * @param mixed $text The optional text. If null, the url will be used as text
   * @param array $attr Additional attributes for the tag
   * @return string the generated html
   */
  static public function email($email, $text = false, $attr = array()) {
    return html::email($email, $text, $attr);
  }

  /**
   * Generates an a tag
   *
   * @param string $href The url for the a tag
   * @param mixed $text The optional text. If null, the url will be used as text
   * @param array $attr Additional attributes for the tag
   * @return string the generated html
   */
  static public function link($href, $text = null, $attr = array()) {
    return html::a($href, $text, $attr);
  }

  /**
   * Returns an array with all words in a string
   *
   * @param string $string
   */
  static public function words($string) {
    preg_match_all('/(\pL{4,})/iu', $string, $m);
    return array_shift($m);
  }

  /**
   * Returns an array with all sentences in a string
   *
   * @param string $string
   * @return string
   */
  static public function sentences($string) {
    return preg_split('/(?<=[.?!])\s+/', $string, -1, PREG_SPLIT_NO_EMPTY);
  }

  /**
   * Returns an array with all lines in a string
   *
   * @param string $string
   * @return array
   */
  static public function lines($string) {
    return str::split($string, PHP_EOL);
  }

  /**
   * Checks if the given string is a URL
   * 
   * @param string $string
   * @return boolean
   */
  static public function isURL($string) {
    return filter_var($string, FILTER_VALIDATE_URL);
  }

  /**
   * Shortens a string and adds an ellipsis if the string is too long
   *
   * <code>
   *
   * echo str::short('This is a very, very, very long string', 10);
   * // output: This is a…
   *
   * echo str::short('This is a very, very, very long string', 10, '####');
   * // output: This i####
   *
   * </code>
   *
   * @param  string  $string The string to be shortened
   * @param  int     $chars The final number of characters the string should have
   * @param  string  $rep The element, which should be added if the string is too long. Ellipsis is the default.
   * @return string  The shortened string
   */
  static public function short($string, $length, $rep = '…') {
    if(!$length) return $string;
    if(static::length($string) <= $length) return $string;
    $string = static::substr($string, 0, $length);
    return $string . $rep;
  }

  /**
   * Creates an excerpt of a string
   * It removes all html tags first and then uses str::short
   *
   * @param  string  $string The string to be shortened
   * @param  int     $chars The final number of characters the string should have
   * @param  boolean $removehtml True: remove the HTML tags from the string first
   * @param  string  $rep The element, which should be added if the string is too long. Ellipsis is the default.
   * @return string  The shortened string
   */
  static public function excerpt($string, $chars = 140, $removehtml = true, $rep='…') {
    if($removehtml) $string = strip_tags($string);
    $string = str_replace(PHP_EOL, ' ', trim($string));
    if(static::length($string) <= $chars) return $string;
    return $chars == 0 ? $string : static::substr($string, 0, strrpos(static::substr($string, 0, $chars), ' ')) . $rep;
  }

  /**
   * The widont function makes sure that there are no
   * typographical widows at the end of a paragraph –
   * that's a single word in the last line
   *
   * @param string $string
   * @return string
   */
  static public function widont($string = '') {
    return preg_replace_callback('|([^\s])\s+([^\s]+)\s*$|', function($matches) {
      if(str::contains($matches[2], '-')) {
        return $matches[1] . ' ' . str_replace('-', '&#8209;', $matches[2]);
      } else {
        return $matches[1] . '&nbsp;' . $matches[2];
      }
    }, $string);
  }

  /**
   * An UTF-8 safe version of substr()
   *
   * @param  string  $str
   * @param  int     $start
   * @param  int     $length
   * @return string
   */
  static public function substr($str, $start, $length = null) {
    return MB ? mb_substr($str, $start, $length == null ? static::length($str) : $length, 'UTF-8') : substr($str, $start, $length);
  }

  /**
   * An UTF-8 safe version of strtolower()
   *
   * @param  string  $str
   * @return string
   */
  static public function lower($str) {
    return MB ? mb_strtolower($str, 'UTF-8') : strtolower($str);
  }

  /**
   * An UTF-8 safe version of strotoupper()
   *
   * @param  string  $str
   * @return string
   */
  static public function upper($str) {
    return MB ? mb_strtoupper($str, 'UTF-8') : strtoupper($str);
  }

  /**
   * An UTF-8 safe version of strlen()
   *
   * @param  string  $str
   * @return string
   */
  static public function length($str) {
    return MB ? mb_strlen($str, 'UTF-8') : strlen($str);
  }

  /**
   * Checks if a str contains another string
   *
   * @param  string  $str
   * @param  string  $needle
   * @param  boolean $i ignore upper/lowercase
   * @return string
   */
  static public function contains($str, $needle, $i = true) {
    if($i) {
      $str    = static::lower($str);
      $needle = static::lower($needle);
    }
    return strstr($str, $needle) ? true : false;
  }

  /**
   * Generates a random string
   *
   * @param  int  $length The length of the random string
   * @return string
   */
  static public function random($length = false, $type = 'alphaNum') {
    $length = $length ? $length : rand(5,10);
    $pool   = static::pool($type);
    shuffle($pool);
    $size   = count($pool) - 1;
    $hash   = '';
    for($x = 0; $x < $length; $x++) {
      $hash .= $pool[rand(0, $size)];
    }
    return $hash;
  }

  /**
   * Convert a string to a safe version to be used in a URL
   *
   * @param  string  $text The unsafe string
   * @param  string  $separator To be used instead of space and other non-word characters.
   * @return string  The safe string
   */
  static public function slug($string, $separator = '-', $allowed = 'a-z0-9') {

    $string = trim($string);
    $string = static::lower($string);
    $string = static::ascii($string);

    // replace spaces with simple dashes
    $string = preg_replace('![^' . $allowed . ']!i','-', $string);
    // remove double dashes
    $string = preg_replace('![-]{2,}!','-', $string);
    // trim trailing and leading dashes
    $string = trim($string, '-');
    // replace slashes with dashes
    $string = str_replace('/', '-', $string);

    return $string;

  }

  /**
   * Better alternative for explode()
   * It takes care of removing empty values
   * and it has a built-in way to skip values
   * which are too short.
   *
   * @param  string  $string The string to split
   * @param  string  $separator The string to split by
   * @param  int     $length The min length of values.
   * @return array   An array of found values
   */
  static public function split($string, $separator = ',', $length = 1) {

    if(is_array($string)) return $string;

    $string = trim($string, $separator);
    $parts  = explode($separator, $string);
    $out    = array();

    foreach($parts AS $p) {
      $p = trim($p);
      if(static::length($p) > 0 and static::length($p) >= $length) $out[] = $p;
    }

    return $out;

  }

  /**
   * An UTF-8 safe version of ucwords()
   *
   * @param  string  $string
   * @return string
   */
  static public function ucwords($string) {
    return MB ? mb_convert_case($string, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($string));
  }

  /**
   * An UTF-8 safe version of ucfirst()
   *
   * @param  string $string
   * @return string
   */
  static public function ucfirst($string) {
    return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1));
  }

  /**
   * Tries to detect the string encoding
   *
   * @param string $string
   * @return string
   */
  static public function encoding($string) {

    if(MB) {
      return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251');
    } else {
      foreach(array('utf-8', 'iso-8859-1', 'windows-1251') as $item) {
        if(md5(iconv($item, $item, $string)) == md5($string)) return $item;
      }
      return false;
    }

  }

  /**
   * Converts a string to a different encoding
   *
   * @param string $string
   * @param string $targetEncoding
   * @param string $sourceEncoding (optional)
   * @return string
   */
  static public function convert($string, $targetEncoding, $sourceEncoding = null) {
    // detect the source encoding if not passed as third argument
    if(is_null($sourceEncoding)) $sourceEncoding = static::encoding($string);
    return iconv($sourceEncoding, $targetEncoding, $string);
  }

  /**
   * Converts a string to UTF-8
   *
   * @param  string  $string
   * @return string
   */
  static public function utf8($string) {
    return static::convert($string, 'utf-8');
  }

  /**
   * A better way to strip slashes
   *
   * @param  string  $string
   * @return string
   */
  static public function stripslashes($string) {
    if(is_array($string)) return $string;
    return get_magic_quotes_gpc() ? stripslashes($string) : $string;
  }

  /**
   * A super simple string template engine,
   * which replaces tags like {mytag} with any other string
   *
   * @param  string $string
   * @param  array  $replace An associative array with keys, which should be replaced and values.
   * @return string
   */
  static public function template($string, $data = array()) {
    $replace = array();
    foreach($data as $key => $value) $replace['{' . $key . '}'] = $value;
    return str_replace(array_keys($replace), array_values($replace), $string);
  }

  /**
   * Convert a string to 7-bit ASCII.
   *
   * @param  string  $string
   * @return string
   */
  static public function ascii($string) {
    $foreign = static::$ascii;
    $string  = preg_replace(array_keys($foreign), array_values($foreign), $string);
    return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string);
  }

  /**
   * Forces a download of the string as text file
   *
   * @param string $string
   * @param string $name Optional name for the downloaded file
   */
  static public function download($string, $name = null) {

    header::download(array(
      'name' => $name ? $name : 'text.txt',
      'size' => static::length($string),
      'mime' => 'text/plain',
    ));

    die($string);

  }

  /**
   * Checks if a string starts with the passed needle
   *
   * @param string $string
   * @param string $needle
   * @return boolean
   */
  static public function startsWith($string, $needle) {
    return $needle === '' or strpos($string, $needle) === 0;
  }

  /**
   * Checks if a string ends with the passed needle
   *
   * @param string $string
   * @param string $needle
   * @return boolean
   */
  static public function endsWith($string, $needle) {
    return $needle === '' or static::substr($string, -static::length($needle)) === $needle;
  }

  /**
   * Get a character pool with various possible combinations
   *
   * @param  string  $type
   * @param  boolean $array
   * @return string
   */
  static public function pool($type, $array = true) {

    $pool = array();

    if(is_array($type)) {
      foreach($type as $t) {
        $pool = array_merge($pool, static::pool($t));
      }
    } else {

      switch($type) {
        case 'alphaLower':
          $pool = range('a','z');
          break;
        case 'alphaUpper':
          $pool = range('A', 'Z');
          break;
        case 'alpha':
          $pool = static::pool(array('alphaLower', 'alphaUpper'));
          break;
        case 'num':
          $pool = range(0, 9);
          break;
        case 'alphaNum':
          $pool = static::pool(array('alpha', 'num'));
          break;
      }

    }

    return $array ? $pool : implode('', $pool);

  }

}
