<!--
Row editor for the edit-rows or add-rows card of table editor

Rows and columns marked for removal have x._remove=true
New columns have col._new=true
Changed rows have r._changed = [ids of changed fields]
  and r._orig = [previous values]

Rows are identified by row._id, columns by col.id
-->

<template>
  <div>
    <div class="col-md-12 d-flex flex-wrap">
      <div class="mt-3">
        <div v-if="hasPages" v-html="pager"></div>
      </div>
      <div class="flex-grow-1"></div>

      <div class="d-flex flex-nowrap mt-3">
        <form :action="route" method="POST" v-if="newRows" class="form-inline mr-2">
          <input type="hidden" name="_token" :value="csrf">
          <input type="hidden" name="action" value="rows.add">
          <input type="hidden" name="redirect" :value="pageUrl">
          <div class="input-group">
            <input name="count" id="newrow-count" type="number"
                   min=1 step=1 value=1 class="form-control" style="width: 6em">
            <div class="input-group-append">
              <button class="btn btn-outline-success">Add</button>
            </div>
          </div>
        </form>

        <a :href="loadCsvUrl" class="btn btn-outline-success mr-2" v-if="newRows && loadCsvUrl">
          <v-icon class="fa-file-excel-o fa-pr" alt="CSV"></v-icon><!--
          -->From CSV
        </a>

        <form :action="route" method="POST" v-if="newRows" class="form-inline mr-2">
          <input type="hidden" name="_token" :value="csrf">
          <input type="hidden" name="action" value="row.remove-empty-new">
          <input type="hidden" name="redirect" :value="pageUrl">
          <button class="btn btn-outline-danger">Remove Empty</button>
        </form>

        <button @click="editAll()" type="button" v-if="!newRows"
                class="btn btn-outline-secondary mr-2">
          Edit All
        </button>

        <button @click="saveAllChanges()" type="button"
                :class="['btn', this.dirtyRows ? 'btn-info' : 'btn-outline-secondary']">
          Save Rows
        </button>
      </div>
    </div>

    <div class="col-md-12 mt-3">
      <table :class="[
        'table', 'table-sm', 'table-fixed', 'td-va-middle',
        {'new-rows': newRows}
      ]">
        <thead>
        <tr>
          <th style="width:6rem"></th>
          <th v-for="col in columns" :class="colClasses(col)" :title="col.name">{{col.title}}</th>
          <th style="width:2rem"><!-- revert icon --></th>
        </tr>
        </thead>
        <tbody>
          <tr v-for="row in rows" :class="row._remove ? 'remove' : ''">
            <td class="text-nowrap">
              <button :class="[
                        'mr-1', 'btn', 'btn-outline-danger',
                        {'active': row._remove}
                      ]"
                      @click.prevent="toggleRowDelete(row._id)">
                <v-icon :class="row._remove ? 'fa-undo' :  'fa-trash-o'"
                        :alt="row._remove ? 'Undo Remove' : 'Remove'" />
              </button><!--
           --><button v-if="newRows"
                      :class="[
                        'mr-1', 'btn',
                        isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary'
                      ]"
                      @click.prevent="submitRowChange(row)">
                <v-icon class="fa-save" alt="Save" />
              </button><!--
           --><button v-else
                      :class="[
                        'btn',
                        isRowChanged(row) ? 'btn-info' : 'btn-outline-secondary',
                        {'disabled': row._remove}
                      ]"
                      @click.prevent="toggleRowEditing(row._id)">
                <v-icon v-if="row._editing" class="fa-save" alt="Save" />
                <v-icon v-else class="fa-pencil" alt="Edit" />
              </button>
            </td>

            <template v-if="row._editing || newRows">
              <td v-for="col in columns" class="pr-0">
                <input v-model="row[col.id]" :class="['form-control', { 'is-invalid': row._errors && row._errors[col.id] }]"
                       :title="(row._errors && row._errors[col.id]) ? row._errors[col.id][0] : null"
                       :ref="`edit-${row._id}-${col.id}`"
                       type="text"
                       @input="markRowDirty(row)"
                       @keydown.down="verticalTab(row._id, col.id, 1)"
                       @keydown.up="verticalTab(row._id, col.id, -1)"
                       @keyup.enter="toggleRowEditing(row._id)">
              </td>
            </template>

            <template v-else>
              <td v-for="col in columns">
                <span class="text-danger strike" title="Original value" v-if="isChanged(row, col.id)">{{row._orig[col.id]}}</span>
                <span>{{ row[col.id] }}</span>
                <v-icon v-if="isChanged(row, col.id)"
                        @click="revertCell(row, col.id)"
                        class="fa-undo text-danger pointer"
                        alt="Revert Change" />
              </td>
            </template>

            <td style="text-align: center;">
              <v-icon @click="discardEdit(row)"
                      v-if="isRowChanged(row) && (row._editing || newRows)"
                      class="fa-undo text-danger pointer"
                      alt="Revert Change" />
            </td>
          </tr>
        </tbody>
      </table>
    </div>

    <div class="col-md-12 d-flex" v-if="hasPages">
      <div v-if="hasPages" v-html="pager"></div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
  @import "base";

  th {
    border-top: 0 none;
  }

  table.new-rows td {
    border: 0 none;
  }
</style>

<script>
  import { query } from './table-editor-utils'

  export default {
    props: {
      /** True if pager should be enabled */
      hasPages: Boolean,
      /** Pager HTML */
      pager: String,
      /** Update route */
      route: String,
      /** Row objects */
      xRows: Object, // key'd by _id
      /** Array of columns */
      columns: Array,
      /** CSRF token */
      csrf: String,
      /** Full URL of the current page */
      pageUrl: String,
      /** URL to import CSV */
      loadCsvUrl: String,
      /** Indicate this is the Add Rows page */
      newRows: {
        type: Boolean,
        default: false,
      }
    },
    data () {
      return {
        /** All rows */
        rows: this.xRows,
        /** Number of dirty rows */
        dirtyRows: 0,
      }
    },
    watch: {
      dirtyRows: function (value) {
        console.log(`Dirty Rows ${value}`)

        if (value > 0) {
          $(window).on('beforeunload', function () {
            return 'Some row changes are not saved.'
          })
        } else {
          $(window).off('beforeunload')
        }
      }
    },
    methods: {
      /** Send a query to the server */
      query (data, sucfn, erfn) {
        query(this.route, data, sucfn, erfn)
      },

      /** Toggle row delete status, remove new rows (when using the editor widget for added rows) */
      toggleRowDelete (_id) {
        let row = this.rows[_id]
        if (!_.isDefined(row)) return

        let remove = !row._remove
        let wasDirty = this.isRowChanged(row)

        this.query({
          action: remove ? 'row.remove' : 'row.restore',
          id: _id
        }, (resp) => {
          if (wasDirty) this.dirtyRows--

          // if response is null, this was a New row
          // and it was discarded without a way back - hard drop
          if (_.isEmpty(resp.data)) {
            this.$delete(this.rows, _id)
          }
          else {
            this.$set(this.rows, _id, resp.data)
          }
        })
      },

      /** Save a change */
      submitRowChange (row) {
        let wasDirty = this.isRowChanged(row)

        this.query({
          action: 'row.update',
          data: row
        }, (resp) => {
          this.$set(this.rows, row._id, resp.data)
          if (wasDirty) this.dirtyRows--
        }, (er) => {
          if (!_.isUndefined(er.errors)) {
            this.$set(this.rows[row._id], '_errors', er.errors)
          }
        })
      },

      /** Toggle editing state - edit or save */
      toggleRowEditing (_id) {
        if (this.rows[_id]._remove) return false // can't edit row marked for removal
        let wasEditing = this.rows[_id]._editing

        if (wasEditing || this.newRows) {
          this.submitRowChange(this.rows[_id])
        } else {
          this.$set(this.rows[_id], '_editing', true)
        }
      },

      /** Compute classes for a value cell */
      colClasses (col) {
        return {
          'text-danger': col._remove,
          'strike': col._remove,
          'text-success': col._new
        }
      },

      /** Test if a value cell is changed */
      isChanged (row, colId) {
        return row._changed && row._changed.indexOf(colId) > -1
      },

      /** Test if any field in the row is changed */
      isRowChanged (row, cached = false) {
        if (this.newRows) {
          return row._changed && row._changed.length
        } else {
          if (cached) return row._dirty
          // check against loadvals array
          for (const [key, value] of Object.entries(row._loadvals)) {
            // changed if differs from orig value and also from previous value from revision
            if (row[key] != value && row[key] != row._orig[key]) return true
          }
          return false
        }
      },

      /** Revert a value cell */
      revertCell (row, colId) {
        this.submitRowChange(_.merge({}, row, {[colId]: row._orig[colId]}))
      },

      /** Mark row as dirty and needing a save */
      markRowDirty (row) {
        let wasDirty = this.isRowChanged(row, true)

        let changes = []
        _.each(this.columns, (col) => {
          let val = row[col.id];
          let orig = row._orig[col.id];
          if (_.isUndefined(orig)) {
            // null in DB
            if (val !== '' && val !== 0) {
              changes.push(col.id)
              console.log('is dirty', JSON.stringify(val))
            }
          } else if (val != orig) {
            changes.push(col.id)
          }
        })

        this.rows[row._id]._changed = changes
        let isDirty = this.isRowChanged(row)

        if (wasDirty && !isDirty) this.dirtyRows--
        else if (!wasDirty && isDirty) this.dirtyRows++

        row._dirty = isDirty
      },

      /** Save all changed rows */
      saveAllChanges () {
        let toChange = _.filter(this.rows, (r) => {
          let changed = this.isRowChanged(r)
          if (!changed && r._editing) {
            this.rows[r._id]._editing = false
          }
          return changed;
        })

        if (toChange.length === 0) return;

        this.query({
          action: 'row.update-many',
          data: toChange
        }, (resp) => {
          _.each(resp.data, (row) => {
            this.$set(this.rows, row._id, row)
          })
          this.dirtyRows = 0
        }, (er) => {
          // TODO
          console.log(er)
          // if (!_.isUndefined(er.errors)) {
          //   this.$set(this.rows[row._id], '_errors', er.errors)
          // }
        })
      },

      discardEdit (row) {
        let wasDirty = this.isRowChanged(row)

        _.each(this.newRows ? row._orig : row._loadvals, (val, id) => {
          row[id] = val
        })
        row._editing = false
        row._dirty = false
        if (this.newRows) row._changed = []

        this.$set(this.rows, row._id, row)

        if (wasDirty) this.dirtyRows--
      },

      editAll() {
        _.each(this.rows, (row, id) => {
          this.$set(this.rows[id], '_editing', true)
        })
      },

      /**
       * Move to next or prev editable cell vertically
       *
       * @param row_id
       * @param col_id
       * @param dir - +1 down, -1 up
       */
      verticalTab (row_id, col_id, dir) {
        if (dir == -1) {
          // up
          let last_editable_row = null
          for (let row of Object.values(this.rows)) {
            if (row._id === row_id) {
              break
            }
            if (row._editing || this.newRows) {
              last_editable_row = row
            }
          }
          if (last_editable_row) {
            this.$refs[`edit-${last_editable_row._id}-${col_id}`][0].focus()
          }
        }
        else {
          // down
          let first_editable_row = null
          let foundself = false
          for (let row of Object.values(this.rows)) {
            if (row._id === row_id) {
              foundself = true
              continue
            }
            if (foundself && (row._editing || this.newRows)) {
              first_editable_row = row
              break
            }
          }
          if (first_editable_row) {
            this.$refs[`edit-${first_editable_row._id}-${col_id}`][0].focus()
          }
        }
      }
    },
  }
</script>
