<template>
  <v-card flat elevation="0">
    <v-card-title v-if="title">{{ title }}</v-card-title>
    <v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
    <v-card-text>
      <v-form>
        <div v-if="isMultiline"
          class="multiline mb-5"
          :style="multilineElementStyle"
          v-html="html"
        >
        </div>
        <v-input v-else
          class="v-text-field"
          :hint="hint"
          persistent-hint
          v-model="text_"
        >
          <label
            class="v-label"
            :class="[ $vuetify.theme.isDark && 'theme--dark', text_ && text_.length && 'v-label--active' ]"
            style="left: 0px; right: auto; position: absolute;"
          >{{ label }}</label>
          <div class="input"
            :class="isMultiline ? 'multiline' : ''"
            v-html="html"
          >
          </div>
        </v-input>

        <v-container>

<!-- Variable fields -->

          <v-row no-gutters class="mb-2">
            <h4>Variable fields</h4>
          </v-row>

          <dougal-fixed-string-decoder-field v-for="(field, key) in fields" :key="key"
            v-model="fields[key]"
            :name="key"
            :colour="getHSLColourFor(key)"
            :readonly="readonly"
          >
            <template v-slot:append v-if="editableFieldList && !readonly">
              <v-btn
                class="ml-3"
                fab
                text
                small
                title="Remove this property"
              >
                <v-icon
                  color="error"
                  @click="removeField(key)"
                >mdi-minus</v-icon>
              </v-btn>
            </template>
          </dougal-fixed-string-decoder-field>

          <v-row dense no-gutters v-if="editableFieldList && !readonly">
            <v-col cols="3">
              <v-text-field
                label="Add new field"
                hint="Enter the name of a new field"
                :error-messages="fieldNameErrors"
                v-model="fieldName"
                append-outer-icon="mdi-plus-circle"
                @keydown.enter.prevent="addField"
              >
                <template v-slot:append-outer>
                  <v-icon
                    color="primary"
                    :disabled="fieldName && !!fieldNameErrors"
                    @click="addField"
                  >mdi-plus</v-icon>
                </template>
              </v-text-field>
            </v-col>
          </v-row>

<!-- Fixed text strings -->

          <v-row no-gutters class="mt-2 mb-2">
            <h4>Fixed strings</h4>
          </v-row>

          <dougal-fixed-string-text v-for="(item, idx) in fixed" :key="idx"
            v-model="fixed[idx]"
            :colour="getHSLColourFor(item.text+item.offset)"
            :readonly="readonly"
          >
            <template v-slot:append v-if="editableFieldList && !readonly">
              <v-btn
                class="ml-3"
                fab
                text
                small
                title="Remove this property"
              >
                <v-icon
                  color="error"
                  @click="removeFixed(idx)"
                >mdi-minus</v-icon>
              </v-btn>
            </template>
          </dougal-fixed-string-text>

          <v-row dense no-gutters v-if="editableFieldList && !readonly">
            <v-col cols="3">
              <v-text-field
                label="Add fixed text"
                hint="Enter text"
                :error-messages="fieldNameErrors"
                v-model="fixedName"
                @keydown.enter.prevent="addFixed"
              >
              </v-text-field>
            </v-col>
            <v-col cols="3">
              <v-text-field
                class="ml-3"
                label="From position"
                hint="Enter offset"
                type="number"
                min="0"
                v-model.number="fixedOffset"
                :readonly="readonly"
                append-outer-icon="mdi-plus-circle"
              >
                <template v-slot:append-outer>
                  <v-icon
                    color="primary"
                    :disabled="!fixedName"
                    @click="addFixed"
                  >mdi-plus</v-icon>
                </template>
              </v-text-field>
            </v-col>
          </v-row>

        </v-container>



      </v-form>
    </v-card-text>
    <v-card-actions>
    </v-card-actions>
  </v-card>
</template>

<style scoped>

.input {
  flex: 1 1 auto;
  line-height: 20px;
  padding: 8px 0 8px;
  min-height: 32px;
  max-height: 32px;
  max-width: 100%;
  min-width: 0px;
  width: 100%;
}

.multiline {
  font-family: mono;
  white-space: pre;
  overflow-x: auto;
  overflow-y: auto;
}

.multiline >>> .line-number {
  display: inline-block;
  font-size: 75%;
  width: 5ex;
  margin-inline-end: 1ex;
  text-align: right;
  border: none;
  position: relative;
  top: -1px;
}

.input, .multiline >>> .chunk-field {
  padding-inline: 1px;
  border: 1px solid;
}

.input, .multiline >>> .chunk-fixed {
  padding-inline: 1px;
  border: 1px dashed;
}

.input, .multiline >>> .chunk-empty {
  padding-inline: 1px;
}

.input, .multiline >>> .chunk-overlap {
  padding-inline: 1px;
  border: 1px solid grey;
  color: grey;
}

.input, .multiline >>> .chunk-mismatch {
  padding-inline: 1px;
  border: 2px solid red !important;
}
</style>

<script>
import { getHSLColourFor } from '@/lib/hsl'
import DougalFixedStringDecoderField from './fixed-string-decoder-field'
import DougalFixedStringText from './fixed-string-text'

export default {
  name: "DougalFixedStringDecoder",

  components: {
    DougalFixedStringDecoderField,
    DougalFixedStringText
  },

  mixins: [
    {
      methods: {
        getHSLColourFor
      }
    }
  ],

  props: {
    text: { type: String, default: "" },
    fixed: { type: Array, default: () => [] },
    fields: { type: Object, default: () => ({}) },
    multiline: Boolean,
    numberedLines: [ Boolean, Number ],
    maxHeight: String,
    editableFieldList: { type: Boolean, default: true },
    readonly: Boolean,
    title: String,
    subtitle: String,
    label: String,
    hint: String,
  },

  data () {
    return {
      //< The reason for not using this.text directly is that at some point
      //< we might extend this component to allow editing the sample text.
      text_: "",
      //< The value of a fixed string that should be always present at a specific position
      fixedName: "",
      fixedOffset: 0,
      //< The name of a new field to add.
      fieldName: ""
    }
  },

  computed: {

    /** Whether to treat the sample text as multiline.
     */
    isMultiline () {
      return this.multiline === true || this.text.includes("\n");
    },

    /* Return the fields as an array sorted by offset
     */
    parts () {
      // return Object.entries(this.fields).sort( (a, b) => a[1].offset - b[1].offset );
      return [
        ...Object.entries(this.fields),
        ...this.fixed.map(i => [ i.text + i.offset, {...i, length: i.text?.length} ])
      ].sort( (a, b) => {
        const offset_a = a.offset ?? a[1].offset;
        const offset_b = b.offset ?? b[1].offset;
        return a - b;
      })
    },

    /* Transform this.parts into {start, end} intervals.
     */
    chunks () {
      const chunks = [];
      const chunk_num = 0;
      for (const [name, part] of this.parts) {
        const chunk = {};
        chunk.start = part.offset;
        chunk.end = part.offset + part.length - 1;
        //chunk.text = this.text_.slice(chunk.start, chunk.end);
        chunk.colour = this.getHSLColourFor(name)
        chunk.class = part.text ? "fixed" : "field";
        chunk.text = part.text;

        chunks.push(chunk);
      }

      return chunks;
    },

    multilineElementStyle () {
      if (this.maxHeight) {
        return `max-height: ${this.maxHeight};`;
      }
      return "";
    },

    /** Return a colourised HTML version of this.text.
     */
    html () {
      if (!this.text_) {
        return;
      }

      if (this.isMultiline) {
        if (typeof this.numberedLines == "number" || this.numberedLines) {
          const offset = typeof this.numberedLines == "number" ? Math.abs(this.numberedLines) : 0;
          return this.text_.split("\n").map( (line, idx) =>
            this.numberLine(offset+idx, this.renderTextLine(line))).join("<br/>");
        } else {
          return this.text_.split("\n").map(this.renderTextLine).join("<br/>");
        }
      } else {
        return this.renderTextLine(this.text_);
      }

    },

    fieldNameErrors () {
      return this.parts.find( i => i[0] == this.fieldName )
        ? "A field with this name already exists"
        : null;
    }

  },

  watch: {

    text () {
      if (this.text != this.text_) {
        this.reset();
      }
    }

  },

  methods: {

    addFixed () {
      if (this.fixedName) {
        const fixed = [
          ...this.fixed,
          { text: this.fixedName, offset: this.fixedOffset }
        ];
        fixed.sort( (a, b) => a.offset - b.offset );
        this.fixedName = null;
        this.fixedOffset = 0;
        this.$emit("update:fixed", fixed);
      }
    },

    addField () {
      if (!this.fieldNameErrors) {
        this.$emit("update:fields", {
          ...this.fields,
          [this.fieldName]: { offset: 0, length: 0 }
        });
        this.fieldName = "";
      }
    },

    // NOTE Not used
    updateField (field, key, value) {
      const fields = {
        ...this.fields,
        [field]: {
          ...this.fields[field],
          [key]: value
        }
      };
      this.$emit("update:fields", fields);
    },

    removeField (key) {
      const fields = {...this.fields};
      delete fields[key];
      this.$emit("update:fields", fields);
    },

    removeFixed (idx) {
      const fixed = [...this.fixed];
      fixed.splice(idx, 1);
      //fixed.sort( (a, b) => a.offset - b.offset );
      this.$emit("update:fixed", fixed);
    },

    /** Return an HSL colour as a function of an input value
     * `str`.
     */
    xgetHSLColourFor () {
      console.log("WILL BE DEFINED ON MOUNT");
    },

    /** Return a `<span>` opening tag.
     */
    style (name, colour) {
      return colour
        ? `<span class="${name}" style="color:${colour};border-color:${colour}">`
        : `<span class="${name}">`;
    },

    /** Return an array of the intervals that intersect `pos`.
     * May be empty.
     */
    chunksFor (pos) {
      return this.chunks.filter( chunk =>
        pos >= chunk.start &&
        pos <= chunk.end
      )
    },

    /*
     * Algorithm:
     *
     * Go through every character of one line of text and determine in which
     * part(s) it falls in, if any. Collect adjacent same parts into <span/>
     * elements.
     */
    renderTextLine (text) {
      const parts = [];

      let prevStyle;

      for (const pos in text) {
        const chunks = this.chunksFor(pos);
        const isEmpty = chunks.length == 0;
        const isOverlap = chunks.length > 1;
        const isMismatch = chunks[0]?.text &&
          (text.substring(chunks[0].start, chunks[0].end+1) != chunks[0].text)

        const style = isEmpty
          ? this.style("chunk-empty")
          : isMismatch
            ? this.style("chunk-mismatch", chunks[0].colour)
            : isOverlap
              ? this.style("chunk-overlap")
              : this.style("chunk-"+chunks[0].class, chunks[0].colour);

        if (style != prevStyle) {
          if (prevStyle) {
            parts.push("</span>");
          }
          parts.push(style);
        }
        parts.push(text[pos]);
        prevStyle = style;
      }

      if (parts.length) {
        parts.push("</span>");
      }

      return parts.join("");
    },

    numberLine (number, line) {
      return `<span class="line-number">${number}</span>${line}`;
    },

    setText (v) {
      //console.log(v);
      this.text_ = v;
    },

    reset () {
      this.text_ = this?.text.replaceAll("\r", "");
    }

  },

  mounted () {
    this.reset();
  }

}
</script>
