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

/**
 * A generic implementation for RFC 4648 base-X encoders/decoders.
 *
 * @link http://tools.ietf.org/html/rfc4648 RFC 4648
 * @see GTBase16, GTBase32, GTBase64
 *
 * @package util
 */
class GTBaseX {

    private $values;
    private $min;
    private $max;
    private $bits;
    private $block;
    private $pad;

    /**
     * Constructs an encoder/decoder using the given characters.
     *
     * Example for Base16:
     *
     * <code>
     * $base = new BaseX('0123456789ABCDEF', false, ' ');
     * </code>
     *
     * @throws GTException
     * @param  string  $alphabet the encoding alphabet
     * @param  boolean $caseSensitive if true both the encoder/decoder will be case sensitive
     * @param  string  $pad the padding characters used to even out the encoded output
     *
     * @see GTBase16, GTBase32, GTBase64
     */
    public function __construct($alphabet, $caseSensitive, $pad) {

        $this->bits = 1;

        while ((1 << $this->bits) < strlen($alphabet)) {
            $this->bits++;
        }

        if ((1 << $this->bits) != strlen($alphabet)) {
            throw new GTException("The size of the encoding alphabet is not a power of 2");
        }

        $this->block = 8 / GTUtil::gcd(8, $this->bits);
        $this->chars = GTUtil::toArray($alphabet);

        $this->min = -1;
        $this->max = -1;

        if ($caseSensitive) {

            $this->addMinMax($alphabet);

            $this->values = array();

            for ($i = 0; $i < ($this->max - $this->min) + 1; $i++) {
                array_push($this->values, -1);
            }

            $this->addChars($alphabet);

        } else {

            $this->addMinMax(strtoupper($alphabet));
            $this->addMinMax(strtolower($alphabet));

            $this->values = array();

            for ($i = 0; $i < ($this->max - $this->min) + 1; $i++) {
                array_push($this->values, -1);
            }

            $this->addChars(strtolower($alphabet));
            $this->addChars(strtoupper($alphabet));

        }

        if ($pad >= $this->min && $pad <= $this->max && $this->values[$pad - $this->min] != -1) {
            throw new GTException("The padding character appears in the encoding alphabet");
        }

        $this->pad = $pad;

    }

    /**
     * Encodes the given bytes into a base-X string.
     *
     * This method also optionally inserts a separator into the result with given frequency.
     *
     * @throws GTException
     * @param  array $bytes the bytes to encode
     * @param  int $offset the start offset in bytes array
     * @param  int $length the number of bytes to encode
     * @param  string $separator if separator is not null it's inserted into the encoded string at frequency
     * @param  int $frequency if separator is not null it's inserted into the encoded string at frequency
     * @return string string containing the encoded data
     */
    public function encode($bytes, $offset = null, $length = null, $separator = null, $frequency = null) {

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

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

        if ($offset == null) {
            $offset = 0;
        }

        if ($length == null) {
            $length = count($bytes);
        }

        $offset = (int) $offset;
        $length = (int) $length;

        if ($offset < 0 || $offset > count($bytes)) {
            throw new GTException("parameter offset out of bounds");
        }

        if ($length < 0 || $offset + $length > count($bytes)) {
            throw new GTException("parameter length out of bounds");
        }

        if ($separator == null) {
            $frequency = 0;

        } else {

            for ($i = 0; $i < strlen($separator); $i++) {
                $c = $separator{$i};

                if ($c >= $this->min && $c <= $this->max && $this->values[$c - $this->min] != -1) {
                    throw new GTException("parameter separator contains characters from the encoding alphabet");
                }
            }
        }

        $result = "";

        $i = 0; // number of output characters produced
        $j = 0; // number of input bytes consumed

        $buff = 0; // buffer of input bits not yet sent to output
        $size = 0; // number of bits in the buffer
        $mask = (1 << $this->bits) - 1;

        while ($this->bits * $i < 8 * $length) {

            if ($frequency > 0 && $i > 0 && $i % $frequency == 0) {
                $result .= $separator;
            }

            while ($size < $this->bits) {

                $next = ($j < $length ? $bytes[$offset + $j] : 0);
                $j++;

                $buff = ($buff << 8) | ($next & 0xff);
                $size += 8;

            }

            $result .= $this->chars[($buff >> ($size - $this->bits)) & $mask];
            $size -= $this->bits;

            $i++;

        }

        // pad

        while ($i % $this->block != 0) {
            if ($frequency > 0 && $i > 0 && $i % $frequency == 0) {
                $result .= $separator;
            }

            $result .= $this->pad;
            $i++;
        }

        return $result;

    }

    /**
     * Decodes the given base-X string into bytes.
     *
     * This method silently ignores any non-base-X characters.
     *
     * @throws GTException
     * @param  string $string the string to decode
     * @return array decoded bytes
     */
    public function decode($string) {

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

        $result = array();

        $i = 0; // number of output bytes produced
        $j = 0; // number of input  bytes consumed

        $buff = 0; // buffer of input bits not yet sent to output
        $size = 0; // number of bits in the buffer

        while ($j < strlen($string)) {

            $next = ord($string{$j});
            $j++;

            if ($next < $this->min || $next > $this->max) {
                continue;
            }

            $next = $this->values[$next - $this->min];

            if ($next == -1) {
                continue;
            }

            $buff = ($buff << $this->bits) | $next;
            $size += $this->bits;

            while ($size >= 8) {

                $result[$i] = (($buff >> ($size - 8)) & 0xff);

                $size -= 8;
                $i++;

            }
        }

        return $result;

    }

    /**
     * Updates $this->min and $this->max so that the range [$this->min .. $this->max] includes all values from $string
     *
     * @param  string $string characters to process
     * @return void
     */
    private function addMinMax($string) {

        for ($i = 0; $i < strlen($string); $i++) {

            $c = ord($string{$i});

            if ($this->min == -1 || $this->min > $c) {
                $this->min = $c;
            }

            if ($this->max == -1 || $this->max < $c) {
                $this->max = $c;
            }
        }
    }

    /**
     * Adds the characters from the given string to $this->values lookup table.
     *
     * @throws GTException
     * @param  string $string characters to process
     * @return void
     */
    private function addChars($string) {

        for ($i = 0; $i < strlen($string); $i++) {

            $c = ord($string{$i}) - $this->min;

            if ($this->values[$c] != -1 && $this->values[$c] != $i) {
                throw new GTException("Duplicate character in encoding alphabet");
            }

            $this->values[$c] = $i;

        }
    }

}

?>
