// Adapted from [base64.js](https://goo.gl/VxhUVz).
//
// Copyright 2007 The Closure Library Authors.
// Modifications Copyright 2016 Mikol Graves.
//
// 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.

(function (context) {
// -----------------------------------------------------------------------------

'use strict';

var id = 'codec';
var dependencies = ['./utf8'];

function factory(utf8) {
  /**
   * @type {Object}
   * @private
   */
  var charactersByByte = {};

  /**
   * @type {Object}
   * @private
   */
  var bytesByCharacter = {};

  /**
   * @const {string}
   * @private
   */
  /* jscs:disable maximumLineLength */
  var ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  /* jscs:enable maximumLineLength */

  (function () {
    for (var x = 0, nx = ALPHABET.length; x < nx; x++) {
      charactersByByte[x] = ALPHABET.charAt(x);
      bytesByCharacter[charactersByByte[x]] = x;
    }
  }());

  /**
   * Base64-encode a string.
   *
   * @param {string} string A string to encode.
   * @return {string} The base64-encoded string.
   */
  function encodeString(string) {
    return encodeBytes(utf8.asBytes(string));
  }

  /**
   * Base64-decode a string.
   *
   * @param {string} string Input to decode (any whitespace is ignored).
   * @return {string} The decoded value.
   */
  function decodeAsString(string) {
    return utf8.asString(decodeAsBytes(string));
  }

  /**
   * Base64-encode an array of bytes.
   *
   * @param {Array<number>} input Bytes (that is numbers in [0, 255]) to encode.
   * @return {string} The base64-encoded string.
   */
  function encodeBytes(bytes) {
    var map = charactersByByte;
    var characters = [];

    for (var i = 0; i < bytes.length; i += 3) {
      var byte1 = bytes[i];
      var haveByte2 = i + 1 < bytes.length;
      var byte2 = haveByte2 ? bytes[i + 1] : 0;
      var haveByte3 = i + 2 < bytes.length;
      var byte3 = haveByte3 ? bytes[i + 2] : 0;

      var b1 = byte1 >> 2;
      var b2 = ((byte1 & 0x03) << 4) | (byte2 >> 4);
      var b3 = ((byte2 & 0x0F) << 2) | (byte3 >> 6);
      var b4 = byte3 & 0x3F;

      if (!haveByte3) {
        b4 = 64;

        if (!haveByte2) {
          b3 = 64;
        }
      }

      characters.push(map[b1], map[b2], map[b3], map[b4]);
    }

    return characters.join('');
  }

  /**
   * Base64-decode a string to an Array of numbers.
   *
   * In base-64 decoding, groups of four characters are converted into three
   * bytes. If the encoder did not apply padding, the input length may not
   * be a multiple of 4.
   *
   * In this case, the last group will have fewer than 4 characters, and
   * padding will be inferred.  If the group has one or two characters, it
   * decodes to one byte. If the group has three characters, it decodes to
   * two bytes.
   *
   * @param {string} input Input to decode. Any whitespace is ignored, and the
   *     input maybe encoded with either supported alphabet (or a mix thereof).
   * @return {!Array<number>} Bytes representing the decoded value.
   */
  function decodeAsBytes(input) {
    var bytes = [];
    var nx = input.length;
    var x = 0;

    /**
     * @param {number} defaultValue Used for end-of-input.
     * @return {number} The next 6-bit value or the default for end-of-input.
     */
    function getByte(defaultValue) {
      while (x < nx) {
        var c = input.charAt(x++);
        var b = bytesByCharacter[c];

        if (b != null) {
          return b;
        }

        if (!/^[\s\xa0]*$/.test(c)) {
          throw Error('Unknown base64 encoding at character: ' + c);
        }

        // We encountered whitespace: loop around to the next input character.
      }

      return defaultValue;
    }

    while (true) {
      var byte1 = getByte(-1);
      var byte2 = getByte(0);
      var byte3 = getByte(64);
      var byte4 = getByte(64);

      // The common case is that all four bytes are present, so if we have byte4
      // we can skip over the truncated input special case handling.
      if (byte4 === 64) {
        if (byte1 === -1) {
          // No input left to decode.
          return bytes;
        }
        // Here we know an intermediate number of bytes are missing. The
        // defaults for byte2, byte3 and byte4 apply the inferred padding rules
        // per the public API documentation. That is, 1 byte missing should
        // yield 2 bytes of output, but 2 or 3 missing bytes yield a single byte
        // of output. (Recall that 64 corresponds the padding character).
      }

      var outByte1 = (byte1 << 2) | (byte2 >> 4);
      bytes.push(outByte1);

      if (byte3 != 64) {
        var outByte2 = ((byte2 << 4) & 0xF0) | (byte3 >> 2);
        bytes.push(outByte2);

        if (byte4 != 64) {
          var outByte3 = ((byte3 << 6) & 0xC0) | byte4;
          bytes.push(outByte3);
        }
      }
    }
  }

  return {
    encodeString: encodeString,
    decodeAsString: decodeAsString,
    encodeBytes: encodeBytes,
    decodeAsBytes: decodeAsBytes
  };
}

// -----------------------------------------------------------------------------
var n = dependencies.length;
var o = 'object';
var r = /([^-_\s])[-_\s]+([^-_\s])/g;
function s(m, a, b) { return a + b.toUpperCase(); }
context = typeof global === o ? global : typeof window === o ? window : context;
if (typeof define === 'function' && define.amd) {
  define(dependencies, function () {
    return factory.apply(context, [].slice.call(arguments));
  });
} else if (typeof module === o && module.exports) {
  for (; n--;) { dependencies[n] = require(dependencies[n]); }
  module.exports = factory.apply(context, dependencies);
} else {
  for (; n--;) { dependencies[n] = context[dependencies[n]]; }
  context[id.replace(r, s)] = factory.apply(context, dependencies);
}
}(this));