import {
  AfterContentInit,
  AfterViewInit,
  Component,
  ElementRef,
  HostBinding,
  OnInit,
  QueryList,
  ViewChildren
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { UploadFile } from '@sasjs/adapter'
import * as XLSX from '@sheet/crypto'
import { globals } from '../_globals'
import {
  EventService,
  LicenceService,
  LoggerService,
  SasService,
  SasStoreService
} from '../services'
import { FileUploader } from '../models/FileUploader.class'

interface XLMapRule {
  XLMAP_ID: string
  XLMAP_SHEET: string
  XLMAP_RANGE_ID: string
  XLMAP_START: string
  XLMAP_FINISH: string
}

interface XLUploadEntry {
  LOAD_REF: string
  XLMAP_ID: string
  XLMAP_RANGE_ID: string
  ROW_NO: number
  COL_NO: number
  VALUE_TXT: string
}

enum Status {
  NoMapSelected,
  FetchingRules,
  ReadyToUpload,
  ExtractingData,
  ReadyToSubmit,
  SubmittingExtractedData,
  Submitting
}

enum Tabs {
  Rules,
  Data
}

@Component({
  selector: 'app-xlmap',
  templateUrl: './xlmap.component.html',
  styleUrls: ['./xlmap.component.scss']
})
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
  @HostBinding('class.content-container') contentContainerClass = true
  @ViewChildren('fileUploadInput')
  fileUploadInputCompList: QueryList<ElementRef> = new QueryList()

  StatusEnum = Status
  TabsEnum = Tabs

  public selectedTab = Tabs.Rules
  public rulesSource = globals.dcLib + '.MPE_XLMAP_RULES'

  public xlmaps: string[] = []
  public selectedXLMapId = ''
  public targetDS = ''
  public searchString = ''
  public xlmapsLoading = true
  public isLoading = false
  public isLoadingDesc = ''
  public status = Status.NoMapSelected

  public xlmapRulesHeaders = [
    'XLMAP_SHEET',
    'XLMAP_RANGE_ID',
    'XLMAP_START',
    'XLMAP_FINISH'
  ]
  public xlmapRulesColumns = [
    {
      data: 'XLMAP_SHEET'
    },
    {
      data: 'XLMAP_RANGE_ID'
    },

    {
      data: 'XLMAP_START'
    },
    {
      data: 'XLMAP_FINISH'
    }
  ]
  public xlmapRules: XLMapRule[] = []

  public xlUploadHeader = ['XLMAP_RANGE_ID', 'ROW_NO', 'COL_NO', 'VALUE_TXT']
  public xlUploadColumns = [
    {
      data: 'XLMAP_RANGE_ID'
    },
    {
      data: 'ROW_NO'
    },
    {
      data: 'COL_NO'
    },
    {
      data: 'VALUE_TXT'
    }
  ]
  public xlData: XLUploadEntry[] = []

  public showUploadModal = false
  public hasBaseDropZoneOver = false
  public filename = ''
  public submitLimitNotice = false

  public uploader: FileUploader = new FileUploader()

  public licenceState = this.licenceService.licenceState

  public hotTableLicenseKey: string | undefined = undefined
  public hotTableMaxRows =
    this.licenceState.value.viewer_rows_allowed || Infinity

  constructor(
    private eventService: EventService,
    private licenceService: LicenceService,
    private loggerService: LoggerService,
    private route: ActivatedRoute,
    private router: Router,
    private sasStoreService: SasStoreService,
    private sasService: SasService
  ) {}

  private blobToFile(theBlob: Blob, fileName: string): File {
    const b: any = theBlob
    b.lastModifiedDate = new Date()
    b.name = fileName
    return b as File
  }

  public xlmapOnClick(xlmap: string) {
    if (xlmap !== this.selectedXLMapId) {
      this.selectedXLMapId = xlmap
      this.viewXLMapRules()
      this.router.navigateByUrl('/home/files/' + xlmap)
    }
  }

  public xlmapListOnFilter() {
    if (this.searchString.length > 0) {
      const array: string[] = globals.xlmaps
      this.xlmaps = array.filter((item) =>
        item.toLowerCase().includes(this.searchString.toLowerCase())
      )
    } else {
      this.xlmaps = globals.xlmaps
    }
  }

  public isActiveXLMap(id: string) {
    return this.selectedXLMapId === id
  }

  public maxWidthChecker(width: any, col: any) {
    if (width > 200) return 200
    else return width
  }

  public getCellConfiguration() {
    return { readOnly: true }
  }

  public rowHeaders() {
    return ' '
  }

  public onShowUploadModal() {
    this.showUploadModal = true
  }

  /**
   * Called by FileDropDirective
   * @param e true if file is dragged over the drop zone
   */
  public fileOverBase(e: boolean): void {
    this.hasBaseDropZoneOver = e
  }

  public getFileDesc(event: any, dropped = false) {
    const file = dropped ? event[0] : event.target.files[0]

    if (!file) return

    const filename = file.name
    this.filename = filename

    const fileType = filename.slice(
      filename.lastIndexOf('.') + 1,
      filename.lastIndexOf('.') + 4
    )

    if (fileType.toLowerCase() === 'xls') {
      this.showUploadModal = false
      this.isLoading = true
      this.isLoadingDesc = 'Extracting Data'
      this.status = Status.ExtractingData

      const reader = new FileReader()
      reader.onload = async (theFile: any) => {
        /* read workbook */
        const bstr = this.toBstr(theFile.target.result)
        let wb: XLSX.WorkBook | undefined = undefined

        const xlsxOptions: XLSX.ParsingOptions = {
          type: 'binary',
          cellDates: false,
          cellFormula: true,
          cellStyles: true,
          cellNF: false,
          cellText: false
        }

        try {
          wb = XLSX.read(bstr, {
            ...xlsxOptions
          })
        } catch (err: any) {
          this.eventService.showAbortModal(
            null,
            err,
            undefined,
            'Error reading file'
          )
        }

        if (!wb) {
          this.isLoading = false
          this.isLoadingDesc = ''
          this.status = Status.ReadyToUpload
          this.uploader.queue.pop()
          return
        }

        this.extractData(wb)
        return
      }

      reader.readAsArrayBuffer(file)
    } else {
      this.isLoading = false
      this.isLoadingDesc = ''
      this.status = Status.ReadyToUpload
      this.showUploadModal = true
      this.uploader.queue.pop()

      const abortMsg =
        'Invalid file type "<b>' +
        this.filename +
        '</b>". Please upload excel file.'
      this.eventService.showAbortModal(null, abortMsg)
    }
  }

  public toBstr(res: any) {
    const bytes = new Uint8Array(res)
    let binary = ''
    const length = bytes.byteLength
    for (let i = 0; i < length; i++) {
      binary += String.fromCharCode(bytes[i])
    }
    return binary
  }

  public discardExtractedData() {
    this.isLoading = false
    this.isLoadingDesc = ''
    this.status = Status.ReadyToUpload
    this.xlData = []
    this.filename = ''
    this.uploader.queue = []
    if (this.fileUploadInputCompList.first) {
      this.fileUploadInputCompList.first.nativeElement.value = ''
    }
  }

  /**
   * Submits attached excel file that is in preview mode
   */
  public submitExcel() {
    if (this.licenceState.value.submit_rows_limit !== Infinity) {
      this.submitLimitNotice = true
      return
    }

    this.submit()
  }

  public submit() {
    if (!this.xlData.length) return

    this.status = Status.Submitting
    this.isLoading = true
    this.isLoadingDesc = 'Submitting extracted data'

    const filesToUpload: UploadFile[] = []

    for (const file of this.uploader.queue) {
      filesToUpload.push({
        file: file,
        fileName: file.name
      })
    }

    const csvContent =
      Object.keys(this.xlData[0]).join(',') +
      '\n' +
      this.xlData.map((row: any) => Object.values(row).join(',')).join('\n')

    const blob = new Blob([csvContent], { type: 'application/csv' })
    const file: File = this.blobToFile(blob, this.filename + '.csv')

    filesToUpload.push({
      file: file,
      fileName: file.name
    })

    const uploadUrl = 'services/editors/loadfile'
    this.sasService
      .uploadFile(uploadUrl, filesToUpload, {
        table: this.targetDS
      })
      .then((res: any) => {
        if (res.sasjsAbort) {
          const abortRes = res
          const abortMsg = abortRes.sasjsAbort[0].MSG
          const macMsg = abortRes.sasjsAbort[0].MAC

          this.eventService.showAbortModal('', abortMsg, {
            SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
            SYSERRORTEXT: abortRes.SYSERRORTEXT,
            MAC: macMsg
          })
        } else if (res.sasparams) {
          const params = res.sasparams[0]
          const tableId = params.DSID
          this.router.navigateByUrl('/stage/' + tableId)
        }
      })
      .catch((err: any) => {
        this.eventService.catchResponseError('file upload', err)
      })
      .finally(() => {
        this.status = Status.ReadyToSubmit
        this.isLoading = false
        this.isLoadingDesc = ''
      })
  }

  public extractData(wb: XLSX.WorkBook) {
    const extractedData: XLUploadEntry[] = []

    this.xlmapRules.forEach((rule) => {
      let sheetName = rule.XLMAP_SHEET
      if (sheetName.startsWith('/')) {
        const temp = sheetName.split('/')[1]
        const sheetIndex = parseInt(temp) - 1
        sheetName = wb.SheetNames[sheetIndex]
      }

      const sheet = wb.Sheets[sheetName]

      const arrayOfObjects = <any[]>XLSX.utils.sheet_to_json(sheet, {
        raw: true,
        header: 'A',
        blankrows: true
      })

      const start = this.getCellAddress(rule.XLMAP_START, arrayOfObjects)
      const finish = this.getFinishingCell(
        start,
        rule.XLMAP_FINISH,
        arrayOfObjects
      )

      const range = `${start}:${finish}`

      const rangedData = <any[]>XLSX.utils.sheet_to_json(sheet, {
        raw: true,
        range: range,
        header: 'A',
        blankrows: true
      })

      for (let i = 0; i < rangedData.length; i++) {
        const row = rangedData[i]
        // Get the keys of the object (excluding '__rowNum__')
        const keys = Object.keys(row).filter((key) => key !== '__rowNum__')

        for (let j = 0; j < keys.length; j++) {
          const key = keys[j]
          const val = row[key]

          extractedData.push({
            LOAD_REF: '0',
            XLMAP_ID: rule.XLMAP_ID,
            XLMAP_RANGE_ID: rule.XLMAP_RANGE_ID,
            ROW_NO: i + 1,
            COL_NO: j + 1,
            VALUE_TXT: val
          })
        }
      }
    })

    this.status = Status.ReadyToSubmit
    this.isLoading = false
    this.isLoadingDesc = ''

    this.xlData = extractedData
    this.selectedTab = Tabs.Data
  }

  public getCellAddress(rule: string, arrayOfObjects: any[]) {
    if (rule.startsWith('ABSOLUTE ')) {
      rule = rule.replace('ABSOLUTE ', '')
    }

    if (rule.startsWith('RELATIVE ')) {
      const rowAndCol = this.extractRowAndCol(rule)

      if (rowAndCol) {
        const { row, column } = rowAndCol

        // Generate an A1-Style address string from a SheetJS cell address
        // Spreadsheet applications typically display ordinal row numbers,
        // where 1 is the first row, 2 is the second row, etc. The numbering starts at 1.
        // SheetJS follows JavaScript counting conventions,
        // where 0 is the first row, 1 is the second row, etc. The numbering starts at 0.
        // Therefore, we have to subtract 1 from row and column to match SheetJS indexing convention
        rule = XLSX.utils.encode_cell({ r: row - 1, c: column - 1 })
      }
    }

    if (rule.startsWith('MATCH ')) {
      let targetValue = ''

      // using a regular expression to match "C[x]:" and extract the value after it
      const match = rule.match(/C\[\d+\]:(.+)/)

      // Check if there is a match
      if (match) {
        // Extract the value after "C[x]:"
        targetValue = match[1]
      }

      // Split the string by spaces to get target row/column
      const splittedArray = rule.split(' ')

      // Extract the second word
      const secondWord = splittedArray[1]

      let targetColumn = ''
      let targetRow = -1
      let cellAddress = ''

      // Check if the secondWord is a number
      if (!isNaN(Number(secondWord))) {
        targetRow = parseInt(secondWord)
      } else {
        targetColumn = secondWord
      }

      if (targetRow !== -1) {
        // sheetJS index starts from 0,
        // therefore, decremented 1 to make it correct row address for js array
        const row = arrayOfObjects[targetRow - 1]
        for (const col in row) {
          if (col !== '__rowNum__' && row[col] === targetValue) {
            cellAddress = col + targetRow
            break
          }
        }
      } else {
        for (let i = 0; i < arrayOfObjects.length; i++) {
          const row = arrayOfObjects[i]
          if (row[targetColumn] === targetValue) {
            // sheetJS index starts from 0,
            // therefore, incremented 1 to make it correct row address
            const rowIndex = i + 1
            cellAddress = targetColumn + rowIndex
            break
          }
        }
      }

      const temp = XLSX.utils.decode_cell(cellAddress)

      const rowAndCol = this.extractRowAndCol(rule)

      if (rowAndCol) {
        const { row, column } = rowAndCol
        rule = XLSX.utils.encode_cell({ r: temp.r + row, c: temp.c + column })
      }
    }

    return rule
  }

  public getFinishingCell(
    start: string,
    finish: string,
    arrayOfObjects: any[]
  ) {
    if (finish === '') {
      return start
    }

    if (finish.startsWith('ABSOLUTE ')) {
      finish = finish.replace('ABSOLUTE ', '')
    }

    if (finish.startsWith('RELATIVE ')) {
      const rowAndCol = this.extractRowAndCol(finish)
      if (rowAndCol) {
        const { row, column } = rowAndCol

        const { r, c } = XLSX.utils.decode_cell(start)

        finish = XLSX.utils.encode_cell({ r: r + row, c: c + column })
      }
    }

    if (finish.startsWith('MATCH ')) {
      finish = this.getCellAddress(finish, arrayOfObjects)
    }

    if (finish === 'LASTDOWN') {
      const { r, c } = XLSX.utils.decode_cell(start)
      const colName = XLSX.utils.encode_col(c)
      let lastNonBlank = r
      for (let i = r + 1; i < arrayOfObjects.length; i++) {
        const row = arrayOfObjects[i]
        if (!row[colName]) {
          break
        }
        lastNonBlank = i
      }
      finish = colName + lastNonBlank
    }

    if (finish === 'BLANKROW') {
      const { r } = XLSX.utils.decode_cell(start)
      let lastNonBlankRow = r
      for (let i = r + 1; i < arrayOfObjects.length; i++) {
        const row = arrayOfObjects[i]
        if (this.isBlankRow(row)) {
          break
        }
        lastNonBlankRow = i
      }
      const row = arrayOfObjects[lastNonBlankRow]

      // Get the keys of the object (excluding '__rowNum__')
      const keys = Object.keys(row).filter((key) => key !== '__rowNum__')

      // Find the key with the highest alphanumeric value (assumes keys are letters)
      const lastColumn = keys.reduce(
        (maxKey, currentKey) => (currentKey > maxKey ? currentKey : maxKey),
        ''
      )

      finish = lastColumn + lastNonBlankRow
    }

    return finish
  }

  public extractRowAndCol(str: string) {
    // Regular expression to match and capture the values inside square brackets
    const regex = /R\[(\d+)\]C\[(\d+)\]/

    // Match the regular expression against the input string
    const match = str.match(regex)

    if (!match) {
      return null
    }

    // Extract values from the match groups
    const row = parseInt(match[1], 10)
    const column = parseInt(match[2], 10)

    return {
      row,
      column
    }
  }

  public isBlankRow(row: any) {
    for (const key in row) {
      if (key !== '__rowNum__') {
        return false
      }
    }
    return true
  }

  async viewXLMapRules() {
    this.isLoading = true
    this.isLoadingDesc = 'Loading excel rules'
    this.status = Status.FetchingRules

    await this.sasStoreService
      .getXLMapRules(this.selectedXLMapId)
      .then((res) => {
        this.targetDS = res.xlmapinfo[0].TARGET_DS
        this.xlmapRules = res.xlmaprules

        this.status = Status.ReadyToUpload
      })
      .catch((err) => {
        this.loggerService.error(err)
      })

    this.isLoading = false
    this.isLoadingDesc = ''
  }

  public getFromGlobals() {
    this.xlmaps = globals.xlmaps
    this.xlmapsLoading = false
  }

  private load() {
    this.xlmaps = globals.xlmaps
    this.xlmapsLoading = false

    const id = this.route.snapshot.params['id']

    if (id && this.xlmaps.includes(id)) {
      this.selectedXLMapId = id
      this.viewXLMapRules()
    }
  }

  ngOnInit() {
    this.licenceService.hot_license_key.subscribe(
      (hot_license_key: string | undefined) => {
        this.hotTableLicenseKey = hot_license_key
      }
    )
  }

  ngAfterViewInit() {
    return
  }

  ngAfterContentInit(): void {
    if (globals.editor.startupSet) {
      this.load()
    } else {
      this.eventService.onStartupDataLoaded.subscribe(() => {
        this.load()
      })
    }
  }
}
