# frozen_string_literal: true
require 'pathname'
require 'singleton'
require 'hamlit/engine'
require 'hamlit/version'

class IncompatibilitiesGenerator
  include Singleton

  Record = Struct.new(:template, :options, :spec_path, :line_number, :caller_lineno, :faml_result, :haml_result, :hamlit_result) do
    def incompatible?
      !all_error? && (faml_result != haml_result || faml_result != hamlit_result || haml_result != hamlit_result)
    end

    def grouped_difference
      if faml_result != haml_result && faml_result != hamlit_result && haml_result != hamlit_result
        { 'Faml' => faml_result, 'Haml' => haml_result, 'Hamlit' => hamlit_result }
      elsif faml_result == haml_result
        { 'Faml, Haml' => faml_result, 'Hamlit' => hamlit_result }
      elsif faml_result == hamlit_result
        { 'Faml, Hamlit' => faml_result, 'Haml' => haml_result }
      else
        { 'Faml' => faml_result, 'Haml, Hamlit' => haml_result }
      end
    end

    def only_hamlit?
      faml_result == haml_result && faml_result != hamlit_result
    end

    private

    def all_error?
      [faml_result, haml_result, hamlit_result].all? { |r| r.is_a?(Exception) }
    end
  end

  def initialize
    @records = []
  end

  def record(template, options, faml_result, example, caller_lineno)
    m = example.location.match(/\A(.+):(\d+)\z/)
    @records.push(Record.new(template, options, m[1], m[2].to_i, caller_lineno, faml_result, render_haml(template, options), render_hamlit(template, options)))
  end

  def write_to(markdown_root)
    markdown_root = Pathname.new(markdown_root)

    incompatibilities = Hash.new { |h, k| h[k] = [] }
    @records.each do |record|
      if record.incompatible?
        incompatibilities[record.spec_path] << record
      end
    end

    incompatibilities.keys.sort.each do |spec_path|
      path = markdown_path(markdown_root, spec_path)
      path.parent.mkpath
      path.open('w') do |f|
        incompatibilities[spec_path].sort_by(&:line_number).each do |record|
          render_difference(f, path, record)
        end
      end
    end
    render_toc(markdown_root, incompatibilities)
  end

  private

  def render_haml(template, options)
    obj = Object.new
    Haml::Engine.new(template, { ugly: true, escape_html: true }.merge(options)).def_method(obj, :haml)
    obj.haml
  rescue StandardError, SyntaxError => e
    e
  end

  def render_hamlit(template, options)
    obj = Object.new
    obj.instance_eval "def hamlit; #{Hamlit::Engine.new(options).call(template)}; end"
    obj.hamlit
  rescue StandardError, SyntaxError => e
    e
  end

  def render_difference(file, path, record)
    spec_path = Pathname.new(record.spec_path).relative_path_from(path.parent).to_s
    file.write <<"EOS"
# [#{record.spec_path}:#{record.line_number}](#{spec_path}#L#{record.line_number})
## #{render_input_title(record.options)}
```haml
#{record.template}
```

EOS
    render_grouped_difference(file, record.grouped_difference)
  end

  def render_input_title(options)
    title = 'Input'
    unless options.empty?
      title += " (with options=#{options.inspect})"
    end
    title
  end

  def render_grouped_difference(file, grouped_results)
    grouped_results.each do |title, result|
      file.write <<"EOS"
## #{render_section(title, result)}
```html
#{render_body(result)}
```

EOS
    end
  end

  def render_section(title, result)
    if result.is_a?(Exception)
      "#{title} (Error)"
    else
      title
    end
  end

  def render_body(result)
    if result.is_a?(Exception)
      result
    else
      result.gsub(/[[:cntrl:]]/) do |m|
        n = m[0].ord
        if n == 0x0a
          # Print NL character as is
          m[0]
        else
          "<0x#{sprintf('%02X', n)}>"
        end
      end
    end
  end

  def markdown_path(markdown_root, spec_path)
    markdown_root.join(spec_path).sub_ext('.md')
  end

  def render_toc(markdown_root, incompatibilities)
    toc_path = markdown_root.join('README.md')
    toc_path.open('w') do |f|
      f.puts '# Incompatibilities'
      f.puts '## Versions'
      f.puts "- Haml #{Haml::VERSION}"
      f.puts "- Faml #{Faml::VERSION}"
      f.puts "- Hamlit #{Hamlit::VERSION}"
      f.puts
      f.puts '## Table of contents'
      incompatibilities.keys.sort.each do |spec_path|
        path = markdown_path(markdown_root, spec_path).relative_path_from(markdown_root)
        link = "[#{path}](#{path})"
        comment =
          if incompatibilities[spec_path].all?(&:only_hamlit?)
            " (Hamlit's incompatibility)"
          end
        f.puts "- #{link}#{comment}"
      end
    end
  end
end
