import 'dart:convert';

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

class DocxEditor {

  File fileDocx;

  /// [fileDocx] The Docx file...
  /// [replaceMap] All text and image to change
  DocxEditor(this.fileDocx, Map<String, ReplaceContent> replaceMap) {
    if (!fileDocx.existsSync())
      throw(PathNotFoundException(fileDocx.path, OSError('File not found', 404)));

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

  /// This function search ID in [replaceMap] and replace by his value or by a image
  List<XmlDocument> editById(Map<String, ReplaceContent> replaceMap) {
    Uint8List content = fileDocx.readAsBytesSync();

    Archive archive = ZipDecoder().decodeBytes(content);
    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);

    print(documentXml.toXmlString(pretty: true, indent: '    '));

    // TODO: Rework this function, doesn't work with ambiguous ID to search, or multiple Id in one paragraphe, separator like '_' or '-' or space
    // TODO: Essayer de parcourir par rapport au paragraphe (<w:p>) et non les runs (<w:r>)
    for (XmlElement node in documentXml.findAllElements('w:r')) {
      String ndText = node.innerText.toLowerCase();
      //Iterable<String> replacesId = replaceMap.keys.where((rm) => rm.toLowerCase() == ndText);
      for (String key in replaceMap.keys) {
        Iterable<String> replacesId = RegExp(key.toLowerCase()).allMatches(ndText).map((m) => m.group(0)!);
        XmlNode? parent;
        for (int i = 0; i < replacesId.length; i++) {
          ReplaceContent replaceCnt = replaceMap[key]!;
          if (replaceCnt.img != null)
            parent = putImage(node, replaceCnt, parent);
          else { /// Replace Text by other text
            int start = node.innerXml.indexOf('<w:t>');
            int end = node.innerXml.indexOf('</w:t>');
            node.innerXml = node.innerXml.replaceRange(start + 5, end, replaceCnt.value!);

          }
        }
      }
    }

    return [documentXml, documentXmlRels];
  }

  /// 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!.file.path}"/>\n';
      int lastIndex = documentXmlStr.lastIndexOf('/>') + 3;
      documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text);
    }
    documentXmlRels = XmlDocument.parse(documentXmlStr);
    return documentXmlRels;
  }

  /// Replaces the text ([node]) with an image ([replaceContent])
  XmlNode? putImage(XmlElement node, ReplaceContent replaceContent, XmlNode? newParent) {

    if (node.parent == null && newParent != null)
      node.attachParent(newParent);
    XmlNode? parent = node.parent;
    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;
  }

  /// Recreate a Archive to add modification and save it on [fileDocx]
  /// [documentsXml] is list -> ['word/document.xml', 'word/_rels/document.xml.rels']
  /// [replaceMap] for upload file image in docx file
  void save(List<XmlDocument> documentsXml, Map<String, ReplaceContent> replaceMap) {
    try {
      final originalBytes = fileDocx.readAsBytesSync();
      final archive = ZipDecoder().decodeBytes(originalBytes);

      final Uint8List uListDocumentXml = utf8.encode(documentsXml[0].toXmlString());
      final Uint8List uListDocumentXmlRels = utf8.encode(documentsXml[1].toXmlString());

      final newArchive = Archive();

      for (ArchiveFile file in archive.files) {
        if (file.name == 'word/document.xml') {
          newArchive.addFile(ArchiveFile(
            'word/document.xml',
            uListDocumentXml.length,
            uListDocumentXml,
          ));
        }
        else if (file.name == 'word/_rels/document.xml.rels') {
          newArchive.addFile(ArchiveFile(
            'word/_rels/document.xml.rels',
            uListDocumentXmlRels.length,
            uListDocumentXmlRels,
          ));
        } else {
          newArchive.addFile(file);
        }
      }
      for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) {
        final imageBytes = replaceCnt.img!.file.readAsBytesSync();
        newArchive.addFile(
          ArchiveFile(
            'word/media/${replaceCnt.img!.file.path}',
            imageBytes.length,
            imageBytes,
          )
        );
      }

      final zipData = ZipEncoder().encode(newArchive);
      fileDocx.writeAsBytesSync(zipData);
    } catch (e) {
      throw StateError('DOCX BUILDER ERROR: $e');
    }
  }
}