require "active_support/hash_with_indifferent_access"

module TinyMCE::Rails
  class Configuration
    class Function < String
      def to_javascript
        self
      end
    end

    def self.defaults
      {
        "selector" => "textarea.tinymce"
      }
    end

    FUNCTION_REGEX = /^function\s*\(/
    RELATIVE_PATH_REGEX = /^(\/|\.{1,2})\S*/

    COMMA = ",".freeze
    SPACE = " ".freeze
    SEMICOLON = ";".freeze

    OPTION_SEPARATORS = {
      "plugins"                       => COMMA,
      "custom_elements"               => COMMA,
      "entities"                      => COMMA,
      "extended_valid_elements"       => COMMA,
      "font_formats"                  => SEMICOLON,
      "fontsize_formats"              => COMMA,
      "invalid_elements"              => COMMA,
      "block_formats"                 => SEMICOLON,
      "valid_children"                => COMMA,
      "valid_elements"                => COMMA,
      "body_id"                       => COMMA,
      "body_class"                    => COMMA,
      "content_css"                   => COMMA,
      "tabfocus_elements"             => COMMA,
      "table_clone_elements"          => SPACE,
      "paste_word_valid_elements"     => COMMA,
      "paste_webkit_styles"           => SPACE,
      "paste_retain_style_properties" => SPACE,
      "spellchecker_languages"        => COMMA
    }

    OPTION_TRANSFORMERS = {
      # Check for files provided in the content_css option to replace them with their actual path.
      # If no corresponding stylesheet is found for a file, it will remain unchanged.
      "content_css" => ->(value) {
        helpers = ActionView::Base.new
        separator = OPTION_SEPARATORS["content_css"]

        value.split(separator).map { |file|
          next file if RELATIVE_PATH_REGEX =~ file
          helpers.stylesheet_path(file.strip) || file
        }.join(separator)
      }
    }

    attr_reader :options

    def initialize(options)
      @options = options
    end

    def self.new_with_defaults(options={})
      config = new(defaults)
      config = config.merge(options) if options
      config
    end

    # Converts options into a String representing a JavaScript object
    # that can be passed directly to tinyMCE.init
    def to_javascript
      options_to_javascript(options_for_tinymce)
    end

    # Converts options into a TinyMCE-friendly format.
    #
    #  1. Joins array values using OPTION_SEPARATORS
    #  2. Converts JavaScript function() strings to Function objects
    #  3. Applies transformations from OPTION_TRANSFORMERS
    def options_for_tinymce
      preprocess_options(options)
    end

    def merge(options)
      self.class.new(self.options.merge(options))
    end

  private
    def array_option?(key, value)
      value.is_a?(Array) && OPTION_SEPARATORS[key]
    end

    def function_option?(value)
      FUNCTION_REGEX =~ value.to_s
    end

    def preprocess_options(options)
      result = {}

      options.each do |key, value|
        result[key] = preprocess_option(key, value)
      end

      result
    end

    def preprocess_option(key, value)
      result = value

      if result.is_a?(Hash)
        result = preprocess_options(value)
      elsif array_option?(key, value)
        result = value.join(OPTION_SEPARATORS[key])
      elsif function_option?(value)
        result = Function.new(value)
      end

      if transformer = OPTION_TRANSFORMERS[key]
        result = transformer.call(result)
      end

      result
    end

    def options_to_javascript(options, indent="")
      next_indent = indent + "  "

      pairs = options.inject([]) do |result, (k, v)|
        if v.is_a?(Hash)
          v = options_to_javascript(v, next_indent)
        elsif v.respond_to?(:to_javascript)
          v = v.to_javascript
        elsif v.respond_to?(:to_json)
          v = v.to_json
        end

        result << [k, v].join(": ")
      end

      "{\n#{next_indent}#{pairs.join(",\n#{next_indent}")}\n#{indent}}"
    end
  end

  class MultipleConfiguration < ActiveSupport::HashWithIndifferentAccess
    def initialize(configurations={})
      configurations = configurations.each_with_object({}) { |(name, options), h|
        h[name] = Configuration.new_with_defaults(options)
      }

      super(configurations)
    end
  end
end
