<?php
/*
 * Copyright 2008-2010 GuardTime AS
 *
 * This file is part of the GuardTime PHP SDK.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @package util
 */

/**
 * Collection of miscellaneous commonly used utility functions.
 *
 * @package util
 */
class GTUtil {

    /**
     * Computes the greatest common divisor (GCD) of two integers.
     *
     * Greatest common divisor is the largest integer that divides both numbers
     * without remainder.
     *
     * <code>
     * echo GTUtil::gcd(4, 8);
     *
     * // output:
     * // 4
     * </code>
     *
     * @static
     * @param  int $a the first integer
     * @param  int $b the second integer
     * @return int the greatest common divisor of a and b or 0 if both are 0
     */
    public static function gcd($a, $b) {

        $a = abs((int) $a);
        $b = abs((int) $b);

        while ($a > 0) {
            $c = $b % $a;
            $b = $a;
            $a = $c;
        }

        return $b;

    }

    /**
     * Computes the least common multiple (LCM) of two integers.
     *
     * Least common multiple is the smallest positive integer that can be divided
     * by both numbers without a remainder.
     *
     * <code>
     * echo GTUtil::lcm(4, 6);
     *
     * // output:
     * // 12
     * </code>
     *
     * @static
     * @throws GTException when the result is too big to fit into {@code int}
     * @param  int $a the first integer
     * @param  int $b the second integer
     * @return int the least common multiple of a and b, or 0 if either a or b is 0
     */
    public static function lcm($a, $b) {

        $a = (int) $a;
        $b = (int) $b;

        if ($a == 0 || $b == 0) {
            return 0;
        }

        $a = abs($a) / GTUtil::gcd($a, $b);
        $b = abs($b);

        if ($a > PHP_INT_MAX / $b) {
            throw new GTException("Integer overflow");
        }

        return $a * $b;
    }

    /**
     * Writes data to file.
     *
     * Example:
     *
     * <code>
     * GTUtil::write('file.txt', array(1, 2, 3));
     * </code>
     *
     * @static
     * @throws GTException
     * @param  string $file file name to write to
     * @param  array $bytes byte array that contains the bytes to write
     * @return void
     *
     * @see read
     */
    public static function write($file, array $bytes) {

        if (empty($file)) {
            throw new GTException("parameter file is required");
        }

        if ($bytes == null) {
            throw new GTException("parameter bytes is required");
        }

        if (!is_array($bytes)) {
            throw new GTException("parameter bytes must be an array");
        }

        $fp = fopen($file, 'wb+');

        if (!$fp) {
            throw new GTException("Unable to open file {$file} for writing");
        }

        if (!fwrite($fp, GTUtil::fromByteArray($bytes))) {
            throw new GTException("Unable to write to bytes to file {$file}");
        }

        if (!fclose($fp)) {
            throw new GTException("Unable to close file after writing {$file}");
        }

    }

    /**
     * Touches specified file.
     *
     * @static
     * @throws GTException
     * @param  $file the file to touch
     * @return void
     */
    public static function touch($file) {

        if (empty($file)) {
            throw new GTException("parameter file is required");
        }

        touch($file);
    }

    /**
     * Reads data from file.
     *
     * @static
     * @throws GTException
     * @param  string $file file name to read from
     * @return array byte array read from $file
     *
     * @see write
     */
    public static function read($file) {

        if (empty($file)) {
            throw new GTException("parameter file is required");
        }

        if (!is_file($file)) {
            throw new GTException("file {$file} does not exist");
        }

        if (!is_readable($file)) {
            throw new GTException("file {$file} is not readable");
        }

        $fp = fopen($file, 'rb');

        if (!$fp) {
            throw new GTException("Unable to open file {$file} for reading");
        }

        $length = filesize($file);

        if ($length > 0) {
            $data = fread($fp, $length);

        } else {
            $data = "";

        }

        if ($data === false) {
            throw new GTException("Unable to read from file {$file}");
        }

        fclose($fp);

        return GTUtil::toByteArray($data);
    }

    /**
     * Wrapper method for OpenSSL asn1parse.
     *
     * @static
     * @param  string $file file containing a valid ASN.1 DER object
     * @return void
     */
    public static function printAsn1($file) {
        passthru("openssl asn1parse -i -inform DER -in {$file}");
    }

    /**
     * Decodes an ASN.1 formatted time to unix timestamp.
     *
     * @static
     * @throws GTException
     * @param  string $time ASN.1 formatted time
     * @param  string $timezone timezone to use
     * @return int php unix timestamp
     */
    public static function decodeTime($time, $timezone = null) {

        if ($timezone != null) {

            $old_timezone = date_default_timezone_get();
            $new_timezone = date_default_timezone_set($timezone);

            if ($new_timezone === false) {
                throw new GTException("Unable to set timezone to {$timezone}");
            }

        }

        if ($time == null) {
            throw new GTException("parameter time must not be empty");
        }

        if (strlen($time) != 15) {
            throw new GTException("parameter time has invalid length");
        }

        $tokens = array(
            'year' => substr($time, 0, 4),
            'month' => substr($time, 4, 2),
            'day' => substr($time, 6, 2),
            'hour' => substr($time, 8, 2),
            'minute' => substr($time, 10, 2),
            'second' => substr($time, 12, 2)
        );

        foreach ($tokens as $name => $value) {
            if (!ctype_digit($value)) {
                throw new GTException("Invalid time encoding, {$name} = {$value}");
            }
        }

        $time = mktime(
            (int) $tokens['hour'],
            (int) $tokens['minute'],
            (int) $tokens['second'],
            (int) $tokens['month'],
            (int) $tokens['day'],
            (int) $tokens['year']
        );

        if ($timezone != null) {
            date_default_timezone_set($old_timezone);
        }

        return $time;
    }

    /**
     * Formats a PHP unix timestamp in a more human readable format (%Y-%m-%d %H:%M:%S UTC)
     *
     * @static
     * @throws GTException
     * @param  $time php unix timestamp
     * @param  $timezone the timezone to use
     * @return string formatted datetime
     */
    public static function formatTime($time, $timezone = null) {

        if ($timezone != null) {

            $old_timezone = date_default_timezone_get();
            $new_timezone = date_default_timezone_set($timezone);

            if ($new_timezone === false) {
                throw new GTException("Unable to set timezone to {$timezone}");
            }

        }

        $formatted = strftime('%Y-%m-%d %H:%M:%S UTC', $time);

        if ($timezone != null) {
            date_default_timezone_set($old_timezone);
        }

        return $formatted;
    }

    /**
     * Pads $array by prepending $value until the size of $array equals $length.
     *
     * @static
     * @param  $array the array to pad
     * @param  $length the length to pad to
     * @param  $value padding value
     * @return void
     */
    public static function lpad(array &$array, $length, $value) {
        while (count($array) < $length) {
            array_unshift($array, $value);
        }
    }

    /**
     * Pads $array by appending $value until the size of $array equals $length.
     *
     * @static
     * @param  $array the array to pad
     * @param  $length the length to pad to
     * @param  $value padding value
     * @return void
     */
    public static function rpad(array &$array, $length, $value) {
        while (count($array) < $length) {
            array_push($array, $value);
        }
    }

    /**
     * Converts a string into an array of characters.
     *
     * <code>
     * $result = GTUtil::toArray('foo');
     *
     * print_r($result);
     *
     * // output:
     * // Array
     * // (
     * //   [0] => f
     * //   [1] => o
     * //   [2] => o
     * // )
     * </code>
     *
     * @static
     * @param  string $string the input string
     * @return array an array built from characters in string
     */
    public static function toArray($string) {

        $result = array();

        if (empty($string)) {
            return $result;
        }

        for ($i = 0; $i < strlen($string); $i++) {
            array_push($result, $string{$i});
        }

        return $result;

    }

    /**
     * Converts an array of characters into a string
     *
     * <code>
     * echo GTUtil::fromArray(array('f', 'o', 'o'));
     *
     * // output:
     * // foo
     * </code>
     *
     * @static
     * @param  $array the input array
     * @return string a string built from characters in array
     */
    public static function fromArray(array $array = null) {

        $result = "";

        if (empty($array)) {
            return $result;
        }

        for ($i = 0; $i < count($array); $i++) {
            $result .= $array[$i];
        }

        return $result;

    }

    /**
     * Converts a string into an array of bytes.
     *
     * Each byte in the resulting array will represent the ASCII value of each
     * character in the input string
     *
     * <code>
     * $result = GTUtil::toByteArray('foo');
     *
     * print_r($result);
     *
     * // output:
     * // Array
     * // (
     * //   [0] => 102
     * //   [1] => 111
     * //   [2] => 111
     * // )
     * </code>
     *
     * @static
     * @param  $string the input string
     * @return array an array of bytes representing the characters in string
     */
    public static function toByteArray($string) {

        $result = array();

        if (empty($string)) {
            return $result;
        }

        for ($i = 0; $i < strlen($string); $i++) {
            array_push($result, ord($string{$i}));
        }

        return $result;

    }

    /**
     * Converts a byte array into a string.
     *
     * Each character in the string will represent the ASCII code of each
     * byte in the input array
     *
     * <code>
     * $bytes = array(
     *   ord('f'),
     *   ord('o'),
     *   ord('o')
     * );
     *
     * echo GTUtil::fromByteArray($bytes);
     *
     * // output:
     * // foo
     * </code>
     *
     * @static
     * @param  $array the input array
     * @return string a string built from bytes in the array
     */
    public static function fromByteArray(array $array = null) {

        $result = "";

        if (empty($array)) {
            return $result;
        }

        for ($i = 0; $i < count($array); $i++) {
            $result .= chr($array[$i]);
        }

        return $result;

    }

    /**
     * Reads a short (2 bytes) integer from the given byte array.
     *
     * @static
     * @throws GTException
     * @param  array $array the byte array to read from
     * @param  int $position the position in the array to read from
     * @return int short integer from the given byte array
     */
    public static function readShort(array $array, $position) {

        if ($position + 2 > count($array) - 1) {
            throw new GTException("Array index out of bounds");
        }

        $integer = new GTBigInteger(
            array_slice($array, $position, 2)
        );

        return (int) $integer->getValue();
    }

    /**
     * Reads an integer (4 bytes) from the given byte array.
     *
     * @static
     * @throws GTException
     * @param  array $array the byte array to read from
     * @param  int $position the position in the array to read from
     * @return int 4 byte integer from the given byte array
     */
    public static function readInt(array $array, $position) {

        if ($position + 4 > count($array) - 1) {
            throw new GTException("Array index out of bounds");
        }

        $integer = new GTBigInteger(
            array_slice($array, $position, 4)
        );

        return (int) $integer->getValue();
    }

    /**
     * Reads a GTBigInteger (8 bytes) from the given byte array.
     *
     * @static
     * @throws GTException
     * @param  array $array the byte array to read from
     * @param  int $position the position in the array to read from
     * @return GTBigInteger 8 byte GTBigInteger from the given byte array
     */
    public static function readLong(array $array, $position) {

        if ($position + 8 > count($array) - 1) {
            throw new GTException("Array index out of bounds");
        }

        return new GTBigInteger(
            array_slice($array, $position, 8)
        );
    }

    /**
     * Computes and adds CRC32 to the given byte array.
     *
     * @static
     * @param  $bytes input byte array
     * @return array bytes with added CRC32 checksum
     */
    public static function addCrc32(array $bytes) {

        $checksum = crc32(GTUtil::fromByteArray($bytes));
        $checksum = sprintf("%u", $checksum); // make unsigned, needed for 32 bit PHP
        $checksum = new GTBigInteger($checksum);
        $checksum = $checksum->toBytes();

        // remove leading 0x0-s
        while (count($checksum) > 4) {
            array_shift($checksum);
        }

        foreach ($checksum as $byte) {
            array_push($bytes, $byte);
        }

        return $bytes;
    }

    /**
     * Gets OpenSSL version as string.
     *
     * @static
     * @return string OpenSSL version as string, example: 0.9.8
     */
    public static function getOpensslVersion() {

        $version = new GTBigInteger(OPENSSL_VERSION_NUMBER);

        $mask = new GTBigInteger(0xF);

        $major = $version->shiftRight(28)->bitAnd($mask)->getValue();
        $minor = $version->shiftRight(20)->bitAnd($mask)->getValue();
        $fix = $version->shiftRight(12)->bitAnd($mask)->getValue();

        return "{$major}.{$minor}.{$fix}";
    }

}

?>
