<script lang="ts">
  import {
    beforeUpdate,
    afterUpdate,
    createEventDispatcher,
    tick,
  } from "svelte";
  import { BlockTitle } from "@gradio/atoms";
  import { Copy, Check } from "@gradio/icons";
  import { fade } from "svelte/transition";
  import type { SelectData } from "@gradio/utils";

  export let value = "";
  export let value_is_output = false;
  export let lines = 1;
  export let placeholder = "Type here...";
  export let label: string;
  export let info: string | undefined = undefined;
  export let disabled = false;
  export let show_label = true;
  export let container = true;
  export let max_lines: number;
  export let type: "text" | "password" | "email" = "text";
  export let show_copy_button = false;
  export let rtl = false;
  export let autofocus = false;
  export let text_align: "left" | "right" | undefined = undefined;
  export let autoscroll = true;

  let el: HTMLTextAreaElement | HTMLInputElement;
  let copied = false;
  let timer: NodeJS.Timeout;
  let can_scroll: boolean;
  let previous_scroll_top = 0;
  let user_has_scrolled_up = false;

  $: value, el && lines !== max_lines && resize({ target: el });

  $: if (value === null) value = "";

  const dispatch = createEventDispatcher<{
    change: string;
    submit: undefined;
    blur: undefined;
    select: SelectData;
    input: undefined;
    focus: undefined;
  }>();

  beforeUpdate(() => {
    can_scroll = el && el.offsetHeight + el.scrollTop > el.scrollHeight - 100;
  });

  const scroll = (): void => {
    if (can_scroll && autoscroll && !user_has_scrolled_up) {
      el.scrollTo(0, el.scrollHeight);
    }
  };

  function handle_change(): void {
    dispatch("change", value);
    if (!value_is_output) {
      dispatch("input");
    }
  }
  afterUpdate(() => {
    if (autofocus) {
      el.focus();
    }
    if (can_scroll && autoscroll) {
      scroll();
    }
    value_is_output = false;
  });
  $: value, handle_change();

  async function handle_copy(): Promise<void> {
    if ("clipboard" in navigator) {
      await navigator.clipboard.writeText(value);
      copy_feedback();
    }
  }

  function copy_feedback(): void {
    copied = true;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      copied = false;
    }, 1000);
  }

  function handle_select(event: Event): void {
    const target: HTMLTextAreaElement | HTMLInputElement = event.target as
      | HTMLTextAreaElement
      | HTMLInputElement;
    const text = target.value;
    const index: [number, number] = [
      target.selectionStart as number,
      target.selectionEnd as number,
    ];
    dispatch("select", { value: text.substring(...index), index: index });
  }

  async function handle_keypress(e: KeyboardEvent): Promise<void> {
    await tick();
    if (e.key === "Enter" && e.shiftKey && lines > 1) {
      e.preventDefault();
      dispatch("submit");
    } else if (
      e.key === "Enter" &&
      !e.shiftKey &&
      lines === 1 &&
      max_lines >= 1
    ) {
      e.preventDefault();
      dispatch("submit");
    }
  }

  function handle_scroll(event: Event): void {
    const target = event.target as HTMLElement;
    const current_scroll_top = target.scrollTop;
    if (current_scroll_top < previous_scroll_top) {
      user_has_scrolled_up = true;
    }
    previous_scroll_top = current_scroll_top;

    const max_scroll_top = target.scrollHeight - target.clientHeight;
    const user_has_scrolled_to_bottom = current_scroll_top >= max_scroll_top;
    if (user_has_scrolled_to_bottom) {
      user_has_scrolled_up = false;
    }
  }

  async function resize(
    event: Event | { target: HTMLTextAreaElement | HTMLInputElement }
  ): Promise<void> {
    await tick();
    if (lines === max_lines) return;

    let max =
      max_lines === undefined
        ? false
        : max_lines === undefined // default
          ? 21 * 11
          : 21 * (max_lines + 1);
    let min = 21 * (lines + 1);

    const target = event.target as HTMLTextAreaElement;
    target.style.height = "1px";

    let scroll_height;
    if (max && target.scrollHeight > max) {
      scroll_height = max;
    } else if (target.scrollHeight < min) {
      scroll_height = min;
    } else {
      scroll_height = target.scrollHeight;
    }

    target.style.height = `${scroll_height}px`;
  }

  function text_area_resize(
    _el: HTMLTextAreaElement,
    _value: string
  ): any | undefined {
    if (lines === max_lines) return;
    _el.style.overflowY = "scroll";
    _el.addEventListener("input", resize);

    if (!_value.trim()) return;
    resize({ target: _el });

    return {
      destroy: () => _el.removeEventListener("input", resize),
    };
  }
</script>

<!-- svelte-ignore a11y-autofocus -->
<label class:container>
  <BlockTitle {show_label} {info}>{label}</BlockTitle>

  {#if lines === 1 && max_lines === 1}
    {#if type === "text"}
      <input
        data-testid="textbox"
        type="text"
        class="scroll-hide"
        dir={rtl ? "rtl" : "ltr"}
        bind:value
        bind:this={el}
        {placeholder}
        {disabled}
        {autofocus}
        on:keypress={handle_keypress}
        on:blur
        on:select={handle_select}
        on:focus
        style={text_align ? "text-align: " + text_align : ""}
      />
    {:else if type === "password"}
      <input
        data-testid="password"
        type="password"
        class="scroll-hide"
        bind:value
        bind:this={el}
        {placeholder}
        {disabled}
        {autofocus}
        on:keypress={handle_keypress}
        on:blur
        on:select={handle_select}
        on:focus
        autocomplete=""
      />
    {:else if type === "email"}
      <input
        data-testid="textbox"
        type="email"
        class="scroll-hide"
        bind:value
        bind:this={el}
        {placeholder}
        {disabled}
        {autofocus}
        on:keypress={handle_keypress}
        on:blur
        on:select={handle_select}
        on:focus
        autocomplete="email"
      />
    {/if}
  {:else}
    {#if show_label && show_copy_button}
      {#if copied}
        <button
          in:fade={{ duration: 300 }}
          aria-label="Copied"
          aria-roledescription="Text copied"><Check /></button
        >
      {:else}
        <button
          on:click={handle_copy}
          aria-label="Copy"
          aria-roledescription="Copy text"><Copy /></button
        >
      {/if}
    {/if}
    <textarea
      data-testid="textbox"
      use:text_area_resize={value}
      class="scroll-hide"
      dir={rtl ? "rtl" : "ltr"}
      bind:value
      bind:this={el}
      {placeholder}
      rows={lines}
      {disabled}
      {autofocus}
      on:keypress={handle_keypress}
      on:blur
      on:select={handle_select}
      on:focus
      on:scroll={handle_scroll}
      style={text_align ? "text-align: " + text_align : ""}
    />
  {/if}
</label>
