<script setup lang="ts">
import { computed, ref } from 'vue';

const props = withDefaults(
  defineProps<{
    autocompletion?: boolean;
    bracketMatching?: boolean;
    foldGutter?: boolean;
    height?: number | string;
    language?: string;
    lineNumbers?: boolean;
    modelValue?: string;
    placeholder?: string;
    readonly?: boolean;
  }>(),
  {
    autocompletion: false,
    bracketMatching: false,
    foldGutter: false,
    height: '240px',
    language: 'text',
    lineNumbers: false,
    modelValue: '',
    placeholder: '',
    readonly: false,
  },
);

const emit = defineEmits<{
  change: [value: string];
  'update:modelValue': [value: string];
}>();

const lineNumbersRef = ref<HTMLPreElement | null>(null);
const textareaRef = ref<HTMLTextAreaElement | null>(null);

const editorStyle = computed(() => ({
  height: typeof props.height === 'number' ? `${props.height}px` : props.height,
}));

const lineNumbersText = computed(() => {
  const count = Math.max(1, props.modelValue.split(/\r\n|\r|\n/).length);
  return Array.from({ length: count }, (_, index) => index + 1).join('\n');
});

const languageLabel = computed(() => {
  const language = props.language === 'python3' ? 'python' : props.language;
  return language.toUpperCase();
});

function handleInput(event: Event) {
  const value = (event.target as HTMLTextAreaElement).value;
  emit('update:modelValue', value);
}

function handleChange(event: Event) {
  const value = (event.target as HTMLTextAreaElement).value;
  emit('change', value);
}

function handleKeydown(event: KeyboardEvent) {
  if (props.readonly || event.key !== 'Tab') {
    return;
  }

  event.preventDefault();
  const target = event.target as HTMLTextAreaElement;
  const start = target.selectionStart;
  const end = target.selectionEnd;
  const value = props.modelValue || '';
  const nextValue = `${value.slice(0, start)}  ${value.slice(end)}`;
  emit('update:modelValue', nextValue);

  requestAnimationFrame(() => {
    target.selectionStart = start + 2;
    target.selectionEnd = start + 2;
  });
}

function handleScroll(event: Event) {
  if (!lineNumbersRef.value) {
    return;
  }

  lineNumbersRef.value.scrollTop = (event.target as HTMLTextAreaElement).scrollTop;
}

defineExpose({
  focus: () => textareaRef.value?.focus(),
});
</script>

<template>
  <div
    class="code-editor"
    :class="{ 'code-editor--readonly': readonly }"
    :style="editorStyle"
  >
    <div class="code-editor__body">
      <pre
        v-if="lineNumbers"
        ref="lineNumbersRef"
        class="code-editor__lines"
        aria-hidden="true"
        >{{ lineNumbersText }}</pre
      >
      <textarea
        ref="textareaRef"
        class="code-editor__textarea"
        :placeholder="placeholder"
        :readonly="readonly"
        :spellcheck="false"
        :value="modelValue"
        @change="handleChange"
        @input="handleInput"
        @keydown="handleKeydown"
        @scroll="handleScroll"
      ></textarea>
      <span v-if="language" class="code-editor__language">
        {{ languageLabel }}
      </span>
    </div>
  </div>
</template>

<style scoped>
.code-editor {
  overflow: hidden;
  border: 1px solid hsl(var(--border));
  border-radius: 6px;
  background: hsl(var(--muted) / 45%);
}

.code-editor__body {
  position: relative;
  display: flex;
  height: 100%;
  min-height: 0;
}

.code-editor__lines {
  min-width: 42px;
  height: 100%;
  margin: 0;
  padding: 10px 8px;
  overflow: hidden;
  border-right: 1px solid hsl(var(--border));
  color: hsl(var(--muted-foreground) / 70%);
  font-family:
    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
    'Courier New', monospace;
  font-size: 12px;
  line-height: 1.6;
  text-align: right;
  user-select: none;
}

.code-editor__textarea {
  width: 100%;
  height: 100%;
  min-width: 0;
  padding: 10px 12px;
  overflow: auto;
  border: 0;
  outline: none;
  resize: none;
  background: transparent;
  color: hsl(var(--foreground));
  font-family:
    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
    'Courier New', monospace;
  font-size: 12px;
  line-height: 1.6;
  tab-size: 2;
}

.code-editor__textarea::placeholder {
  color: hsl(var(--muted-foreground) / 55%);
}

.code-editor__language {
  position: absolute;
  right: 8px;
  bottom: 6px;
  pointer-events: none;
  color: hsl(var(--muted-foreground) / 55%);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0;
}

.code-editor--readonly .code-editor__textarea {
  cursor: default;
}
</style>
