use utf8;
package Document::OOXML;
use Moose;
use namespace::autoclean;

# ABSTRACT: Manipulation of Office Open XML files

use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Carp;
use List::Util qw(any);
use XML::LibXML;

use Document::OOXML::ContentTypes;
use Document::OOXML::Part::WordprocessingML;
use Document::OOXML::Document::Wordprocessor;


my $RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';

my %ROOT_PART_REL_TYPES = (
    transitionalDocument => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
    strictDocument       => 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument',
);

my %PART_TYPES = (
    WordprocessingML => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
);


sub read_document {
    my $class = shift;
    my $filename = shift;

    my $zip = Archive::Zip->new();

    my $zip_status = $zip->read($filename);
    croak("Cannot read: $zip_status") unless $zip_status == AZ_OK;

    my $content_types = do {
        my $ct_xml = $zip->contents('[Content_Types].xml')
            or croak("No member named '/[Content_Types].xml'. Is it OOXML?");

        Document::OOXML::ContentTypes->new_from_xml($ct_xml);
    };

    my $base_rels = $zip->contents('_rels/.rels')
        or croak("No member named '_rels/.rels' in document. Is it OOXML?");

    my $xml = XML::LibXML->load_xml( string => $base_rels );
    my $xpc = XML::LibXML::XPathContext->new();
    $xpc->registerNs('r' => $RELATIONSHIPS_NS);

    # The "old"/transitional XML uses schemas.openxmlformats.org
    # "New"/ISO standard/strict XML uses purl.oclc.org/ooxml
    my ($document_part_relation) = $xpc->findnodes(
        qq{
            /r:Relationships/r:Relationship[
                \@Type='$ROOT_PART_REL_TYPES{transitionalDocument}'
                or
                \@Type='$ROOT_PART_REL_TYPES{strictDocument}'
            ]
        },
        $xml->documentElement,
    );

    my $id        = $xpc->findvalue('@Id', $document_part_relation);
    my $type      = $xpc->findvalue('@Type', $document_part_relation);
    my $part_name = $xpc->findvalue('@Target', $document_part_relation);

    my $strict;
    if ($type eq $ROOT_PART_REL_TYPES{strictDocument}) {
        $strict = 1;
    } else {
        $strict = 0;
    }

    my $part_contents = $zip->contents($part_name)
        or croak("No member named '$part_name' in document. Is it OOXML?");

    my $doc_part = _parse_part(
        content_type  => $content_types->get_content_type_for_part("/$part_name"),
        contents      => $part_contents,
        part_name     => $part_name,
        is_strict     => $strict,
    );

    my $document_class;
    if ($doc_part->isa('Document::OOXML::Part::WordprocessingML')) {
        $document_class = 'Document::OOXML::Document::Wordprocessor';
    }
    else {
        croak("Unsupported document type");
    }

    my $ooxml = $document_class->new(
        content_types => $content_types,
        filename      => $filename,
        source        => $zip,
        is_strict     => $strict,
    );

    # Parts have weak references to the document they're in, so they don't
    # create reference loops.
    #
    # They can use this reference to find or add other parts (images,
    # headers, footers, etc.) referenced by the main document.
    $doc_part->document($ooxml);
    $ooxml->set_document_part($doc_part);

    return $ooxml;
}

sub _parse_part {
    my %args =  @_;

    if ($args{content_type} eq $PART_TYPES{WordprocessingML}) {
        return Document::OOXML::Part::WordprocessingML->new_from_xml(
            $args{part_name},
            $args{contents},
            $args{is_strict} ? 1 : 0,
        );
    }

    croak("Unknown part of type '$args{content_type}'");
}

__PACKAGE__->meta->make_immutable;

__END__

=pod

=encoding UTF-8

=head1 NAME

Document::OOXML - Manipulation of Office Open XML files

=head1 VERSION

version 0.172600

=head1 SYNOPSIS

    my $doc = Document::OOXML->read_document('some.docx');

    $doc->search_and_replace(
        qr/examples?/,
        sub { return reverse(shift); }
    );

=head1 DESCRIPTION

This module provides a way to open, modify and save Office Open XML files
(also known as OOXML or Microsoft Office XML).

=head1 METHODS

=head2 read_document($filename)

Opens the file named C<$filename> and parses it.

If the file doesn't appear to be a valid package, it will croak.

Returns an instance of a subclass of L<Document::OOXML::Document> that can
be used to manipulate the contents of the document:

=over

=item * L<Document::OOXML::Document::Wordprocessor>

=back

=head1 SEE ALSO

The format of Office Open XML files is described in the
L<ISO/IEC 29500|https://www.iso.org/standard/71691.html> and
L<ECMA-376|https://www.ecma-international.org/publications/standards/Ecma-376.htm>
standards.

=head1 AUTHOR

Martijn van de Streek <martijn@vandestreek.net>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Martijn van de Streek.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
