require 'epub/publication/package'

module EPUB
  module Publication
    class Package
      def to_xml(options={:encoding => 'UTF-8'})
        Nokogiri::XML::Builder.new(options) {|xml|
          attrs = {
            'version'           => '3.0',
            'xmlns'             => EPUB::NAMESPACES['opf'],
            'unique-identifier' => unique_identifier.id
          }
          [
           ['dir', dir],
           ['id', id],
           ['xml:lang', xml_lang],
           ['prefix', prefix.reduce('') {|attr, (pfx, iri)| [attr, [pfx, iri].join(':')].join(' ')}]
          ].each do |(name, value)|
            next if value.nil? or value.empty?
            attrs[name] = value
          end
          xml.package_(attrs) do
            (EPUB::Publication::Package::CONTENT_MODELS - [:bindings, :guide]).each do |model|
              __send__(model).to_xml_fragment xml
            end
            if bindings and !bindings.media_types.empty?
              bindings.to_xml_fragment xml
            end
          end
        }.to_xml
      end

      def make
        (CONTENT_MODELS - [:bindings, :guide]).each do |model|
          klass = self.class.const_get(model.to_s.capitalize)
          obj = klass.new
          __send__ "#{model}=", obj
        end
        yield self if block_given?
        self
      end

      def edit
        yield self if block_given?
        save
      end

      def make_metadata
        self.metadata = Metadata.new
        metadata.make do
          yield metadata if block_given?
        end
        metadata
      end

      def make_manifest
        self.manifest = Manifest.new
        manifest.make do
          yield manifest if block_given?
        end
        manifest
      end

      def make_spine
        self.spine = Spine.new
        spine.make do
          yield spine if block_given?
        end
        spine
      end

      def make_bindings
        self.bindings = Bindings.new
        bindings.make do
          yield bindings if block_given?
        end
        bindings
      end

      def save
        book.container_adapter.write book.epub_file, book.rootfile_path, to_xml
      end

      module ContentModel
        # @param [Nokogiri::XML::Builder::NodeBuilder] node
        # @param [Object] model
        # @param [Array<Symbol|String>] attributes names of attribute.
        def to_xml_attribute(node, model, attributes)
          attributes.each do |attr|
            val = model.__send__(attr)
            node[attr.to_s.gsub('_', '-')] = val if val
          end
        end
      end

      class Metadata
        include ContentModel

        def make
          yield self if block_given?
          unless unique_identifier
            if identifiers.empty?
              identifier = DCMES.new
              identifier.id = 'pub-id'
              identifier.content = UUID.create.to_s
              self.dc_identifiers << identifier
              self.unique_identifier = identifier
            else
              self.unique_identifier = identifiers.first
            end
          end

          unless metas.any? {|meta| meta.property == 'dcterms:modified'}
            modified = Meta.new
            modified.property = 'dcterms:modified'
            # modified.content = Time.now.utc.strftime('%FT%TZ')
            modified.content = Time.now.utc.iso8601
            self.metas << modified
          end

          self
        end

        def to_xml_fragment(xml)
          xml.metadata_('xmlns:dc' => EPUB::NAMESPACES['dc']) {
            (DC_ELEMS - [:languages]).each do |elems|
              singular = elems[0..-2]
              singular += 's' if elems == :rights
              singular += '_'
              __send__("dc_#{elems}").each do |elem|
                node = xml['dc'].__send__(singular, elem.content)
                to_xml_attribute node, elem, [:id, :dir]
                node['xml:lang'] = elem.lang if elem.lang
              end
            end

            languages.each do |language|
              xml['dc'].language language.content
            end

            metas.each do |meta|
              next unless meta.valid? # TODO: Consider whther to drop or keep as is
              node = xml.meta(meta.content)
              to_xml_attribute node, meta, [:property, :id, :scheme]
              node['refines'] = "##{meta.refines.id}" if meta.refines
            end

            links.each do |link|
              node = xml.link
              to_xml_attribute node, link, [:href, :id, :media_type]
              node['rel'] = link.rel.to_a.join(' ') if link.rel
              node['refines'] = "##{link.refines.id}" if link.refines
            end
          }
        end

        # Shortcut to set title from String
        # @param title [String]
        def title=(title)
          t = Title.new
          t.content = title
          self.dc_titles = [t]
          title
        end

        # Shortcut to set language from String
        # @param lang_code [String]
        def language=(lang_code)
          lang = DCMES.new
          lang.content = lang_code
          self.dc_languages = [lang]
          lang_code
        end

        # Shortcut to set one creator from String
        # @param name [String]
        def creator=(name)
          creator = DCMES.new
          creator.content = name
          self.dc_creators = [creator]
          name
        end

        class Meta
          def valid?
            property
          end
        end
      end

      class Manifest
        include ContentModel

        def make
          yield self if block_given?

          # @todo more careful
          unless items.any? &:nav?
            nav = Item.new
            nav.id = 'toc'
            nav.href = Addressable::URI.parse('nav.xhtml')
            nav.media_type = 'application/xhtml+xml'
            nav.properties << 'nav'

            nav_doc = ContentDocument::Navigation.new
            nav_doc.item = nav

            nav_nav = ContentDocument::Navigation::Navigation.new
            nav_nav.type = ContentDocument::Navigation::Navigation::Type::TOC
            nav_nav.items = items.select(&:xhtml?).map {|item; nav|
              nav = ContentDocument::Navigation::Item.new
              nav.item = item
              nav.href = item.href
              nav.text = File.basename(item.href.normalize.request_uri, '.*')
              nav
            }
            nav_doc.navigations << nav_nav

            nav.content = nav_doc.to_xml

            self << nav
          end

          self
        end

        # @return [Item]
        def make_item(options={})
          item = Item.new
          [:id, :href, :media_type, :properties, :media_overlay].each do |attr|
            next unless options.key? attr
            item.__send__ "#{attr}=", options[attr]
          end
          item.manifest = self
          yield item if block_given?
          self << item
          item
        end

        def to_xml_fragment(xml)
          node = xml.manifest_ {
            items.each do |item|
              item_node = xml.item_
              to_xml_attribute item_node, item, [:id, :href, :media_type, :media_overlay]
              item_node['properties'] = item.properties.to_a.join(' ') unless item.properties.empty?
              item_node['fallback'] = item.fallback.id if item.fallback
            end
          }
          to_xml_attribute node, self, [:id]
        end

        class Item
          attr_accessor :content, :content_file

          # @raise StandardError when no content nor content_file
          # @todo Don't read content from file when +content_file+ exists. If container adapter is Archive::Zip, it writes content to file twice.
          def save
            content_to_save =
              if content
                content
              elsif content_file
                File.read(content_file)
              else
                raise 'no content nor content_file'
              end
            book = manifest.package.book
            book.container_adapter.write book.epub_file, entry_name, content_to_save
          end

          # Save document into EPUB archive when block ended
          def edit
            yield if block_given?
            save
          end

          # Save document into EPUB archive at end of block
          # @yield [REXML::Document]
          def edit_with_rexml
            require 'rexml/document'
            doc = REXML::Document.new(read)
            yield doc if block_given?
            self.content = doc.to_s
            save
          end

          # Save document into EPUB archive at end of block
          # @yield [Nokgiri::XML::Document]
          def edit_with_nokogiri
            doc = Nokogiri.XML(read)
            yield doc if block_given?
            self.content = doc.to_xml
            save
          end
        end
      end

      class Spine
        include ContentModel

        def make
          yield self if block_given?
          self
        end

        def make_itemref
          itemref = Itemref.new
          self << itemref
          yield itemref if block_given?
          itemref
        end

        def to_xml_fragment(xml)
          node = xml.spine_ {
            itemrefs.each do |itemref|
              itemref_node = xml.itemref
              to_xml_attribute itemref_node, itemref, [:idref, :id]
              itemref_node['linear'] = 'no' unless itemref.linear?
              itemref_node['properties'] = itemref.properties.join(' ') unless itemref.properties.empty?
            end
          }
          to_xml_attribute node, self, [:id, :toc, :page_progression_direction]
        end
      end

      class Bindings
        include ContentModel

        def make
          yield self if block_given?
          self
        end

        def make_media_type
          media_type = MediaType.new
          self << media_type
          yield media_type if block_given?
          media_type
        end

        def to_xml_fragment(xml)
          xml.bindings_ {
            media_types.each do |media_type|
              media_type_node = xml.mediaType
              to_xml_attribute media_type_node, media_type, [:media_type]
              media_type_node['handler'] = media_type.handler.id if media_type.handler && media_type.handler.id
            end
          }
        end
      end
    end
  end
end