# frozen_string_literal: true

require 'spec_helper'
require 'pathname'

describe Prawn::Font do
  let(:pdf) { create_pdf }

  describe 'Font behavior' do
    it 'defaults to Helvetica if no font is specified' do
      pdf = Prawn::Document.new
      expect(pdf.font.name).to eq('Helvetica')
    end
  end

  describe 'Font objects' do
    it 'is equal' do
      font1 = Prawn::Document.new.font
      font2 = Prawn::Document.new.font

      expect(font1).to eql(font2)
    end

    it 'alwayses be the same key' do
      font1 = Prawn::Document.new.font
      font2 = Prawn::Document.new.font

      hash = {}

      hash[font1] = 'Original'
      hash[font2] = 'Overwritten'

      expect(hash.size).to eq(1)

      expect(hash[font1]).to eq('Overwritten')
      expect(hash[font2]).to eq('Overwritten')
    end
  end

  describe '#width_of' do
    it 'takes character spacing into account' do
      original_width = pdf.width_of('hello world')
      pdf.character_spacing(7) do
        expect(pdf.width_of('hello world')).to eq(original_width + 10 * 7)
      end
    end

    it 'excludes newlines' do
      # Use a TTF font that has a non-zero width for \n
      pdf.font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf")

      expect(pdf.width_of("\nhello world\n")).to eq(
        pdf.width_of('hello world')
      )
    end

    it 'takes formatting into account' do
      normal_hello = pdf.width_of('hello')
      inline_bold_hello = pdf.width_of('<b>hello</b>', inline_format: true)
      expect(inline_bold_hello).to be > normal_hello

      pdf.font('Helvetica', style: :bold) do
        expect(inline_bold_hello).to eq(pdf.width_of('hello'))
      end
    end

    it 'accepts :style as an argument' do
      styled_bold_hello = pdf.width_of('hello', style: :bold)
      pdf.font('Helvetica', style: :bold) do
        expect(styled_bold_hello).to eq(pdf.width_of('hello'))
      end
    end

    it 'reports missing font with style' do
      expect do
        pdf.font('Nada', style: :bold) do
          pdf.width_of('hello')
        end
      end.to raise_error(Prawn::Errors::UnknownFont, /Nada \(bold\)/)
    end

    it 'calculates styled widths correctly using TTFs' do
      pdf.font_families.update(
        'DejaVu Sans' => {
          normal: "#{Prawn::DATADIR}/fonts/DejaVuSans.ttf",
          bold: "#{Prawn::DATADIR}/fonts/DejaVuSans-Bold.ttf"
        }
      )
      styled_bold_hello = nil
      bold_hello = nil
      plain_hello = nil

      pdf.font('DejaVu Sans') do
        styled_bold_hello = pdf.width_of('hello', style: :bold)
      end
      pdf.font('DejaVu Sans', style: :bold) do
        bold_hello = pdf.width_of('hello')
      end

      pdf.font('DejaVu Sans') do
        plain_hello = pdf.width_of('hello')
      end

      expect(plain_hello).to_not eq(bold_hello)

      expect(styled_bold_hello).to eq(bold_hello)
    end

    it 'does not treat minus as if it were a hyphen', issue: 578 do
      expect(pdf.width_of('-0.75')).to be < pdf.width_of('25.00')
    end
  end

  describe '#font_size' do
    it 'allows setting font size in DSL style' do
      pdf.font_size 20
      expect(pdf.font_size).to eq(20)
    end
  end

  describe 'font style support' do
    it 'complains if there is no @current_page' do
      pdf_without_page = Prawn::Document.new(skip_page_creation: true)

      expect { pdf_without_page.font 'Helvetica' }
        .to raise_error(Prawn::Errors::NotOnPage)
    end

    it 'allows specifying font style by style name and font family' do
      pdf.font 'Courier', style: :bold
      pdf.text 'In Courier bold'

      pdf.font 'Courier', style: :bold_italic
      pdf.text 'In Courier bold-italic'

      pdf.font 'Courier', style: :italic
      pdf.text 'In Courier italic'

      pdf.font 'Courier', style: :normal
      pdf.text 'In Normal Courier'

      pdf.font 'Helvetica'
      pdf.text 'In Normal Helvetica'

      text = PDF::Inspector::Text.analyze(pdf.render)
      expect(text.font_settings.map { |e| e[:name] }).to eq(
        %i[
          Courier-Bold Courier-BoldOblique Courier-Oblique
          Courier Helvetica
        ]
      )
    end

    it 'allows font familes to be defined in a single dfont' do
      file = "#{Prawn::DATADIR}/fonts/Panic+Sans.dfont"
      pdf.font_families['Panic Sans'] = {
        normal: { file: file, font: 'PanicSans' },
        italic: { file: file, font: 'PanicSans-Italic' },
        bold: { file: file, font: 'PanicSans-Bold' },
        bold_italic: { file: file, font: 'PanicSans-BoldItalic' }
      }

      pdf.font 'Panic Sans', style: :italic
      pdf.text 'In PanicSans-Italic'

      text = PDF::Inspector::Text.analyze(pdf.render)
      name = text.font_settings.map { |e| e[:name] }.first.to_s
      name = name.sub(/\w+\+/, 'subset+')
      expect(name).to eq('subset+PanicSans-Italic')
    end

    it 'allows font familes to be defined in a single ttc' do
      file = "#{Prawn::DATADIR}/fonts/DejaVuSans.ttc"
      pdf.font_families['DejaVu Sans'] = {
        normal: { file: file, font: 0 },
        bold: { file: file, font: 1 }
      }

      pdf.font 'DejaVu Sans', style: :bold
      pdf.text 'In PanicSans-Bold'

      text = PDF::Inspector::Text.analyze(pdf.render)
      name = text.font_settings.map { |e| e[:name] }.first.to_s
      name = name.sub(/\w+\+/, 'subset+')
      expect(name).to eq('subset+DejaVuSans-Bold')
    end

    it 'allows fonts to be indexed by name in a ttc file' do
      file = "#{Prawn::DATADIR}/fonts/DejaVuSans.ttc"
      pdf.font_families['DejaVu Sans'] = {
        normal: { file: file, font: 'DejaVu Sans' },
        bold: { file: file, font: 'DejaVu Sans Bold' }
      }

      pdf.font 'DejaVu Sans', style: :bold
      pdf.text 'In PanicSans-Bold'

      text = PDF::Inspector::Text.analyze(pdf.render)
      name = text.font_settings.map { |e| e[:name] }.first.to_s
      name = name.sub(/\w+\+/, 'subset+')
      expect(name).to eq('subset+DejaVuSans-Bold')
    end

    it 'accepts Pathname objects for font files' do
      file = Pathname.new("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
      pdf.font_families['DejaVu Sans'] = {
        normal: file
      }

      pdf.font 'DejaVu Sans'
      pdf.text 'In DejaVu Sans'

      text = PDF::Inspector::Text.analyze(pdf.render)
      name = text.font_settings.map { |e| e[:name] }.first.to_s
      name = name.sub(/\w+\+/, 'subset+')
      expect(name).to eq('subset+DejaVuSans')
    end

    it 'accepts IO objects for font files' do
      File.open "#{Prawn::DATADIR}/fonts/DejaVuSans.ttf" do |io|
        pdf.font_families['DejaVu Sans'] = {
          normal: described_class.load(pdf, io)
        }

        pdf.font 'DejaVu Sans'
        pdf.text 'In DejaVu Sans'

        text = PDF::Inspector::Text.analyze(pdf.render)
        name = text.font_settings.map { |e| e[:name] }.first.to_s
        name = name.sub(/\w+\+/, 'subset+')
        expect(name).to eq('subset+DejaVuSans')
      end
    end
  end

  describe 'Transactional font handling' do
    it 'allows setting of size directly when font is created' do
      pdf.font 'Courier', size: 16
      expect(pdf.font_size).to eq(16)
    end

    it 'allows temporary setting of a new font using a transaction' do
      pdf.font 'Helvetica', size: 12

      pdf.font 'Courier', size: 16 do
        expect(pdf.font.name).to eq('Courier')
        expect(pdf.font_size).to eq(16)
      end

      expect(pdf.font.name).to eq('Helvetica')
      expect(pdf.font_size).to eq(12)
    end

    it 'masks font size when using a transacation' do
      pdf.font 'Courier', size: 16 do
        expect(pdf.font_size).to eq(16)
      end

      pdf.font 'Times-Roman'
      pdf.font 'Courier'

      expect(pdf.font_size).to eq(12)
    end
  end

  describe 'Document#page_fonts' do
    it 'registers fonts properly by page' do
      pdf.font 'Courier'
      pdf.text('hello')

      pdf.font 'Helvetica'
      pdf.text('hello')

      pdf.font 'Times-Roman'
      pdf.text('hello')

      %w[Courier Helvetica Times-Roman].each do |f|
        page_should_include_font(f)
      end

      pdf.start_new_page

      pdf.font 'Helvetica'
      pdf.text('hello')

      page_should_include_font('Helvetica')
      page_should_not_include_font('Courier')
      page_should_not_include_font('Times-Roman')
    end

    def page_includes_font?(font)
      pdf.page.fonts.values.map { |e| e.data[:BaseFont] }.include?(font.to_sym)
    end

    def page_should_include_font(font)
      expect(page_includes_font?(font)).to eq true
    end

    def page_should_not_include_font(font)
      expect(page_includes_font?(font)).to eq false
    end
  end

  describe 'AFM fonts' do
    let(:times) { pdf.find_font 'Times-Roman' }

    it 'calculates string width taking into account accented characters' do
      input = win1252_string("\xE9") # é in win-1252
      expect(times.compute_width_of(input, size: 12))
        .to eq(times.compute_width_of('e', size: 12))
    end

    it 'calculates string width taking into account kerning pairs' do
      expect(times.compute_width_of(win1252_string('To'), size: 12))
        .to eq(13.332)
      expect(times.compute_width_of(
        win1252_string('To'),
        size: 12,
        kerning: true
      )).to eq(12.372)

      input = win1252_string("T\xF6") # Tö in win-1252
      expect(times.compute_width_of(input, size: 12, kerning: true))
        .to eq(12.372)
    end

    it 'encodes text without kerning by default' do
      expect(times.encode_text(win1252_string('To'))).to eq([[0, 'To']])
      input = win1252_string("T\xE9l\xE9") # Télé in win-1252
      expect(times.encode_text(input)).to eq([[0, input]])
      expect(times.encode_text(win1252_string('Technology')))
        .to eq([[0, 'Technology']])
      expect(times.encode_text(win1252_string('Technology...')))
        .to eq([[0, 'Technology...']])
    end

    it 'encodes text with kerning if requested' do
      expect(times.encode_text(win1252_string('To'), kerning: true))
        .to eq([[0, ['T', 80, 'o']]])
      input = win1252_string("T\xE9l\xE9") # Télé in win-1252
      output = win1252_string("\xE9l\xE9") # élé  in win-1252
      expect(times.encode_text(input, kerning: true))
        .to eq([[0, ['T', 70, output]]])
      expect(times.encode_text(win1252_string('Technology'), kerning: true))
        .to eq([[0, ['T', 70, 'echnology']]])
      expect(times.encode_text(win1252_string('Technology...'), kerning: true))
        .to eq([[0, ['T', 70, 'echnology', 65, '...']]])
    end

    describe 'when normalizing encoding' do
      it 'does not modify the original string when normalize_encoding() '\
        'is used' do
        original = 'Foo'
        normalized = times.normalize_encoding(original)
        expect(original.equal?(normalized)).to eq false
      end
    end

    it 'omits /Encoding for symbolic fonts' do
      zapf = pdf.find_font 'ZapfDingbats'
      font_dict = zapf.__send__(:register, nil)
      expect(font_dict.data[:Encoding]).to be_nil
    end
  end

  describe '#glyph_present' do
    it 'returns true when present in an AFM font' do
      font = pdf.find_font('Helvetica')
      expect(font.glyph_present?('H')).to eq true
    end

    it 'returns false when absent in an AFM font' do
      font = pdf.find_font('Helvetica')
      expect(font.glyph_present?('再')).to eq false
    end

    it 'returns true when present in a TTF font' do
      font = pdf.find_font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
      expect(font.glyph_present?('H')).to eq true
    end

    it 'returns false when absent in a TTF font' do
      font = pdf.find_font("#{Prawn::DATADIR}/fonts/DejaVuSans.ttf")
      expect(font.glyph_present?('再')).to eq false

      font = pdf.find_font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf")
      expect(font.glyph_present?('€')).to eq false
    end
  end

  describe 'TTF fonts' do
    let(:font) { pdf.find_font "#{Prawn::DATADIR}/fonts/DejaVuSans.ttf" }

    it 'calculates string width taking into account accented characters' do
      expect(font.compute_width_of('é', size: 12)).to eq(
        font.compute_width_of('e', size: 12)
      )
    end

    it 'calculates string width taking into account kerning pairs' do
      expect(font.compute_width_of('To', size: 12)).to be_within(0.01).of(14.65)
      expect(font.compute_width_of('To', size: 12, kerning: true))
        .to be_within(0.01).of(12.61)
    end

    it 'encodes text without kerning by default' do
      expect(font.encode_text('To')).to eq([[0, 'To']])

      tele = "T\216l\216"
      result = font.encode_text('Télé')
      expect(result.length).to eq(1)
      expect(result[0][0]).to eq(0)
      expect(result[0][1].bytes.to_a).to eq(tele.bytes.to_a)

      expect(font.encode_text('Technology')).to eq([[0, 'Technology']])
      expect(font.encode_text('Technology...')).to eq([[0, 'Technology...']])
      expect(font.encode_text('Teχnology...')).to eq([
        [0, 'Te'],
        [1, '!'], [0, 'nology...']
      ])
    end

    it 'encodes text with kerning if requested' do
      expect(font.encode_text('To', kerning: true)).to eq([
        [0, ['T', 169.921875, 'o']]
      ])
      expect(font.encode_text('Technology', kerning: true)).to eq([
        [0, ['T', 169.921875, 'echnology']]
      ])
      expect(font.encode_text('Technology...', kerning: true)).to eq([
        [0, ['T', 169.921875, 'echnology', 142.578125, '...']]
      ])
      expect(font.encode_text('Teχnology...', kerning: true)).to eq([
        [0, ['T', 169.921875, 'e']],
        [1, '!'],
        [0, ['nology', 142.578125, '...']]
      ])
    end

    it 'uses the ascender, descender, and cap height from the TTF verbatim' do
      # These metrics are relative to the font's own bbox. They should not be
      # scaled with font size.
      ref = pdf.ref!({})
      font.__send__(:embed, ref, 0)

      # Pull out the embedded font descriptor
      descriptor = ref.data[:FontDescriptor].data
      expect(descriptor[:Ascent]).to eq(759)
      expect(descriptor[:Descent]).to eq(-240)
      expect(descriptor[:CapHeight]).to eq(759)
    end

    describe 'when normalizing encoding' do
      it 'does not modify the original string with normalize_encoding()' do
        original = 'Foo'
        normalized = font.normalize_encoding(original)
        expect(original.equal?(normalized)).to eq false
      end
    end
  end

  describe 'OTF fonts' do
    let(:font) { pdf.find_font "#{Prawn::DATADIR}/fonts/Bodoni-Book.otf" }

    it 'calculates string width taking into account accented characters' do
      expect(font.compute_width_of('é', size: 12)).to eq(
        font.compute_width_of('e', size: 12)
      )
    end

    it 'uses the ascender, descender, and cap height from the OTF verbatim' do
      # These metrics are relative to the font's own bbox. They should not be
      # scaled with font size.
      ref = pdf.ref!({})
      font.__send__(:embed, ref, 0)

      # Pull out the embedded font descriptor
      descriptor = ref.data[:FontDescriptor].data
      expect(descriptor[:Ascent]).to eq(1023)
      expect(descriptor[:Descent]).to eq(-200)
      expect(descriptor[:CapHeight]).to eq(3072)
    end

    describe 'when normalizing encoding' do
      it 'does not modify the original string with normalize_encoding()' do
        original = 'Foo'
        normalized = font.normalize_encoding(original)
        expect(original).to_not be_equal(normalized)
      end
    end
  end

  describe 'DFont fonts' do
    let(:file) { "#{Prawn::DATADIR}/fonts/Panic+Sans.dfont" }

    it 'lists all named fonts' do
      list = Prawn::Fonts::DFont.named_fonts(file)
      expect(list).to match_array(%w[
        PanicSans PanicSans-Bold PanicSans-BoldItalic PanicSans-Italic
      ])
    end

    it 'counts the number of fonts in the file' do
      expect(Prawn::Fonts::DFont.font_count(file)).to eq(4)
    end

    it 'defaults selected font to the first one if not specified' do
      font = pdf.find_font(file)
      expect(font.basename).to eq('PanicSans')
    end

    it 'allows font to be selected by index' do
      font = pdf.find_font(file, font: 2)
      expect(font.basename).to eq('PanicSans-Italic')
    end

    it 'allows font to be selected by name' do
      font = pdf.find_font(file, font: 'PanicSans-BoldItalic')
      expect(font.basename).to eq('PanicSans-BoldItalic')
    end

    it 'caches font object based on selected font' do
      f1 = pdf.find_font(file, font: 'PanicSans')
      f2 = pdf.find_font(file, font: 'PanicSans-Bold')
      expect(f2.object_id).to_not eq(f1.object_id)
      expect(pdf.find_font(file, font: 'PanicSans').object_id)
        .to eq(f1.object_id)
      expect(pdf.find_font(file, font: 'PanicSans-Bold').object_id)
        .to eq(f2.object_id)
    end
  end

  describe '#character_count(text)' do
    it 'works on TTF fonts' do
      pdf.font("#{Prawn::DATADIR}/fonts/gkai00mp.ttf")
      expect(pdf.font.character_count('こんにちは世界')).to eq(7)
      expect(pdf.font.character_count('Hello, world!')).to eq(13)
    end

    it 'works on AFM fonts' do
      expect(pdf.font.character_count('Hello, world!')).to eq(13)
    end
  end
end
