#
# Copyright (c) 2018-2023 Kenshi Muto
#
# 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 'fileutils'
require 'optparse'
require 'review'
require 'review/i18n'
require 'yaml'
require 'digest'

module ReVIEW
  class Update
    def self.execute(*args)
      new.execute(*args)
    end

    # should be
    TARGET_VERSION = '5.0'
    EPUB_VERSION = '3'
    HTML_VERSION = '5'
    TEX_DOCUMENTCLASS = ['review-jsbook', 'review-jlreq']
    TEX_DOCUMENTCLASS_BAD = ['jsbook', nil]
    TEX_DOCUMENTCLASS_OPTS = 'media=print,paper=a5'
    TEX_COMMAND = 'uplatex'
    TEX_OPTIONS = '-interaction=nonstopmode -file-line-error -halt-on-error'
    DVI_COMMAND = 'dvipdfmx'
    DVI_OPTIONS = '-d 5 -z 9'

    attr_reader :config_ymls, :locale_ymls, :catalog_ymls, :tex_ymls, :epub_ymls
    attr_accessor :force, :specified_template

    def initialize
      @template = '__DEFAULT__'
      @specified_template = nil
      @force = nil
      @logger = ReVIEW.logger
      @review_dir = File.dirname(File.expand_path('..', __dir__))
      @config_ymls = []
      @locale_ymls = []
      @catalog_ymls = []
      @tex_ymls = []
      @epub_ymls = []

      @backup = true
    end

    def execute(*args)
      parse_options(args)
      dir = Dir.pwd

      parse_ymls(dir)
      check_old_catalogs(dir)

      show_version

      if @config_ymls.empty?
        @logger.error t("!! No *.yml file with 'review_version' was found. Aborted. !!")
        raise ApplicationError
      end

      check_own_files(dir)
      update_version
      update_rakefile(dir)
      update_epub_version
      update_locale
      update_tex_parameters

      if @template
        if @template == '__DEFAULT__'
          @template = TEX_DOCUMENTCLASS[0]
        end
        update_tex_stys(@template, dir)
      end

      update_tex_command
      update_dvi_command

      puts t('Finished.')
    rescue ApplicationError
      exit 1
    end

    def t(message, args = [])
      unless I18n.get(message)
        I18n.set(message, message) # just copy
      end
      I18n.t(message, args)
    end

    def confirm(message, args = [], default = true)
      if @force
        @logger.info t(message, args)
        if default
          @logger.info ' yes'
          return true
        else
          @logger.info 'no'
          return nil
        end
      end

      loop do
        print t(message, args)
        if default
          print ' [y]/n '
        else
          print ' y/[n] '
        end
        case gets.chomp.downcase
        when 'yes', 'y'
          return true
        when 'no', 'n'
          return nil
        when ''
          return default
        end
      end
    end

    def rewrite_yml(yml, key, val)
      content = File.read(yml)
      content.gsub!(/^(\s*)#{key}:.*$/, '\1' + "#{key}: #{val}")
      if @backup
        FileUtils.mv(yml, "#{yml}-old")
      end
      File.write(yml, content)
    end

    def parse_options(args)
      opts = OptionParser.new
      opts.version = ReVIEW::VERSION
      opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [option]"
      opts.on('-h', '--help', 'print this message and quit.') do
        puts opts.help
        exit 0
      end
      opts.on('--latex-template name', 'specify LaTeX template name. (default: review-jsbook)') do |tname|
        @specified_template = tname
      end

      begin
        opts.parse!(args)
      rescue OptionParser::ParseError => e
        @logger.error e.message
        $stderr.puts opts.help
        raise ApplicationError
      end

      if @specified_template
        tdir = File.join(@review_dir, 'templates/latex', @specified_template)
        unless File.exist?(tdir)
          @logger.error "!! #{tdir} not found. Aborted. !!"
          raise ApplicationError
        end
      end
    end

    def parse_ymls(dir)
      language = 'en'

      Dir.glob(File.join(dir, '*.yml')).sort.each do |yml|
        begin
          config = YAMLLoader.safe_load_file(yml)
          if config['language'].present?
            language = config['language']
          end

          if config['review_version'].present?
            @config_ymls.push(yml)
          end
          if config['texdocumentclass'].present? ||
             config['texcommand'].present? ||
             config['texoptions'].present? ||
             config['dvicommand'].present? ||
             config['dvioptions'].present? ||
             config['pdfmaker'].present?
            @tex_ymls.push(yml)
          end
          if config['epubmaker'].present? || config['epubversion'].present? ||
             config['htmlversion'].present?
            @epub_ymls.push(yml)
          end
          if config['locale'].present?
            @locale_ymls.push(yml)
          end
          if config['PREDEF'].present? || config['CHAPS'].present? ||
             config['APPENDIX'].present? || config['POSTDEF'].present?
            @catalog_ymls.push(yml)
          end
        rescue Psych::SyntaxError
          @logger.error "!! #{yml} is broken. Ignored. !!"
        end
      end
      I18n.setup(language)

      @config_ymls.uniq!
      @locale_ymls.uniq!
      @catalog_ymls.uniq!
      @tex_ymls.uniq!
      @epub_ymls.uniq!
    end

    def check_old_catalogs(dir)
      files = Dir.glob(File.join(dir, '*')).map do |fname|
        if %w[PREDEF CHAPS POSTDEF PART].include?(File.basename(fname))
          File.basename(fname)
        else
          return nil
        end
      end.compact

      unless files.empty?
        @logger.error t("!! %s file(s) is obsoleted. Run 'review-catalog-converter' to convert to 'catalog.yml' and remove old files. Aborted. !!", files.join(', '))
        raise ApplicationError
      end
    end

    def show_version
      puts t('** review-update updates your project to %s **', ReVIEW::VERSION)
    end

    def check_own_files(dir)
      if File.exist?(File.join(dir, 'layouts/layout.tex.erb'))
        # rubocop:disable Style/SoleNestedConditional
        unless confirm('** There is custom layouts/layout.tex.erb file. Updating may break to make PDF until you fix layout.tex.erb. Do you really proceed to update? **', [], nil)
          raise ApplicationError
        end
        # rubocop:enable Style/SoleNestedConditional
      end

      if File.exist?(File.join(dir, 'review-ext.rb'))
        @logger.info t('** There is review-ext.rb file. You need to update it by yourself. **')
      end
    end

    def update_version
      @config_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        if config['review_version'].to_f.round(1) == TARGET_VERSION.to_f.round(1)
          next
        end

        flag = true
        if config['review_version'].to_f > TARGET_VERSION.to_f
          flag = nil
        end

        if confirm("%s: Update '%s' to '%s'?", [File.basename(yml), 'review_version', TARGET_VERSION], flag)
          rewrite_yml(yml, 'review_version', TARGET_VERSION)
        end
      end
    end

    def update_rakefile(dir)
      taskdir = File.join(dir, 'lib/tasks')
      FileUtils.mkdir_p(taskdir)

      master_rakefile = File.join(@review_dir, 'samples/sample-book/src/Rakefile')

      target_rakefile = File.join(dir, 'Rakefile')
      if File.exist?(target_rakefile)
        if Digest::SHA256.hexdigest(File.read(target_rakefile)) != Digest::SHA256.hexdigest(File.read(master_rakefile))
          # rubocop:disable Style/SoleNestedConditional
          if confirm('%s will be overridden with Re:VIEW version (%s). Do you really proceed?', ['Rakefile', master_rakefile])
            FileUtils.mv(target_rakefile, "#{target_rakefile}-old")
            FileUtils.cp(master_rakefile, target_rakefile)
          end
          # rubocop:enable Style/SoleNestedConditional
        end
      else
        @logger.info t('new file %s is created.', [target_rakefile]) unless @force
        FileUtils.cp(master_rakefile, target_rakefile)
      end

      master_rakefile = File.join(@review_dir, 'samples/sample-book/src/lib/tasks/review.rake')
      target_rakefile = File.join(taskdir, 'review.rake')
      if File.exist?(target_rakefile)
        # rubocop:disable Style/SoleNestedConditional
        if Digest::SHA256.hexdigest(File.read(target_rakefile)) != Digest::SHA256.hexdigest(File.read(master_rakefile))
          if confirm('%s will be overridden with Re:VIEW version (%s). Do you really proceed?', ['lib/tasks/review.rake', master_rakefile])
            FileUtils.mv(target_rakefile, "#{target_rakefile}-old")
            FileUtils.cp(master_rakefile, target_rakefile)
          end
        end
        # rubocop:enable Style/SoleNestedConditional
      else
        @logger.info t('new file %s is created.', [target_rakefile]) unless @force
        FileUtils.cp(master_rakefile, target_rakefile)
      end
    end

    def update_epub_version
      @epub_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        if config['epubversion'].present? && config['epubversion'].to_f < EPUB_VERSION.to_f
          # rubocop:disable Style/SoleNestedConditional
          if confirm("%s: Update '%s' to '%s' from '%s'?", [File.basename(yml), 'epubversion', EPUB_VERSION, config['epubversion']])
            rewrite_yml(yml, 'epubversion', EPUB_VERSION)
          end
          # rubocop:enable Style/SoleNestedConditional
        end
        if !config['htmlversion'].present? || config['htmlversion'].to_f >= HTML_VERSION.to_f
          next
        end

        if confirm("%s: Update '%s' to '%s' from '%s'?", [File.basename(yml), 'htmlversion', HTML_VERSION, config['htmlversion']])
          rewrite_yml(yml, 'htmlversion', HTML_VERSION)
        end
      end
    end

    def update_locale
      @locale_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        if !config['chapter_quote'].present? || config['chapter_quote'].scan('%s').size != 1
          next
        end

        v = config['chapter_quote'].sub('%s', '%s %s')
        if confirm("%s: 'chapter_quote' now takes 2 values. Update '%s' to '%s'?", [File.basename(yml), config['chapter_quote'], v])
          rewrite_yml(yml, 'chapter_quote', v)
        end
      end
    end

    def update_tex_parameters
      @tex_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        unless config['texdocumentclass']
          next
        end

        if TEX_DOCUMENTCLASS.include?(config['texdocumentclass'][0])
          if @specified_template.present? && config['texdocumentclass'][0] != @specified_template
            # want to use other template?
            @logger.error t("%s: !! 'texdocumentclass' uses new class '%s' already, but you specified '%s'. This tool can't handle such migration. Ignored. !!", [File.basename(yml), config['texdocumentclass'][0], @specified_template])
            @template = nil
          else
            @template = config['texdocumentclass'][0]

            if @template == 'review-jsbook'
              update_review_jsbook_opts(yml, config['texdocumentclass'][1])
            end
          end

          # no need to update
          next
        end

        if TEX_DOCUMENTCLASS_BAD.include?(config['texdocumentclass'][0])
          cno = TEX_DOCUMENTCLASS_BAD.index(config['texdocumentclass'][0])

          if @specified_template && @specified_template != TEX_DOCUMENTCLASS[cno]
            # not default, manually selected
            unless confirm("%s: 'texdocumentclass' uses the old class '%s'. By default it is migrated to '%s', but you specify '%s'. Do you really migrate 'texdocumentclass' to '%s'?",
                           [File.basename(yml), TEX_DOCUMENTCLASS_BAD[cno],
                            TEX_DOCUMENTCLASS[cno],
                            @specified_template, @specified_template])
              @template = nil
              next
            end
            @template = @specified_template
          else
            # default migration
            @template = TEX_DOCUMENTCLASS[cno]
            unless confirm("%s: 'texdocumentclass' uses the old class '%s'. By default it is migrated to '%s'. Do you really migrate 'texdocumentclass' to '%s'?", [File.basename(yml), TEX_DOCUMENTCLASS_BAD[cno], @template, @template])
              @template = nil
              next
            end
          end

          flag, modified_opts = convert_documentclass_opts(yml, @template, config['texdocumentclass'][1])
          rewrite_documentclass_opts_by_flag(flag, yml, config['texdocumentclass'][1], modified_opts)
        else
          @template = nil
          @logger.error t("%s: ** 'texdocumentclass' specifies '%s'. Because this is unknown class for this tool, you need to update it by yourself if it won't work. **", [File.basename(yml), config['texdocumentclass'][0]])
        end
      end
    end

    def rewrite_documentclass_opts_by_flag(flag, yml, old_opts, modified_opts)
      if flag # successfully converted
        @logger.info t("%s: previous 'texdocumentclass' option '%s' is safely replaced with '%s'.", [File.basename(yml), old_opts, modified_opts])
      else # something wrong
        unless confirm("%s: previous 'texdocumentclass' option '%s' couldn't be converted fully. '%s' is suggested. Do you really proceed?", [File.basename(yml), old_opts, modified_opts], nil)
          @template = nil
          return nil
        end
      end

      rewrite_yml(yml, 'texdocumentclass', %Q(["#{@template}", "#{modified_opts}"]))
    end

    def update_review_jsbook_opts(yml, old_opts)
      modified_opts = old_opts.gsub(/Q=([^,]+)/, 'fontsize=\1Q').
                      gsub(/W=([^,]+)/, 'line_length=\1zw').
                      gsub(/L=([^,]+)/, 'number_of_lines=\1').
                      gsub(/H=([^,]+)/, 'baselineskip=\1H').
                      gsub(/head=([^,]+)/, 'head_space=\1')

      if modified_opts == old_opts
        return nil
      end

      rewrite_documentclass_opts_by_flag(true, yml, old_opts, modified_opts)
    end

    def convert_documentclass_opts(yml, cls, prev_opts)
      # XXX: at this time, review-jsbook and review-jlreq uses same parameters
      opts = []
      flag = true
      case cls
      when 'review-jsbook' # at this time, it ignores keyval
        prev_opts.split(/\s*,\s*/).each do |v|
          case v
          when 'a4j', 'a5j', 'b4j', 'b5j', 'a3paper', 'a4paper', 'a5paper', 'a6paper', 'b4paper', 'b5paper', 'b6paper', 'letterpaper', 'legalpaper', 'executivepaper'
            opts << "paper=#{v.sub('j', '').sub('paper', '')}"
          when /[\d.]+ptj/ # not cared...
            opts << "fontsize=#{v.sub('j', '')}"
          when /[\d.]+pt/, /[\d.]+Q/
            opts << "fontsize=#{v}"
          when 'landscape', 'oneside', 'twoside', 'vartwoside', 'onecolumn',
               'twocolumn', 'titlepage', 'notitlepage', 'openright',
               'openany', 'leqno', 'fleqn', 'disablejfam', 'draft', 'final',
               'mingoth', 'winjis', 'jis', 'papersize', 'english', 'report',
               'jslogo', 'nojslogo'
            # pass-through
            opts << v
          when 'uplatex', 'nomag', 'usemag', 'nomag*', 'tombow', 'tombo', 'mentuke', 'autodetect-engine'
            # can be ignored
            next
          else
            flag = nil
          end
        end
        opts << 'media=print'
        opts << 'cover=false'
      when 'review-jlreq'
        # at this time, only think about jsbook->jlreq
        prev_opts.split(/\s*,\s*/).each do |v|
          case v
          when 'a4j', 'a5j', 'b4j', 'b5j', 'a3paper', 'a4paper', 'a5paper', 'a6paper', 'b4paper', 'b5paper', 'b6paper', 'letterpaper', 'legalpaper', 'executivepaper'
            opts << "paper=#{v.sub('j', '').sub('paper', '')}"
          when /[\d.]+ptj/ # not cared...
            opts << "fontsize=#{v.sub('j', '')}"
          when /[\d.]+pt/, /[\d.]+Q/
            opts << "fontsize=#{v}"
          when 'landscape', 'oneside', 'twoside', 'onecolumn', 'twocolumn', 'titlepage', 'notitlepage', 'openright', 'openany', 'leqno', 'fleqn', 'draft', 'final', 'report'
            # pass-through
            opts << v
          when 'uplatex', 'nomag', 'usemag', 'nomag*', 'tombow', 'tombo', 'mentuke', 'autodetect-engine'
            # can be ignored
            next
          else
            # 'vartwoside', 'disablejfam', 'mingoth', 'winjis', 'jis', 'papersize', 'english', 'jslogo', 'nojslogo'
            flag = nil
          end
        end
        opts << 'media=print'
        opts << 'cover=false'
      else
        flag = nil
        @logger.error t("%s: ** '%s' is unknown class. Ignored. **", [File.basename(yml), cls])
      end
      return flag, opts.join(',')
    end

    def update_tex_stys(template, dir)
      texmacrodir = File.join(dir, 'sty')
      FileUtils.mkdir_p(texmacrodir)

      tdir = File.join(@review_dir, 'templates/latex', template)
      Dir.glob(File.join(tdir, '*.*')).each do |master_styfile|
        target_styfile = File.join(texmacrodir, File.basename(master_styfile))

        unless File.exist?(target_styfile)
          # just copy
          @logger.info t('new file %s is created.', [target_styfile]) unless @force
          FileUtils.cp(master_styfile, target_styfile)
          next
        end
        if File.basename(target_styfile) == 'review-custom.sty'
          next
        end

        if Digest::SHA256.hexdigest(File.read(target_styfile)) == Digest::SHA256.hexdigest(File.read(master_styfile))
          # same
          next
        end

        if confirm('%s will be overridden with Re:VIEW version (%s). Do you really proceed?', [target_styfile, master_styfile])
          FileUtils.mv(target_styfile, "#{target_styfile}-old")
          FileUtils.cp(master_styfile, target_styfile)
        end
      end

      if template == 'review-jsbook'
        unless File.exist?(File.join(texmacrodir, 'jsbook.cls'))
          @logger.info t('new file %s is created.', [File.join(texmacrodir, 'jsbook.cls')]) unless @force
          FileUtils.cp(File.join(@review_dir, 'vendor/jsclasses/jsbook.cls'), File.join(texmacrodir, 'jsbook.cls'))
        end

        unless File.exist?(File.join(texmacrodir, 'gentombow.sty'))
          @logger.info t('new file %s is created.', [File.join(texmacrodir, 'gentombow.sty')]) unless @force
          FileUtils.cp(File.join(@review_dir, 'vendor/gentombow/gentombow.sty'), File.join(texmacrodir, 'gentombow.sty'))
        end
      end
    end

    def update_tex_command
      @tex_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        if !config['texcommand'] || config['texcommand'] !~ /\s+-/
          next
        end

        # option should be moved to texoptions
        cmd, opts = config['texcommand'].split(/\s+-/, 2)
        opts = "-#{opts}"

        unless confirm("%s: 'texcommand' has options ('%s'). Move it to 'texoptions'?", [File.basename(yml), opts])
          next
        end

        if config['texoptions'].present?
          config['texoptions'] += " #{opts}"
          rewrite_yml(yml, 'texcommand', %Q("#{cmd}"))
          rewrite_yml(yml, 'texoptions', %Q("#{config['texoptions']}"))
        else
          rewrite_yml(yml, 'texcommand', %Q("#{cmd}"\ntexoptions: "#{TEX_OPTIONS} #{opts}"))
        end
      end
    end

    def update_dvi_command
      @tex_ymls.each do |yml|
        config = YAMLLoader.safe_load_file(yml)
        if !config['dvicommand'] || config['dvicommand'] !~ /\s+-/
          next
        end

        # option should be moved to dvioptions
        cmd, opts = config['dvicommand'].split(/\s+-/, 2)
        opts = "-#{opts}"

        unless confirm("%s: 'dvicommand' has options ('%s'). Move it to 'dvioptions'?", [File.basename(yml), opts])
          next
        end

        if config['dvioptions'].present?
          config['dvioptions'] += " #{opts}"
          rewrite_yml(yml, 'dvicommand', %Q("#{cmd}"))
          rewrite_yml(yml, 'dvioptions', %Q("#{config['dvioptions']}"))
        else
          rewrite_yml(yml, 'dvicommand', %Q("#{cmd}"\ndvioptions: "#{DVI_OPTIONS} #{opts}"))
        end
      end
    end
  end
end
