# Copyright (c) 2008-2021 Minero Aoki, Kenshi Muto
#               1999-2007 Minero Aoki
#
# This program is free software.
# You can distribute or modify this program under the terms of
# the GNU LGPL, Lesser General Public License version 2.1.
# For details of the GNU LGPL, see the file "COPYING".
#

require 'review/book'
require 'review/version'
require 'optparse'
require 'review/plaintextbuilder'

module ReVIEW
  class PLAINTEXTTocBuilder < PLAINTEXTBuilder
    def headline(level, label, caption)
      if @chapter.is_a?(ReVIEW::Book::Part)
        print "\x01H0\x01" # XXX: don't modify level value. level value will be handled in sec_counter#prefix()
      else
        print "\x01H#{level}\x01"
      end
      # embed header information for tocparser
      super
    end

    def base_block(type, lines, caption = nil)
      puts "\x01STARTLIST\x01"
      super
      puts "\x01ENDLIST\x01"
    end

    def blank
      @blank_seen = true
    end
  end

  class TOCPrinter
    class Counter
      def initialize(name: nil, level: nil, headline: nil, lines: nil, chars: nil, list_lines: nil, text_lines: nil, part: nil)
        @name = name
        @level = level
        @headline = headline
        @lines = lines
        @chars = chars
        @list_lines = list_lines
        @text_lines = text_lines
        @part = part
      end

      attr_accessor :name, :level, :headline, :lines, :chars, :list_lines, :text_lines, :part
    end

    def self.execute(*args)
      new.execute(*args)
    end

    def initialize
      @logger = ReVIEW.logger
      @yamlfile = 'config.yml'
      @upper = 4
      @indent = true
      @buildonly = nil
      @detail = nil
      @calc_char_width = nil
    end

    attr_accessor :calc_char_width

    def execute(*args)
      parse_options(args)
      begin
        @config = ReVIEW::Configure.create(yamlfile: @yamlfile)
        @book = ReVIEW::Book::Base.new('.', config: @config)
        unless File.readable?(@yamlfile)
          raise ReVIEW::FileNotFound, "No such fiile or can't open #{@yamlfile}."
        end
      rescue ReVIEW::ConfigError, ReVIEW::FileNotFound, ReVIEW::CompileError, ReVIEW::ApplicationError => e
        @logger.error e.message
        exit 1
      end

      I18n.setup(@config['language'])

      if @detail
        begin
          require 'unicode/eaw'
          @calc_char_width = true
        rescue LoadError
          @logger.warn('not found unicode/eaw library. page volume may be unreliable.')
          @calc_char_width = nil
        end
      end

      print_result(build_result_array)
    end

    def build_result_array
      result_array = []
      begin
        @book.parts.each do |part|
          if part.name.present? && (@buildonly.nil? || @buildonly.include?(part.name))
            result_array.push(Counter.new(part: 'start'))
            if part.file?
              content = build_chap(part)
              result_array.concat(parse_contents(part.name, @upper, content))
            else
              title = part.format_number + I18n.t('chapter_postfix') + part.title
              result_array.push(
                Counter.new(name: '', lines: 1, chars: title.size, list_lines: 0, text_lines: 1),
                Counter.new(level: 0, headline: title, lines: 1, chars: title.size, list_lines: 0, text_lines: 1)
              )
            end
          end

          part.chapters.each do |chap|
            if @buildonly.nil? || @buildonly.include?(chap.name)
              content = build_chap(chap)
              result_array.concat(parse_contents(chap.name, @upper, content))
            end
          end
          if part.name.present? && (@buildonly.nil? || @buildonly.include?(part.name))
            result_array.push(Counter.new(part: 'end'))
          end
        end
      rescue ReVIEW::FileNotFound, ReVIEW::CompileError => e
        @logger.error e
        exit 1
      end

      result_array
    end

    def print_result(result_array)
      result_array.each do |result|
        if result.part
          next
        end

        if result.name
          # file information
          if @detail
            puts '============================='
            printf("%6dC %5dL %5dP  %s\n", result.chars, result.lines, calc_pages(result).ceil, result.name)
            puts '-----------------------------'
          end
          next
        end

        # section information
        if @detail
          printf('%6dC %5dL %5.1fP  ', result.chars, result.lines, calc_pages(result))
        end
        if @indent && result.level
          print '  ' * (result.level == 0 ? 0 : result.level - 1)
        end
        puts result.headline
      end
    end

    def calc_pages(result)
      (result.list_lines.to_f / @book.page_metric.list.n_lines) +
        (result.text_lines.to_f / @book.page_metric.text.n_lines)
    end

    def calc_linesize(line)
      return line.size unless @calc_char_width

      line.each_char.inject(0) do |result, char|
        # XXX: should include A also?
        if %i[Na H N].include?(Unicode::Eaw.property(char))
          result + 0.5 # halfwidth
        else
          result + 1
        end
      end
    end

    def parse_contents(name, upper, content)
      headline_array = []
      counter = Counter.new(lines: 0, chars: 0, list_lines: 0, text_lines: 0)
      listmode = nil

      content.each_line(chomp: true) do |l|
        if l.start_with?("\x01STARTLIST\x01")
          listmode = true
          next
        elsif l.start_with?("\x01ENDLIST\x01")
          listmode = nil
          next
        elsif l =~ /\A\x01H(\d)\x01/
          # headline
          level = $1.to_i
          l = $'
          if level <= upper
            if counter.chars > 0
              headline_array.push(counter)
            end
            headline = l
            counter = Counter.new(
              level: level,
              headline: headline,
              lines: 1,
              chars: headline.size,
              list_lines: 0,
              text_lines: 1
            )
            next
          end
        end

        counter.lines += 1
        counter.chars += l.size

        if listmode
          # code list: calculate line wrapping
          counter.list_lines += calc_line_wrapping(l, mode: :list)
        else
          # normal paragraph: calculate line wrapping
          counter.text_lines += calc_line_wrapping(l, mode: :text)
        end
      end
      headline_array.push(counter)

      total = calc_total_count(name, headline_array)
      headline_array.unshift(total)
    end

    def calc_line_wrapping(line, mode:)
      return 1 if line.size == 0

      case mode
      when :list
        ((calc_linesize(line) - 1) / @book.page_metric.list.n_columns) + 1
      else # mode == :text
        ((calc_linesize(line) - 1) / @book.page_metric.text.n_columns) + 1
      end
    end

    def calc_total_count(name, headline_array)
      total = Counter.new(name: name,
                          lines: 0,
                          chars: 0,
                          list_lines: 0,
                          text_lines: 0)

      headline_array.each do |h|
        next unless h.lines

        total.lines += h.lines
        total.chars += h.chars
        total.list_lines += h.list_lines
        total.text_lines += h.text_lines
      end

      total
    end

    def build_chap(chap)
      compiler = ReVIEW::Compiler.new(ReVIEW::PLAINTEXTTocBuilder.new)
      begin
        compiler.compile(@book.chapter(chap.name))
      rescue ReVIEW::ApplicationError => e
        @logger.error e.message
        ''
      end
    end

    def parse_options(args)
      opts = OptionParser.new
      opts.version = ReVIEW::VERSION
      opts.on('--yaml=YAML', 'Read configurations from YAML file.') { |yaml| @yamlfile = yaml }
      opts.on('-y', '--only file1,file2,...', 'list only specified files.') do |v|
        @buildonly = v.split(/\s*,\s*/).map { |m| m.strip.sub(/\.re\Z/, '') }
      end
      opts.on('-l', '--level N', 'list upto N level (default=4)') do |n|
        @upper = n.to_i
      end
      opts.on('-d', '--detail', 'show characters and lines of each section.') do
        @detail = true
      end
      opts.on('--noindent', "don't indent headlines.") do
        @indent = nil
      end
      opts.on('--help', 'print this message and quit.') do
        puts opts.help
        exit 0
      end
      begin
        opts.parse!(args)
      rescue OptionParser::ParseError => e
        @logger.error e.message
        $stderr.puts opts.help
        exit 1
      end
    end
  end
end
