# frozen_string_literal: true

class ThemeJavascriptCompiler
  COLOCATED_CONNECTOR_REGEX =
    %r{\A(?<prefix>.*/?)connectors/(?<outlet>[^/]+)/(?<name>[^/\.]+)\.(?<extension>.+)\z}

  class CompileError < StandardError
  end

  @@terser_disabled = false
  def self.disable_terser!
    raise "Tests only" if !Rails.env.test?
    @@terser_disabled = true
  end

  def self.enable_terser!
    raise "Tests only" if !Rails.env.test?
    @@terser_disabled = false
  end

  def initialize(theme_id, theme_name)
    @theme_id = theme_id
    @output_tree = []
    @theme_name = theme_name
  end

  def compile!
    if !@compiled
      @compiled = true
      @output_tree.freeze
      output =
        if !has_content?
          { "code" => "" }
        elsif @@terser_disabled
          { "code" => raw_content }
        else
          DiscourseJsProcessor::Transpiler.new.terser(@output_tree.to_h, terser_config)
        end
      @content = output["code"]
      @source_map = output["map"]
    end
    [@content, @source_map]
  rescue DiscourseJsProcessor::TranspileError => e
    message = "[THEME #{@theme_id} '#{@theme_name}'] Compile error: #{e.message}"
    @content = "console.error(#{message.to_json});\n"
    [@content, @source_map]
  end

  def terser_config
    # Based on https://github.com/ember-cli/ember-cli-terser/blob/28df3d90a5/index.js#L12-L26
    {
      sourceMap: {
        includeSources: true,
        root: "theme-#{@theme_id}/",
      },
      compress: {
        negate_iife: false,
        sequences: 30,
        drop_debugger: false,
      },
      output: {
        semicolons: false,
      },
    }
  end

  def content
    compile!
    @content
  end

  def source_map
    compile!
    @source_map
  end

  def raw_content
    @output_tree.map { |filename, source| source }.join("")
  end

  def has_content?
    @output_tree.present?
  end

  def prepend_settings(settings_hash)
    @output_tree.prepend ["settings.js", <<~JS]
      (function() {
        if ('require' in window) {
          require("discourse/lib/theme-settings-store").registerSettings(#{@theme_id}, #{settings_hash.to_json});
        }
      })();
    JS
  end

  def append_tree(tree, for_tests: false)
    # Replace legacy extensions
    tree.transform_keys! do |filename|
      if filename.ends_with? ".js.es6"
        filename.sub(/\.js\.es6\z/, ".js")
      elsif filename.ends_with? ".raw.hbs"
        filename.sub(/\.raw\.hbs\z/, ".hbr")
      else
        filename
      end
    end

    # Some themes are colocating connector JS under `/connectors`. Move template to /templates to avoid module name clash
    tree.transform_keys! do |filename|
      match = COLOCATED_CONNECTOR_REGEX.match(filename)
      next filename if !match

      is_template = match[:extension] == "hbs"
      is_in_templates_directory = match[:prefix].split("/").last == "templates"

      if is_template && !is_in_templates_directory
        "#{match[:prefix]}templates/connectors/#{match[:outlet]}/#{match[:name]}.#{match[:extension]}"
      elsif !is_template && is_in_templates_directory
        "#{match[:prefix].chomp("templates/")}connectors/#{match[:outlet]}/#{match[:name]}.#{match[:extension]}"
      else
        filename
      end
    end

    # Handle colocated components
    tree.dup.each_pair do |filename, content|
      is_component_template =
        filename.end_with?(".hbs") &&
          filename.start_with?("discourse/components/", "admin/components/")
      next if !is_component_template
      template_contents = content

      hbs_invocation_options = { moduleName: filename, parseOptions: { srcName: filename } }
      hbs_invocation = "hbs(#{template_contents.to_json}, #{hbs_invocation_options.to_json})"

      prefix = <<~JS
        import { hbs } from 'ember-cli-htmlbars';
        const __COLOCATED_TEMPLATE__ = #{hbs_invocation};
      JS

      js_filename = filename.sub(/\.hbs\z/, ".js")
      js_contents = tree[js_filename] # May be nil for template-only component
      if js_contents && !js_contents.include?("export default")
        message =
          "#{filename} does not contain a `default export`. Did you forget to export the component class?"
        js_contents += "throw new Error(#{message.to_json});"
      end

      if js_contents.nil?
        # No backing class, use template-only
        js_contents = <<~JS
          import templateOnly from '@ember/component/template-only';
          export default templateOnly();
        JS
      end

      js_contents = prefix + js_contents

      tree[js_filename] = js_contents
      tree.delete(filename)
    end

    # Transpile and write to output
    tree.each_pair do |filename, content|
      module_name, extension = filename.split(".", 2)
      module_name = "test/#{module_name}" if for_tests
      if extension == "js"
        append_module(content, module_name)
      elsif extension == "hbs"
        append_ember_template(module_name, content)
      elsif extension == "hbr"
        append_raw_template(module_name.sub("discourse/templates/", ""), content)
      else
        append_js_error(filename, "unknown file extension '#{extension}' (#{filename})")
      end
    rescue CompileError => e
      append_js_error filename, "#{e.message} (#{filename})"
    end
  end

  def append_ember_template(name, hbs_template)
    module_name = name
    module_name = "/#{module_name}" if !module_name.start_with?("/")
    module_name = "discourse/theme-#{@theme_id}#{module_name}"

    # Mimics the ember-cli implementation
    # https://github.com/ember-cli/ember-cli-htmlbars/blob/d5aa14b3/lib/template-compiler-plugin.js#L18-L26
    script = <<~JS
      import { hbs } from 'ember-cli-htmlbars';
      export default hbs(#{hbs_template.to_json}, { moduleName: #{module_name.to_json} });
    JS

    template_module = DiscourseJsProcessor.transpile(script, "", module_name, theme_id: @theme_id)
    @output_tree << ["#{name}.js", <<~JS]
      if ('define' in window) {
      #{template_module}
      }
    JS
  rescue MiniRacer::RuntimeError, DiscourseJsProcessor::TranspileError => ex
    raise CompileError.new ex.message
  end

  def raw_template_name(name)
    name = name.sub(/\.(raw|hbr)\z/, "")
    name.inspect
  end

  def append_raw_template(name, hbs_template)
    compiled =
      DiscourseJsProcessor::Transpiler.new.compile_raw_template(hbs_template, theme_id: @theme_id)
    source_for_comment = hbs_template.gsub("*/", '*\/').indent(4, " ")
    @output_tree << ["#{name}.js", <<~JS]
      (function() {
        /*
      #{source_for_comment}
        */
        const addRawTemplate = requirejs('discourse-common/lib/raw-templates').addRawTemplate;
        const template = requirejs('discourse-common/lib/raw-handlebars').template(#{compiled});
        addRawTemplate(#{raw_template_name(name)}, template);
      })();
    JS
  rescue MiniRacer::RuntimeError, DiscourseJsProcessor::TranspileError => ex
    raise CompileError.new ex.message
  end

  def append_raw_script(filename, script)
    @output_tree << [filename, script + "\n"]
  end

  def append_module(script, name, include_variables: true)
    original_filename = name
    name = "discourse/theme-#{@theme_id}/#{name.gsub(%r{\Adiscourse/}, "")}"

    script = "#{theme_settings}#{script}" if include_variables
    transpiler = DiscourseJsProcessor::Transpiler.new
    @output_tree << ["#{original_filename}.js", <<~JS]
      if ('define' in window) {
      #{transpiler.perform(script, "", name, theme_id: @theme_id).strip}
      }
    JS
  rescue MiniRacer::RuntimeError, DiscourseJsProcessor::TranspileError => ex
    raise CompileError.new ex.message
  end

  def append_js_error(filename, message)
    message = "[THEME #{@theme_id} '#{@theme_name}'] Compile error: #{message}"
    append_raw_script filename, "console.error(#{message.to_json});"
  end

  private

  def theme_settings
    <<~JS
      const settings = require("discourse/lib/theme-settings-store")
        .getObjectForTheme(#{@theme_id});
      const themePrefix = (key) => `theme_translations.#{@theme_id}.${key}`;
    JS
  end
end
