# captcha.js

    const _ = require('underscore');
    const { tap } = require('./utils');

    module.exports = AV => {
      /**
       * @class
       * @example
       * AV.Captcha.request().then(captcha => {
       *   captcha.bind({
       *     textInput: 'code', // the id for textInput
       *     image: 'captcha',
       *     verifyButton: 'verify',
       *   }, {
       *     success: (validateCode) => {}, // next step
       *     error: (error) => {}, // present error.message to user
       *   });
       * });
       */
      AV.Captcha = function Captcha(options, authOptions) {
        this._options = options;
        this._authOptions = authOptions;
        /**
         * The image url of the captcha
         * @type string
         */
        this.url = undefined;
        /**
         * The captchaToken of the captcha.
         * @type string
         */
        this.captchaToken = undefined;
        /**
         * The validateToken of the captcha.
         * @type string
         */
        this.validateToken = undefined;
      };

      /**
       * Refresh the captcha
       * @return {Promise.<string>} a new capcha url
       */
      AV.Captcha.prototype.refresh = function refresh() {
        return AV.Cloud._requestCaptcha(this._options, this._authOptions).then(
          ({ captchaToken, url }) => {
            _.extend(this, { captchaToken, url });
            return url;
          }
        );
      };

      /**
       * Verify the captcha
       * @param {String} code The code from user input
       * @return {Promise.<string>} validateToken if the code is valid
       */
      AV.Captcha.prototype.verify = function verify(code) {
        return AV.Cloud.verifyCaptcha(code, this.captchaToken).then(
          tap(validateToken => (this.validateToken = validateToken))
        );
      };

      if (process.env.CLIENT_PLATFORM === 'Browser') {
        /**
         * Bind the captcha to HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
         * @param [elements]
         * @param {String|HTMLInputElement} [elements.textInput] An input element typed text, or the id for the element.
         * @param {String|HTMLImageElement} [elements.image] An image element, or the id for the element.
         * @param {String|HTMLElement} [elements.verifyButton] A button element, or the id for the element.
         * @param [callbacks]
         * @param {Function} [callbacks.success] Success callback will be called if the code is verified. The param `validateCode` can be used for further SMS request.
         * @param {Function} [callbacks.error] Error callback will be called if something goes wrong, detailed in param `error.message`.
         */
        AV.Captcha.prototype.bind = function bind(
          { textInput, image, verifyButton },
          { success, error }
        ) {
          if (typeof textInput === 'string') {
            textInput = document.getElementById(textInput);
            if (!textInput)
              throw new Error(`textInput with id ${textInput} not found`);
          }
          if (typeof image === 'string') {
            image = document.getElementById(image);
            if (!image) throw new Error(`image with id ${image} not found`);
          }
          if (typeof verifyButton === 'string') {
            verifyButton = document.getElementById(verifyButton);
            if (!verifyButton)
              throw new Error(`verifyButton with id ${verifyButton} not found`);
          }

          this.__refresh = () =>
            this.refresh()
              .then(url => {
                image.src = url;
                if (textInput) {
                  textInput.value = '';
                  textInput.focus();
                }
              })
              .catch(err => console.warn(`refresh captcha fail: ${err.message}`));
          if (image) {
            this.__image = image;
            image.src = this.url;
            image.addEventListener('click', this.__refresh);
          }

          this.__verify = () => {
            const code = textInput.value;
            this.verify(code)
              .catch(err => {
                this.__refresh();
                throw err;
              })
              .then(success, error)
              .catch(err => console.warn(`verify captcha fail: ${err.message}`));
          };
          if (textInput && verifyButton) {
            this.__verifyButton = verifyButton;
            verifyButton.addEventListener('click', this.__verify);
          }
        };

        /**
         * unbind the captcha from HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
         */
        AV.Captcha.prototype.unbind = function unbind() {
          if (this.__image)
            this.__image.removeEventListener('click', this.__refresh);
          if (this.__verifyButton)
            this.__verifyButton.removeEventListener('click', this.__verify);
        };
      }

      /**
       * Request a captcha
       * @param [options]
       * @param {Number} [options.width] width(px) of the captcha, ranged 60-200
       * @param {Number} [options.height] height(px) of the captcha, ranged 30-100
       * @param {Number} [options.size=4] length of the captcha, ranged 3-6. MasterKey required.
       * @param {Number} [options.ttl=60] time to live(s), ranged 10-180. MasterKey required.
       * @return {Promise.<AV.Captcha>}
       */
      AV.Captcha.request = (options, authOptions) => {
        const captcha = new AV.Captcha(options, authOptions);
        return captcha.refresh().then(() => captcha);
      };
    };