<template>
  <h2 class="column-title">Workers</h2>

  <div class="btn-bar-group">
    <worker-actions-bar />

    <div class="align-right">
      <status-filter-bar
        :availableStatuses="availableStatuses"
        :activeStatuses="shownStatuses"
        classPrefix="worker-"
        @click="toggleStatusFilter" />
    </div>
  </div>

  <div>
    <div class="workers-list with-clickable-row" id="flamenco_workers_list"></div>
  </div>
</template>

<script>
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import { WorkerMgtApi } from '@/manager-api';
import { indicator, workerStatus } from '@/statusindicator';
import { getAPIClient } from '@/api-client';
import { useWorkers } from '@/stores/workers';

import StatusFilterBar from '@/components/StatusFilterBar.vue';
import WorkerActionsBar from '@/components/workers/WorkerActionsBar.vue';

export default {
  name: 'WorkersTable',
  props: ['activeWorkerID'],
  emits: ['tableRowClicked'],
  components: {
    StatusFilterBar,
    WorkerActionsBar,
  },
  data: () => {
    return {
      workers: useWorkers(),

      shownStatuses: [],
      availableStatuses: [], // Will be filled after data is loaded from the backend.
    };
  },
  mounted() {
    window.workersTableVue = this;

    const vueComponent = this;
    const options = {
      // See pkg/api/flamenco-openapi.yaml, schemas WorkerSummary and EventWorkerUpdate.
      columns: [
        // Useful for debugging when there are many similar workers:
        // { title: "ID", field: "id", headerSort: false, formatter: (cell) => cell.getData().id.substr(0, 8), },
        {
          title: 'Status',
          field: 'status',
          sorter: 'string',
          formatter: (cell) => {
            const data = cell.getData();
            const dot = indicator(data.status, 'worker-');
            const asString = workerStatus(data);
            return `${dot} ${asString}`;
          },
        },
        { title: 'Name', field: 'name', sorter: 'string' },
        { title: 'Version', field: 'version', sorter: 'string' },
      ],
      rowFormatter(row) {
        const data = row.getData();
        const isActive = data.id === vueComponent.activeWorkerID;
        row.getElement().classList.toggle('active-row', isActive);
      },
      initialSort: [{ column: 'name', dir: 'asc' }],
      layout: 'fitDataFill',
      layoutColumnsOnNewData: true,
      height: '360px', // Must be set in order for the virtual DOM to function correctly.
      data: [], // Will be filled via a Flamenco API request.
      selectable: false, // The active worker is tracked by click events, not row selection.
    };
    this.tabulator = new Tabulator('#flamenco_workers_list', options);
    this.tabulator.on('rowClick', this.onRowClick);
    this.tabulator.on('tableBuilt', this._onTableBuilt);

    window.addEventListener('resize', this.recalcTableHeight);
  },
  unmounted() {
    window.removeEventListener('resize', this.recalcTableHeight);
  },
  watch: {
    activeWorkerID(newWorkerID, oldWorkerID) {
      this._reformatRow(oldWorkerID);
      this._reformatRow(newWorkerID);
    },
    availableStatuses() {
      // Statuses changed, so the filter bar could have gone from "no statuses"
      // to "any statuses" (or one row of filtering stuff to two, I don't know)
      // and changed height.
      this.$nextTick(this.recalcTableHeight);
    },
  },
  computed: {
    selectedIDs() {
      return this.tabulator.getSelectedData().map((worker) => worker.id);
    },
  },
  methods: {
    onReconnected() {
      // If the connection to the backend was lost, we have likely missed some
      // updates. Just fetch the data and start from scratch.
      this.fetchAllWorkers();
    },
    sortData() {
      const tab = this.tabulator;
      tab.setSort(tab.getSorters()); // This triggers re-sorting.
    },
    _onTableBuilt() {
      this.tabulator.setFilter(this._filterByStatus);
      this.fetchAllWorkers();
    },
    fetchAllWorkers() {
      const api = new WorkerMgtApi(getAPIClient());
      api.fetchWorkers().then(this.onWorkersFetched, function (error) {
        // TODO: error handling.
        console.error(error);
      });
    },
    onWorkersFetched(data) {
      this.tabulator.setData(data.workers);
      this._refreshAvailableStatuses();
      this.recalcTableHeight();
    },
    processWorkerUpdate(workerUpdate) {
      if (!this.tabulator.initialized) return;

      // Contrary to tabulator.getRow(), rowManager.findRow() doesn't log a
      // warning when the row cannot be found,
      const existingRow = this.tabulator.rowManager.findRow(workerUpdate.id);

      let promise;
      // TODO: clean up the code below:
      if (existingRow) {
        if (workerUpdate.deleted_at) {
          // This is a deletion, not a regular update.
          promise = existingRow.delete();
        } else {
          // Tabbulator doesn't update ommitted fields, but if `status_change`
          // is ommitted it means "no status change requested"; this should still
          // force an update of the `status_change` field.
          if (!workerUpdate.status_change) {
            workerUpdate.status_change = null;
          }
          promise = this.tabulator.updateData([workerUpdate]);
          // Tabulator doesn't know we're using 'status_change' in the 'status'
          // column, so it also won't know to redraw when that field changes.
          promise.then(() => existingRow.reinitialize(true));
        }
      } else {
        promise = this.tabulator.addData([workerUpdate]);
      }
      promise
        .then(this.sortData)
        .then(() => {
          this.tabulator.redraw();
        }) // Resize columns based on new data.
        .then(this._refreshAvailableStatuses);

      // TODO: this should also resize the columns, as the status column can
      // change sizes considerably.
    },

    onRowClick(event, row) {
      // Take a copy of the data, so that it's decoupled from the tabulator data
      // store. There were some issues where navigating to another worker would
      // overwrite the old worker's ID, and this prevents that.
      const rowData = plain(row.getData());
      this.$emit('tableRowClicked', rowData);
    },
    toggleStatusFilter(status) {
      const asSet = new Set(this.shownStatuses);
      if (!asSet.delete(status)) {
        asSet.add(status);
      }
      this.shownStatuses = Array.from(asSet).sort();
      this.tabulator.refreshFilter();
    },
    _filterByStatus(worker) {
      if (this.shownStatuses.length == 0) {
        return true;
      }
      return this.shownStatuses.indexOf(worker.status) >= 0;
    },
    _refreshAvailableStatuses() {
      const statuses = new Set();
      for (let row of this.tabulator.getData()) {
        statuses.add(row.status);
      }
      this.availableStatuses = Array.from(statuses).sort();
    },

    _reformatRow(workerID) {
      // Use tab.rowManager.findRow() instead of `tab.getRow()` as the latter
      // logs a warning when the row cannot be found.
      const row = this.tabulator.rowManager.findRow(workerID);
      if (!row) return;
      if (row.reformat) row.reformat();
      else if (row.reinitialize) row.reinitialize(true);
    },

    /**
     * Recalculate the appropriate table height to fit in the column without making that scroll.
     */
    recalcTableHeight() {
      if (!this.tabulator.initialized) {
        // Sometimes this function is called too early, before the table was initialised.
        // After the table is initialised it gets resized anyway, so this call can be ignored.
        return;
      }
      const table = this.tabulator.element;
      const tableContainer = table.parentElement;
      const outerContainer = tableContainer.parentElement;
      if (!outerContainer) {
        // This can happen when the component was removed before the function is
        // called. This is possible due to the use of Vue's `nextTick()`
        // function.
        return;
      }

      const availableHeight = outerContainer.clientHeight - 12; // TODO: figure out where the -12 comes from.

      if (tableContainer.offsetParent != tableContainer.parentElement) {
        // `offsetParent` is assumed to be the actual column in the 3-column
        // view. To ensure this, it's given `position: relative` in the CSS
        // styling.
        console.warn(
          'JobsTable.recalcTableHeight() only works when the offset parent is the real parent of the element.'
        );
        return;
      }

      const tableHeight = availableHeight - tableContainer.offsetTop;
      this.tabulator.setHeight(tableHeight);
    },
  },
};
</script>
