import * as ko from "knockout";
import * as _ from "underscore";

import * as DataTable from "datatables.net-dt";
import loadingIndicator3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
import * as DataTableBuilder from "./DataTableBuilder";
import DataTableOperationManager from "./DataTableOperationManager";
import * as DataTableOperations from "./DataTableOperations";
import TableEntityListViewModel from "./TableEntityListViewModel";

/**
 * Custom binding manager of datatable
 */
var tableEntityListViewModelMap: {
  [key: string]: {
    tableViewModel: TableEntityListViewModel;
    operationManager: DataTableOperationManager;
    $dataTable: JQuery;
  };
} = {};

function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
  var tableEntityListViewModel = bindingContext.$data;
  tableEntityListViewModel.notifyColumnChanges = onTableColumnChange;
  var $dataTable = $(element);
  var queryTablesTab = bindingContext.$parent;
  var operationManager = new DataTableOperationManager(
    $dataTable,
    tableEntityListViewModel,
    queryTablesTab.tableCommands,
  );

  tableEntityListViewModelMap[queryTablesTab.tabId] = {
    tableViewModel: tableEntityListViewModel,
    operationManager: operationManager,
    $dataTable: $dataTable,
  };

  createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
  $(window).resize(updateTableScrollableRegionMetrics);
  operationManager.focusTable(); // Also selects the first row if needed.
  // Attach the arrow key event handler to the table element
  $dataTable.on("keydown", (event: JQueryEventObject) => {
    handleArrowKey(element, valueAccessor, allBindings, viewModel, bindingContext, event);
  });
}

function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
  var columnsFilter: boolean[] = null;
  var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel;
  if (queryTablesTab.queryViewModel()) {
    queryTablesTab.queryViewModel().queryBuilderViewModel().updateColumnOptions();
  }
  createDataTable(
    tableEntityListViewModel.tablePageStartIndex,
    tableEntityListViewModel,
    queryTablesTab,
    true,
    columnsFilter,
  );
}

function createDataTable(
  startIndex: number,
  tableEntityListViewModel: TableEntityListViewModel,
  queryTablesTab: QueryTablesTab,
  destroy: boolean = false,
  columnsFilter: boolean[] = null,
): void {
  var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable;
  if (destroy) {
    // Find currently displayed columns.
    var currentColumns: string[] = tableEntityListViewModel.headers;

    // Calculate how many more columns need to added to the current table.
    var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length;

    // This is needed as current solution of adding column is more like a workaround
    // The official support for dynamically add column is not yet there
    // Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support
    for (var i = 0; i < columnsToAdd; i++) {
      $(".dataTables_scrollHead table thead tr th").eq(0).after("<th></th>");
    }
    tableEntityListViewModel.table.destroy();
    $dataTable.empty();
  }

  var jsonColTable = [];

  for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
    jsonColTable.push({
      sTitle: Utilities.htmlEncode(tableEntityListViewModel.headers[i]),
      data: tableEntityListViewModel.headers[i],
      aTargets: [i],
      mRender: bindColumn,
      visible: !!columnsFilter ? columnsFilter[i] : true,
    });
  }

  tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTable.Config>{
    // WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
    // so that they don't get interpreted as HTML in our page.
    colReorder: true,
    aoColumnDefs: jsonColTable,
    stateSave: false,
    dom: "RZlfrtip",
    oColReorder: {
      iFixedColumns: 1,
    },
    displayStart: startIndex,
    bPaginate: true,
    pagingType: "full_numbers",
    bProcessing: true,
    oLanguage: {
      sInfo: "Results _START_ - _END_ of _TOTAL_",
      oPaginate: {
        sFirst: "<<",
        sNext: ">",
        sPrevious: "<",
        sLast: ">>",
      },
      sProcessing: `<img style="width: 28px; height: 6px; " src="${loadingIndicator3Squares}">`,
      oAria: {
        sSortAscending: "",
        sSortDescending: "",
      },
    },
    destroy: destroy,
    bInfo: true,
    bLength: false,
    bLengthChange: false,
    scrollX: true,
    scrollCollapse: true,
    iDisplayLength: 100,
    serverSide: true,
    ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab
    fnServerData: getServerData,
    fnRowCallback: bindClientId,
    fnInitComplete: initializeTable,
    fnDrawCallback: updateSelectionStatus,
  });

  (tableEntityListViewModel.table.table(0).container() as Element)
    .querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
    .forEach((table) => {
      table.setAttribute(
        "summary",
        `Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`,
      );
    });

  (tableEntityListViewModel.table.table(0).container() as Element)
    .querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
    .forEach((table) => {
      table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
    });
}

function bindColumn(data: any, type: string, full: any) {
  var displayedValue: any = null;
  if (data) {
    displayedValue = data._;

    // SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML
    displayedValue = Utilities.htmlEncode(displayedValue);

    // Css' empty psuedo class can only tell the difference of whether a cell has values.
    // A cell has no values no matter it's empty or it has no such a property.
    // To distinguish between an empty cell and a non-existing property cell,
    // we add a whitespace to the empty cell so that css will treat it as a cell with values.
    if (displayedValue === "" && data.$ === Constants.TableType.String) {
      displayedValue = " ";
    }
  }
  return displayedValue;
}

function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
  tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache(
    sSource,
    aoData,
    fnCallback,
    oSettings,
  );
}

/**
 * Bind table data information to row element so that we can track back to the table data
 * from UI elements.
 */
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
  if (aData.PartitionKey && aData.PartitionKey._) {
    $(nRow).attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr, aData.PartitionKey._);
  }
  $(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
  return nRow;
}

function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
  $(".dataTable tr.selected").attr("tabindex", "-1").removeClass("selected");

  const selected =
    bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected();
  selected &&
    selected.forEach((b: Entities.ITableEntity) => {
      var sel = DataTableOperations.getRowSelector([
        {
          key: Constants.htmlAttributeNames.dataTablePartitionKeyAttr,
          value: b.PartitionKey && b.PartitionKey._ && b.PartitionKey._.toString(),
        },
        {
          key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
          value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
        },
      ]);

      $(sel).attr("tabindex", "0").focus().addClass("selected");
    });
  //selected = bindingContext.$data.selected();
}
function handleArrowKey(
  element: any,
  valueAccessor: any,
  allBindings: any,
  viewModel: any,
  bindingContext: any,
  event: JQueryEventObject,
) {
  const isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow;
  const isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow;

  if (isUpArrowKey || isDownArrowKey) {
    const $dataTable = $(element);
    let $selectedRow = $dataTable.find("tr.selected");

    if ($selectedRow.length === 0) {
      // No row is currently selected, select the first row
      $selectedRow = $dataTable.find("tr:first");
      $selectedRow.addClass("selected");
    } else {
      const $targetRow = isUpArrowKey ? $selectedRow.prev("tr") : $selectedRow.next("tr");

      if ($targetRow.length > 0) {
        // Remove the selected class from the current row and add it to the target row
        $selectedRow.removeClass("selected").attr("tabindex", "-1");
        $targetRow.addClass("selected").attr("tabindex", "0");
        $targetRow.focus();
      }
    }

    event.preventDefault();
  }
}

function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
  // do nothing for now
}

function initializeTable(): void {
  updateTableScrollableRegionMetrics();
  initializeEventHandlers();
}

function updateTableScrollableRegionMetrics(): void {
  updateTableScrollableRegionHeight();
  updateTableScrollableRegionWidth();
}

/*
 * Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page.
 */
function updateTableScrollableRegionHeight(): void {
  $(".tab-pane").each(function (index, tabElement) {
    if (!$(tabElement).hasClass("tableContainer")) {
      return;
    }

    // Add some padding to the table so it doesn't get too close to the container border.
    var dataTablePaddingBottom = 10;
    var bodyHeight = $(window).height();
    var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
    var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
    var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
    const notificationConsoleHeight = 32; /** Header height **/

    var scrollHeight =
      bodyHeight -
      dataTablesScrollBodyPosY -
      dataTablesPaginateElem.outerHeight(true) -
      dataTablePaddingBottom -
      notificationConsoleHeight;

    //info and paginate control are stacked
    if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
      scrollHeight -= dataTablesInfoElem.outerHeight(true);
    }

    // TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
    // in the current version of JQuery we are using.  Ideally, we would upgrade JQuery and use this line instead:
    // $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
    var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
    var style = getComputedStyle(element);
    var actualHeight = parseInt(style.height);
    var change = element.offsetHeight - scrollHeight;
    $(tabElement)
      .find(Constants.htmlSelectors.dataTableScrollBodySelector)
      .height(actualHeight - change);
  });
}

/*
 * Update the table's scrollable region width to make efficient use of the remaining space.
 */
function updateTableScrollableRegionWidth(): void {
  $(".tab-pane").each(function (index, tabElement) {
    if (!$(tabElement).hasClass("tableContainer")) {
      return;
    }

    var bodyWidth = $(window).width();
    var dataTablesScrollBodyPosLeft = $(tabElement)
      .find(Constants.htmlSelectors.dataTableScrollBodySelector)
      .offset().left;
    var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;

    // jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
    // We work around that by setting the height for it's container instead.
    $(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth);
  });
}

function initializeEventHandlers(): void {
  var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
  var $firstHeader: JQuery = $headers.first();
  var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);

  $headers
    .on("keydown", (event: JQueryEventObject) => {
      Utilities.onEnter(event, ($sourceElement: JQuery) => {
        $sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
      });

      // Bind shift+tab from first header back to search input field
      Utilities.onTab(
        event,
        ($sourceElement: JQuery) => {
          var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);

          if (sourceIndex === firstIndex) {
            event.preventDefault();
          }
        },
        /* metaKey */ null,
        /* shiftKey */ true,
        /* altKey */ null,
      );

      // Also reset color if [shift-] tabbing away from button while holding down 'enter'
      Utilities.onTab(event, ($sourceElement: JQuery) => {
        $sourceElement.css("background-color", "");
      });
    })
    .on("keyup", (event: JQueryEventObject) => {
      Utilities.onEnter(event, ($sourceElement: JQuery) => {
        $sourceElement.css("background-color", "");
      });
    });
}

function updateSelectionStatus(oSettings: any): void {
  var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
  if ($dataTableRows) {
    for (var i = 0; i < $dataTableRows.length; i++) {
      var $row: JQuery = $dataTableRows.eq(i);
      var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
      var partitionKey: string = $row.attr(Constants.htmlAttributeNames.dataTablePartitionKeyAttr);
      var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
      if (table.isItemSelected(table.getTableEntityKeys(rowKey, partitionKey))) {
        $row.attr("tabindex", "0");
      }
    }
  }

  updateDataTableFocus(oSettings.ajax);

  DataTableOperations.setPaginationButtonEventHandlers();
}

// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void {
  var $activeElement: JQuery<Element> = $(document.activeElement);
  var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
  var storageExplorerFrameHasFocus: boolean = document.hasFocus();
  var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
  if (operationManager) {
    if (isFocusLost && storageExplorerFrameHasFocus) {
      // We get here when no control is active, meaning that the table update was triggered
      // from a dialog, the context menu or by clicking on a toolbar control or header.
      // Note that giving focus to the table also selects the first row if needed.
      // The document.hasFocus() ensures that the table will only get focus when the
      // focus was lost (i.e. "body has the focus") within the Storage Explorer frame
      // i.e. not when the focus is lost because it is in another frame
      // e.g. a daytona dialog or in the Activity Log.
      operationManager.focusTable();
    }
    if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
      // If table header is selected, focus is shifted to the selected element as part of accessibility
      $activeElement && $activeElement.focus();
    }
  }
}

(<any>ko.bindingHandlers).tableSource = {
  init: bindDataTable,
  update: dataChanged,
};

(<any>ko.bindingHandlers).tableSelection = {
  update: selectionChanged,
};

(<any>ko.bindingHandlers).readOnly = {
  update: function (element: any, valueAccessor: any) {
    var value = ko.utils.unwrapObservable(valueAccessor());
    if (value) {
      element.setAttribute("readOnly", true);
    } else {
      element.removeAttribute("readOnly");
    }
  },
};
