import { DEFAULT_REVISION_ID } from "../constants";
import { removeFalsyAttributes } from "../helpers/misc";
import { _t } from "../translation";
import { WorkbookData } from "../types/workbook_data";
import {
  ImportedFiles,
  XLSXExternalBook,
  XLSXFileStructure,
  XLSXImageFile,
  XLSXImportData,
  XLSXWorksheet,
  XLSXXmlDocuments,
  XMLString,
} from "../types/xlsx";
import { CONTENT_TYPES } from "./constants";
import { convertNamedRanges } from "./conversion/named_ranges_conversion";
import { convertSheets } from "./conversion/sheet_conversion";
import { convertBorders, convertFormats, convertStyles } from "./conversion/style_conversion";
import { convertTables } from "./conversion/table_conversion";
import { XlsxExternalBookExtractor } from "./extraction/external_book_extractor";
import { XlsxMiscExtractor } from "./extraction/misc_extractor";
import { XlsxSheetExtractor } from "./extraction/sheet_extractor";
import { XlsxStyleExtractor } from "./extraction/style_extractor";
import { getXLSXFilesOfType } from "./helpers/xlsx_helper";
import { XLSXImportWarningManager } from "./helpers/xlsx_parser_error_manager";
import { escapeTagNamespaces, parseXML } from "./helpers/xml_helpers";

const EXCEL_IMPORT_VERSION = "18.4.2";

export class XlsxReader {
  warningManager: XLSXImportWarningManager;
  xmls: XLSXXmlDocuments;
  images: XLSXImageFile[];

  constructor(files: ImportedFiles) {
    this.warningManager = new XLSXImportWarningManager();

    this.xmls = {};
    this.images = [];
    for (const key of Object.keys(files)) {
      // Random files can be in xlsx (like a bin file for printer settings)
      if (key.endsWith(".xml") || key.endsWith(".rels")) {
        const contentString = escapeTagNamespaces(files[key] as string);

        this.xmls[key] = parseXML(new XMLString(contentString));
      } else if (key.includes("media/image")) {
        this.images.push({
          fileName: key,
          imageSrc: files[key]["imageSrc"],
        });
      }
    }
  }

  convertXlsx(): WorkbookData {
    const xlsxData = this.getXlsxData();
    return this.convertImportedData(xlsxData);
  }

  // ---------------------------------------------------------------------------
  // Parsing XMLs
  // ---------------------------------------------------------------------------

  private getXlsxData(): XLSXImportData {
    const xlsxFileStructure = this.buildXlsxFileStructure();

    const theme = xlsxFileStructure.theme
      ? new XlsxMiscExtractor(
          xlsxFileStructure.theme,
          xlsxFileStructure,
          this.warningManager
        ).getTheme()
      : undefined;

    const sharedStrings = xlsxFileStructure.sharedStrings
      ? new XlsxMiscExtractor(
          xlsxFileStructure.sharedStrings,
          xlsxFileStructure,
          this.warningManager
        ).getSharedStrings()
      : [];

    const namedRanges = new XlsxMiscExtractor(
      xlsxFileStructure.workbook,
      xlsxFileStructure,
      this.warningManager
    ).getNamedRanges();

    // Sort sheets by file name : the sheets will always be named sheet1.xml, sheet2.xml, ... in order
    const sheets = xlsxFileStructure.sheets
      .sort((a, b) => a.file.fileName.localeCompare(b.file.fileName, undefined, { numeric: true }))
      .map((sheetFile): XLSXWorksheet => {
        return new XlsxSheetExtractor(
          sheetFile,
          xlsxFileStructure,
          this.warningManager,
          theme
        ).getSheet();
      });

    const externalBooks = xlsxFileStructure.externalLinks.map(
      (externalLinkFile): XLSXExternalBook => {
        return new XlsxExternalBookExtractor(
          externalLinkFile,
          xlsxFileStructure,
          this.warningManager
        ).getExternalBook();
      }
    );

    const styleExtractor = new XlsxStyleExtractor(xlsxFileStructure, this.warningManager, theme);

    return {
      fonts: styleExtractor.getFonts(),
      fills: styleExtractor.getFills(),
      borders: styleExtractor.getBorders(),
      dxfs: styleExtractor.getDxfs(),
      numFmts: styleExtractor.getNumFormats(),
      styles: styleExtractor.getStyles(),
      sheets: sheets,
      sharedStrings,
      externalBooks,
      chartIds: [],
      imageIds: [],
      namedRanges,
    };
  }

  private buildXlsxFileStructure(): XLSXFileStructure {
    const xlsxFileStructure = {
      sheets: getXLSXFilesOfType(CONTENT_TYPES.sheet, this.xmls),
      workbook:
        getXLSXFilesOfType(CONTENT_TYPES.workbook, this.xmls)[0] ||
        getXLSXFilesOfType(CONTENT_TYPES.macroEnabledWorkbook, this.xmls)[0] ||
        getXLSXFilesOfType(CONTENT_TYPES.templateWorkbook, this.xmls)[0] ||
        getXLSXFilesOfType(CONTENT_TYPES.macroEnabledTemplateWorkbook, this.xmls)[0] ||
        getXLSXFilesOfType(CONTENT_TYPES.excelAddInWorkbook, this.xmls)[0],
      styles: getXLSXFilesOfType(CONTENT_TYPES.styles, this.xmls)[0],
      sharedStrings: getXLSXFilesOfType(CONTENT_TYPES.sharedStrings, this.xmls)[0],
      theme: getXLSXFilesOfType(CONTENT_TYPES.themes, this.xmls)[0],
      charts: getXLSXFilesOfType(CONTENT_TYPES.chart, this.xmls),
      figures: getXLSXFilesOfType(CONTENT_TYPES.drawing, this.xmls),
      tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),
      pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),
      externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),
      images: this.images,
    };

    if (!xlsxFileStructure.workbook.rels) {
      throw Error(_t("Cannot find workbook relations file"));
    }

    return xlsxFileStructure;
  }

  // ---------------------------------------------------------------------------
  // Conversion
  // ---------------------------------------------------------------------------

  convertImportedData(data: XLSXImportData): WorkbookData {
    const convertedData = {
      version: EXCEL_IMPORT_VERSION,
      sheets: convertSheets(data, this.warningManager),
      styles: convertStyles(data, this.warningManager),
      formats: convertFormats(data, this.warningManager),
      borders: convertBorders(data, this.warningManager),
      namedRanges: convertNamedRanges(data, this.warningManager),
      revisionId: DEFAULT_REVISION_ID,
    } as WorkbookData;

    convertTables(convertedData, data);

    // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging
    Object.keys(data.styles).map((key) => {
      data.styles[key] = removeFalsyAttributes(data.styles[key]);
    });

    return convertedData;
  }
}
