# frozen_string_literal: true

class ThemeJavascriptCompiler

  module PrecompilerExtension
    def initialize(theme_id)
      super()
      @theme_id = theme_id
    end

    def discourse_node_manipulator
      <<~JS

      // Helper to replace old themeSetting syntax
      function generateHelper(settingParts) {
        const settingName = settingParts.join('.');
        return {
            "path": {
              "type": "PathExpression",
              "original": "theme-setting",
              "this": false,
              "data": false,
              "parts": [
                "theme-setting"
              ],
              "depth":0
            },
            "params": [
              {
                type: "NumberLiteral",
                value: #{@theme_id},
                original: #{@theme_id}
              },
              {
                "type": "StringLiteral",
                "value": settingName,
                "original": settingName
              }
            ],
            "hash": {
              "type": "Hash",
              "pairs": [
                {
                  "type": "HashPair",
                  "key": "deprecated",
                  "value": {
                    "type": "BooleanLiteral",
                    "value": true,
                    "original": true
                  }
                }
              ]
            }
          }
      }

      function manipulatePath(path) {
        // Override old themeSetting syntax when it's a param inside another node
        if(path.parts && path.parts[0] == "themeSettings"){
          const settingParts = path.parts.slice(1);
          path.type = "SubExpression";
          Object.assign(path, generateHelper(settingParts))
        }
      }

      function manipulateNode(node) {
        // Magically add theme id as the first param for each of these helpers)
        if (node.path.parts && ["theme-i18n", "theme-prefix", "theme-setting"].includes(node.path.parts[0])) {
          if(node.params.length === 1){
            node.params.unshift({
              type: "NumberLiteral",
              value: #{@theme_id},
              original: #{@theme_id}
            })
          }
        }

        // Override old themeSetting syntax when it's in its own node
        if (node.path.parts && node.path.parts[0] == "themeSettings") {
          Object.assign(node, generateHelper(node.path.parts.slice(1)))
        }
      }
      JS
    end

    def source
      [super, discourse_node_manipulator, discourse_extension].join("\n")
    end
  end

  class RawTemplatePrecompiler < Barber::Precompiler
    include PrecompilerExtension

    def discourse_extension
      <<~JS
        let _superCompile = Handlebars.Compiler.prototype.compile;
        Handlebars.Compiler.prototype.compile = function(program, options) {

          // `replaceGet()` in raw-handlebars.js.es6 adds a `get` in front of things
          // so undo this specific case for the old themeSettings.blah syntax
          let visitor = new Handlebars.Visitor();
          visitor.mutating = true;
          visitor.MustacheStatement = (node) => {
            if(node.path.original == 'get'
              && node.params
              && node.params[0]
              && node.params[0].parts
              && node.params[0].parts[0] == 'themeSettings'){
                node.path.parts = node.params[0].parts
                node.params = []
            }
          };
          visitor.accept(program);

          [
            ["SubExpression", manipulateNode],
            ["MustacheStatement", manipulateNode],
            ["PathExpression", manipulatePath]
          ].forEach((pass) => {
            let visitor = new Handlebars.Visitor();
            visitor.mutating = true;
            visitor[pass[0]] = pass[1];
            visitor.accept(program);
          })

          return _superCompile.apply(this, arguments);
        };
      JS
    end
  end

  class EmberTemplatePrecompiler < Barber::Ember::Precompiler
    include PrecompilerExtension

    def discourse_extension
      <<~JS
        Ember.HTMLBars.registerPlugin('ast', function() {
          return {
            name: 'theme-template-manipulator',
            visitor: {
              SubExpression: manipulateNode,
              MustacheStatement: manipulateNode,
              PathExpression: manipulatePath
            }
          }
        });
      JS
    end
  end

  class CompileError < StandardError
  end

  attr_accessor :content

  def initialize(theme_id, theme_name)
    @theme_id = theme_id
    @content = +""
    @theme_name = theme_name
  end

  def prepend_settings(settings_hash)
    @content.prepend <<~JS
      (function() {
        if ('Discourse' in window && Discourse.__container__) {
          Discourse.__container__
            .lookup("service:theme-settings")
            .registerSettings(#{@theme_id}, #{settings_hash.to_json});
        }
      })();
    JS
  end

  # TODO Error handling for handlebars templates
  def append_ember_template(name, hbs_template)
    name = name.inspect
    compiled = EmberTemplatePrecompiler.new(@theme_id).compile(hbs_template)
    content << <<~JS
      (function() {
        if ('Ember' in window) {
          Ember.TEMPLATES[#{name}] = Ember.HTMLBars.template(#{compiled});
        }
      })();
    JS
  rescue Barber::PrecompilerError => e
    raise CompileError.new e.instance_variable_get(:@error) # e.message contains the entire template, which could be very long
  end

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

  def append_raw_template(name, hbs_template)
    compiled = RawTemplatePrecompiler.new(@theme_id).compile(hbs_template)
    @content << <<~JS
      (function() {
        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 Barber::PrecompilerError => e
    raise CompileError.new e.instance_variable_get(:@error) # e.message contains the entire template, which could be very long
  end

  def append_plugin_script(script, api_version)
    @content << transpile(script, api_version)
  end

  def append_raw_script(script)
    @content << script + "\n"
  end

  def append_module(script, name, include_variables: true)
    script = "#{theme_variables}#{script}" if include_variables
    transpiler = DiscourseJsProcessor::Transpiler.new
    @content << transpiler.perform(script, "", name)
  rescue MiniRacer::RuntimeError => ex
    raise CompileError.new ex.message
  end

  def append_js_error(message)
    @content << "console.error('Theme Transpilation Error:', #{message.inspect});"
  end

  private

  def theme_variables
    <<~JS
      const __theme_name__ = "#{@theme_name.gsub('"', "\\\"")}";
      const settings = Discourse.__container__
        .lookup("service:theme-settings")
        .getObjectForTheme(#{@theme_id});
      const themePrefix = (key) => `theme_translations.#{@theme_id}.${key}`;
    JS
  end

  def transpile(es6_source, version)
    transpiler = DiscourseJsProcessor::Transpiler.new(skip_module: true)
    wrapped = <<~PLUGIN_API_JS
      (function() {
        if ('Discourse' in window && typeof Discourse._registerPluginCode === 'function') {
          #{theme_variables}
          Discourse._registerPluginCode('#{version}', api => {
            try {
            #{es6_source}
            } catch(err) {
              const rescue = require("discourse/lib/utilities").rescueThemeError;
              rescue(__theme_name__, err, api);
            }
          });
        }
      })();
    PLUGIN_API_JS

    transpiler.perform(wrapped)
  rescue MiniRacer::RuntimeError => ex
    raise CompileError.new ex.message
  end
end
