# frozen_string_literal: true

require "relaton_bib/typed_uri"
require "relaton_bib/document_identifier"
require "relaton_bib/copyright_association"
require "relaton_bib/formatted_string"
require "relaton_bib/contribution_info"
require "relaton_bib/bibliographic_date"
require "relaton_bib/series"
require "relaton_bib/document_status"
require "relaton_bib/organization"
require "relaton_bib/document_relation_collection"
require "relaton_bib/typed_title_string"
require "relaton_bib/formatted_ref"
require "relaton_bib/medium"
require "relaton_bib/classification"
require "relaton_bib/validity"
require "relaton_bib/document_relation"
require "relaton_bib/bib_item_locality"
require "relaton_bib/xml_parser"
require "relaton_bib/bibtex_parser"
require "relaton_bib/biblio_note"
require "relaton_bib/biblio_version"
require "relaton_bib/workers_pool"
require "relaton_bib/hash_converter"
require "relaton_bib/place"
require "relaton_bib/structured_identifier"
require "relaton_bib/editorial_group"
require "relaton_bib/ics"

module RelatonBib
  # Bibliographic item
  class BibliographicItem
    include RelatonBib

    TYPES = %W[article book booklet conference manual proceedings presentation
               thesis techreport standard unpublished map electronic\sresource
               audiovisual film video broadcast graphic_work music patent
               inbook incollection inproceedings journal].freeze

    # @return [TrueClass, FalseClass, NilClass]
    attr_accessor :all_parts

    # @return [String, NilClass]
    attr_reader :id, :type, :docnumber, :edition, :doctype

    # @!attribute [r] title
    # @return [Array<RelatonBib::TypedTitleString>]

    # @return [Array<RelatonBib::TypedUri>]
    attr_reader :link

    # @return [Array<RelatonBib::DocumentIdentifier>]
    attr_reader :docidentifier

    # @return [Array<RelatonBib::BibliographicDate>]
    attr_accessor :date

    # @return [Array<RelatonBib::ContributionInfo>]
    attr_reader :contributor

    # @return [RelatonBib::BibliongraphicItem::Version, NilClass]
    attr_reader :version

    # @return [Array<RelatonBib::BiblioNote>]
    attr_reader :biblionote

    # @return [Array<String>] language Iso639 code
    attr_reader :language

    # @return [Array<String>] script Iso15924 code
    attr_reader :script

    # @return [RelatonBib::FormattedRef, NilClass]
    attr_reader :formattedref

    # @!attribute [r] abstract
    #   @return [Array<RelatonBib::FormattedString>]

    # @return [RelatonBib::DocumentStatus, NilClass]
    attr_reader :status

    # @return [Array<RelatonBib::CopyrightAssociation>]
    attr_reader :copyright

    # @return [RelatonBib::DocRelationCollection]
    attr_reader :relation

    # @return [Array<RelatonBib::Series>]
    attr_reader :series

    # @return [RelatonBib::Medium, NilClass]
    attr_reader :medium

    # @return [Array<RelatonBib::Place>]
    attr_reader :place

    # @return [Array<RelatonBib::BibItemLocality>]
    attr_reader :extent

    # @return [Array<Strig>]
    attr_reader :accesslocation, :license

    # @return [Array<Relaton::Classification>]
    attr_reader :classification

    # @return [RelatonBib:Validity, NilClass]
    attr_reader :validity

    # @return [Date]
    attr_reader :fetched

    # @return [Array<RelatonBib::LocalizedString>]
    attr_reader :keyword

    # @return [RelatonBib::EditorialGroup, nil]
    attr_reader :editorialgroup

    # @return [Array<RelatonBib:ICS>]
    attr_reader :ics

    # @return [RelatonBib::StructuredIdentifierCollection]
    attr_reader :structuredidentifier

    # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
    # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

    # @param id [String, NilClass]
    # @param title [Array<RelatonBib::TypedTitleString>]
    # @param formattedref [RelatonBib::FormattedRef, NilClass]
    # @param type [String, NilClass]
    # @param docid [Array<RelatonBib::DocumentIdentifier>]
    # @param docnumber [String, NilClass]
    # @param language [Arra<String>]
    # @param script [Array<String>]
    # @param docstatus [RelatonBib::DocumentStatus, NilClass]
    # @param edition [String, NilClass]
    # @param version [RelatonBib::BibliographicItem::Version, NilClass]
    # @param biblionote [Array<RelatonBib::BiblioNote>]
    # @param series [Array<RelatonBib::Series>]
    # @param medium [RelatonBib::Medium, NilClas]
    # @param place [Array<String, RelatonBib::Place>]
    # @param extent [Array<Relaton::BibItemLocality>]
    # @param accesslocation [Array<String>]
    # @param classification [Array<RelatonBib::Classification>]
    # @param validity [RelatonBib:Validity, NilClass]
    # @param fetched [Date, NilClass] default nil
    # @param keyword [Array<String>]
    # @param doctype [String]
    # @param editorialgroup [RelatonBib::EditorialGroup, nil]
    # @param ics [Array<RelatonBib::ICS>]
    # @param structuredidentifier [RelatonBib::StructuredIdentifierCollection]
    #
    # @param copyright [Array<Hash, RelatonBib::CopyrightAssociation>]
    # @option copyright [Array<Hash, RelatonBib::ContributionInfo>] :owner
    # @option copyright [String] :from
    # @option copyright [String, NilClass] :to
    # @option copyright [String, NilClass] :scope
    #
    # @param date [Array<Hash>]
    # @option date [String] :type
    # @option date [String] :from
    # @option date [String] :to
    #
    # @param contributor [Array<Hash>]
    # @option contributor [RealtonBib::Organization, RelatonBib::Person]
    # @option contributor [String] :type
    # @option contributor [String] :from
    # @option contributor [String] :to
    # @option contributor [String] :abbreviation
    # @option contributor [Array<Array<String,Array<String>>>] :role
    #
    # @param abstract [Array<Hash, RelatonBib::FormattedString>]
    # @option abstract [String] :content
    # @option abstract [String] :language
    # @option abstract [String] :script
    # @option abstract [String] :type
    #
    # @param relation [Array<Hash>]
    # @option relation [String] :type
    # @option relation [RelatonBib::BibliographicItem,
    #                   RelatonIso::IsoBibliographicItem] :bibitem
    # @option relation [Array<RelatonBib::Locality,
    #                   RelatonBib::LocalityStack>] :locality
    # @option relation [Array<RelatonBib::SourceLocality,
    #                   RelatonBib::SourceLocalityStack>] :source_locality
    #
    # @param link [Array<Hash, RelatonBib::TypedUri>]
    # @option link [String] :type
    # @option link [String] :content
    def initialize(**args)
      if args[:type] && !TYPES.include?(args[:type])
        warn %{[relaton-bib] document type "#{args[:type]}" is invalid.}
      end

      @title = (args[:title] || []).map do |t|
        t.is_a?(Hash) ? TypedTitleString.new(t) : t
      end

      @date = (args[:date] || []).map do |d|
        d.is_a?(Hash) ? BibliographicDate.new(d) : d
      end

      @contributor = (args[:contributor] || []).map do |c|
        if c.is_a? Hash
          e = c[:entity].is_a?(Hash) ? Organization.new(c[:entity]) : c[:entity]
          ContributionInfo.new(entity: e, role: c[:role])
        else c
        end
      end

      @abstract = (args[:abstract] || []).map do |a|
        a.is_a?(Hash) ? FormattedString.new(a) : a
      end

      @copyright = args.fetch(:copyright, []).map do |c|
        c.is_a?(Hash) ? CopyrightAssociation.new(c) : c
      end

      @docidentifier  = args[:docid] || []
      @formattedref   = args[:formattedref] if title.empty?
      @id             = args[:id] || makeid(nil, false)
      @type           = args[:type]
      @docnumber      = args[:docnumber]
      @edition        = args[:edition]
      @version        = args[:version]
      @biblionote     = args.fetch :biblionote, []
      @language       = args.fetch :language, []
      @script         = args.fetch :script, []
      @status         = args[:docstatus]
      @relation       = DocRelationCollection.new(args[:relation] || [])
      @link           = args.fetch(:link, []).map do |s|
        if s.is_a?(Hash) then TypedUri.new(s)
        elsif s.is_a?(String) then TypedUri.new(content: s)
        else s
        end
      end
      @series         = args.fetch :series, []
      @medium         = args[:medium]
      @place          = args.fetch(:place, []).map { |pl| pl.is_a?(String) ? Place.new(name: pl) : pl }
      @extent         = args[:extent] || []
      @accesslocation = args.fetch :accesslocation, []
      @classification = args.fetch :classification, []
      @validity       = args[:validity]
      @fetched        = args.fetch :fetched, nil # , Date.today # we should pass the fetched arg from scrappers
      @keyword        = (args[:keyword] || []).map { |kw| LocalizedString.new(kw) }
      @license        = args.fetch :license, []
      @doctype        = args[:doctype]
      @editorialgroup = args[:editorialgroup]
      @ics            = args.fetch :ics, []
      @structuredidentifier = args[:structuredidentifier]
    end
    # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
    # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

    # @param lang [String] language code Iso639
    # @return [RelatonBib::FormattedString, Array<RelatonBib::FormattedString>]
    def abstract(lang: nil)
      if lang
        @abstract.detect { |a| a.language.include? lang }
      else
        @abstract
      end
    end

    def makeid(id, attribute)
      return nil if attribute && !@id_attribute

      id ||= @docidentifier.reject { |i| i.type == "DOI" }[0]
      return unless id

      # contribs = publishers.map { |p| p&.entity&.abbreviation }.join '/'
      # idstr = "#{contribs}#{delim}#{id.project_number}"
      # idstr = id.project_number.to_s
      idstr = id.id.gsub(/:/, "-").gsub /\s/, ""
      # if id.part_number&.size&.positive? then idstr += "-#{id.part_number}"
      idstr.strip
    end

    # @return [String]
    def shortref(identifier, **opts)
      pubdate = date.select { |d| d.type == "published" }
      year = if opts[:no_year] || pubdate.empty? then ""
             else ":" + pubdate&.first&.on&.year.to_s
             end
      year += ": All Parts" if opts[:all_parts] || @all_parts

      "#{makeid(identifier, false)}#{year}"
    end

    # @param builder [Nokogiri::XML::Builder, NillClass] (nil)
    # @return [String]
    def to_xml(builder = nil, **opts, &block)
      if builder
        render_xml builder, **opts, &block
      else
        Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
          render_xml xml, **opts, &block
        end.doc.root.to_xml
      end
    end

    # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

    # @return [Hash]
    def to_hash
      hash = {}
      hash["id"] = id if id
      hash["title"] = single_element_array(title) if title&.any?
      hash["link"] = single_element_array(link) if link&.any?
      hash["type"] = type if type
      hash["docid"] = single_element_array(docidentifier) if docidentifier&.any?
      hash["docnumber"] = docnumber if docnumber
      hash["date"] = single_element_array(date) if date&.any?
      hash["contributor"] = single_element_array(contributor) if contributor&.any?
      hash["edition"] = edition if edition
      hash["version"] = version.to_hash if version
      hash["revdate"] = revdate if revdate
      hash["biblionote"] = single_element_array(biblionote) if biblionote&.any?
      hash["language"] = single_element_array(language) if language&.any?
      hash["script"] = single_element_array(script) if script&.any?
      hash["formattedref"] = formattedref.to_hash if formattedref
      hash["abstract"] = single_element_array(abstract) if abstract&.any?
      hash["docstatus"] = status.to_hash if status
      hash["copyright"] = single_element_array(copyright) if copyright&.any?
      hash["relation"] = single_element_array(relation) if relation&.any?
      hash["series"] = single_element_array(series) if series&.any?
      hash["medium"] = medium.to_hash if medium
      hash["place"] = single_element_array(place) if place&.any?
      hash["extent"] = single_element_array(extent) if extent&.any?
      hash["accesslocation"] = single_element_array(accesslocation) if accesslocation&.any?
      hash["classification"] = single_element_array(classification) if classification&.any?
      hash["validity"] = validity.to_hash if validity
      hash["fetched"] = fetched.to_s if fetched
      hash["keyword"] = single_element_array(keyword) if keyword&.any?
      hash["license"] = single_element_array(license) if license&.any?
      hash["doctype"] = doctype if doctype
      hash["editorialgroup"] = editorialgroup.to_hash if editorialgroup
      hash["ics"] = single_element_array ics if ics.any?
      if structuredidentifier&.presence?
        hash["structuredidentifier"] = structuredidentifier.to_hash
      end
      hash
    end
    # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

    # @param bibtex [BibTeX::Bibliography, NilClass]
    # @return [String]
    def to_bibtex(bibtex = nil)
      item = BibTeX::Entry.new
      item.type = bibtex_type
      item.key = id
      bibtex_title item
      item.edition = edition if edition
      bibtex_author item
      bibtex_contributor item
      item.address = place.first.name if place.any?
      bibtex_note item
      bibtex_relation item
      bibtex_extent item
      bibtex_date item
      bibtex_series item
      bibtex_classification item
      item.keywords = keyword.map(&:content).join(", ") if keyword.any?
      bibtex_docidentifier item
      item.timestamp = fetched.to_s if fetched
      bibtex_link item
      bibtex ||= BibTeX::Bibliography.new
      bibtex << item
      bibtex.to_s
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

    # @param lang [String] language code Iso639
    # @return [Array<RelatonIsoBib::TypedTitleString>]
    def title(lang: nil)
      if lang then @title.select { |t| t.title.language&.include? lang }
      else @title
      end
    end

    # @param type [Symbol] type of url, can be :src/:obp/:rss
    # @return [String]
    def url(type = :src)
      @link.detect { |s| s.type == type.to_s }.content.to_s
    end

    def abstract=(value)
      @abstract = value
    end

    def deep_clone
      dump = Marshal.dump self
      Marshal.load dump
    end

    def disable_id_attribute
      @id_attribute = false
    end

    # remove title part components and abstract
    def to_all_parts
      me = deep_clone
      me.disable_id_attribute
      me.relation << RelatonBib::DocumentRelation.new(
        type: "instance", bibitem: self,
      )
      me.language.each do |l|
        me.title.delete_if { |t| t.type == "title-part" }
        ttl = me.title.select { |t| t.type != "main" && t.title.language&.include?(l) }
        tm_en = ttl.map { |t| t.title.content }.join " – "
        me.title.detect { |t| t.type == "main" && t.title.language&.include?(l) }&.title&.content = tm_en
      end
      me.abstract = []
      me.docidentifier.each(&:remove_part)
      me.docidentifier.each(&:all_parts)
      me.structuredidentifier.remove_part
      me.structuredidentifier.all_parts
      me.docidentifier.each &:remove_date
      me.structuredidentifier&.remove_date
      me.all_parts = true
      me
    end

    # convert ISO:yyyy reference to reference to most recent
    # instance of reference, removing date-specific infomration:
    # date of publication, abstracts. Make dated reference Instance relation
    # of the redacated document
    def to_most_recent_reference
      me = deep_clone
      disable_id_attribute
      me.relation << DocumentRelation.new(type: "instance", bibitem: self)
      me.abstract = []
      me.date = []
      me.docidentifier.each &:remove_date
      me.structuredidentifier&.remove_date
      me.id&.sub! /-[12]\d\d\d/, ""
      me
    end

    # If revision_date exists then returns it else returns published date or nil
    # @return [String, NilClass]
    def revdate
      @revdate ||= if version&.revision_date
                     version.revision_date
                   else
                     date.detect { |d| d.type == "published" }&.on&.to_s
                   end
    end

    private

    # @return [String]
    def bibtex_title(item)
      title.each do |t|
        case t.type
        when "main" then item.tile = t.title.content
        end
      end
    end

    # @return [String]
    def bibtex_type
      case type
      when "standard", nil then "misc"
      else type
      end
    end

    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength

    # @param [BibTeX::Entry]
    def bibtex_author(item)
      authors = contributor.select do |c|
        c.entity.is_a?(Person) && c.role.map(&:type).include?("author")
      end.map &:entity

      return unless authors.any?

      item.author = authors.map do |a|
        if a.name.surname
          "#{a.name.surname}, #{a.name.forename.map(&:to_s).join(' ')}"
        else
          a.name.completename.to_s
        end
      end.join " and "
    end

    # @param [BibTeX::Entry]
    def bibtex_contributor(item)
      contributor.each do |c|
        rls = c.role.map(&:type)
        if rls.include?("publisher") then item.publisher = c.entity.name
        elsif rls.include?("distributor")
          case type
          when "techreport" then item.institution = c.entity.name
          when "inproceedings", "conference", "manual", "proceedings"
            item.organization = c.entity.name
          when "mastersthesis", "phdthesis" then item.school = c.entity.name
          end
        end
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

    # @param [BibTeX::Entry]
    def bibtex_note(item)
      biblionote.each do |n|
        case n.type
        when "annote" then item.annote = n.content
        when "howpublished" then item.howpublished = n.content
        when "comment" then item.comment = n.content
        when "tableOfContents" then item.content = n.content
        when nil then item.note = n.content
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_relation(item)
      rel = relation.detect { |r| r.type == "partOf" }
      if rel
        title_main = rel.bibitem.title.detect { |t| t.type == "main" }
        item.booktitle = title_main.title.content
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_extent(item)
      extent.each do |e|
        case e.type
        when "chapter" then item.chapter = e.reference_from
        when "page"
          value = e.reference_from
          value += "-#{e.reference_to}" if e.reference_to
          item.pages = value
        when "volume" then item.volume = e.reference_from
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_date(item)
      date.each do |d|
        case d.type
        when "published"
          item.year = d.on.year
          item.month = d.on.month
        when "accessed" then item.urldate = d.on.to_s
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_series(item)
      series.each do |s|
        case s.type
        when "journal"
          item.journal = s.title.title
          item.number = s.number if s.number
        when nil then item.series = s.title.title
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_classification(item)
      classification.each do |c|
        case c.type
        when "type" then item["type"] = c.value
        # when "keyword" then item.keywords = c.value
        when "mendeley" then item["mendeley-tags"] = c.value
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_docidentifier(item)
      docidentifier.each do |i|
        case i.type
        when "isbn" then item.isbn = i.id
        when "lccn" then item.lccn = i.id
        when "issn" then item.issn = i.id
        end
      end
    end

    # @param [BibTeX::Entry]
    def bibtex_link(item)
      link.each do |l|
        case l.type
        when "doi" then item.doi = l.content
        when "file" then item.file2 = l.content
        when "src" then item.url = l.content
        end
      end
    end

    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
    # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
    # rubocop:disable Style/NestedParenthesizedCalls, Metrics/BlockLength

    # @param builder [Nokogiri::XML::Builder]
    # @return [String]
    def render_xml(builder, **opts)
      root = opts[:bibdata] ? :bibdata : :bibitem
      xml = builder.send(root) do
        builder.fetched fetched if fetched
        title.each { |t| builder.title { t.to_xml builder } }
        formattedref&.to_xml builder
        link.each { |s| s.to_xml builder }
        docidentifier.each { |di| di.to_xml builder }
        builder.docnumber docnumber if docnumber
        date.each { |d| d.to_xml builder, **opts }
        contributor.each do |c|
          builder.contributor do
            c.role.each { |r| r.to_xml builder }
            c.to_xml builder
          end
        end
        builder.edition edition if edition
        version&.to_xml builder
        biblionote.each { |n| n.to_xml builder }
        language.each { |l| builder.language l }
        script.each { |s| builder.script s }
        abstract.each { |a| builder.abstract { a.to_xml(builder) } }
        status&.to_xml builder
        copyright&.each { |c| c.to_xml builder }
        relation.each { |r| r.to_xml builder, **opts }
        series.each { |s| s.to_xml builder }
        medium&.to_xml builder
        place.each { |pl| pl.to_xml builder }
        extent.each { |e| builder.extent { e.to_xml builder } }
        accesslocation.each { |al| builder.accesslocation al }
        license.each { |l| builder.license l }
        classification.each { |cls| cls.to_xml builder }
        keyword.each { |kw| builder.keyword { kw.to_xml(builder) } }
        validity&.to_xml builder
        if block_given? then yield builder
        elsif opts[:bibdata] && (doctype || editorialgroup || ics&.any? ||
                                 structuredidentifier&.presence?)
          builder.ext do |b|
            b.doctype doctype if doctype
            editorialgroup&.to_xml b
            ics.each { |i| i.to_xml b }
            structuredidentifier&.to_xml b
          end
        end
      end
      xml[:id] = id if id && !opts[:bibdata] && !opts[:embedded]
      xml[:type] = type if type
      xml
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
    # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
    # rubocop:enable Style/NestedParenthesizedCalls, Metrics/BlockLength
  end
end