require 'pathname'
require 'execjs'
require 'json'

IS_SECTION = /^\s*\[(.+)\]\s*$/

module AutoprefixerRails
  # Ruby to JS wrapper for Autoprefixer processor instance
  class Processor

    def initialize(params = { })
      @params = params || { }
    end

    # Process `css` and return result.
    #
    # Options can be:
    # * `from` with input CSS file name. Will be used in error messages.
    # * `to` with output CSS file name.
    # * `map` with true to generate new source map or with previous map.
    def process(css, opts = { })
      opts = convert_options(opts)

      apply_wrapper =
        "(function(opts, pluginOpts) {" +
        "return eval(process.apply(this, opts, pluginOpts));" +
        "})"

      pluginOpts = params_with_browsers(opts[:from]).merge(opts)
      processOpts = {
        from: pluginOpts.delete(:from),
        to:   pluginOpts.delete(:to),
        map:  pluginOpts.delete(:map)
      }

      begin
        result = runtime.call(apply_wrapper, [css, processOpts, pluginOpts])
      rescue ExecJS::ProgramError => e
        contry_error = 'BrowserslistError: ' +
          'Country statistics is not supported ' +
          'in client-side build of Browserslist'
        if e.message == contry_error
          raise 'Country statistics is not supported in AutoprefixerRails. ' +
                'Use Autoprefixer with webpack or other Node.js builder.'
        else
          raise e
        end
      end

      Result.new(result['css'], result['map'], result['warnings'])
    end

    # Return, which browsers and prefixes will be used
    def info
      runtime.eval("autoprefixer(#{ js_params }).info()")
    end

    # Parse Browserslist config
    def parse_config(config)
      sections = { 'defaults' => [] }
      current  = 'defaults'
      config.gsub(/#[^\n]*/, '')
            .split(/\n/)
            .map(&:strip)
            .reject(&:empty?)
            .each do |line|
              if line =~ IS_SECTION
                current = line.match(IS_SECTION)[1].strip
                sections[current] ||= []
              else
                sections[current] << line
              end
            end
      sections
    end

    private

    def params_with_browsers(from = nil)
      unless from
        if defined? Rails and Rails.respond_to?(:root) and Rails.root
          from = Rails.root.join('app/assets/stylesheets').to_s
        else
          from = '.'
        end
      end

      params = @params
      if not params.has_key?(:browsers) and from
        file = find_config(from)
        if file
          env    = params[:env].to_s || 'development'
          config = parse_config(file)
          params = params.dup
          if config[env]
            params[:browsers] = config[env]
          else
            params[:browsers] = config['defaults']
          end
        end
      end

      params
    end

    # Convert params to JS string and add browsers from Browserslist config
    def js_params
      '{ ' +
        params_with_browsers.map { |k, v| "#{k}: #{v.inspect}"}.join(', ') +
      ' }'
    end

    # Convert ruby_options to jsOptions
    def convert_options(opts)
      converted = { }

      opts.each_pair do |name, value|
        if name =~ /_/
          name = name.to_s.gsub(/_\w/) { |i| i.gsub('_', '').upcase }.to_sym
        end
        value = convert_options(value) if value.is_a? Hash
        converted[name] = value
      end

      converted
    end

    # Try to find Browserslist config
    def find_config(file)
      path = Pathname(file).expand_path

      while path.parent != path
        config1 = path.join('browserslist')
        return config1.read if config1.exist? and not config1.directory?

        config2 = path.join('.browserslistrc')
        return config2.read if config2.exist? and not config1.directory?

        path = path.parent
      end

      nil
    end

    # Lazy load for JS library
    def runtime
      @runtime ||= begin
        if ExecJS.eval('typeof(Array.prototype.map)') != 'function'
          raise "Current ExecJS runtime does't support ES5. " +
                "Please install node.js."
        end

        ExecJS.compile(build_js)
      end
    end

    # Cache autoprefixer.js content
    def read_js
      @@js ||= begin
        root = Pathname(File.dirname(__FILE__))
        path = root.join("../../vendor/autoprefixer.js")
        path.read.gsub(/Object.setPrototypeOf\(chalk[^)]+\)/, '')
      end
    end

    def polyfills
      <<-JS
        if (typeof Uint8Array === "undefined")
          global.Uint8Array = Array;
        if (typeof ArrayBuffer === "undefined")
          global.ArrayBuffer = Array;
        if (typeof Set === "undefined") {
          global.Set = function (values) { this.values = values }
          global.Set.prototype = {
            has: function (i) { return this.values.indexOf(i) !== -1 }
          }
        }
        if (typeof Map === "undefined") {
          global.Map = function () { this.data = { } }
          global.Map.prototype = {
            set: function (k, v) { this.data[k] = v },
            get: function (k) { return this.data[k] },
            has: function (k) {
              return Object.keys(this.data).indexOf(k) !== -1
            },
          }
        }
          Math.log2 = Math.log2 ||
          function(x) { return Math.log(x) * Math.LOG2E; };
        Math.sign = Math.sign ||
          function(x) {
            x = +x;
            if (x === 0 || isNaN(x)) return Number(x);
            return x > 0 ? 1 : -1;
          };
        Array.prototype.fill = Array.prototype.fill ||
          function(value) {
            var O = Object(this);
            var len = O.length >>> 0;
            var start = arguments[1];
            var relativeStart = start >> 0;
            var k = relativeStart < 0 ?
              Math.max(len + relativeStart, 0) :
              Math.min(relativeStart, len);
            var end = arguments[2];
            var relativeEnd = end === undefined ?
              len : end >> 0;
            var final = relativeEnd < 0 ?
              Math.max(len + relativeEnd, 0) :
              Math.min(relativeEnd, len);
            while (k < final) {
              O[k] = value;
              k++;
            }
            return O;
          };
        if (!Object.assign) {
          Object.assign = function(target, firstSource) {
            var to = Object(target);
            for (var i = 1; i < arguments.length; i++) {
              var nextSource = arguments[i];
              if (nextSource === undefined || nextSource === null) continue;
              var keysArray = Object.keys(Object(nextSource));
              for (var n = 0, len = keysArray.length; n < len; n++) {
                var nextKey = keysArray[n];
                var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
                if (desc !== undefined && desc.enumerable) {
                  to[nextKey] = nextSource[nextKey];
                }
              }
            }
            return to;
          }
        }
        if (!Array.isView) {
          Array.isView = function () {
            return false
          }
        }
      JS
    end

    # Return processor JS with some extra methods
    def build_js
      'var global = this;' + polyfills + read_js + process_proxy
    end

    # Return JS code for process method proxy
    def process_proxy
      <<-JS
        var processor;
        var process = function() {
          var result = autoprefixer.process.apply(autoprefixer, arguments);
          var warns  = result.warnings().map(function (i) {
            delete i.plugin;
            return i.toString();
          });
          var map = result.map ? result.map.toString() : null;
          return { css: result.css, map: map, warnings: warns };
        };
      JS
    end
  end
end
