import 'dart:convert';

import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:xml/xml.dart';
import 'package:docx/docx.dart';

class DocxEditor {

  File? fileInDocx;
  Uint8List? binaryInDocx;
  String? pathOutDocx;
  String? name;
  Map<String, ReplaceContent> replaceMap;

  /// [fileInDocx] The Docx file
  /// [binaryInDocx] The Docx binary (for web generaly)
  /// [pathOutDocx] The path where new file is save (optional)
  /// [replaceMap] All text and image to change (required)
  DocxEditor({
    this.fileInDocx,
    this.binaryInDocx,
    this.name,
    this.pathOutDocx,
    required this.replaceMap
  }) {

    if (fileInDocx != null) {
      if (!fileInDocx!.existsSync())
        throw(PathNotFoundException(fileInDocx!.path, OSError('File not found', 404)));
      binaryInDocx = fileInDocx!.readAsBytesSync();
    }
    else if (binaryInDocx == null)
      throw(ArgumentError('file or binary is mandatory to be define'));

    List<XmlDocument> documentsXml = editById(replaceMap);
    save(documentsXml, this);
  }

  /// This function search ID in [replaceMap] and replace by his value or by a image
  List<XmlDocument> editById(Map<String, ReplaceContent> replaceMap) {
    Archive archive = ZipDecoder().decodeBytes(binaryInDocx!);
    ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/document.xml');
    XmlDocument documentXml = XmlDocument.parse(utf8.decode(archiveFile.content as List<int>));

    XmlDocument documentXmlRels = addImageinArchive(archive, replaceMap);

    // sort descending orderz
    final keys = replaceMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length));

    final pattern = RegExp(
      keys.map((key) => '(?<=^|\\s)${RegExp.escape(key)}(?=\\s|\$)').join('|'),
    );

    // replace image
    for (XmlElement node in documentXml.findAllElements('w:t')) {
      String ndText = node.innerText.toLowerCase();
      if (ndText.isEmpty)
        continue;
      XmlElement? parent;
      for (String key in replaceMap.keys) {
        ReplaceContent replaceCnt = replaceMap[key]!;
        if (replaceCnt.img == null)
          continue;
        // ignore: unused_local_variable
        for (RegExpMatch regMatch in RegExp('(?<=^|\\s)${key.toLowerCase()}(?=\\s|\$)').allMatches(ndText))
          parent = putImage(node.parentElement, replaceCnt, parent);
      }
    } 

    // Only replace text!
    for (XmlElement paragraph in documentXml.findAllElements('w:p')) {
      List<XmlElement> texts = paragraph.findAllElements('w:t').toList();
      if (texts.isEmpty)
        continue;

      String original = texts.map((t) {
        return t.innerText;
      }).join();

      String replaced = original.replaceAllMapped(pattern, (match) {
        ReplaceContent rc = replaceMap[match[0]]!;
        return rc.img == null ? rc.value! : '';
      });

      if (original == replaced)
        continue;

      final firstRun = paragraph.findElements('w:r').first;
      final newRun = firstRun.copy();
      newRun.findAllElements('w:t').first.innerText = replaced; // Update with new text

      // replace the first w:r with the new
      firstRun.replace(newRun);
    }

      return [documentXml, documentXmlRels];
  }

  // TODO: Find a solution to put multiple image in only one <w:r>
  /// Replaces the text ([node]) with an image ([replaceContent])
  XmlElement? putImage(XmlElement? node, ReplaceContent replaceContent, XmlElement? newParent) {

    if (node!.parentElement == null && newParent != null)
      node.attachParent(newParent);
    XmlElement? parent = node.parentElement;
    String nodeStr = node.toXmlString();
    node.replace(XmlDocumentFragment.parse("""
      $nodeStr
      <w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
          xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
          xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
          xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
          xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
        <w:drawing>
          <wp:inline distT="0" distB="0" distL="0" distR="0">
            <wp:extent cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
            <wp:docPr id="1" name="Image1"/>
            <wp:cNvGraphicFramePr/>
            <a:graphic>
              <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
                <pic:pic>
                  <pic:blipFill>
                    <a:blip r:embed="rId${replaceContent.img!.id}"/>
                    <a:stretch>
                      <a:fillRect/>
                    </a:stretch>
                  </pic:blipFill>
                  <pic:spPr>
                    <a:xfrm>
                      <a:off x="0" y="0"/>
                      <a:ext cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
                    </a:xfrm>
                    <a:prstGeom prst="rect">
                      <a:avLst/>
                    </a:prstGeom>
                  </pic:spPr>
                </pic:pic>
              </a:graphicData>
            </a:graphic>
          </wp:inline>
        </w:drawing>
      </w:r>
      """));
    return parent;
  }

  /// Add all image in [replaceMap] to Archive in 'word/_rels/document.xml.rels'
  XmlDocument addImageinArchive(Archive archive, Map<String, ReplaceContent> replaceMap) {

    ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels');
    XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List<int>));
    String documentXmlStr = documentXmlRels.toXmlString();
    RegExp exp = RegExp(r'Id="rId([0-9]+)"');
    Iterable<RegExpMatch> matches = exp.allMatches(documentXmlStr);
    int maxId = matches.isEmpty ? 1 : int.parse(matches.last.group(1)!);

    for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) {
      replaceCnt.img!.id = ++maxId;
      String text = '<Relationship Id="rId$maxId" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${replaceCnt.img!.name}"/>\n';
      int lastIndex = documentXmlStr.lastIndexOf('/>') + 3;
      documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text);
    }
    documentXmlRels = XmlDocument.parse(documentXmlStr);
    return documentXmlRels;
  }

}
