require 'singleton'
require 'sablon/configuration/html_tag'

module Sablon
  # Handles storing configuration data for the sablon module
  class Configuration
    include Singleton

    attr_accessor :permitted_html_tags, :defined_style_conversions

    def initialize
      initialize_html_tags
      initialize_css_style_conversion
    end

    # Adds a new tag to the permitted tags hash or replaces an existing one
    def register_html_tag(tag_name, type = :inline, **options)
      tag = HTMLTag.new(tag_name, type, **options)
      @permitted_html_tags[tag.name] = tag
    end

    # Removes a tag from the permitted tgs hash, returning it
    def remove_html_tag(tag_name)
      @permitted_html_tags.delete(tag_name)
    end

    # Adds a new style property converter for the specified ast class and
    # CSS property name. The ast_class variable should be the class name
    # in lowercased snakecase as a symbol, i.e. MyClass -> :my_class.
    # The converter passed in must be a proc that accepts
    # a single argument (the value) and returns two values: the WordML property
    # name and its value. The converted property value can be a string, hash
    # or array.
    def register_style_converter(ast_node, prop_name, converter)
      # create a new ast node hash if needed
      unless @defined_style_conversions[ast_node]
        @defined_style_conversions[ast_node] = {}
      end
      # add the style converter to the node's hash
      @defined_style_conversions[ast_node][prop_name] = converter
    end

    # Deletes a CSS converter from the hash by specifying the AST class
    # in lowercased snake case and the property name.
    def remove_style_converter(ast_node, prop_name)
      @defined_style_conversions[ast_node].delete(prop_name)
    end

    private

    # Defines all of the initial HTML tags to be used by HTMLconverter
    def initialize_html_tags
      @permitted_html_tags = {}
      tags = {
        # special tag used for elements with no parent, i.e. top level
        '#document-fragment' => { type: :block, ast_class: :root, allowed_children: %i[_block _inline] },

        # block level tags
        table: { type: :block, ast_class: :table, allowed_children: %i[caption thead tbody tfoot tr ]},
        tr: { type: :block, ast_class: :table_row, allowed_children: %i[th td] },
        th: { type: :block, ast_class: :table_cell, properties: { b: nil, jc: 'center' }, allowed_children: %i[_block _inline] },
        td: { type: :block, ast_class: :table_cell, allowed_children: %i[_block _inline] },
        div: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Normal' }, allowed_children: :_inline },
        p: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Paragraph' }, allowed_children: :_inline },
        caption: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Caption' }, allowed_children: :_inline },
        h1: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading1' }, allowed_children: :_inline },
        h2: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading2' }, allowed_children: :_inline },
        h3: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading3' }, allowed_children: :_inline },
        h4: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading4' }, allowed_children: :_inline },
        h5: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading5' }, allowed_children: :_inline },
        h6: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading6' }, allowed_children: :_inline },
        ol: { type: :block, ast_class: :list, properties: { pStyle: 'ListNumber' }, allowed_children: %i[ol li] },
        ul: { type: :block, ast_class: :list, properties: { pStyle: 'ListBullet' }, allowed_children: %i[ul li] },
        li: { type: :block, ast_class: :list_paragraph },

        # inline style tags for tables
        thead: { type: :inline, ast_class: nil, properties: { tblHeader: nil }, allowed_children: :tr },
        tbody: { type: :inline, ast_class: nil, properties: {}, allowed_children: :tr },
        tfoot: { type: :inline, ast_class: nil, properties: {}, allowed_children: :tr },

        # inline style tags
        span: { type: :inline, ast_class: nil, properties: {} },
        strong: { type: :inline, ast_class: nil, properties: { b: nil } },
        b: { type: :inline, ast_class: nil, properties: { b: nil } },
        em: { type: :inline, ast_class: nil, properties: { i: nil } },
        i: { type: :inline, ast_class: nil, properties: { i: nil } },
        u: { type: :inline, ast_class: nil, properties: { u: 'single' } },
        s: { type: :inline, ast_class: nil, properties: { strike: 'true' } },
        sub: { type: :inline, ast_class: nil, properties: { vertAlign: 'subscript' } },
        sup: { type: :inline, ast_class: nil, properties: { vertAlign: 'superscript' } },

        # inline content tags
        a: { type: :inline, ast_class: :hyperlink, properties: { rStyle: 'Hyperlink' } },
        text: { type: :inline, ast_class: :run, properties: {}, allowed_children: [] },
        br: { type: :inline, ast_class: :newline, properties: {}, allowed_children: [] }
      }
      # add all tags to the config object
      tags.each do |tag_name, settings|
        type = settings.delete(:type)
        register_html_tag(tag_name, type, **settings)
      end
    end

    # Defines an initial set of CSS -> WordML conversion lambdas stored in
    # a nested hash structure where the first key is the AST class and the
    # second is the conversion lambda
    def initialize_css_style_conversion
      @defined_style_conversions = {
        # styles shared or common logic across all node types go here.
        # Special conversion lambdas such as :_border can be
        # defined here for reuse across several AST nodes. Care must
        # be taken to avoid possible naming conflicts, hence the underscore.
        # AST class keys should be stored with their names converted from
        # camelcase to lowercased snakecase, i.e. TestCase = test_case
        node: {
          'background-color' => lambda { |v|
            return 'shd', { val: 'clear', fill: v.delete('#') }
          },
          _border: lambda { |v|
            props = { sz: 2, val: 'single', color: '000000' }
            vals = v.split
            vals[1] = 'single' if vals[1] == 'solid'
            #
            props[:sz] = @defined_style_conversions[:node][:_sz].call(vals[0])
            props[:val] = vals[1] if vals[1]
            props[:color] = vals[2].delete('#') if vals[2]
            #
            return props
          },
          _sz: lambda { |v|
            return nil unless v
            (2 * Float(v.gsub(/[^\d.]/, '')).ceil).to_s
          },
          'text-align' => ->(v) { return 'jc', v }
        },
        # Styles specific to the Table AST class
        table: {
          'border' => lambda { |v|
            props = @defined_style_conversions[:node][:_border].call(v)
            #
            return 'tblBorders', [
              { top: props }, { start: props }, { bottom: props },
              { end: props }, { insideH: props }, { insideV: props }
            ]
          },
          'margin' => lambda { |v|
            vals = v.split.map do |s|
              @defined_style_conversions[:node][:_sz].call(s)
            end
            #
            props = [vals[0], vals[0], vals[0], vals[0]] if vals.length == 1
            props = [vals[0], vals[1], vals[0], vals[1]] if vals.length == 2
            props = [vals[0], vals[1], vals[2], vals[1]] if vals.length == 3
            props = [vals[0], vals[1], vals[2], vals[3]] if vals.length > 3
            return 'tblCellMar', [
              { top: { w: props[0], type: 'dxa' } },
              { end: { w: props[1], type: 'dxa' } },
              { bottom: { w: props[2], type: 'dxa' } },
              { start: { w: props[3], type: 'dxa' } }
            ]
          },
          'cellspacing' => lambda { |v|
            v = @defined_style_conversions[:node][:_sz].call(v)
            return 'tblCellSpacing', { w: v, type: 'dxa' }
          },
          'width' => lambda { |v|
            v = @defined_style_conversions[:node][:_sz].call(v)
            return 'tblW', { w: v, type: 'dxa' }
          }
        },
        # Styles specific to the TableCell AST class
        table_cell: {
          'border' => lambda { |v|
            value = @defined_style_conversions[:table]['border'].call(v)[1]
            return 'tcBorders', value
          },
          'colspan' => ->(v) { return 'gridSpan', v },
          'margin' => lambda { |v|
            value = @defined_style_conversions[:table]['margin'].call(v)[1]
            return 'tcMar', value
          },
          'rowspan' => lambda { |v|
            return 'vMerge', 'restart' if v == 'start'
            return 'vMerge', v if v == 'continue'
            return 'vMerge', nil if v == 'end'
          },
          'vertical-align' => ->(v) { return 'vAlign', v },
          'white-space' => lambda { |v|
            return 'noWrap', nil if v == 'nowrap'
            return 'tcFitText', 'true' if v == 'fit'
          },
          'width' => lambda { |v|
            value = @defined_style_conversions[:table]['width'].call(v)[1]
            return 'tcW', value
          }
        },
        # Styles specific to the Paragraph AST class
        paragraph: {
          'border' => lambda { |v|
            props = @defined_style_conversions[:node][:_border].call(v)
            #
            return 'pBdr', [
              { top: props }, { bottom: props },
              { left: props }, { right: props }
            ]
          },
          'vertical-align' => ->(v) { return 'textAlignment', v }
        },
        # Styles specific to a run of text
        run: {
          'color' => ->(v) { return 'color', v.delete('#') },
          'font-size' => lambda { |v|
            return 'sz', @defined_style_conversions[:node][:_sz].call(v)
          },
          'font-style' => lambda { |v|
            return 'b', nil if v =~ /bold/
            return 'i', nil if v =~ /italic/
          },
          'font-weight' => ->(v) { return 'b', nil if v =~ /bold/ },
          'text-decoration' => lambda { |v|
            supported = %w[line-through underline]
            props = v.split
            return props[0], 'true' unless supported.include? props[0]
            return 'strike', 'true' if props[0] == 'line-through'
            return 'u', 'single' if props.length == 1
            return 'u', { val: props[1], color: 'auto' } if props.length == 2
            return 'u', { val: props[1], color: props[2].delete('#') }
          },
          'vertical-align' => lambda { |v|
            return 'vertAlign', 'subscript' if v =~ /sub/
            return 'vertAlign', 'superscript' if v =~ /super/
          }
        }
      }
    end
  end
end