import {
  CdkDragDrop,
  CdkDragEnd,
  CdkDragMove,
  moveItemInArray,
  transferArrayItem
} from '@angular/cdk/drag-drop'
import {
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  NgZone,
  OnDestroy,
  OnInit,
  Output,
  QueryList,
  ViewChildren,
  ViewEncapsulation
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { SASjsConfig } from '@sasjs/adapter'
import Handsontable from 'handsontable'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { cloneDeep } from 'lodash-es'
import { Subscription } from 'rxjs'
import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery'
import { Libinfo } from 'src/app/models/sas/common/Libinfo'
import { PublicViewtablesServiceResponse } from 'src/app/models/sas/public-viewtables.model'
import { EventService } from 'src/app/services/event.service'
import { HelperService } from 'src/app/services/helper.service'
import { LicenceService } from 'src/app/services/licence.service'
import { LoggerService } from 'src/app/services/logger.service'
import { SasStoreService } from 'src/app/services/sas-store.service'
import { SasService } from 'src/app/services/sas.service'
import { AutocompleteComponent } from 'src/app/shared/autocomplete/autocomplete.component'
import { LibraryClickEmitter } from 'src/app/shared/dc-tree/models/LibraryClickEmitter'
import { TableClickEmitter } from 'src/app/shared/dc-tree/models/TableClickEmitter'
import { mergeColsRules } from 'src/app/shared/dc-validator/utils/mergeColsRules'
import { buildExportMenuItem } from 'src/app/shared/hot-export/hot-export.util'
import { globals, initFilter } from 'src/app/_globals'
import { ViewboxHotTable } from './models/viewbox-hot-table.model'
import { ViewboxTable } from './models/viewbox-table.model'
import { Viewbox } from './models/viewbox.model'

@Component({
  selector: 'app-viewboxes',
  templateUrl: './viewboxes.component.html',
  styleUrls: ['./viewboxes.component.scss'],
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
  @ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
  @ViewChildren('dragHandleCorner')
  dragHandleCornerQuery!: QueryList<ElementRef>
  @ViewChildren(HotTableComponent)
  hotTableComponents!: QueryList<HotTableComponent>

  private _viewboxModal: boolean = false
  get viewboxModal(): boolean {
    return this._viewboxModal
  }
  @Input() set viewboxModal(value: boolean) {
    // If feature is disabled, prevent modal show up and show demo notice
    if (this.licenceState.value.viewbox === false && !!value) {
      this.eventService.showDemoLimitModal('Viewboxes')
      this.viewboxModalChange.emit(false)
      return
    }
    this._viewboxModal = value

    if (!!value) this.unsetSelectedViewbox()
  }
  @Output() viewboxModalChange: EventEmitter<boolean> =
    new EventEmitter<boolean>()

  public libraries!: Array<any>
  public tables: any
  public libinfo: Libinfo[] | null = null
  public librariesLoading: boolean = true

  public viewboxes: Viewbox[] = []

  public selectedViewbox: Viewbox | undefined
  public selectedViewboxTable: ViewboxTable | undefined

  public defaultConfig: Viewbox = {
    id: -1,
    library: '',
    table: '',
    width: 500,
    height: 300,
    x: 0,
    y: 150,
    columns: []
  }

  public sasjsConfig: SASjsConfig = new SASjsConfig()

  public hotTableDefault: ViewboxHotTable = {
    data: [],
    headerPks: [],
    $dataformats: {},
    allColHeaders: [],
    colHeadersHidden: [],
    colHeadersVisible: [],
    colHeaders: [],
    contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
    copyPaste: {
      copyColumnHeaders: true,
      copyColumnHeadersOnly: true
    },
    columns: [],
    cols: [],
    height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load
    settings: {},
    hiddenColumns: true,
    manualColumnMove: false,
    afterGetColHeader: undefined,
    licenseKey: undefined,
    dropdownMenu: undefined
  }

  public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map()
  public viewboxTables: ViewboxTable[] = []

  public filteringViewbox: Viewbox | undefined

  public filter: boolean = false
  public filterLoading: boolean = false
  public clauses: any
  public nullVariables: boolean = false
  public filterLibds: string | undefined
  public _query: Subscription | undefined

  public licenceState = this.licenceService.licenceState
  public Infinity = Infinity

  public maxViewboxes: number =
    this.licenceState.value.viewbox_limit === Infinity
      ? 6
      : this.licenceState.value.viewbox_limit || 6

  constructor(
    private ngZone: NgZone,
    private licenceService: LicenceService,
    private sasService: SasService,
    private eventService: EventService,
    private sasStoreService: SasStoreService,
    private loggerService: LoggerService,
    private helperService: HelperService,
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private cdf: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    // Load libraries
    this.sasStoreService
      .viewLibs()
      .then((res: any) => {
        this.libraries = res.saslibs
      })
      .catch((err: any) => {
        this.loggerService.error(err)
      })
      .finally(() => {
        this.librariesLoading = false
      })

    // Listen for filtering data
    this._query = this.sasStoreService.query.subscribe((query: any) => {
      this.clauses = query.obj
      this.filterLibds = query.libds
    })

    this.sasjsConfig = this.sasService.getSasjsConfig()

    this.licenceService.hot_license_key.subscribe(
      (hot_license_key: string | undefined) => {
        this.hotTableDefault.licenseKey = hot_license_key
      }
    )

    const viewboxesQueryParam =
      this.activatedRoute.snapshot.queryParams.viewboxes

    if (viewboxesQueryParam) {
      if (this.licenceState.value.viewbox === false) {
        setTimeout(() =>
          this.eventService.showDemoLimitModal('Linking Viewboxes')
        )
        this.router.navigate([], {
          relativeTo: this.activatedRoute,
          queryParams: {}
        })
      } else {
        this.viewboxes = this.decodeUrlData(viewboxesQueryParam)

        setTimeout(() => {
          this.setAllHandleTransform()
        })
      }
    }

    this.reLoadViewboxtables(this.viewboxes)
  }

  ngAfterViewInit(): void {
    // Set handles for box resize and ensure HOT components are properly initialized
    setTimeout(() => {
      this.setAllHandleTransform()

      // Force refresh of any existing HOT instances after view init
      this.viewboxes.forEach((viewbox) => {
        if (this.getViewboxTableIndex(viewbox) > -1) {
          this.refreshTableAfterResize(viewbox)
        }
      })
    }, 1000)
  }

  // Maximum number of open viewboxes reached
  get viewboxLimitReached(): boolean {
    return this.viewboxes.length >= this.maxViewboxes
  }

  clrModalOpenChange(open: boolean) {
    this.viewboxModalChange.emit(open)
  }

  libraryOnClick(data: LibraryClickEmitter) {
    if (!data.tablesLoaded)
      this.loadTables(data.library.LIBRARYREF, data.library)
  }

  /**
   * Adding new Viewbox - Fired when table is selected from the `dc-tree`
   * @param data Selected table data coming from the `dc-tree`
   */
  async tableOnClick(data: TableClickEmitter) {
    if (this.viewboxLimitReached) return

    const viewbox = {
      ...this.defaultConfig,
      table: data.libTable,
      library: data.library.LIBRARYNAME,
      loadingData: true,
      filter_pk: '0',
      id: this.viewboxes.length + 1,
      x: window.innerWidth - this.defaultConfig.width,
      y: 150
    }

    this.viewboxes.push(viewbox)

    setTimeout(() => {
      this.setAllHandleTransform()
    })

    const libDataset = `${data.library.LIBRARYREF}.${data.libTable}`

    await this.loadData(libDataset, viewbox)

    viewbox.loadingData = false
    this.eventService.dispatchEvent('resize') //Force HOT refresh
    this.snapToGrid() //it will call viewboxChanged
  }

  /**
   * Laods tables to populate `dc-tree`
   * @param lib
   * @param library
   */
  loadTables(lib: string, library?: any) {
    this.sasStoreService
      .viewTables(lib)
      .then((res: PublicViewtablesServiceResponse) => {
        let tables = res.mptables.map(function (item: any) {
          return item.MEMNAME
        })

        this.libinfo = res.libinfo || []
        this.tables = tables

        if (library) {
          library['tables'] = tables
          library['libinfo'] = this.libinfo
          library['loadingTables'] = false

          if (tables.length > 0) library['expanded'] = true
        }
      })
      .catch((err: any) => {
        this.loggerService.error(err)
      })
  }

  /**
   * Re-loads data particular Viewbox (preserving the filtering PK)
   * @param libDataset library.table
   * @param viewbox viewbox to reload data for
   * @returns Promise
   */
  async reloadData(libDataset: string, viewbox: Viewbox) {
    return new Promise((resolve, reject) => {
      let viewboxTable = this.viewboxTables.find(
        (vbt) => vbt.viewboxId === viewbox.id
      )

      this.sasStoreService
        .viewData(libDataset, parseInt(viewbox.filter_pk || '0'))
        .then((res: any) => {
          if (viewboxTable) {
            viewboxTable.hotTable.data = res.viewdata

            // Update settings with new data
            this.createViewboxTableSettings(viewbox)

            resolve(null)
          } else {
            resolve(null)
          }
        })
        .catch(() => {
          reject()
        })
    })
  }

  /**
   * Initial data load for particular Viewbox
   * If data has been already found for particular Viewbox
   * and filtering is not active it will return stored data
   * instead of sending new request
   *
   * @param libDataset library.table
   * @param viewbox
   * @returns Empty Promise - used for awaiting only
   */
  async loadData(libDataset: string, viewbox: Viewbox): Promise<void> {
    return new Promise((resolve, reject) => {
      let existingViewboxTable: boolean = false

      let viewboxTable = this.viewboxTables.find(
        (vbt) => vbt.viewboxId === viewbox.id
      )
      if (viewboxTable && (viewbox.filter_pk === '0' || !viewbox.filter_pk))
        resolve()

      this.sasStoreService
        .viewData(libDataset, parseInt(viewbox.filter_pk || '0'))
        .then((res: any) => {
          if (!viewboxTable) {
            viewboxTable = {
              viewboxId: viewbox.id,
              viewboxLibDataset: libDataset,
              hotTable: cloneDeep(this.hotTableDefault)
            }
          } else {
            existingViewboxTable = true
          }

          viewboxTable.hotTable.data = res.viewdata
          viewboxTable.hotTable.$dataformats = res.$viewdata
          viewboxTable.hotTable.cols = res.cols

          mergeColsRules(viewboxTable.hotTable.cols, [], res.$viewdata)

          let columns: any[] = []
          let colArr: string[] = []

          for (let key in res.viewdata[0]) {
            if (key) {
              colArr.push(key)
            }
          }

          for (let index = 0; index < colArr.length; index++) {
            columns.push({ data: colArr[index] })
          }

          viewboxTable.hotTable.headerPks = cloneDeep(
            res.sasparams[0].PK_FIELDS.split(' ')
          )
          viewboxTable.hotTable.allColHeaders = colArr.filter(
            (col) => !viewboxTable!.hotTable.headerPks.includes(col)
          )
          viewboxTable.hotTable.colHeadersHidden = cloneDeep(
            viewboxTable.hotTable.allColHeaders
          )
          viewboxTable.hotTable.colHeadersVisible = colArr.filter((col) =>
            viewboxTable!.hotTable.headerPks.includes(col)
          )

          viewboxTable.hotTable.colHeaders = colArr
          viewboxTable.hotTable.columns = columns

          if (viewbox.columns && viewbox.columns.length > 0) {
            viewboxTable.hotTable.manualColumnMove = viewbox.columns

            viewbox.columns?.map((col: number, index: number) => {
              const colProp = colArr[col]
              const hiddenColIndex =
                viewboxTable!.hotTable.colHeadersHidden.indexOf(colProp)

              if (hiddenColIndex > -1) {
                viewboxTable!.hotTable.colHeadersHidden.splice(
                  hiddenColIndex,
                  1
                )
                viewboxTable!.hotTable.colHeadersVisible[index] = colProp
              }
            })
          } else {
            viewboxTable.hotTable.colHeadersVisible.push(
              ...viewboxTable.hotTable.colHeadersHidden.splice(0, 10)
            )
          }

          viewboxTable.hotTable.colHeadersVisible =
            viewboxTable.hotTable.colHeadersVisible.filter((x) => x) //remove empty slots

          if (!existingViewboxTable) this.viewboxTables.push(viewboxTable)

          viewbox.query = this.helperService.deepClone(res.query)
          viewbox.filterText = res.sasparams[0].FILTER_TEXT

          // Create settings for this viewbox
          this.createViewboxTableSettings(viewbox)

          setTimeout(() => {
            this.updateHotColumns(
              viewboxTable!.hotTable.colHeadersHidden || [],
              viewbox.id
            )

            // HOT Settings are bound in HTML but some settings due to timing issues
            // requires to be updated after the HOT is instanced
            // Use a longer timeout to ensure the HOT component is fully initialized
            setTimeout(() => {
              const hotInstance = this.getViewboxHotInstance(viewbox.id)

              if (hotInstance) {
                hotInstance.updateSettings({
                  manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
                  afterGetColHeader: (col: number, th: any) => {
                    const column = hotInstance?.colToProp(col) as string

                    // header columns styling - primary keys
                    const isPKCol =
                      column &&
                      viewboxTable!.hotTable.headerPks.indexOf(column) > -1

                    if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
                    // Dark mode
                    th.classList.add(globals.handsontable.darkTableHeaderClass)
                  }
                })
                hotInstance.render()
              }

              if (this.selectedViewbox) {
                this.resetSelectedViewbox(viewbox)
              }
            }, 500)
          }, 100)

          resolve()
        })
        .catch((err: any) => {
          this.loggerService.error(err)

          reject()
        })
    })
  }

  /**
   * Reloads SAS data for each viewbox that exists
   * @param viewboxes viewboxes array
   */
  reLoadViewboxtables(viewboxes: Viewbox[]) {
    viewboxes.map((viewbox: Viewbox) => {
      const libDataset = `${viewbox.library}.${viewbox.table}`
      viewbox.loadingData = true

      this.loadData(libDataset, viewbox).then(() => {
        viewbox.loadingData = false
      })
    })
  }

  // HOT cols max width
  maxWidthCheker(width: any, col: any) {
    if (width > 200) return 200
    else return width
  }

  /**
   * Used to pair `Viewbox` with it's data
   * Which is stored in different array - ViewboxTables
   * @param viewbox
   */
  getViewboxTableIndex(viewbox: Viewbox): number {
    const index = this.viewboxTables.findIndex(
      (x) => x.viewboxId === viewbox.id
    )

    return index
  }

  /**
   * Create and store Handsontable settings for a specific viewbox
   * @param viewbox
   */
  private createViewboxTableSettings(viewbox: Viewbox): void {
    const viewboxTableIndex = this.getViewboxTableIndex(viewbox)
    if (viewboxTableIndex === -1) {
      this.viewboxHotSettings.set(viewbox.id, {})
      return
    }

    const viewboxTable = this.viewboxTables[viewboxTableIndex]
    const calculatedHeight = this.calculateTableHeight(viewbox)

    // HOT v17 settings - data will be loaded manually after initialization
    const settings: Handsontable.GridSettings = {
      colHeaders: viewboxTable.hotTable.colHeaders,
      columns: viewboxTable.hotTable.columns,
      height: calculatedHeight,
      readOnly: true,
      modifyColWidth: this.maxWidthCheker,
      copyPaste: viewboxTable.hotTable.copyPaste,
      // Built here (not in hotTableDefault) because that template is cloneDeep'd,
      // which would strip the export callbacks. Client-side view export.
      contextMenu: {
        items: {
          copy_with_column_headers: {},
          copy_column_headers_only: {},
          export_file: buildExportMenuItem(() => 'viewbox')
        }
      },
      multiColumnSorting: true,
      viewportRowRenderingOffset: 50,
      filters: true,
      dropdownMenu: viewboxTable.hotTable.dropdownMenu,
      stretchH: 'all',
      cells: viewboxTable.hotTable.cells,
      maxRows: viewboxTable.hotTable.maxRows || Infinity,
      manualColumnResize: true,
      rowHeaders: true,
      licenseKey: viewboxTable.hotTable.licenseKey,
      theme: 'ht-theme-classic'
    }

    // Force a new object reference to trigger change detection
    this.viewboxHotSettings.set(viewbox.id, { ...settings })

    // Force change detection to ensure the template updates
    setTimeout(() => {
      this.cdf.detectChanges()

      // Try to get the HOT instance and force a render
      setTimeout(() => {
        const hotInstance = this.getViewboxHotInstance(viewbox.id)
        if (hotInstance) {
          // Load data manually - this is required for HOT v16 Angular wrapper
          hotInstance.loadData(viewboxTable.hotTable.data)
          hotInstance.render()
        }
      }, 500)
    })
  }

  /**
   * Get stored Handsontable settings for a specific viewbox
   * @param viewbox
   */
  getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings {
    return this.viewboxHotSettings.get(viewbox.id) || {}
  }

  /**
   * Viewbox resize
   * @param dragHandle
   * @param target
   */
  resize(
    dragHandle: HTMLElement,
    target: HTMLElement
  ): { width: number; height: number } {
    const dragRect = dragHandle.getBoundingClientRect()
    const targetRect = target.getBoundingClientRect()

    const width = dragRect.left - targetRect.left + dragRect.width
    const height = dragRect.top - targetRect.top + dragRect.height

    target.style.width = width + 'px'
    target.style.height = height + 'px'

    this.setAllHandleTransform()

    this.helperService.debounceCall(1000, () => {
      this.viewboxChanged()
      this.eventService.dispatchEvent('resize')

      // Refresh all viewbox tables after resize and update their settings
      this.viewboxes.forEach((viewbox) => {
        // Settings will include updated heights when accessed
        this.refreshTableAfterResize(viewbox)
      })
    })

    return {
      width,
      height
    }
  }

  /**
   * Calls `resize()` outside of angular zone
   * Running functions via #runOutsideAngular allows you to escape Angular's
   * zone and do work that doesn't trigger Angular change-detection or is subject
   * to Angular's error handling.
   * @param dragHandle
   * @param resizeBox
   * @param viewbox
   * @param $event
   */
  dragMove(
    dragHandle: HTMLElement,
    resizeBox: any,
    viewbox: Viewbox,
    $event: CdkDragMove<any>
  ) {
    this.ngZone.runOutsideAngular(() => {
      const newDimnesion = this.resize(dragHandle, resizeBox)

      viewbox.width = newDimnesion.width
      viewbox.height = newDimnesion.height
    })
  }

  /**
   * Sets the 'resize' handle in the correct corner position of the all boxes
   */
  setAllHandleTransform() {
    this.resizeBoxQuery.forEach((resizeBox: ElementRef) => {
      const rect = resizeBox.nativeElement.getBoundingClientRect()
      const handleId = `handle_${resizeBox.nativeElement.id}`

      const dragHandleCorner = this.dragHandleCornerQuery.find(
        (el, i) => el.nativeElement.id === handleId
      )
      this.setHandleTransform(dragHandleCorner?.nativeElement, rect, 'both')
    })
  }

  /**
   * Sets the 'resize' handle in the correct corner position of the box
   */
  setHandleTransform(
    dragHandle: HTMLElement,
    targetRect: ClientRect | DOMRect,
    position: 'x' | 'y' | 'both'
  ) {
    const dragRect = dragHandle.getBoundingClientRect()
    let translateX = targetRect.width - dragRect.width
    let translateY = targetRect.height - dragRect.height

    //Fine tune
    translateX += 5
    translateY += 5

    if (position === 'x') {
      dragHandle.style.transform = `translate(${translateX}px, 0)`
    }

    if (position === 'y') {
      dragHandle.style.transform = `translate(0, ${translateY}px)`
    }

    if (position === 'both') {
      dragHandle.style.transform = `translate(${translateX}px, ${translateY}px)`
    }
  }

  /**
   * When clicked on Viewbox, it will focus it
   * Focused Viewbox is always bring to top
   * @param viewbox
   */
  focusViewbox(viewbox: Viewbox) {
    this.viewboxes.map((vbox) => {
      vbox.focused = false
    })

    viewbox.focused = true
  }

  /**
   * On drag end Viewbox position is updated
   * As well as the URL
   * @param event
   * @param viewbox
   */
  viewboxDragEnded(event: CdkDragEnd, viewbox: Viewbox) {
    let element = event.source.getRootElement()
    let boundingClientRect = element.getBoundingClientRect()

    viewbox.x = boundingClientRect.left
    viewbox.y = boundingClientRect.top

    this.viewboxChanged()
  }

  /**
   * Snap to grid calculates the best grid possible
   * for given screen width and height
   * Configurable options are (in PX):
   * gap - gaps between boxes and left/right sides
   * topOffset - clearance on the top
   * bottomOffset - clearance on the bottom
   */
  snapToGrid() {
    const windowWidth = window.innerWidth
    const windowHeight = window.innerHeight

    const gap = 5 //px configurable
    const topOffset = 250 //px configurable
    const bottomOffset = 60 //px configurable

    const elementsInTopRow = Math.ceil(this.viewboxes.length / 2)
    const elementsInBottomRow = Math.floor(this.viewboxes.length / 2)
    const noOfGapsTop = elementsInTopRow + 1
    const noOfGapsBottom = elementsInBottomRow + 1
    const viewboxWidthTop = (windowWidth - gap * noOfGapsTop) / elementsInTopRow
    const viewboxWidthBottom =
      (windowWidth - gap * noOfGapsBottom) / elementsInBottomRow

    const viewboxHeight = (windowHeight - topOffset - bottomOffset) / 2

    let x = 0
    let y = topOffset
    let height = viewboxHeight
    let viewbox_i = 0
    let row_i = 0

    for (let i = 0; i < this.viewboxes.length; i++) {
      let viewbox = this.viewboxes[i]
      let topRow = !(i > elementsInTopRow - 1)
      const width = topRow ? viewboxWidthTop : viewboxWidthBottom
      if (!topRow && row_i === 0) {
        viewbox_i = 0
        row_i++
        x = 0
      }

      viewbox.x = gap + x + viewbox_i * (width + gap)
      viewbox.y = y + row_i * (height + gap)
      viewbox.width = width
      viewbox.height = height

      viewbox_i++
    }

    this.viewboxChanged()

    setTimeout(() => {
      this.setAllHandleTransform()

      // Refresh all tables after snap to grid
      this.viewboxes.forEach((viewbox) => {
        // Settings will include correct height when accessed
        this.refreshTableAfterResize(viewbox)
      })
    })
  }

  minimizeAll() {
    this.viewboxes.forEach((viewbox: Viewbox) => {
      viewbox.minimized = true
    })

    this.viewboxChanged()
  }

  restoreAll() {
    this.viewboxes.forEach((viewbox: Viewbox) => {
      viewbox.minimized = false
    })

    this.viewboxChanged()
  }

  /**
   * Resets Viewbox to default position (top right corner)
   * @param viewbox
   */
  resetPosSize(viewbox: Viewbox) {
    viewbox.x = window.innerWidth - this.defaultConfig.width
    viewbox.y = this.defaultConfig.y
    viewbox.width = this.defaultConfig.width
    viewbox.height = this.defaultConfig.height
    this.viewboxChanged()
  }

  minimize(viewbox: Viewbox) {
    viewbox.minimized = true

    this.viewboxChanged()
  }

  restore(viewbox: Viewbox) {
    viewbox.minimized = false

    this.viewboxChanged()

    // Refresh table after restoring
    setTimeout(() => {
      // Settings will include correct height when accessed
      this.refreshTableAfterResize(viewbox)
    }, 100)
  }

  collapse(viewbox: Viewbox) {
    viewbox.collapsed = true
    this.viewboxChanged()
  }

  expand(viewbox: Viewbox) {
    viewbox.collapsed = false
    this.viewboxChanged()

    // Refresh table after expanding
    setTimeout(() => {
      // Settings will include correct height when accessed
      this.refreshTableAfterResize(viewbox)
    }, 100)
  }

  /**
   * Close Viewbox and remove it's data stored in `paired ViewboxTable array`
   * @param viewbox
   */
  close(viewbox: Viewbox) {
    const index = this.viewboxes.findIndex((vb) => vb.id === viewbox.id)

    const viewtableIndex = this.viewboxTables.findIndex(
      (vbt) => vbt.viewboxId === viewbox.id
    )

    if (index > -1) this.viewboxes.splice(index, 1)
    if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1)

    // Clean up settings for this viewbox
    this.viewboxHotSettings.delete(viewbox.id)

    if (this.selectedViewbox?.id === viewbox.id) {
      this.unsetSelectedViewbox()
    }

    globals.viewboxes[viewbox.id] = this.helperService.deepClone(initFilter)

    this.viewboxChanged()
  }

  /**
   * Selects Viewbox in the Viewbox Manager for the columns to be configurated
   * @param viewbox
   */
  selectViewbox(viewbox: Viewbox) {
    if (
      this.selectedViewboxTable === undefined &&
      this.selectedViewbox === undefined
    ) {
      this.resetSelectedViewbox(viewbox)
    } else {
      if (viewbox.id === this.selectedViewbox?.id) {
        this.unsetSelectedViewbox()
      } else {
        this.resetSelectedViewbox(viewbox)
      }
    }
  }

  /**
   * Column drop called after column dragged and dropped
   * for Viewbox column to be configurated
   * @param event
   * @returns
   */
  columnsDrop(event: CdkDragDrop<string[]>) {
    if (!this.selectedViewboxTable?.hotTable.colHeadersHidden) return

    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      )
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      )
    }

    if (
      this.selectedViewboxTable.hotTable &&
      typeof this.selectedViewboxTable.hotTable.colHeaders === 'object'
    ) {
      const colProp = event.item.data
      const finalIndex = event.currentIndex

      this.updateColumnOrderHot(
        colProp,
        finalIndex,
        this.selectedViewboxTable.viewboxId
      )
    }
  }

  /**
   * Viewbox Manager searching for columns to be added
   * @param inputRef Input element
   * @param value
   * @param columns
   */
  onColsearchChange(
    inputRef: AutocompleteComponent,
    value: string,
    columns: string[]
  ) {
    const index = columns.indexOf(value)

    columns.splice(index, 1)
    inputRef.value = ''

    if (this.selectedViewboxTable?.hotTable) {
      this.selectedViewboxTable.hotTable.colHeadersVisible.push(value)

      this.updateHotColumns(
        this.selectedViewboxTable?.hotTable.colHeadersHidden,
        this.selectedViewboxTable.viewboxId
      )

      this.updateColumnOrderHot(
        value,
        this.selectedViewboxTable.hotTable.colHeadersVisible.length - 1,
        this.selectedViewboxTable.viewboxId
      )
    }
  }

  /**
   * Viewbox Manager removing the column
   * @param column
   */
  onColRemove(column: string) {
    if (this.selectedViewboxTable?.hotTable) {
      const index =
        this.selectedViewboxTable.hotTable.colHeadersVisible.indexOf(column)
      this.selectedViewboxTable.hotTable.colHeadersVisible.splice(index, 1)
      this.selectedViewboxTable.hotTable.colHeadersHidden.push(column)

      this.updateHotColumns(
        this.selectedViewboxTable?.hotTable.colHeadersHidden,
        this.selectedViewboxTable.viewboxId
      )

      this.updateColumnOrderHot(
        column,
        this.selectedViewboxTable.hotTable.colHeadersVisible.length,
        this.selectedViewboxTable.viewboxId
      )
    }
  }

  /**
   * Re setting the viewbox from parameter to selected viewbox
   * And sets the correct data for that viewbox
   * @param viewbox
   */
  resetSelectedViewbox(viewbox: Viewbox) {
    this.selectedViewbox = viewbox
    this.selectedViewboxTable = this.viewboxTables.find(
      (vbt) => vbt.viewboxId === viewbox.id
    )
  }

  unsetSelectedViewbox() {
    this.selectedViewbox = undefined
    this.selectedViewboxTable = undefined
  }

  stopPropagation(event: any) {
    event.stopPropagation()
  }

  /**
   * Opens filter dialog for given Viewbox
   * @param viewbox
   */
  openFilter(viewbox: Viewbox) {
    this.selectViewbox(viewbox)

    const viewboxTable = this.viewboxTables[this.getViewboxTableIndex(viewbox)]

    this.filterLibds = `${viewbox.library}.${viewbox.table}`
    this.filteringViewbox = viewbox

    this.filter = true
    this.cdf.detectChanges()

    this.sasStoreService.setQueryVariables(
      this.filterLibds,
      viewboxTable.hotTable.cols
    )
  }

  /**
   * Opens table edit in new tab
   * @param viewbox
   */
  openTableEdit(viewbox: Viewbox) {
    const libDataset = viewbox.library + '.' + viewbox.table
    let url = location.href.slice(0, location.href.indexOf('#'))
    url = `${url}#/editor/${libDataset}`

    window.open(url, '_blank')
  }

  /**
   * Resets the data in filter dialog
   * It will also reset the filtering in the given Viewbox
   */
  resetFilter() {
    if (this.filteringViewbox) {
      this.filteringViewbox.filter_pk = '0'
      this.reloadTableData(this.filteringViewbox)
      this.filter = false
      this.viewboxChanged()
      globals.viewboxes[this.filteringViewbox.id] =
        this.helperService.deepClone(initFilter)
    }
  }

  /**
   * Sending filtering request for given Viewbox
   */
  sendClause() {
    this.filterLoading = true
    let nullVariableArr = []
    let emptyVariablesArr = []

    // to check number of empty clauses
    if (typeof this.clauses === 'undefined') {
      this.nullVariables = true
      this.filterLoading = false
      return
    } else {
      let query = this.clauses.queryObj

      for (let index = 0; index < query.length; index++) {
        const el = query[index].elements
        nullVariableArr = el.filter(function (item: any) {
          return item.variable === null
        })
        if (nullVariableArr.length) {
          emptyVariablesArr.push(el)
        }
      }
    }

    if (emptyVariablesArr.length) {
      this.nullVariables = true
      this.filterLoading = false
      return
    } else {
      try {
        if (this.clauses !== undefined && this.filterLibds) {
          const filterQuery: FilterQuery = {
            groupLogic: this.clauses.groupLogic,
            filterGroups: []
          }
          this.clauses.queryObj.forEach((group: any) => {
            const filterGroup: FilterGroup = {
              filterClauses: []
            }
            group.elements.forEach((clause: any) => {
              filterGroup.filterClauses.push(
                this.helperService.deepClone(clause)
              )
            })
            filterGroup.clauseLogic = group.clauseLogic
            filterQuery.filterGroups.push(
              this.helperService.deepClone(filterGroup)
            )
          })

          const filterQueryClauseTable =
            this.sasStoreService.createFilterQueryTable(filterQuery)

          this.sasStoreService
            .saveQuery(this.filterLibds, filterQueryClauseTable)
            .then((res: any) => {
              const id = res.result[0].FILTER_RK
              const table = res.result[0].FILTER_TABLE

              this.filteringViewbox!.filter_pk = id
              this.loadData(this.filterLibds!, this.filteringViewbox!).then(
                () => {
                  this.filter = false
                  this.filterLoading = false
                }
              )

              this.viewboxChanged()
            })
            .catch((err: any) => {
              this.filterLoading = false
            })
        }
      } catch (error: any) {
        this.filterLoading = false
      }
    }
  }

  async searchTable(inputElement: any, viewbox: Viewbox) {
    viewbox.searchLoading = true

    let searchValue = inputElement.value

    let libDataset = viewbox.library + '.' + viewbox.table
    let filter_pk = parseInt(viewbox.filter_pk || '0')

    const viewboxTable = this.viewboxTables.find(
      (vbt) => vbt.viewboxId === viewbox.id
    )

    if (!viewboxTable) return

    await this.sasStoreService
      .viewDataSearch(searchValue, viewbox.searchNumeric, libDataset, filter_pk)
      .then((res: any) => {
        if (!res.sasparams && !res.viewData) {
          viewbox.searchLoading = true
          return
        }

        viewboxTable.hotTable.data = res.viewdata

        // Update settings with new data
        this.createViewboxTableSettings(viewbox)
      })
      .catch((err: any) => {
        this.loggerService.error(err)
      })

    viewbox.searchLoading = false
  }

  async reloadTableData(viewbox: Viewbox) {
    const libDataset = `${viewbox.library}.${viewbox.table}`

    viewbox.loadingData = true

    await this.reloadData(libDataset, viewbox)

    viewbox.loadingData = false
    this.eventService.dispatchEvent('resize') //Force HOT refresh
  }

  /**
   * Updates the HOT columns Order and Visibility
   * @param hiddenColProps Array of indexes of columns to be hidden
   * @param viewboxId Viewbox ID for which to apply
   */
  private updateHotColumns(hiddenColProps: string[], viewboxId: number) {
    this.updateHiddenColumnsHot(hiddenColProps, viewboxId)

    this.setColumnOrder(viewboxId)

    // Settings will be regenerated when accessed
  }

  /**
   * HOT Columns ordering
   * @param colProp Column name
   * @param finalIndex Index of where to position it
   * @param viewboxId
   */
  private updateColumnOrderHot(
    colProp: string,
    finalIndex: number,
    viewboxId: number
  ) {
    const hotInstance = this.getViewboxHotInstance(viewboxId)

    if (hotInstance) {
      const column = hotInstance.propToCol(colProp) as number
      const plugin = hotInstance.getPlugin('manualColumnMove')

      plugin.moveColumn(column, finalIndex)
      hotInstance.render()

      this.setColumnOrder(viewboxId)
    }
  }

  public tableEditExists(viewbox: Viewbox) {
    const editTables = globals.editor.libsAndTables
    const library = viewbox.library
    const table = viewbox.table

    // If this line is undefined, that means startupservice failed or similar.
    if (!editTables[library]) return false

    return editTables[library].includes(table)
  }

  private setColumnOrder(viewboxId: number) {
    const viewbox = this.viewboxes.find((vb) => vb.id === viewboxId)

    if (viewbox) {
      const columnsOrder = this.createColumnOrder(viewboxId)
      viewbox.columns = columnsOrder.length > 0 ? columnsOrder : viewbox.columns
    }

    this.viewboxChanged()
  }

  /**
   * Creating column order in such format to be encoded in URL
   * @param viewboxId
   */
  private createColumnOrder(viewboxId: number): number[] {
    const hotInstance = this.getViewboxHotInstance(viewboxId)
    if (!hotInstance) return []

    const hotCols = hotInstance.getColHeader() as string[]
    const sasCols = this.selectedViewboxTable?.hotTable.colHeaders as string[]

    if (!sasCols) return []

    const columnsVisibleLength: number =
      this.selectedViewboxTable?.hotTable?.colHeadersVisible.length || 5 //if unexpected happens limit will Be 5 columns
    const columnOrder: number[] = []

    hotCols.map((hotCol: string, index: number) => {
      if (index < columnsVisibleLength) {
        const indexofSasCol = sasCols.indexOf(hotCol)

        if (indexofSasCol > -1) columnOrder.push(indexofSasCol)
      }
    })

    return columnOrder
  }

  private updateHiddenColumnsHot(colProps: string[], viewboxId: number) {
    const hotInstance = this.getViewboxHotInstance(viewboxId)

    if (hotInstance) {
      const columns = colProps.map((prop: string) => {
        return hotInstance.propToCol(prop) as number
      })

      hotInstance.updateSettings({
        hiddenColumns: {
          columns: columns
        }
      })

      hotInstance.render()
    }
  }

  /**
   * Calculate available height for Handsontable
   * @param viewbox The viewbox to calculate height for
   * @returns Available height in pixels
   */
  calculateTableHeight(viewbox: Viewbox): number {
    // Calculate the exact height of the content div
    const dragHandleHeight = 20
    const searchFormHeight = 36
    const padding = 2
    // Return the exact remaining height for the table with minimum height
    const calculatedHeight =
      viewbox.height - dragHandleHeight - searchFormHeight - padding

    return calculatedHeight
  }

  /**
   * Refresh Handsontable instance after resize
   * @param viewbox The viewbox to refresh
   */
  refreshTableAfterResize(viewbox: Viewbox): void {
    const hotInstance = this.getViewboxHotInstance(viewbox.id)
    if (hotInstance) {
      // Force the table to recalculate its dimensions
      setTimeout(() => {
        try {
          // Update height setting and refresh
          hotInstance.updateSettings({
            height: this.calculateTableHeight(viewbox)
          })
          hotInstance.refreshDimensions()
          hotInstance.render()
        } catch (error) {
          // If refresh fails, try again later
          setTimeout(() => {
            try {
              hotInstance.updateSettings({
                height: this.calculateTableHeight(viewbox)
              })
              hotInstance.refreshDimensions()
            } catch (e) {
              console.warn(
                'Failed to refresh HOT dimensions for viewbox',
                viewbox.id,
                e
              )
            }
          }, 500)
        }
      }, 100)
    }
  }

  /**
   *
   * @param viewboxId
   * @returns HOT Instance from the given Viewbox
   */
  private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined {
    if (!viewboxId || !this.hotTableComponents) return

    // Find the component corresponding to this viewbox
    // Since we only show one table per viewbox and they're rendered in order,
    // we can match by the viewbox's position in the array
    const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId)
    if (viewboxIndex === -1) return

    // Get the HOT component at this index
    const hotComponents = this.hotTableComponents.toArray()
    let hotComponentIndex = 0

    // Count how many viewboxes before this one have loaded tables
    for (let i = 0; i < viewboxIndex; i++) {
      if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) {
        hotComponentIndex++
      }
    }

    const hotTableComponent = hotComponents[hotComponentIndex]
    return hotTableComponent?.hotInstance || undefined
  }

  /**
   * Called after any Viewbox change that needs to be stored to URL
   * It does the data encoding and storing to URL
   */
  private viewboxChanged() {
    let queryParams: any
    const urlData = this.encodeUrlData(this.viewboxes)

    if (urlData.length > 0) {
      queryParams = {
        viewboxes: urlData
      }
    }

    this.router.navigate([], {
      relativeTo: this.activatedRoute,
      queryParams
    })

    this.prepareFilterCache()
  }

  /**
   * Prepare the init values to `globals` for the filtering
   * that will be used in caching values later
   */
  private prepareFilterCache() {
    for (let viewbox of this.viewboxes) {
      if (!globals.viewboxes[viewbox.id])
        globals.viewboxes[viewbox.id] = this.helperService.deepClone(initFilter)

      if (viewbox.query && viewbox.query.length > 0) {
        const viewboxTable = this.viewboxTables.find(
          (vbt) => vbt.viewboxId === viewbox.id
        )
        const globalsPath = `viewboxes.${viewbox.id}`

        globals.viewboxes[viewbox.id].filter.query = viewbox.query
        globals.viewboxes[viewbox.id].filter.libds =
          viewbox.library + '.' + viewbox.table
        this.sasStoreService.initializeGlobalFilterClause(
          globalsPath,
          viewboxTable?.hotTable.cols
        )
      }
    }
  }

  /**
   * It will encode/inject viewboxes data from URL
   * Url data pattern:
   *
   * {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns}
   * 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456
   *
   * ; <- single viewbox separation symbol
   */
  private encodeUrlData(viewboxes: Viewbox[]): string {
    let urlData: string = ''

    viewboxes.map((viewbox: Viewbox, index: number) => {
      urlData += `${viewbox.id}-${viewbox.library}-${viewbox.table}-${
        viewbox.width
      }-${viewbox.height}-${viewbox.x}-${viewbox.y}-${
        viewbox.collapsed ? 1 : 0
      }-${viewbox.minimized ? 1 : 0}-${viewbox.filter_pk || 0}${
        viewbox.columns && viewbox.columns.length > 0
          ? '-' + viewbox.columns?.join(',')
          : ''
      }`

      if (index !== viewboxes.length - 1) urlData += ';'
    })

    return urlData
  }

  /**
   * It will decode/parse viewboxes data from URL
   * Url data pattern:
   *
   * {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns}
   * 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456
   *
   * ; <- single viewbox separation symbol
   */
  private decodeUrlData(urlData: string): Viewbox[] {
    const urlDataMap = {
      id: 0,
      library: 1,
      table: 2,
      width: 3,
      height: 4,
      x: 5,
      y: 6,
      collapsed: 7,
      minimized: 8,
      filter_pk: 9,
      columns: 10
    }

    let viewboxes: Viewbox[] = []

    const separatedViewboxes = urlData.split(';')

    separatedViewboxes.map((viewboxString: string) => {
      const viewboxDataArr = viewboxString.split('-')

      viewboxes.push({
        id: parseInt(viewboxDataArr[urlDataMap.id]),
        library: viewboxDataArr[urlDataMap.library],
        table: viewboxDataArr[urlDataMap.table],
        width: parseInt(viewboxDataArr[urlDataMap.width]),
        height: parseInt(viewboxDataArr[urlDataMap.height]),
        x: parseInt(viewboxDataArr[urlDataMap.x]),
        y: parseInt(viewboxDataArr[urlDataMap.y]),
        collapsed: !!parseInt(viewboxDataArr[urlDataMap.collapsed]),
        minimized: !!parseInt(viewboxDataArr[urlDataMap.minimized]),
        columns:
          viewboxDataArr[urlDataMap.columns]
            ?.split(',')
            .map((x) => parseInt(x)) || [],
        filter_pk: viewboxDataArr[urlDataMap.filter_pk]
      })
    })

    return viewboxes
  }

  ngOnDestroy(): void {
    this._query?.unsubscribe()
  }
}
