import { ParameterType } from 'jspsych';

const info = {
  name: 'captcha',
  parameters: {
    prompt: {
      type: ParameterType.HTML_STRING,
      default: '',
    },
    difficulty: {
      type: ParameterType.INT,
      default: 3,
    },
    button_label: {
      type: ParameterType.STRING,
      default: 'Submit',
    },
    allow_refresh: {
      type: ParameterType.BOOL,
      default: true,
    },
    require_correct: {
      type: ParameterType.BOOL,
      default: true,
    },
    error_text: {
      type: ParameterType.HTML_STRING,
      default: 'That entry did not match, please try again.',
    },
  },
};

class CaptchaPlugin {
  constructor(jsPsych) {
    this.jsPsych = jsPsych;
    this.characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  }

  trial(display_element, trial) {
    const start_time = performance.now();
    const attempt_counts = { submission_count: 0, refresh_count: 0 };
    const difficulty_settings = this.resolve_difficulty(trial.difficulty);
    let captcha_text = this.generate_text(difficulty_settings.length);
    let last_target_at_submission = captcha_text;

    display_element.innerHTML = this.build_markup(
      trial.prompt,
      trial.button_label,
      trial.allow_refresh
    );

    const canvas_element = display_element.querySelector(
      '.jspsych_captcha_canvas'
    );
    const canvas_context = canvas_element.getContext('2d');
    const input_element = display_element.querySelector(
      '.jspsych_captcha_input'
    );
    const button_element = display_element.querySelector(
      '.jspsych_captcha_submit'
    );
    const refresh_button = display_element.querySelector(
      '.jspsych_captcha_refresh'
    );
    const error_element = display_element.querySelector(
      '.jspsych_captcha_error'
    );

    const draw_captcha = () => {
      this.draw_captcha(canvas_context, captcha_text, difficulty_settings);
    };

    draw_captcha();
    input_element.focus();

    if (refresh_button) {
      refresh_button.addEventListener('click', () => {
        attempt_counts.refresh_count += 1;
        captcha_text = this.generate_text(difficulty_settings.length);
        draw_captcha();
        error_element.textContent = '';
        input_element.value = '';
        input_element.focus();
      });
    }

    const length_hint_element = display_element.querySelector(
      '.jspsych_captcha_length_hint'
    );

    const update_button_state = () => {
      const current_length = input_element.value.trim().length;
      const required_length = captcha_text.length;
      button_element.disabled = current_length < required_length;
      if (length_hint_element) {
        length_hint_element.textContent =
          current_length < required_length
            ? `Type ${required_length - current_length} more character(s) to continue.`
            : '';
      }
    };

    const finish_trial = (response_value, is_correct) => {
      const trial_data = {
        rt: performance.now() - start_time,
        response: response_value,
        participant_entry: response_value,
        participant_entry_length: response_value.length,
        captcha_target: last_target_at_submission,
        correct: is_correct,
        submission_count: attempt_counts.submission_count,
        manual_refreshes: attempt_counts.refresh_count,
        difficulty_label: difficulty_settings.label,
        difficulty_length: difficulty_settings.length,
        difficulty_level: difficulty_settings.level,
      };
      display_element.innerHTML = '';
      this.jsPsych.finishTrial(trial_data);
    };

    const validate_response = () => {
      attempt_counts.submission_count += 1;
      const response_value = input_element.value.trim().toUpperCase();
      last_target_at_submission = captcha_text;
      const is_correct = response_value === captcha_text;

      if (response_value.length < captcha_text.length) {
        error_element.textContent = `Please enter all ${captcha_text.length} characters shown in the captcha.`;
        update_button_state();
        return;
      }

      if (is_correct || !trial.require_correct) {
        finish_trial(response_value, is_correct);
        return;
      }

      error_element.textContent = trial.error_text;
      captcha_text = this.generate_text(difficulty_settings.length);
      draw_captcha();
      input_element.value = '';
      input_element.focus();
    };

    button_element.addEventListener('click', validate_response);
    input_element.addEventListener('keydown', event => {
      if (event.key === 'Enter') {
        event.preventDefault();
        validate_response();
      }
    });
    input_element.addEventListener('input', update_button_state);
    update_button_state();
  }

  build_markup(prompt, button_label, allow_refresh) {
    const prompt_html = prompt
      ? `<div class="jspsych_captcha_prompt">${prompt}</div>`
      : '';
    const refresh_button = allow_refresh
      ? '<button type="button" class="jspsych_captcha_refresh" aria-label="Get a new captcha">↻</button>'
      : '';

    return `
      <div class="jspsych_captcha">
        ${prompt_html}
        <div class="jspsych_captcha_canvas_wrapper">
          <canvas class="jspsych_captcha_canvas" width="260" height="110" role="img" aria-label="Captcha"></canvas>
          ${refresh_button}
        </div>
        <label class="jspsych_captcha_label" aria-label="Enter the characters you see into the textbox below.">
          <input type="text" autocomplete="off" autocorrect="off" autocapitalize="characters" spellcheck="false" class="jspsych_captcha_input" />
          <span class="jspsych_captcha_length_hint"></span>
        </label>
        <button type="button" class="jspsych-btn jspsych_captcha_submit">${button_label}</button>
        <div class="jspsych_captcha_error" role="status" aria-live="polite"></div>
      </div>
    `;
  }

  draw_captcha(canvas_context, captcha_text, settings) {
    const { width, height } = canvas_context.canvas;
    canvas_context.clearRect(0, 0, width, height);
    const gradient = canvas_context.createLinearGradient(0, 0, width, height);
    gradient.addColorStop(0, '#f8fafc');
    gradient.addColorStop(1, '#e2e8f0');
    canvas_context.fillStyle = gradient;
    canvas_context.fillRect(0, 0, width, height);

    for (let index = 0; index < settings.noise_lines; index++) {
      canvas_context.beginPath();
      canvas_context.moveTo(Math.random() * width, Math.random() * height);
      canvas_context.bezierCurveTo(
        Math.random() * width,
        Math.random() * height,
        Math.random() * width,
        Math.random() * height,
        Math.random() * width,
        Math.random() * height
      );
      canvas_context.lineWidth = 1 + Math.random() * 2;
      canvas_context.strokeStyle = `rgba(30, 41, 59, ${0.15 +
        Math.random() * 0.2})`;
      canvas_context.stroke();
    }

    canvas_context.font = `bold ${settings.font_size}px 'Courier New', monospace`;
    canvas_context.textBaseline = 'middle';
    canvas_context.textAlign = 'center';

    const horizontal_step = width / (captcha_text.length + 1);
    const center_y = height / 2;

    for (let index = 0; index < captcha_text.length; index++) {
      const current_character = captcha_text[index];
      canvas_context.save();
      const jitter_y = this.random_between(-settings.jitter, settings.jitter);
      canvas_context.translate(
        horizontal_step * (index + 1),
        center_y + jitter_y
      );
      canvas_context.rotate(
        this.random_between(-settings.rotation, settings.rotation)
      );
      canvas_context.fillStyle = '#000000';
      canvas_context.fillText(current_character, 0, 0);
      canvas_context.restore();
    }

    const noise_pixels = Math.floor(width * height * settings.speckle_density);
    const image_data = canvas_context.getImageData(0, 0, width, height);
    for (let index = 0; index < noise_pixels; index++) {
      const pixel_offset =
        (Math.floor(Math.random() * width) +
          Math.floor(Math.random() * height) * width) *
        4;
      const shade = Math.random() > 0.5 ? 255 : 0;
      image_data.data[pixel_offset] = shade;
      image_data.data[pixel_offset + 1] = shade;
      image_data.data[pixel_offset + 2] = shade;
      image_data.data[pixel_offset + 3] = 255;
    }
    canvas_context.putImageData(image_data, 0, 0);
  }

  generate_text(length) {
    let generated_text = '';
    for (let index = 0; index < length; index++) {
      generated_text += this.characters.charAt(
        Math.floor(Math.random() * this.characters.length)
      );
    }
    return generated_text;
  }

  resolve_difficulty(difficulty_input) {
    const default_level = 3;
    let normalized_level = Number(difficulty_input);
    if (!Number.isFinite(normalized_level)) {
      normalized_level = default_level;
    }
    normalized_level = Math.round(Math.min(6, Math.max(1, normalized_level)));

    const base_length = 7;
    const base_font_size = 20;
    const base_rotation = 0.25;
    const max_rotation = base_rotation * 3;
    const base_noise_lines = 4;
    const base_speckle_density = 0.01;
    const base_jitter = 25;
    const noise_line_step = 0.8;
    const speckle_step = 0.0015;
    const jitter_step = 1.6;
    const rotation_step = (max_rotation - base_rotation) / 5;

    const rotation = base_rotation + rotation_step * (normalized_level - 1);
    const noise_lines = Math.round(
      base_noise_lines + noise_line_step * (normalized_level - 1)
    );
    const speckle_density =
      base_speckle_density + speckle_step * (normalized_level - 1);
    const jitter = base_jitter + jitter_step * (normalized_level - 1);

    return {
      length: base_length,
      noise_lines: noise_lines,
      rotation: rotation,
      jitter: jitter,
      font_size: base_font_size,
      speckle_density: speckle_density,
      label: `level_${normalized_level}`,
      level: normalized_level,
    };
  }

  random_between(min, max) {
    return Math.random() * (max - min) + min;
  }
}

CaptchaPlugin.info = info;

export default CaptchaPlugin;
