//
//  ContainerView.swift
//  qlik-trial-react-native-straight-table
//
//  Created by Vittorio Cellucci on 2022-01-14.
//

import Foundation
import SDWebImage

public typealias EventCallback = (NSDictionary?) -> Void

@objc
@objcMembers
public class ContainerView: UIView {
  var created = false
  var tableTheme: TableTheme?
  var dataSize: DataSize?
  var dataColumns: [DataColumn]?
  var dataRows: [DataRow]?
  var totals: Totals?
  let selectionsEngine = SelectionsEngine()
  let hScrollViewDelegate = HorizontalScrollViewDelegate()
  var needsGrabbers = true
  var cellStyle: CellStyle?
  var headerStyle: HeaderStyle?
  var defaultCalculated = false
  var menuTranslations: MenuTranslations?
  var horizontalScrollView: UIScrollView?
  var columnWidths = ColumnWidths()
  var maxHeaderLineCount = 1
  var maxTotalsLineCount = 1
  var maxCollectionViewsLineCount = 1
  weak var firstColumnTable: TableView?
  weak var multiColumnTable: TableView?

  // Callbacks for events - accessible from Objective-C bridge
  public var onEndReachedCallback: EventCallback?
  public var onVerticalScrollEndedCallback: EventCallback?
  public var onHeaderPressedCallback: EventCallback?
  public var onExpandCellCallback: EventCallback?
  public var onSearchColumnCallback: EventCallback?

  @objc public var onSelectionsChangedCallback: EventCallback? {
    didSet {
      selectionsEngine.onSelectionsChanged = onSelectionsChangedCallback
    }
  }

  @objc public var onConfirmSelectionsCallback: EventCallback? {
    didSet {
      selectionsEngine.onConfirmSelections = onConfirmSelectionsCallback
    }
  }

  public var containerWidth: NSNumber?
  public var freezeFirstColumn: Bool = false
  public var isDataView: Bool = false

  @objc public var clearSelections: NSString? {
    didSet {
      if let clearSelections = clearSelections {
        if clearSelections.compare("yes") == .orderedSame {
          selectionsEngine.clear()
        }
      }
    }
  }

  @objc public var size: NSDictionary = [:] {
    didSet {
      do {
        let json = try JSONSerialization.data(withJSONObject: size)
        dataSize = try JSONDecoder().decode(DataSize.self, from: json)
        if let first = firstColumnTable, let multi = multiColumnTable {
          first.dataCollectionView?.dataSize = dataSize
          multi.dataCollectionView?.dataSize = dataSize
          first.dataCollectionView?.totalCellsView?.totalRows = dataSize?.qcy ?? 0
        }
      } catch {
        print(error)
      }
    }
  }

  @objc public var theme: NSDictionary = [:] {
    didSet {
      do {
        let json = try JSONSerialization.data(withJSONObject: theme)
        tableTheme = try JSONDecoder().decode(TableTheme.self, from: json)
        applyThemeToExistingTables()
      } catch {
        print(error)
      }
    }
  }

  @objc public var cols: NSDictionary = [:] {
    didSet {
      do {
        let previousTotalsPosition = totals?.position
        let hadTotals = totals != nil
        let safeCols = sanitizeJSON(cols)
        let json = try JSONSerialization.data(withJSONObject: safeCols)
        let decodedCols = try JSONDecoder().decode(Cols.self, from: json)

        // If column count changed, this is a new table - wipe everything and wait for fresh data
        if dataColumns != nil && decodedCols.header?.count != dataColumns?.count {
          created = false
          for view in subviews {
            view.removeFromSuperview()
          }
          firstColumnTable = nil
          multiColumnTable = nil
          dataColumns = nil
          dataRows = nil
          totals = nil
          // Store new columns but don't build table yet - wait for rows
          dataColumns = decodedCols.header
          totals = decodedCols.totals
          return
        }

        // Normal column update for same table
        dataColumns = decodedCols.header
        totals = decodedCols.totals
        let hasTotals = totals != nil
        let totalsPositionChanged = previousTotalsPosition != totals?.position
        let totalsVisibilityChanged = hadTotals != hasTotals

        guard let firstTable = self.firstColumnTable else { return }
        guard let multiTable = self.multiColumnTable else { return }

        if let dataColumns = dataColumns {
          if totalsPositionChanged || totalsVisibilityChanged {
            rebuildTotalsLayout(
              firstTable: firstTable,
              multiTable: multiTable,
              dataColumns: dataColumns
            )
          } else if totals != nil, firstTable.totalView == nil, !isDataView {
            let factory = TableViewFactory(containerView: self,
                                    columnWidths: columnWidths,
                                    dataColumns: dataColumns,
                                    freezeFirstCol: freezeFirstColumn)
            factory.insertTotalsIntoExistingTable(for: firstTable, withRange: 0..<1, first: true)
            factory.insertTotalsIntoExistingTable(for: multiTable, withRange: 1..<columnWidths.count(), first: false)
          } else if let firstTotals = firstTable.totalView, let multiTotals = multiTable.totalView {
            firstTotals.resetTotals(totals)
            multiTotals.resetTotals(totals)
          }
        }

        if dataColumns != nil {
          if let firstHeader = firstTable.headerView, let multiHeader = multiTable.headerView {
            firstHeader.updateColumns(dataColumns!)
            multiHeader.updateColumns(dataColumns!)
          }
        }
      } catch {
        print(error)
      }
    }
  }

  private func rewireDataCollectionConstraints(for tableView: TableView, position: String?) {
    guard let dataCollectionView = tableView.dataCollectionView else { return }
    guard let headerView = tableView.headerView else { return }

    let existingConstraints = tableView.constraints.filter { c in
      (c.firstItem === dataCollectionView && (c.firstAttribute == .top || c.firstAttribute == .bottom)) ||
      (c.secondItem === dataCollectionView && (c.secondAttribute == .top || c.secondAttribute == .bottom))
    }
    NSLayoutConstraint.deactivate(existingConstraints)
    tableView.removeConstraints(existingConstraints)

    var constraints = [NSLayoutConstraint]()
    if let totalsView = tableView.totalView {
      if position == "bottom" {
        constraints.append(dataCollectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor))
        constraints.append(dataCollectionView.bottomAnchor.constraint(equalTo: totalsView.topAnchor))
      } else {
        constraints.append(dataCollectionView.topAnchor.constraint(equalTo: totalsView.bottomAnchor))
        constraints.append(dataCollectionView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor))
      }
    } else {
      constraints.append(dataCollectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor))
      constraints.append(dataCollectionView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor))
    }

    NSLayoutConstraint.activate(constraints)
    tableView.addConstraints(constraints)
  }

  private func rebuildTotalsLayout(firstTable: TableView, multiTable: TableView, dataColumns: [DataColumn]) {
    if let firstTotals = firstTable.totalView {
      firstTotals.removeFromSuperview()
      firstTable.totalView = nil
    }
    if let multiTotals = multiTable.totalView {
      multiTotals.removeFromSuperview()
      multiTable.totalView = nil
    }

    if totals != nil, !isDataView {
      let factory = TableViewFactory(containerView: self,
                              columnWidths: columnWidths,
                              dataColumns: dataColumns,
                              freezeFirstCol: freezeFirstColumn)
      factory.insertTotalsIntoExistingTable(for: firstTable, withRange: 0..<1, first: true)
      factory.insertTotalsIntoExistingTable(for: multiTable, withRange: 1..<columnWidths.count(), first: false)
    } else {
      rewireDataCollectionConstraints(for: firstTable, position: nil)
      rewireDataCollectionConstraints(for: multiTable, position: nil)
    }
  }

  @objc public var rows: NSDictionary = [:] {
    didSet {
      do {
        NotificationCenter.default.post(name: Notification.Name.onClearSelectionBand, object: nil)
        let json = try JSONSerialization.data(withJSONObject: rows)
        let decodedRows = try JSONDecoder().decode(RowsObject.self, from: json)

        // Validate row/column compatibility - if mismatch, just store rows and wait
        if self.dataRows != nil && dataRows?.count != 0 &&
           self.dataColumns != nil && dataRows?[0].cells.count != dataColumns?.count {
          dataRows = decodedRows.rows
          return
        }

        if dataRows == nil || (decodedRows.reset == true) {
          self.dataRows = decodedRows.rows

          if self.dataRows != nil {
            // If no table exists and we have both columns and rows, create it
            if self.firstColumnTable == nil && self.multiColumnTable == nil &&
               frame.size.width != 0 && dataColumns != nil {
              DispatchQueue.main.async { [weak self] in
                self?.layoutTable()
              }
              return
            }

            // Update existing tables
            if let firstColumnTable = self.firstColumnTable {
              firstColumnTable.dataCollectionView?.dataSize = dataSize
              firstColumnTable.dataCollectionView?.appendData(rows: dataRows!)
              firstColumnTable.dataCollectionView?.scrollToTop()
            }

            if let multiColumnTable = self.multiColumnTable {
              multiColumnTable.dataCollectionView?.dataSize = dataSize
              multiColumnTable.dataCollectionView?.appendData(rows: dataRows!)
              multiColumnTable.dataCollectionView?.scrollToTop()
            }
          }
        } else {
          // Append new rows to existing data
          if let newRows = decodedRows.rows {
            if dataRows != nil {
              dataRows?.append(contentsOf: newRows)
              if let firstColumnTable = self.firstColumnTable, let multiColumnTable = self.multiColumnTable {
                firstColumnTable.dataCollectionView?.appendData(rows: dataRows!)
                multiColumnTable.dataCollectionView?.appendData(rows: dataRows!)
              }
            }
          }
        }

        if decodedRows.reset == true && clearSelections != nil && clearSelections!.compare("yes") == .orderedSame {
          selectionsEngine.clear()
        }
      } catch {
        print(error)
      }
    }
  }

  @objc public var cellContentStyle: NSDictionary = [:] {
    didSet {
      do {
        let json = try JSONSerialization.data(withJSONObject: cellContentStyle)
        let decodedCellStyle = try JSONDecoder().decode(CellContentStyle.self, from: json)
        cellStyle = CellStyle(cellContentStyle: decodedCellStyle)
      } catch {
        print(error)
      }
    }
  }

  @objc public var headerContentStyle: NSDictionary = [:] {
    didSet {
      do {
        let json = try JSONSerialization.data(withJSONObject: headerContentStyle)
        let decodedHeaderStyle = try JSONDecoder().decode(HeaderContentStyle.self, from: json)
        headerStyle = HeaderStyle(headerContentSyle: decodedHeaderStyle)
      } catch {
        print(error)
      }
    }
  }

  @objc public var name: String? {
    didSet {
      columnWidths.key = name
    }
  }

  @objc public var translations: NSDictionary = [:] {
    didSet {
      do {
        let json = try JSONSerialization.data(withJSONObject: translations)
        let decodedTranslations = try JSONDecoder().decode(Translations.self, from: json)
        menuTranslations = decodedTranslations.menu
      } catch {
        print(error)
      }
    }
  }

  public override var bounds: CGRect {
    didSet {
     layoutTable()
    }
  }

  func layoutTable() {
    guard let dataColumns = dataColumns else {return}
    guard let dataRows = dataRows else {return}
    columnWidths.loadDefaultWidths(bounds, columnCount: dataColumns.count, dataRows: dataRows, dataCols: dataColumns)

    if !created {
      created = true
      let tableViewFactory = TableViewFactory(containerView: self,
                                              columnWidths: columnWidths,
                                              dataColumns: dataColumns,
                                              freezeFirstCol: freezeFirstColumn)
      tableViewFactory.create()
      firstColumnTable = tableViewFactory.firstColumnTableView
      multiColumnTable = tableViewFactory.multiColumnTableView
      setNeedsLayout()
      DispatchQueue.main.async {
        self.horizontalScrollView?.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
        self.firstColumnTable?.dataCollectionView?.postSignalVisibleRows(scrollsToTop: true)
        self.testTruncation()
        self.updateVScrollPos()
      }
    } else {
      guard let firstColumnTable = self.firstColumnTable else { return }
      guard let multiColumnTable = self.multiColumnTable else { return }
      hScrollViewDelegate.captureFirstColumnWidth()
      firstColumnTable.resizeCells()
      multiColumnTable.resizeCells()
      DispatchQueue.main.async {
        self.testTruncation()
        self.firstColumnTable?.dataCollectionView?.postSignalVisibleRows(scrollsToTop: true)
        self.updateVScrollPos()
      }
    }
  }

  func testTruncation() {
    let headerWrap = headerStyle?.headerContentStyle?.wrap ?? true
    let cellWrap = cellStyle?.cellContentStyle?.wrap ?? true
    if headerWrap {
      testHeaders()
    }
    if cellWrap {
      testCollectionViews()
    }
  }

  func testHeaders() {
    guard let firstHeader = firstColumnTable?.headerView else { return }
    guard let multiHeader = multiColumnTable?.headerView else { return }
    var headerLineCount = 0
    headerLineCount = max(firstHeader.getMaxLineCount(), headerLineCount)
    headerLineCount = max(multiHeader.getMaxLineCount(), headerLineCount)

    if headerLineCount != maxHeaderLineCount {
      maxHeaderLineCount = headerLineCount
      let height = Double(maxHeaderLineCount) * (headerStyle?.lineHeight ?? 1.0)
      firstHeader.dynamicHeightAnchor.constant = height + (PaddedLabel.PaddingSize * 2.0)
      multiHeader.dynamicHeightAnchor.constant = height + (PaddedLabel.PaddingSize * 2.0)
      firstColumnTable?.updateGrabbers(height + (PaddedLabel.PaddingSize * 2.0))
      multiColumnTable?.updateGrabbers(height + (PaddedLabel.PaddingSize * 2.0))
      firstHeader.layoutIfNeeded()
      multiHeader.layoutIfNeeded()
      layoutIfNeeded()
      DispatchQueue.main.async {
        self.firstColumnTable?.dataCollectionView?.postSignalVisibleRows(scrollsToTop: false)
      }
    }
    testTotals()
  }

  func testTotals() {
    guard let firstTotal = firstColumnTable?.totalView else { return }
    guard let multiTotal = multiColumnTable?.totalView else { return }
    var lineCount = 0
    lineCount = max(firstTotal.getMaxLineCount(), lineCount)
    lineCount = max(multiTotal.getMaxLineCount(), lineCount)

    // uses cellcontent style for style, but header.wrap for checking wrap
    if lineCount != maxTotalsLineCount {
      maxTotalsLineCount = lineCount
      let height = Double(maxTotalsLineCount) * (cellStyle?.lineHeight ?? 1.0)
      firstTotal.dynamicHeight.constant = height + (PaddedLabel.PaddingSize * 2.0)
      multiTotal.dynamicHeight.constant = height + (PaddedLabel.PaddingSize * 2.0)
      firstTotal.layoutIfNeeded()
      multiTotal.layoutIfNeeded()
      layoutIfNeeded()
      DispatchQueue.main.async {
        self.firstColumnTable?.dataCollectionView?.postSignalVisibleRows(scrollsToTop: false)
      }
    }
  }

  func testCollectionViews() {
    guard let first = firstColumnTable?.dataCollectionView else { return }
    guard let multi = multiColumnTable?.dataCollectionView else { return }
    var lineCount = 0
    lineCount = max(first.getMaxLineCount(), lineCount)
    lineCount = max(multi.getMaxLineCount(), lineCount)
    if lineCount != maxCollectionViewsLineCount {
      maxCollectionViewsLineCount = lineCount
      first.setMaxLineCount(maxCollectionViewsLineCount)
      multi.setMaxLineCount(maxCollectionViewsLineCount)
      DispatchQueue.main.async {
        self.firstColumnTable?.dataCollectionView?.postSignalVisibleRows(scrollsToTop: false)
      }
      layoutIfNeeded()
    }
  }

  func sanitizeJSON(_ object: Any) -> Any {
      if let dict = object as? [String: Any] {
          var newDict: [String: Any] = [:]
          for (key, value) in dict {
              newDict[key] = sanitizeJSON(value)
          }
          return newDict
      }
      if let array = object as? [Any] {
          return array.map { sanitizeJSON($0) }
      }

      if let number = object as? NSNumber {
          let doubleValue = number.doubleValue
          if doubleValue.isNaN || doubleValue.isInfinite {
              return NSNull()   // or return 0
          }
      }
      return object
  }

  private func resolvedTableBackgroundColor() -> UIColor {
    if isDataView {
      return .white
    }
    return ColorParser.fromCSS(cssString: tableTheme?.backgroundColor ?? "white")
  }

  private func applyThemeToExistingTables() {
    let apply = {
      let bg = self.resolvedTableBackgroundColor()
      self.backgroundColor = bg

      let tables = [self.firstColumnTable, self.multiColumnTable]
      for table in tables {
        table?.backgroundColor = bg
        table?.dataCollectionView?.tableTheme = self.tableTheme
        table?.dataCollectionView?.childCollectionView?.backgroundColor = bg
        table?.dataCollectionView?.childCollectionView?.reloadData()
      }
    }

    if Thread.isMainThread {
      apply()
    } else {
      DispatchQueue.main.async {
        apply()
      }
    }
  }

  public override func layoutSubviews() {
    super.layoutSubviews()

    // Ensure table is created on initial layout if we have data but no table views
    if !created && dataColumns != nil && dataRows != nil && bounds.size.width > 0 {
      layoutTable()
    }

    self.backgroundColor = isDataView ? .white : ColorParser.fromCSS(cssString: tableTheme?.backgroundColor ?? "white")
    firstColumnTable?.dataCollectionView?.childCollectionView?.setScrollableArea(self.frame)
    multiColumnTable?.dataCollectionView?.childCollectionView?.setScrollableArea(self.frame)
    updateVScrollPos()
  }

  func updateVScrollPos() {
    let totalWidth = columnWidths.getTotalWidth()
    let rawX = firstColumnTable?.horizontalScrolLView?.contentOffset.x ?? 0.0
    var right = max(abs(self.frame.width  -  totalWidth) - rawX, 0)
    if totalWidth < frame.width {
      right = 2.0
    }
    firstColumnTable?.dataCollectionView?.childCollectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: right)
    multiColumnTable?.dataCollectionView?.childCollectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: right)
  }

}
