<?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
 */

/**
 * BigInteger implementation based on PHP BCMath.
 *
 * @link http://www.php.net/manual/en/book.bc.php
 *
 * @package util
 */
class GTBigInteger {

    /**
     * Constructs a new instance of GTBigInteger.
     *
     * Both negative and positive integers of any size are supported.
     *
     * Example:
     *
     * <code>
     * $int1 = new GTBigInteger(4);
     * $int2 = new GTBigInteger("-12312321351878937123123098123");
     * $int3 = new GTBigInteger(array(1, 226, 64));
     * </code>
     *
     * @throws GTException
     * @param  string|int|array $value value as string or integer or byte array
     */
    public function __construct($value) {

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

        if (is_array($value)) {

            $bytes = $value;

            $value = '0';
            $sign = '';

            $length = count($bytes);

            if ($length <= 0) {
                throw new GTException("Invalid length: {$length}");
            }

            if ($bytes[0] >> 7 == 1) {

                $sign = '-';

                for ($i = 0; $i < $length; $i++) {
                    $bytes[$i] = ~$bytes[$i] & 0xFF;
                }
            }

            for ($i = 0; $i < $length; $i++) {
                $value = bcadd($value, bcmul($bytes[$i], bcpow(256, $length - $i - 1, 0), 0), 0);
            }

            if ($sign == '-') {
                $value = bcadd($value, 1);
            }

            $value = $sign . $value;

        } else if ($value instanceof GTBigInteger) {
            $value = $value->getValue();
        }

        $value = (string) $value;

        $chars = array(
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-'
        );

        foreach (GTUtil::toArray($value) as $c) {
            if (!in_array($c, $chars, true)) {
                throw new GTException("parameter value contains invalid character: {$c}");
            }
        }

        $this->value = $value;
    }

    /**
     * Gets the integer value as string.
     *
     * @return string integer value as string
     */
    public function getValue() {
        return $this->value;
    }

    /**
     * Compares this GTBigInteger to another GTBigInteger.
     *
     * @param  GTBigInteger $integer the other GTBigInteger
     * @return int 0 if both integers are equal, 1 if current integer is larger than the other, -1 otherwise
     */
    public function comp(GTBigInteger $integer) {
        return bccomp($this->value, $integer->value, 0);
    }

    /**
     * Adds this GTBigInteger and another GTBigInteger.
     *
     * @param  GTBigInteger $integer the other GTBigInteger
     * @return GTBigInteger the sum of the two integers
     */
    public function add(GTBigInteger $integer) {
        return new GTBigInteger(bcadd($this->value, $integer->value, 0));
    }

    /**
     * Subtracts another GTBigInteger from this integer.
     *
     * @param  GTBigInteger $integer the GTBigInteger to subtract from this one
     * @return GTBigInteger the result of the subtraction
     */
    public function sub(GTBigInteger $integer) {
        return new GTBigInteger(bcsub($this->value, $integer->value, 0));
    }

    /**
     * Multiplies another GTBigInteger with this GTBigInteger.
     *
     * @param  GTBigInteger $integer the GTBigInteger to multiply with
     * @return GTBigInteger the result of the multiplication
     */
    public function mul(GTBigInteger $integer) {
        return new GTBigInteger(bcmul($this->value, $integer->value, 0));
    }

    /**
     * Divides this GTBigInteger with another GTBigInteger.
     *
     * @param  GTBigInteger $integer the GTBigInteger to divide by
     * @return GTBigInteger the result of the division
     */
    public function div(GTBigInteger $integer) {
        return new GTBigInteger(bcdiv($this->value, $integer->value, 0));
    }

    /**
     * Gets the modulus of this GTBigInteger using another GTBigInteger as modulus.
     *
     * @param  int $integer modulus
     * @return GTBigInteger the modulus
     */
    public function mod($modulus) {
        return new GTBigInteger(bcmod($this->value, $modulus));
    }

    /**
     * Raises this GTBigInteger to the power of another GTBigInteger.
     *
     * @param  GTBigInteger $integer power
     * @return GTBigInteger result of raising this GTBigInteger to the power of another GTBigInteger
     */
    public function pow(GTBigInteger $integer) {
        return new GTBigInteger(bcpow($this->value, $integer->value, 0));
    }

    /**
     * Returns a GTBigInteger whose value is (this << $step).
     *
     * @param  int $step shift distance
     * @return GTBigInteger this << $step
     */
    public function shiftLeft($step) {
        return new GTBigInteger(bcmul($this->value, bcpow(2, $step)));
    }

    /**
     * Returns a GTBigInteger whose value is (this >> $step).
     *
     * @param  int $step shift distance
     * @return GTBigInteger this >> $step
     */
    public function shiftRight($step) {
        return new GTBigInteger(bcdiv($this->value, bcpow(2, $step)));
    }

    /**
     * Returns a GTBitInteger whose value is ($this | $integer).
     *
     * @param  GTBigInteger $integer value to be OR'ed with this GTBigInteger
     * @return GTBigInteger $this | $integer
     */
    public function bitOr(GTBigInteger $integer) {

        $bytes1 = $this->toBytes();
        $bytes2 = $integer->toBytes();

        $length = max(count($bytes1), count($bytes2));

        GTUtil::lpad($bytes1, $length, 0x0);
        GTUtil::lpad($bytes2, $length, 0x0);

        $result = array();

        for ($i = 0; $i < $length; $i++) {
            $result[$i] = $bytes1[$i] | $bytes2[$i];
        }

        return new GTBigInteger($result);
    }

    /**
     * Returns a GTBigInteger whose value is ($this ^ $integer).
     *
     * @param  GTBigInteger $integer value to be XOR'ed with this GTBigInteger
     * @return GTBigInteger $this | $integer
     */
    public function bitXor(GTBigInteger $integer) {

        $bytes1 = $this->toBytes();
        $bytes2 = $integer->toBytes();

        $length = max(count($bytes1), count($bytes2));

        GTUtil::lpad($bytes1, $length, 0x0);
        GTUtil::lpad($bytes2, $length, 0x0);

        $result = array();

        for ($i = 0; $i < $length; $i++) {
            $result[$i] = $bytes1[$i] ^ $bytes2[$i];
        }

        return new GTBigInteger($result);
    }

    /**
     * Returns a GTBigInteger whose value is ($this & $integer).
     *
     * @param  GTBigInteger $integer value to be AND'ed with this GTBigInteger
     * @return GTBigInteger $this & $integer
     */
    public function bitAnd(GTBigInteger $integer) {

        $bytes1 = $this->toBytes();
        $bytes2 = $integer->toBytes();

        $length = max(count($bytes1), count($bytes2));

        GTUtil::lpad($bytes1, $length, 0x0);
        GTUtil::lpad($bytes2, $length, 0x0);

        $result = array();

        for ($i = 0; $i < $length; $i++) {
            $result[$i] = $bytes1[$i] & $bytes2[$i];
        }

        return new GTBigInteger($result);
    }

    /**
     * Returns a GTBigInteger whose value is (~$this).
     *
     *
     * @return GTBigInteger ~$this
     */
    public function bitNot() {

        $result = array();

        foreach ($this->toBytes() as $byte) {
            array_push($result, ~$byte & 0xFF);
        }

        return new GTBigInteger($result);
    }

    /**
     * Returns an array of bytes that contains this integers two's complement representation.
     *
     * Example:
     *
     * <code>
     * $int = new GTBigInteger(123456);
     *
     * print_r($int->toBytes());
     *
     * // Array
     * // (
     * //   [0] => 1
     * //   [1] => 226
     * //   [2] => 64
     * // )
     * </code>
     *
     * @return array byte array that contains two's complement representation of this integer
     */
    public function toBytes() {

        $result = '';

        $buff = $this->value;
        $sign = '';

        if ($buff{0} == '-') {
            $sign = '-';
            $buff = substr($buff, 1);
        }

        while (bccomp($buff, 255, 0) == 1) {

            $rest = bcmod($buff, 256);
            $buff = bcdiv($buff, 256, 0);

            $result = chr($rest) . $result;
        }

        $result = chr($buff) . $result;

        $bytes = GTUtil::toByteArray($result);

        if ($sign == '-') {

            for ($i = 0; $i < count($bytes); $i++) {
                $bytes[$i] = ~$bytes[$i] & 0xFF;
            }

            array_unshift($bytes, 0x0);

            $int = new GTBigInteger($bytes);
            $int = $int->add(new GTBigInteger(1));

            $bytes = $int->toBytes();

            // strip leading 0 bytes
            while (count($bytes) > 0 && $bytes[0] == 0x0) {
                array_shift($bytes);
            }

            return $bytes;

        } else {

            // add leading 0x0 so the integer isn't considered negative
            if (count($bytes) > 0 && $bytes[0] >> 7 == 1) {
                array_unshift($bytes, 0x0);
            }

            return $bytes;

        }

    }

    /**
     * Returns a bit string that contains the bits of this integer.
     *
     * Example:
     *
     * <code>
     * $int = new GTBigInteger(4);
     * echo $int->toBits();
     *
     * // 00000100
     * </code>
     *
     * @return string bit string containing the bits of this integer
     */
    public function toBits() {

        $result = '';

        foreach ($this->toBytes() as $byte) {

            $byte = decbin($byte);

            while (strlen($byte) % 8 != 0) {
                $byte = '0' . $byte;
            }

            $result .= $byte;
        }

        return $result;

    }

}

?>
