#Writexlsx gem
# -*- coding: utf-8 -*-
# frozen_string_literal: true
require 'tmpdir'
#require 'tempfile'
require 'digest/md5'
#require 'stringio'
#require 'delegate'
require 'date'
require 'kconv'
#require 'fileutils'
#require 'singleton'

module Writexlsx_kot
  OFFICE_URL = 'http://schemas.microsoft.com/office/'   # :nodoc:
  WriteXLSX_VERSION = "1.11.2"
module Utility
    ROW_MAX       = 1048576  # :nodoc:
    COL_MAX       = 16384    # :nodoc:
    STR_MAX       = 32767    # :nodoc:
    SHEETNAME_MAX = 31  # :nodoc:
    CHAR_WIDTHS   = {
      ' '  =>  3, '!' =>  5, '"' =>  6, '#' =>  7, '$' =>  7, '%' => 11,
      '&'  => 10, "'" =>  3, '(' =>  5, ')' =>  5, '*' =>  7, '+' =>  7,
      ','  =>  4, '-' =>  5, '.' =>  4, '/' =>  6, '0' =>  7, '1' =>  7,
      '2'  =>  7, '3' =>  7, '4' =>  7, '5' =>  7, '6' =>  7, '7' =>  7,
      '8'  =>  7, '9' =>  7, ':' =>  4, ';' =>  4, '<' =>  7, '=' =>  7,
      '>'  =>  7, '?' =>  7, '@' => 13, 'A' =>  9, 'B' =>  8, 'C' =>  8,
      'D'  =>  9, 'E' =>  7, 'F' =>  7, 'G' =>  9, 'H' =>  9, 'I' =>  4,
      'J'  =>  5, 'K' =>  8, 'L' =>  6, 'M' => 12, 'N' => 10, 'O' => 10,
      'P'  =>  8, 'Q' => 10, 'R' =>  8, 'S' =>  7, 'T' =>  7, 'U' =>  9,
      'V'  =>  9, 'W' => 13, 'X' =>  8, 'Y' =>  7, 'Z' =>  7, '[' =>  5,
      '\\' =>  6, ']' =>  5, '^' =>  7, '_' =>  7, '`' =>  4, 'a' =>  7,
      'b'  =>  8, 'c' =>  6, 'd' =>  8, 'e' =>  8, 'f' =>  5, 'g' =>  7,
      'h'  =>  8, 'i' =>  4, 'j' =>  4, 'k' =>  7, 'l' =>  4, 'm' => 12,
      'n'  =>  8, 'o' =>  8, 'p' =>  8, 'q' =>  8, 'r' =>  5, 's' =>  6,
      't'  =>  5, 'u' =>  8, 'v' =>  7, 'w' => 11, 'x' =>  7, 'y' =>  7,
      'z'  =>  6, '{' =>  5, '|' =>  7, '}' =>  5, '~' =>  7
    }.freeze

    #
    # xl_rowcol_to_cell($row, col, row_absolute, col_absolute)
    #
    def xl_rowcol_to_cell(row_or_name, col, row_absolute = false, col_absolute = false)
      if row_or_name.is_a?(Integer)
        row_or_name += 1      # Change from 0-indexed to 1 indexed.
      end
      col_str = xl_col_to_name(col, col_absolute)
      "#{col_str}#{absolute_char(row_absolute)}#{row_or_name}"
    end

    #
    # Returns: [row, col, row_absolute, col_absolute]
    #
    # The row_absolute and col_absolute parameters aren't documented because they
    # mainly used internally and aren't very useful to the user.
    #
    def xl_cell_to_rowcol(cell)
      cell =~ /(\$?)([A-Z]{1,3})(\$?)(\d+)/

      col_abs = ::Regexp.last_match(1) != ""
      col     = ::Regexp.last_match(2)
      row_abs = ::Regexp.last_match(3) != ""
      row     = ::Regexp.last_match(4).to_i

      # Convert base26 column string to number
      # All your Base are belong to us.
      chars = col.split("")
      expn = 0
      col = 0

      chars.reverse.each do |char|
        col += (char.ord - 'A'.ord + 1) * (26**expn)
        expn += 1
      end

      # Convert 1-index to zero-index
      row -= 1
      col -= 1

      [row, col, row_abs, col_abs]
    end

    def xl_col_to_name(col, col_absolute)
      col_str = ColName.instance.col_str(col)
      if col_absolute
        "#{absolute_char(col_absolute)}#{col_str}"
      else
        # Do not allocate new string
        col_str
      end
    end

    def xl_range(row_1, row_2, col_1, col_2,
                 row_abs_1 = false, row_abs_2 = false, col_abs_1 = false, col_abs_2 = false)
      range1 = xl_rowcol_to_cell(row_1, col_1, row_abs_1, col_abs_1)
      range2 = xl_rowcol_to_cell(row_2, col_2, row_abs_2, col_abs_2)

      if range1 == range2
        range1
      else
        "#{range1}:#{range2}"
      end
    end

    def xl_range_formula(sheetname, row_1, row_2, col_1, col_2)
      # Use Excel's conventions and quote the sheet name if it contains any
      # non-word character or if it isn't already quoted.
      sheetname = "'#{sheetname}'" if sheetname =~ /\W/ && !(sheetname =~ /^'/)

      range1 = xl_rowcol_to_cell(row_1, col_1, 1, 1)
      range2 = xl_rowcol_to_cell(row_2, col_2, 1, 1)

      "=#{sheetname}!#{range1}:#{range2}"
    end

    #
    # xl_string_pixel_width($string)
    #
    # Get the pixel width of a string based on individual character widths taken
    # from Excel. UTF8 characters are given a default width of 8.
    #
    # Note, Excel adds an additional 7 pixels padding to a cell.
    #
    def xl_string_pixel_width(string)
      length = 0
      string.to_s.split("").each { |char| length += CHAR_WIDTHS[char] || 8 }

      length
    end

    #
    # Sheetnames used in references should be quoted if they contain any spaces,
    # special characters or if the look like something that isn't a sheet name.
    # TODO. We need to handle more special cases.
    #
    def quote_sheetname(sheetname) # :nodoc:
      # Use Excel's conventions and quote the sheet name if it comtains any
      # non-word character or if it isn't already quoted.
      name = sheetname.dup
      if name =~ /\W/ && !(name =~ /^'/)
        # Double quote and single quoted strings.
        name = name.gsub("'", "''")
        name = "'#{name}'"
      end
      name
    end

    def check_dimensions(row, col)
      raise WriteXLSXDimensionError if !row || row >= ROW_MAX || !col || col >= COL_MAX

      0
    end

    #
    # convert_date_time(date_time_string)
    #
    # The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format
    # and converts it to a decimal number representing a valid Excel date.
    #
    def convert_date_time(date_time_string)       # :nodoc:
      date_time = date_time_string.to_s.sub(/^\s+/, '').sub(/\s+$/, '').sub(/Z$/, '')

      # Check for invalid date char.
      return nil if date_time =~ /[^0-9T:\-.Z]/

      # Check for "T" after date or before time.
      return nil unless date_time =~ /\dT|T\d/

      days      = 0 # Number of days since epoch
      seconds   = 0 # Time expressed as fraction of 24h hours in seconds

      # Split into date and time.
      date, time = date_time.split("T")

      # We allow the time portion of the input DateTime to be optional.
      if time
        # Match hh:mm:ss.sss+ where the seconds are optional
        if time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/
          hour   = ::Regexp.last_match(1).to_i
          min    = ::Regexp.last_match(2).to_i
          sec    = ::Regexp.last_match(4).to_f || 0
        else
          return nil # Not a valid time format.
        end

        # Some boundary checks
        return nil if hour >= 24
        return nil if min  >= 60
        return nil if sec  >= 60

        # Excel expresses seconds as a fraction of the number in 24 hours.
        seconds = ((hour * 60 * 60) + (min * 60) + sec) / (24.0 * 60 * 60)
      end

      # We allow the date portion of the input DateTime to be optional.
      return seconds if date == ''

      # Match date as yyyy-mm-dd.
      if date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/
        year   = ::Regexp.last_match(1).to_i
        month  = ::Regexp.last_match(2).to_i
        day    = ::Regexp.last_match(3).to_i
      else
        return nil  # Not a valid date format.
      end

      # Set the epoch as 1900 or 1904. Defaults to 1900.
      # Special cases for Excel.
      unless date_1904?
        return      seconds if date == '1899-12-31' # Excel 1900 epoch
        return      seconds if date == '1900-01-00' # Excel 1900 epoch
        return 60 + seconds if date == '1900-02-29' # Excel false leapday
      end

      # We calculate the date by calculating the number of days since the epoch
      # and adjust for the number of leap days. We calculate the number of leap
      # days by normalising the year in relation to the epoch. Thus the year 2000
      # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
      #
      epoch   = date_1904? ? 1904 : 1900
      offset  = date_1904? ? 4 : 0
      norm    = 300
      range   = year - epoch

      # Set month days and check for leap year.
      mdays   = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      leap    = 0
      leap    = 1  if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
      mdays[1] = 29 if leap != 0

      # Some boundary checks
      return nil if year  < epoch or year  > 9999
      return nil if month < 1     or month > 12
      return nil if day   < 1     or day   > mdays[month - 1]

      # Accumulate the number of days since the epoch.
      days = day                               # Add days for current month
      (0..month - 2).each do |m|
        days += mdays[m]                      # Add days for past months
      end
      days += range * 365                      # Add days for past years
      days += (range / 4)    # Add leapdays
      days -= ((range + offset) / 100)    # Subtract 100 year leapdays
      days += ((range + offset + norm) / 400)    # Add 400 year leapdays
      days -= leap                             # Already counted above

      # Adjust for Excel erroneously treating 1900 as a leap year.
      days += 1 if !date_1904? and days > 59

      date_time = sprintf("%0.10f", days + seconds)
      date_time = date_time.sub(/\.?0+$/, '') if date_time =~ /\./
      if date_time =~ /\./
        date_time.to_f
      else
        date_time.to_i
      end
    end

    def escape_url(url)
      unless url =~ /%[0-9a-fA-F]{2}/
        # Escape the URL escape symbol.
        url = url.gsub("%", "%25")

        # Escape whitespae in URL.
        url = url.gsub(/[\s\x00]/, '%20')

        # Escape other special characters in URL.
        re = /(["<>\[\]`^{}])/
        while re =~ url
          match = $~[1]
          url = url.sub(re, sprintf("%%%x", match.ord))
        end
      end

      url
    end

    def absolute_char(absolute)
      absolute ? '$' : ''
    end

    def xml_str
      @writer.string
    end

    def self.delete_files(path)
      if FileTest.file?(path)
        File.delete(path)
      elsif FileTest.directory?(path)
        Dir.foreach(path) do |file|
          next if file =~ /^\.\.?$/  # '.' or '..'

          delete_files(path.sub(%r{/+$}, "") + '/' + file)
        end
        Dir.rmdir(path)
      end
    end

    def put_deprecate_message(method)
      warn("Warning: calling deprecated method #{method}. This method will be removed in a future release.")
    end

    # Check for a cell reference in A1 notation and substitute row and column
    def row_col_notation(row_or_a1)   # :nodoc:
      substitute_cellref(row_or_a1) if row_or_a1.respond_to?(:match) && row_or_a1.to_s =~ /^\D/
    end

    #
    # Substitute an Excel cell reference in A1 notation for  zero based row and
    # column values in an argument list.
    #
    # Ex: ("A4", "Hello") is converted to (3, 0, "Hello").
    #
    def substitute_cellref(cell, *args)       # :nodoc:
      #      return [*args] if cell.respond_to?(:coerce) # Numeric

      normalized_cell = cell.upcase

      case normalized_cell
      # Convert a column range: 'A:A' or 'B:G'.
      # A range such as A:A is equivalent to A1:65536, so add rows as required
      when /\$?([A-Z]{1,3}):\$?([A-Z]{1,3})/
        row1, col1 =  xl_cell_to_rowcol(::Regexp.last_match(1) + '1')
        row2, col2 =  xl_cell_to_rowcol(::Regexp.last_match(2) + ROW_MAX.to_s)
        [row1, col1, row2, col2, *args]
      # Convert a cell range: 'A1:B7'
      when /\$?([A-Z]{1,3}\$?\d+):\$?([A-Z]{1,3}\$?\d+)/
        row1, col1 =  xl_cell_to_rowcol(::Regexp.last_match(1))
        row2, col2 =  xl_cell_to_rowcol(::Regexp.last_match(2))
        [row1, col1, row2, col2, *args]
      # Convert a cell reference: 'A1' or 'AD2000'
      when /\$?([A-Z]{1,3}\$?\d+)/
        row1, col1 = xl_cell_to_rowcol(::Regexp.last_match(1))
        [row1, col1, *args]
      else
        raise("Unknown cell reference #{normalized_cell}")
      end
    end

    def underline_attributes(underline)
      if underline == 2
        [%w[val double]]
      elsif underline == 33
        [%w[val singleAccounting]]
      elsif underline == 34
        [%w[val doubleAccounting]]
      else
        []    # Default to single underline.
      end
    end

    #
    # Write the <color> element.
    #
    def write_color(name, value, writer = @writer) # :nodoc:
      attributes = [[name, value]]

      writer.empty_tag('color', attributes)
    end

    PERL_TRUE_VALUES = [false, nil, 0, "0", "", [], {}].freeze
    #
    # return perl's boolean result
    #
    def ptrue?(value)
      if PERL_TRUE_VALUES.include?(value)
        false
      else
        true
      end
    end

    def check_parameter(params, valid_keys, method)
      invalids = params.keys - valid_keys
      unless invalids.empty?
        raise WriteXLSXOptionParameterError,
              "Unknown parameter '#{invalids.join(", ")}' in #{method}."
      end
      true
    end

    #
    # Check that row and col are valid and store max and min values for use in
    # other methods/elements.
    #
    def check_dimensions_and_update_max_min_values(row, col, ignore_row = 0, ignore_col = 0)       # :nodoc:
      check_dimensions(row, col)
      store_row_max_min_values(row) if ignore_row == 0
      store_col_max_min_values(col) if ignore_col == 0

      0
    end

    def store_row_max_min_values(row)
      @dim_rowmin = row if !@dim_rowmin || (row < @dim_rowmin)
      @dim_rowmax = row if !@dim_rowmax || (row > @dim_rowmax)
    end

    def store_col_max_min_values(col)
      @dim_colmin = col if !@dim_colmin || (col < @dim_colmin)
      @dim_colmax = col if !@dim_colmax || (col > @dim_colmax)
    end

    def float_to_str(float)
      return '' unless float

      if float == float.to_i
        float.to_i.to_s
      else
        float.to_s
      end
    end

    #
    # Convert user defined legend properties to the structure required internally.
    #
    def legend_properties(params)
      legend = Writexlsx_kot::Chart::Legend.new

      legend.position      = params[:position] || 'right'
      legend.delete_series = params[:delete_series]
      legend.font          = convert_font_args(params[:font])

      # Set the legend layout.
      legend.layout = layout_properties(params[:layout])

      # Turn off the legend.
      legend.position = 'none' if params[:none]

      # Set the line properties for the legend.
      line = line_properties(params[:line])

      # Allow 'border' as a synonym for 'line'.
      line = line_properties(params[:border]) if params[:border]

      # Set the fill properties for the legend.
      fill = fill_properties(params[:fill])

      # Set the pattern properties for the legend.
      pattern = pattern_properties(params[:pattern])

      # Set the gradient fill properties for the legend.
      gradient = gradient_properties(params[:gradient])

      # Pattern fill overrides solid fill.
      fill = nil if pattern

      # Gradient fill overrides solid and pattern fills.
      if gradient
        pattern = nil
        fill    = nil
      end

      # Set the legend layout.
      layout = layout_properties(params[:layout])

      legend.line     = line
      legend.fill     = fill
      legend.pattern  = pattern
      legend.gradient = gradient
      legend.layout   = layout

      @legend = legend
    end

    #
    # Convert user defined layout properties to the format required internally.
    #
    def layout_properties(args, is_text = false)
      return unless ptrue?(args)

      properties = is_text ? %i[x y] : %i[x y width height]

      # Check for valid properties.
      args.keys.each do |key|
        raise "Property '#{key}' not allowed in layout options\n" unless properties.include?(key.to_sym)
      end

      # Set the layout properties
      layout = {}
      properties.each do |property|
        value = args[property]
        # Convert to the format used by Excel for easier testing.
        layout[property] = sprintf("%.17g", value)
      end

      layout
    end

    #
    # Convert vertices from pixels to points.
    #
    def pixels_to_points(vertices)
      _col_start, _row_start, _x1,    _y1,
      _col_end,   _row_end,   _x2,    _y2,
      left,      top,       width, height  = vertices.flatten

      left   *= 0.75
      top    *= 0.75
      width  *= 0.75
      height *= 0.75

      [left, top, width, height]
    end

    def v_shape_attributes_base(id)
      [
        ['id',    "_x0000_s#{id}"],
        ['type',  type]
      ]
    end

    def v_shape_style_base(z_index, vertices)
      left, top, width, height = pixels_to_points(vertices)

      left_str    = float_to_str(left)
      top_str     = float_to_str(top)
      width_str   = float_to_str(width)
      height_str  = float_to_str(height)
      z_index_str = float_to_str(z_index)

      shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
    end

    def shape_style_base(left_str, top_str, width_str, height_str, z_index_str)
      [
        'position:absolute;',
        'margin-left:',
        left_str, 'pt;',
        'margin-top:',
        top_str, 'pt;',
        'width:',
        width_str, 'pt;',
        'height:',
        height_str, 'pt;',
        'z-index:',
        z_index_str, ';'
      ]
    end

    #
    # Write the <v:fill> element.
    #
    def write_fill
      @writer.empty_tag('v:fill', fill_attributes)
    end

    #
    # Write the <v:path> element.
    #
    def write_comment_path(gradientshapeok, connecttype)
      attributes      = []

      attributes << %w[gradientshapeok t] if gradientshapeok
      attributes << ['o:connecttype', connecttype]

      @writer.empty_tag('v:path', attributes)
    end

    #
    # Write the <x:Anchor> element.
    #
    def write_anchor
      col_start, row_start, x1, y1, col_end, row_end, x2, y2 = vertices
      data = [col_start, x1, row_start, y1, col_end, x2, row_end, y2].join(', ')

      @writer.data_element('x:Anchor', data)
    end

    #
    # Write the <x:AutoFill> element.
    #
    def write_auto_fill
      @writer.data_element('x:AutoFill', 'False')
    end

    #
    # Write the <div> element.
    #
    def write_div(align, font = nil)
      style = "text-align:#{align}"
      attributes = [['style', style]]

      @writer.tag_elements('div', attributes) do
        if font
          # Write the font element.
          write_font(font)
        end
      end
    end

    #
    # Write the <font> element.
    #
    def write_font(font)
      caption = font[:_caption]
      face    = 'Calibri'
      size    = 220
      color   = '#000000'

      attributes = [
        ['face',  face],
        ['size',  size],
        ['color', color]
      ]
      @writer.data_element('font', caption, attributes)
    end

    #
    # Write the <v:stroke> element.
    #
    def write_stroke
      attributes = [%w[joinstyle miter]]

      @writer.empty_tag('v:stroke', attributes)
    end

    def r_id_attributes(id)
      ['r:id', "rId#{id}"]
    end

    def write_xml_declaration
      @writer.xml_decl
      yield
      @writer.crlf
      @writer.close
    end

    #
    # Convert user defined line properties to the structure required internally.
    #
    def line_properties(line) # :nodoc:
      line_fill_properties(line) do
        value_or_raise(dash_types, line[:dash_type], 'dash type')
      end
    end

    #
    # Convert user defined fill properties to the structure required internally.
    #
    def fill_properties(fill) # :nodoc:
      line_fill_properties(fill)
    end

    #
    # Convert user defined pattern properties to the structure required internally.
    #
    def pattern_properties(args) # :nodoc:
      pattern = {}

      return nil unless args

      # Check the pattern type is present.
      return nil unless args.has_key?(:pattern)

      # Check the foreground color is present.
      retuen nil unless args.has_key?(:fg_color)

      types = {
        'percent_5'                => 'pct5',
        'percent_10'               => 'pct10',
        'percent_20'               => 'pct20',
        'percent_25'               => 'pct25',
        'percent_30'               => 'pct30',
        'percent_40'               => 'pct40',

        'percent_50'               => 'pct50',
        'percent_60'               => 'pct60',
        'percent_70'               => 'pct70',
        'percent_75'               => 'pct75',
        'percent_80'               => 'pct80',
        'percent_90'               => 'pct90',

        'light_downward_diagonal'  => 'ltDnDiag',
        'light_upward_diagonal'    => 'ltUpDiag',
        'dark_downward_diagonal'   => 'dkDnDiag',
        'dark_upward_diagonal'     => 'dkUpDiag',
        'wide_downward_diagonal'   => 'wdDnDiag',
        'wide_upward_diagonal'     => 'wdUpDiag',

        'light_vertical'           => 'ltVert',
        'light_horizontal'         => 'ltHorz',
        'narrow_vertical'          => 'narVert',
        'narrow_horizontal'        => 'narHorz',
        'dark_vertical'            => 'dkVert',
        'dark_horizontal'          => 'dkHorz',

        'dashed_downward_diagonal' => 'dashDnDiag',
        'dashed_upward_diagonal'   => 'dashUpDiag',
        'dashed_horizontal'        => 'dashHorz',
        'dashed_vertical'          => 'dashVert',
        'small_confetti'           => 'smConfetti',
        'large_confetti'           => 'lgConfetti',

        'zigzag'                   => 'zigZag',
        'wave'                     => 'wave',
        'diagonal_brick'           => 'diagBrick',
        'horizontal_brick'         => 'horzBrick',
        'weave'                    => 'weave',
        'plaid'                    => 'plaid',

        'divot'                    => 'divot',
        'dotted_grid'              => 'dotGrid',
        'dotted_diamond'           => 'dotDmnd',
        'shingle'                  => 'shingle',
        'trellis'                  => 'trellis',
        'sphere'                   => 'sphere',

        'small_grid'               => 'smGrid',
        'large_grid'               => 'lgGrid',
        'small_check'              => 'smCheck',
        'large_check'              => 'lgCheck',
        'outlined_diamond'         => 'openDmnd',
        'solid_diamond'            => 'solidDmnd'
      }

      # Check for valid types.
      if types[args[:pattern]]
        pattern[:pattern] = types[args[:pattern]]
      else
        raise "Unknown pattern type '#{args[:pattern]}'"
      end

      pattern[:bg_color] = args[:bg_color] || '#FFFFFF'
      pattern[:fg_color] = args[:fg_color]

      pattern
    end

    def line_fill_properties(params)
      return { _defined: 0 } unless params

      ret = params.dup
      ret[:dash_type] = yield if block_given? && ret[:dash_type]
      ret[:_defined] = 1
      ret
    end

    def dash_types
      {
        solid:               'solid',
        round_dot:           'sysDot',
        square_dot:          'sysDash',
        dash:                'dash',
        dash_dot:            'dashDot',
        long_dash:           'lgDash',
        long_dash_dot:       'lgDashDot',
        long_dash_dot_dot:   'lgDashDotDot',
        dot:                 'dot',
        system_dash_dot:     'sysDashDot',
        system_dash_dot_dot: 'sysDashDotDot'
      }
    end

    def value_or_raise(hash, key, msg)
      raise "Unknown #{msg} '#{key}'" if hash[key.to_sym].nil?

      hash[key.to_sym]
    end

    def palette_color(index)
      # Adjust the colour index.
      idx = index - 8

      r, g, b = @palette[idx]
      sprintf("%02X%02X%02X", r, g, b)
    end

    #
    # Workbook の生成時のオプションハッシュを解析する
    #
    def process_workbook_options(*params)
      case params.size
      when 0
        [{}, {}]
      when 1 # one hash
        options_keys = %i[tempdir date_1904 optimization excel2003_style strings_to_urls]

        hash = params.first
        options = hash.reject { |k, _v| !options_keys.include?(k) }

        default_format_properties =
          hash[:default_format_properties] ||
          hash.reject { |k, _v| options_keys.include?(k) }

        [options, default_format_properties.dup]
      when 2 # array which includes options and default_format_properties
        options, default_format_properties = params
        default_format_properties ||= {}

        [options.dup, default_format_properties.dup]
      end
    end

    #
    # Convert user defined font values into private hash values.
    #
    def convert_font_args(params)
      return unless params

      font = params_to_font(params)

      # Convert font size units.
      font[:_size] *= 100 if font[:_size] && font[:_size] != 0

      # Convert rotation into 60,000ths of a degree.
      font[:_rotation] = 60_000 * font[:_rotation].to_i if ptrue?(font[:_rotation])

      font
    end

    def params_to_font(params)
      {
        _name:         params[:name],
        _color:        params[:color],
        _size:         params[:size],
        _bold:         params[:bold],
        _italic:       params[:italic],
        _underline:    params[:underline],
        _pitch_family: params[:pitch_family],
        _charset:      params[:charset],
        _baseline:     params[:baseline] || 0,
        _rotation:     params[:rotation]
      }
    end

    #
    # Write the <c:txPr> element.
    #
    def write_tx_pr(font, is_y_axis = nil) # :nodoc:
      rotation = nil
      rotation = font[:_rotation] if font && font.respond_to?(:[]) && font[:_rotation]
      @writer.tag_elements('c:txPr') do
        # Write the a:bodyPr element.
        write_a_body_pr(rotation, is_y_axis)
        # Write the a:lstStyle element.
        write_a_lst_style
        # Write the a:p element.
        write_a_p_formula(font)
      end
    end

    #
    # Write the <a:bodyPr> element.
    #
    def write_a_body_pr(rot, is_y_axis = nil) # :nodoc:
      rot = -5400000 if !rot && ptrue?(is_y_axis)
      attributes = []
      if rot
        if rot == 16_200_000
          # 270 deg/stacked angle.
          attributes << ['rot',  0]
          attributes << %w[vert wordArtVert]
        elsif rot == 16_260_000
          # 271 deg/stacked angle.
          attributes << ['rot',  0]
          attributes << %w[vert eaVert]
        else
          attributes << ['rot',  rot]
          attributes << %w[vert horz]
        end
      end

      @writer.empty_tag('a:bodyPr', attributes)
    end

    #
    # Write the <a:lstStyle> element.
    #
    def write_a_lst_style # :nodoc:
      @writer.empty_tag('a:lstStyle')
    end

    #
    # Write the <a:p> element for formula titles.
    #
    def write_a_p_formula(font = nil) # :nodoc:
      @writer.tag_elements('a:p') do
        # Write the a:pPr element.
        write_a_p_pr_formula(font)
        # Write the a:endParaRPr element.
        write_a_end_para_rpr
      end
    end

    #
    # Write the <a:pPr> element for formula titles.
    #
    def write_a_p_pr_formula(font) # :nodoc:
      @writer.tag_elements('a:pPr') { write_a_def_rpr(font) }
    end

    #
    # Write the <a:defRPr> element.
    #
    def write_a_def_rpr(font = nil) # :nodoc:
      write_def_rpr_r_pr_common(
        font,
        get_font_style_attributes(font),
        'a:defRPr'
      )
    end

    def write_def_rpr_r_pr_common(font, style_attributes, tag)  # :nodoc:
      latin_attributes = get_font_latin_attributes(font)
      has_color = ptrue?(font) && ptrue?(font[:_color])

      if !latin_attributes.empty? || has_color
        @writer.tag_elements(tag, style_attributes) do
          write_a_solid_fill(color: font[:_color]) if has_color
          write_a_latin(latin_attributes) unless latin_attributes.empty?
        end
      else
        @writer.empty_tag(tag, style_attributes)
      end
    end

    #
    # Get the font latin attributes from a font hash.
    #
    def get_font_latin_attributes(font)
      return [] unless font
      return [] unless font.respond_to?(:[])

      attributes = []
      attributes << ['typeface', font[:_name]]            if ptrue?(font[:_name])
      attributes << ['pitchFamily', font[:_pitch_family]] if font[:_pitch_family]
      attributes << ['charset', font[:_charset]]          if font[:_charset]

      attributes
    end

    #
    # Write the <a:solidFill> element.
    #
    def write_a_solid_fill(fill) # :nodoc:
      @writer.tag_elements('a:solidFill') do
        if fill[:color]
          # Write the a:srgbClr element.
          write_a_srgb_clr(color(fill[:color]), fill[:transparency])
        end
      end
    end

    #
    # Write the <a:srgbClr> element.
    #
    def write_a_srgb_clr(color, transparency = nil) # :nodoc:
      tag        = 'a:srgbClr'
      attributes = [['val', color]]

      if ptrue?(transparency)
        @writer.tag_elements(tag, attributes) do
          write_a_alpha(transparency)
        end
      else
        @writer.empty_tag(tag, attributes)
      end
    end

    #
    # Convert the user specified colour index or string to a rgb colour.
    #
    def color(color_code) # :nodoc:
      if color_code and color_code =~ /^#[0-9a-fA-F]{6}$/
        # Convert a HTML style #RRGGBB color.
        color_code.sub(/^#/, '').upcase
      else
        index = Format.color(color_code)
        raise "Unknown color '#{color_code}' used in chart formatting." unless index

        palette_color(index)
      end
    end

    #
    # Get the font style attributes from a font hash.
    #
    def get_font_style_attributes(font)
      return [] unless font
      return [] unless font.respond_to?(:[])

      attributes = []
      attributes << ['sz', font[:_size]]      if ptrue?(font[:_size])
      attributes << ['b',  font[:_bold]]      if font[:_bold]
      attributes << ['i',  font[:_italic]]    if font[:_italic]
      attributes << %w[u sng]             if font[:_underline]

      # Turn off baseline when testing fonts that don't have it.
      attributes << ['baseline', font[:_baseline]] if font[:_baseline] != -1
      attributes
    end

    #
    # Write the <a:endParaRPr> element.
    #
    def write_a_end_para_rpr # :nodoc:
      @writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
    end
  end
module Gradient
    def gradient_properties(args)
      return unless ptrue?(args)

      gradient = {}

      types    = {
        'linear'      => 'linear',
        'radial'      => 'circle',
        'rectangular' => 'rect',
        'path'        => 'shape'
      }

      # Check the colors array exists and is valid.
      raise "Gradient must include colors array" unless ptrue?(args[:colors])
      # Check the colors array has the right number of entries.
      raise "Gradient colors array must include at least 2 values" if args[:colors].size < 2

      gradient[:colors] = args[:colors]

      if ptrue?(args[:positions])
        # Check the positions array has the right number of entries.
        raise "Gradient positions not equal to numbers of colors" unless args[:positions].size == args[:colors].size

        # Check the positions are in the correct range.
        args[:positions].each do |pos|
          raise "Gradient position '#{pos} must be in range 0 <= pos <= 100" if pos < 0 || pos > 100
        end
        gradient[:positions] = args[:positions]
      else
        # Use the default gradient positions.
        case args[:colors].size
        when 2
          gradient[:positions] = [0, 100]
        when 3
          gradient[:positions] = [0, 50, 100]
        when 4
          gradient[:positions] = [0, 33, 66, 100]
        else
          raise "Must specify gradient positions"
        end
      end

      # Set the gradient angle.
      if args[:angle]
        angle = args[:angle]

        raise "Gradient angle '#{angle} must be in range 0 <= pos < 360" if angle < 0 || angle > 359.9

        gradient[:angle] = angle
      else
        gradient[:angle] = 90
      end

      # Set the gradient type.
      if args[:type]
        type = args[:type]

        raise "Unknow gradient type '#{type}'" unless types[type]

        gradient[:type] = types[type]
      else
        gradient[:type] = 'linear'
      end

      gradient
    end
  end
module WriteDPtPoint
    #
    # Write an individual <c:dPt> element. Override the parent method to add
    # markers.
    #
    def write_d_pt_point(index, point)
      @writer.tag_elements('c:dPt') do
        # Write the c:idx element.
        write_idx(index)
        @writer.tag_elements('c:marker') do
          # Write the c:spPr element.
          write_sp_pr(point)
        end
      end
    end
  end
module ZipFileUtils
  # src  file or directory
  # dest  zip filename
  # options :fs_encoding=[UTF-8,Shift_JIS,EUC-JP]
  def self.zip(src, dest, options = {})
    src = File.expand_path(src)
    dest = File.expand_path(dest)
    FileUtils.rm_f(dest)
    Zip_kot::ZipFile.open(dest, Zip_kot::ZipFile::CREATE) do |zf|
      if File.file?(src)
        zf.add(encode_path(File.basename(src), options[:fs_encoding]), src)
        break
      else
        each_dir_for(src) do |path|
          if File.file?(path)
            zf.add(encode_path(relative(path, src), options[:fs_encoding]), path)
          elsif File.directory?(path)
            zf.mkdir(encode_path(relative(path, src), options[:fs_encoding]))
          end
        end
      end
    end
    FileUtils.chmod(0o644, dest)
  end

  # src  zip filename
  # dest  destination directory
  # options :fs_encoding=[UTF-8,Shift_JIS,EUC-JP]
  def self.unzip(src, dest, options = {})
    FileUtils.makedirs(dest)
    Zip_kot::InputStream.open(src) do |is|
      loop do
        entry = is.get_next_entry
        break unless entry

        dir = File.dirname(entry.name)
        FileUtils.makedirs(dest + '/' + dir)
        path = encode_path(dest + '/' + entry.name, options[:fs_encoding])
        if entry.file?
          File.open(path, File::CREAT | File::WRONLY | File::BINARY) do |w|
            w.puts(is.read)
          end
        else
          FileUtils.makedirs(path)
        end
      end
    end
  end

  def self.each_dir_for(dir_path, &block)
    each_file_for(dir_path, &block)
  end

  def self.each_file_for(path, &block)
    if File.file?(path)
      yield(path)
      return true
    end
    dir = Dir.open(path)
    file_exist = false
    dir.each do |file|
      next if ['.', '..'].include?(file)

      file_exist = true if each_file_for(path + "/" + file, &block)
    end
    yield(path) unless file_exist
    file_exist
  end

  def self.relative(path, base_dir)
    path[base_dir.length + 1..path.length] if path.index(base_dir) == 0
  end

  def self.encode_path(path, encode_s)
    return path unless encode_s

    case encode_s
    when 'UTF-8'
      path.toutf8
    when 'Shift_JIS'
      path.tosjis
    when 'EUC-JP'
      path.toeuc
    else
      path
    end
  end
end

module Package
    class XMLWriterSimple
      XMLNS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'

      def initialize
        @io = StringIO.new
        # Will allocate new string once, then use allocated string
        # Key is tag name
        # Only tags without attributes will be cached
        @tag_start_cache = {}
        @tag_end_cache = {}
      end

      def set_xml_writer(filename = nil)
        @filename = filename
      end

      def xml_decl(encoding = 'UTF-8', standalone = true)
        str = %(<?xml version="1.0" encoding="#{encoding}" standalone="#{standalone ? "yes" : "no"}"?>\n)
        io_write(str)
      end

      def tag_elements(tag, attributes = nil)
        start_tag(tag, attributes)
        yield
        end_tag(tag)
      end

      def tag_elements_str(tag, attributes = nil)
        start_tag_str(tag, attributes) +
          yield +
          end_tag_str(tag)
      end

      def start_tag(tag, attr = nil)
        io_write(start_tag_str(tag, attr))
      end

      def start_tag_str(tag, attr = nil)
        if attr.nil? || attr.empty?
          @tag_start_cache[tag] ||= "<#{tag}>"
        else
          "<#{tag}#{key_vals(attr)}>"
        end
      end

      def end_tag(tag)
        io_write(end_tag_str(tag))
      end

      def end_tag_str(tag)
        @tag_end_cache[tag] ||= "</#{tag}>"
      end

      def empty_tag(tag, attr = nil)
        str = "<#{tag}#{key_vals(attr)}/>"
        io_write(str)
      end

      def data_element(tag, data, attr = nil)
        tag_elements(tag, attr) { io_write(escape_data(data)) }
      end

      #
      # Optimised tag writer ?  for shared strings <si> elements.
      #
      def si_element(data, attr)
        tag_elements('si') { data_element('t', data, attr) }
      end

      #
      # Optimised tag writer for shared strings <si> rich string elements.
      #
      def si_rich_element(data)
        io_write("<si>#{data}</si>")
      end

      def characters(data)
        io_write(escape_data(data))
      end

      def crlf
        io_write("\n")
      end

      def close
        File.open(@filename, "wb:utf-8:utf-8") { |f| f << string } if @filename
        @io.close
      end

      def string
        @io.string
      end

      def io_write(str)
        @io << str
        str
      end

      private

      def key_vals(attribute)
        if attribute
          result = "".dup
          attribute.each do |attr|
            # Generate and concat %( #{key}="#{val}") values for attribute pair
            result << " "
            result << attr.first.to_s
            result << '="'
            result << escape_attributes(attr.last).to_s
            result << '"'
          end
          result
        end
      end

      def escape_attributes(str = '')
        return str unless str.respond_to?(:match) && str =~ /["&<>\n]/

        str
          .gsub("&", "&amp;")
          .gsub('"', "&quot;")
          .gsub("<", "&lt;")
          .gsub(">", "&gt;")
          .gsub("\n", "&#xA;")
      end

      def escape_data(str = '')
        if str.respond_to?(:match) && str =~ /[&<>]/
          str.gsub("&", '&amp;')
             .gsub("<", '&lt;')
             .gsub(">", '&gt;')
        else
          str
        end
      end
    end
	class App
      include Writexlsx_kot::Utility
      attr_writer :doc_security

      def initialize(workbook)
        @writer = Package::XMLWriterSimple.new
        @workbook      = workbook
        @part_names    = []
        @heading_pairs = []
        @properties    = {}
        @doc_security  = 0
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_properties do
            write_application
            write_doc_security
            write_scale_crop
            write_heading_pairs
            write_titles_of_parts
            write_manager
            write_company
            write_links_up_to_date
            write_shared_doc
            write_hyperlink_base
            write_hyperlinks_changed
            write_app_version
          end
        end
      end

      def add_worksheet_heading_pairs
        add_heading_pair(
          [
            'Worksheets',
            @workbook.worksheets.reject do |s|
              s.is_chartsheet? || s.very_hidden?
            end.count
          ]
        )
      end

      def add_chartsheet_heading_pairs
        add_heading_pair(['Charts', @workbook.chartsheet_count])
      end

      def add_worksheet_part_names
        @workbook.worksheets
          .reject { |sheet| sheet.is_chartsheet? || sheet.very_hidden? }
          .each  do |sheet|
          add_part_name(sheet.name)
        end
      end

      def add_chartsheet_part_names
        @workbook.worksheets
          .select { |sheet| sheet.is_chartsheet? }
          .each   { |sheet| add_part_name(sheet.name) }
      end

      def add_part_name(part_name)
        @part_names.push(part_name)
      end

      def add_named_range_heading_pairs
        range_count = @workbook.named_ranges.size

        add_heading_pair(['Named Ranges', range_count]) if range_count != 0
      end

      def add_named_ranges_parts
        @workbook.named_ranges.each { |named_range| add_part_name(named_range) }
      end

      def add_heading_pair(heading_pair)
        return if heading_pair[1] == 0

        @heading_pairs.push(['lpstr', heading_pair[0]], ['i4', heading_pair[1]])
      end

      #
      # Set the document properties.
      #
      def set_properties(properties)
        @properties = properties
      end

      private

      #
      # Write the <Properties> element.
      #
      def write_properties(&block)
        schema = 'http://schemas.openxmlformats.org/officeDocument/2006/'
        attributes = [
          ['xmlns',     "#{schema}extended-properties"],
          ['xmlns:vt',  "#{schema}docPropsVTypes"]
        ]

        @writer.tag_elements('Properties', attributes, &block)
      end

      #
      # Write the <Application> element.
      #
      def write_application
        data = 'Microsoft Excel'

        @writer.data_element('Application', data)
      end

      #
      # Write the <DocSecurity> element.
      #
      def write_doc_security
        @writer.data_element('DocSecurity', @doc_security)
      end

      #
      # Write the <ScaleCrop> element.
      #
      def write_scale_crop
        data = 'false'

        @writer.data_element('ScaleCrop', data)
      end

      #
      # Write the <HeadingPairs> element.
      #
      def write_heading_pairs
        @writer.tag_elements('HeadingPairs') do
          write_vt_vector('variant', @heading_pairs)
        end
      end

      #
      # Write the <TitlesOfParts> element.
      #
      def write_titles_of_parts
        @writer.tag_elements('TitlesOfParts') do
          parts_data = @part_names.collect { |part_name| ['lpstr', part_name] }
          write_vt_vector('lpstr', parts_data)
        end
      end

      #
      # Write the <vt:vector> element.
      #
      def write_vt_vector(base_type, data)
        attributes = [
          ['size',     data.size],
          ['baseType', base_type]
        ]

        @writer.tag_elements('vt:vector', attributes) do
          data.each do |a|
            if base_type == 'variant'
              @writer.tag_elements('vt:variant') { write_vt_data(*a) }
            else
              write_vt_data(*a)
            end
          end
        end
      end

      #
      # Write the <vt:*> elements such as <vt:lpstr> and <vt:if>.
      #
      def write_vt_data(type, data)
        @writer.data_element("vt:#{type}", data)
      end

      #
      # Write the <Company> element.
      #
      def write_company
        data = @properties[:company] || ''

        @writer.data_element('Company', data)
      end

      #
      # Write the <Manager> element.
      #
      def write_manager
        data = @properties[:manager]

        return unless data

        @writer.data_element('Manager', data)
      end

      #
      # Write the <LinksUpToDate> element.
      #
      def write_links_up_to_date
        data = 'false'

        @writer.data_element('LinksUpToDate', data)
      end

      #
      # Write the <SharedDoc> element.
      #
      def write_shared_doc
        data = 'false'

        @writer.data_element('SharedDoc', data)
      end

      #
      # Write the <HyperlinkBase> element.
      #
      def write_hyperlink_base
        data = @properties[:hyperlink_base]

        return unless data

        @writer.data_element('HyperlinkBase', data)
      end

      #
      # Write the <HyperlinksChanged> element.
      #
      def write_hyperlinks_changed
        data = 'false'

        @writer.data_element('HyperlinksChanged', data)
      end

      #
      # Write the <AppVersion> element.
      #
      def write_app_version
        data = '12.0000'

        @writer.data_element('AppVersion', data)
      end
    end
	class Button
      include Writexlsx_kot::Utility

      attr_accessor :font, :macro, :vertices, :description

      def v_shape_attributes(id, z_index)
        attributes = v_shape_attributes_base(id)
        attributes << ['alt', description] if description

        attributes << ['style', (v_shape_style_base(z_index, vertices) + style_addition).join]
        attributes << ['o:button',    't']
        attributes << ['fillcolor',   color]
        attributes << ['strokecolor', 'windowText [64]']
        attributes << ['o:insetmode', 'auto']
        attributes
      end

      def type
        '#_x0000_t201'
      end

      def color
        'buttonFace [67]'
      end

      def style_addition
        ['mso-wrap-style:tight']
      end

      def write_shape(writer, id, z_index)
        @writer = writer

        attributes = v_shape_attributes(id, z_index)

        @writer.tag_elements('v:shape', attributes) do
          # Write the v:fill element.
          write_fill
          # Write the o:lock element.
          write_rotation_lock
          # Write the v:textbox element.
          write_textbox
          # Write the x:ClientData element.
          write_client_data
        end
      end

      # attributes for <v:fill> element.
      def fill_attributes
        [
          ['color2',             'buttonFace [67]'],
          ['o:detectmouseclick', 't']
        ]
      end

      #
      # Write the <o:lock> element.
      #
      def write_rotation_lock
        attributes = [
          ['v:ext',    'edit'],
          %w[rotation t]
        ]
        @writer.empty_tag('o:lock', attributes)
      end

      #
      # Write the <v:textbox> element.
      #
      def write_textbox
        attributes = [
          ['style', 'mso-direction-alt:auto'],
          ['o:singleclick', 'f']
        ]

        @writer.tag_elements('v:textbox', attributes) do
          # Write the div element.
          write_div('center', font)
        end
      end

      #
      # Write the <x:ClientData> element.
      #
      def write_client_data
        attributes = [%w[ObjectType Button]]

        @writer.tag_elements('x:ClientData', attributes) do
          # Write the x:Anchor element.
          write_anchor
          # Write the x:PrintObject element.
          write_print_object
          # Write the x:AutoFill element.
          write_auto_fill
          # Write the x:FmlaMacro element.
          write_fmla_macro
          # Write the x:TextHAlign element.
          write_text_halign
          # Write the x:TextVAlign element.
          write_text_valign
        end
      end

      #
      # Write the <x:PrintObject> element.
      #
      def write_print_object
        @writer.data_element('x:PrintObject', 'False')
      end

      #
      # Write the <x:FmlaMacro> element.
      #
      def write_fmla_macro
        @writer.data_element('x:FmlaMacro', macro)
      end

      #
      # Write the <x:TextHAlign> element.
      #
      def write_text_halign
        @writer.data_element('x:TextHAlign', 'Center')
      end

      #
      # Write the <x:TextVAlign> element.
      #
      def write_text_valign
        @writer.data_element('x:TextVAlign', 'Center')
      end
    end
	class Comment
      include Writexlsx_kot::Utility

      DEFAULT_COLOR  = 81  # what color ?
      DEFAULT_WIDTH  = 128
      DEFAULT_HEIGHT = 74

      attr_reader :row, :col, :string, :color, :vertices
      attr_reader :font_size, :font_family
      attr_accessor :author, :visible

      def initialize(workbook, worksheet, row, col, string, options = {})
        options ||= {}
        @workbook    = workbook
        @worksheet   = worksheet
        @row = row
        @col = col
        options_parse(row, col, options)
        @string = string[0, STR_MAX]
        @start_row   ||= default_start_row(row)
        @start_col   ||= default_start_col(col)
        @visible     = options[:visible]
        @x_offset    = options[:x_offset] || default_x_offset(col)
        @y_offset    = options[:y_offset] || default_y_offset(row)
        @x_scale     = options[:x_scale]  || 1
        @y_scale     = options[:y_scale]  || 1
        @width       = (0.5 + ((options[:width]  || DEFAULT_WIDTH)  * @x_scale)).to_i
        @height      = (0.5 + ((options[:height] || DEFAULT_HEIGHT) * @y_scale)).to_i
        @vertices    = @worksheet.position_object_pixels(
          @start_col, @start_row, @x_offset, @y_offset,
          @width, @height
        ) << [@width, @height]
      end

      def backgrount_color(color)
        color_id = Format.color(color)

        if color_id.to_s =~ /^#[0-9A-F]{6}/i
          @color = color_id.to_s
        elsif color_id == 0
          @color = '#ffffe1'
        else
          rgb = @workbook.palette[color_id - 8]
          @color = "##{rgb_color(rgb)} [#{color_id}]"
        end
      end

      # Minor modification to allow comparison testing. Change RGB colors
      # from long format, ffcc00 to short format fc0 used by VML.
      def rgb_color(rgb)
        r, g, b = rgb
        result = sprintf("%02x%02x%02x", r, g, b)
        result = "#{::Regexp.last_match(1)}#{::Regexp.last_match(2)}#{::Regexp.last_match(3)}" if result =~ /^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/
        result
      end

      def default_start_row(row)
        case row
        when 0
          0
        when ROW_MAX - 3
          ROW_MAX - 7
        when ROW_MAX - 2
          ROW_MAX - 6
        when ROW_MAX - 1
          ROW_MAX - 5
        else
          row - 1
        end
      end

      def default_start_col(col)
        case col
        when COL_MAX - 3
          COL_MAX - 6
        when COL_MAX - 2
          COL_MAX - 5
        when COL_MAX - 1
          COL_MAX - 4
        else
          col + 1
        end
      end

      def default_x_offset(col)
        case col
        when COL_MAX - 3, COL_MAX - 2, COL_MAX - 1
          49
        else
          15
        end
      end

      def default_y_offset(row)
        case row
        when 0
          2
        when ROW_MAX - 3, ROW_MAX - 2
          16
        when ROW_MAX - 1
          14
        else
          10
        end
      end

      def v_shape_attributes(id, z_index)
        attr = v_shape_attributes_base(id)
        attr << ['style', (v_shape_style_base(z_index, vertices) + style_addition).join]
        attr << ['fillcolor',   color]
        attr << ['o:insetmode', 'auto']
        attr
      end

      def type
        '#_x0000_t202'
      end

      def style_addition
        ['visibility:', visibility]
      end

      def write_shape(writer, id, z_index)
        @writer = writer

        attributes = v_shape_attributes(id, z_index)

        @writer.tag_elements('v:shape', attributes) do
          # Write the v:fill element.
          write_fill
          # Write the v:shadow element.
          write_shadow
          # Write the v:path element.
          write_comment_path(nil, 'none')
          # Write the v:textbox element.
          write_textbox
          # Write the x:ClientData element.
          write_client_data
        end
      end

      def visibility
        ptrue?(visible) ? 'visible' : 'hidden'
      end

      #
      # Write the <v:fill> element.
      #
      def fill_attributes
        [
          ['color2', '#ffffe1']
        ]
      end

      #
      # Write the <v:shadow> element.
      #
      def write_shadow
        attributes = [
          %w[on t],
          %w[color black],
          %w[obscured t]
        ]

        @writer.empty_tag('v:shadow', attributes)
      end

      #
      # Write the <v:textbox> element.
      #
      def write_textbox
        attributes = [
          ['style', 'mso-direction-alt:auto']
        ]

        @writer.tag_elements('v:textbox', attributes) do
          # Write the div element.
          write_div('left')
        end
      end

      #
      # Write the <x:ClientData> element.
      #
      def write_client_data
        attributes = [
          %w[ObjectType Note]
        ]

        @writer.tag_elements('x:ClientData', attributes) do
          @writer.empty_tag('x:MoveWithCells')
          @writer.empty_tag('x:SizeWithCells')
          # Write the x:Anchor element.
          write_anchor
          # Write the x:AutoFill element.
          write_auto_fill
          # Write the x:Row element.
          @writer.data_element('x:Row', row)
          # Write the x:Column element.
          @writer.data_element('x:Column', col)
          # Write the x:Visible element.
          @writer.empty_tag('x:Visible') if ptrue?(visible)
        end
      end

      attr_writer :writer

      def font_name
        @font
      end

      private

      def options_parse(row, col, options)
        @color       = backgrount_color(options[:color] || DEFAULT_COLOR)
        @author      = options[:author]
        @start_cell  = options[:start_cell]
        @start_row, @start_col = if @start_cell
                                   substitute_cellref(@start_cell)
                                 else
                                   [options[:start_row], options[:start_col]]
                                 end
        @visible     = options[:visible]
        @x_offset    = options[:x_offset]       || default_x_offset(col)
        @y_offset    = options[:y_offset]       || default_y_offset(row)
        @x_scale     = options[:x_scale]        || 1
        @y_scale     = options[:y_scale]        || 1
        @font        = options[:font]           || 'Tahoma'
        @font_size   = options[:font_size]      || 8
        @font_family = options[:font_family]    || 2
        @width       = (0.5 + ((options[:width]  || DEFAULT_WIDTH)  * @x_scale)).to_i
        @height      = (0.5 + ((options[:height] || DEFAULT_HEIGHT) * @y_scale)).to_i
      end
    end
    class Comments
      include Writexlsx_kot::Utility

      def initialize(worksheet)
        @worksheet = worksheet
        @writer = Package::XMLWriterSimple.new
        @author_ids = {}
        @comments = {}
      end

      def [](row)
        @comments[row]
      end

      def add(workbook, worksheet, row, col, string, options)
        @comments[row] ||= {}
        @comments[row][col] = [workbook, worksheet, row, col, string, options]
      end

      def empty?
        @comments.empty?
      end

      def size
        sorted_comments.size
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_comments do
            write_authors(sorted_comments)
            write_comment_list(sorted_comments)
          end
        end
      end

      def sorted_comments
        unless @sorted_comments
          @sorted_comments = []
          # We sort the comments by row and column but that isn't strictly required.
          @comments.keys.sort.each do |row|
            @comments[row].keys.sort.each do |col|
              user_options = @comments[row][col]
              comment = Comment.new(*user_options)
              @comments[row][col] = comment

              # Set comment visibility if required and not already user defined.
              @comments[row][col].visible ||= 1 if comments_visible?

              # Set comment author if not already user defined.
              @comments[row][col].author ||= @worksheet.comments_author
              @sorted_comments << @comments[row][col]
            end
          end
        end

        @sorted_comments
      end

      def has_comment_in_row?(row)
        !!@comments[row]
      end

      private

      def comments_visible?
        @worksheet.comments_visible?
      end

      #
      # Write the <comments> element.
      #
      def write_comments(&block)
        attributes = [['xmlns', XMLWriterSimple::XMLNS]]

        @writer.tag_elements('comments', attributes, &block)
      end

      #
      # Write the <authors> element.
      #
      def write_authors(comment_data)
        author_count = 0

        @writer.tag_elements('authors') do
          comment_data.each do |comment|
            author = comment.author || ''
            next unless author && !@author_ids[author]

            # Store the author id.
            @author_ids[author] = author_count
            author_count += 1

            # Write the author element.
            write_author(author)
          end
        end
      end

      #
      # Write the <author> element.
      #
      def write_author(data)
        @writer.data_element('author', data)
      end

      #
      # Write the <commentList> element.
      #
      def write_comment_list(comment_data)
        @writer.tag_elements('commentList') do
          comment_data.each { |comment| write_comment(comment) }
        end
      end

      #
      # Write the <comment> element.
      #
      def write_comment(comment)
        ref = xl_rowcol_to_cell(comment.row, comment.col)
        attributes = [['ref', ref]]

        author_id = (@author_ids[comment.author] if comment.author) || 0
        attributes << ['authorId', author_id]

        @writer.tag_elements('comment', attributes) do
          write_text(comment)
        end
      end

      #
      # Write the <text> element.
      #
      def write_text(comment)
        @writer.tag_elements('text') do
          # Write the text r element.
          write_text_r(comment)
        end
      end

      #
      # Write the <r> element.
      #
      def write_text_r(comment)
        @writer.tag_elements('r') do
          # Write the rPr element.
          write_r_pr(comment)
          # Write the text r element.
          write_text_t(comment)
        end
      end

      #
      # Write the text <t> element.
      #
      def write_text_t(comment)
        text = comment.string
        attributes = []

        attributes << ['xml:space', 'preserve'] if text =~ /^\s/ || text =~ /\s$/

        @writer.data_element('t', text, attributes)
      end

      #
      # Write the <rPr> element.
      #
      def write_r_pr(comment)
        @writer.tag_elements('rPr') do
          # Write the sz element.
          write_sz(comment.font_size)
          # Write the color element.
          write_color
          # Write the rFont element.
          write_r_font(comment.font_name)
          # Write the family element.
          write_family(comment.font_family)
        end
      end

      #
      # Write the <sz> element.
      #
      def write_sz(val)
        attributes = [['val', val]]

        @writer.empty_tag('sz', attributes)
      end

      #
      # Write the <color> element.
      #
      def write_color
        @writer.empty_tag('color', [['indexed', 81]])
      end

      #
      # Write the <rFont> element.
      #
      def write_r_font(val)
        attributes = [['val', val]]

        @writer.empty_tag('rFont', attributes)
      end

      #
      # Write the <family> element.
      #
      def write_family(val)
        attributes = [['val', val]]

        @writer.empty_tag('family', attributes)
      end
    end
    class ConditionalFormat
      include Writexlsx_kot::Utility

      def self.factory(worksheet, *args)
        range, param  =
          Package::ConditionalFormat
          .new(worksheet, nil, nil)
          .range_param_for_conditional_formatting(*args)

        case param[:type]
        when 'cellIs'
          CellIsFormat.new(worksheet, range, param)
        when 'aboveAverage'
          AboveAverageFormat.new(worksheet, range, param)
        when 'top10'
          Top10Format.new(worksheet, range, param)
        when 'containsText', 'notContainsText', 'beginsWith', 'endsWith'
          TextOrWithFormat.new(worksheet, range, param)
        when 'timePeriod'
          TimePeriodFormat.new(worksheet, range, param)
        when 'containsBlanks', 'notContainsBlanks', 'containsErrors', 'notContainsErrors'
          BlanksOrErrorsFormat.new(worksheet, range, param)
        when 'colorScale'
          ColorScaleFormat.new(worksheet, range, param)
        when 'dataBar'
          DataBarFormat.new(worksheet, range, param)
        when 'expression'
          ExpressionFormat.new(worksheet, range, param)
        when 'iconSet'
          IconSetFormat.new(worksheet, range, param)
        else # when 'duplicateValues', 'uniqueValues'
          ConditionalFormat.new(worksheet, range, param)
        end
      end

      attr_reader :range

      def initialize(worksheet, range, param)
        @worksheet = worksheet
        @range = range
        @param = param
        @writer = @worksheet.writer
      end

      def write_cf_rule
        @writer.empty_tag('cfRule', attributes)
      end

      def write_cf_rule_formula_tag(tag = formula)
        @writer.tag_elements('cfRule', attributes) do
          write_formula_tag(tag)
        end
      end

      def write_formula_tag(data) # :nodoc:
        data = data.sub(/^=/, '') if data.respond_to?(:sub)
        @writer.data_element('formula', data)
      end

      #
      # Write the <cfvo> element.
      #
      def write_cfvo(type, value, criteria = nil)
        attributes = [['type', type]]
        attributes << ['val', value] if value

        attributes << ['gte', 0] if ptrue?(criteria)

        @writer.empty_tag('cfvo', attributes)
      end

      def attributes
        attr = []
        attr << ['type', type]
        attr << ['dxfId',    format]   if format
        attr << ['priority', priority]
        attr << ['stopIfTrue', 1] if stop_if_true
        attr
      end

      def type
        @param[:type]
      end

      def format
        @param[:format]
      end

      def priority
        @param[:priority]
      end

      def stop_if_true
        @param[:stop_if_true]
      end

      def criteria
        @param[:criteria]
      end

      def maximum
        @param[:maximum]
      end

      def minimum
        @param[:minimum]
      end

      def value
        @param[:value]
      end

      def direction
        @param[:direction]
      end

      def formula
        @param[:formula]
      end

      def min_type
        @param[:min_type]
      end

      def min_value
        @param[:min_value]
      end

      def min_color
        @param[:min_color]
      end

      def mid_type
        @param[:mid_type]
      end

      def mid_value
        @param[:mid_value]
      end

      def mid_color
        @param[:mid_color]
      end

      def max_type
        @param[:max_type]
      end

      def max_value
        @param[:max_value]
      end

      def max_color
        @param[:max_color]
      end

      def bar_color
        @param[:bar_color]
      end

      def bar_border_color
        @param[:bar_border_color]
      end

      def bar_negative_color
        @param[:bar_negative_color]
      end

      def bar_negative_color_same
        @param[:bar_negative_color_same]
      end

      def bar_no_border
        @param[:bar_no_border]
      end

      def bar_axis_position
        @param[:bar_axis_position]
      end

      def bar_axis_color
        @param[:bar_axis_color]
      end

      def icon_style
        @param[:icon_style]
      end

      def total_icons
        @param[:total_icons]
      end

      def icons
        @param[:icons]
      end

      def icons_only
        @param[:icons_only]
      end

      def reverse_icons
        @param[:reverse_icons]
      end

      def bar_only
        @param[:bar_only]
      end

      def range_param_for_conditional_formatting(*args)  # :nodoc:
        range_start_cell_for_conditional_formatting(*args)
        param_for_conditional_formatting(*args)

        handling_of_text_criteria        if @param[:type] == 'text'
        handling_of_time_period_criteria if @param[:type] == 'timePeriod'
        handling_of_blanks_error_types

        [@range, @param]
      end

      private

      def handling_of_text_criteria
        case @param[:criteria]
        when 'containsText'
          @param[:type]    = 'containsText'
          @param[:formula] =
            %!NOT(ISERROR(SEARCH("#{@param[:value]}",#{@start_cell})))!
        when 'notContains'
          @param[:type]    = 'notContainsText'
          @param[:formula] =
            %!ISERROR(SEARCH("#{@param[:value]}",#{@start_cell}))!
        when 'beginsWith'
          @param[:type] = 'beginsWith'
          @param[:formula] =
            %!LEFT(#{@start_cell},#{@param[:value].size})="#{@param[:value]}"!
        when 'endsWith'
          @param[:type] = 'endsWith'
          @param[:formula] =
            %!RIGHT(#{@start_cell},#{@param[:value].size})="#{@param[:value]}"!
        else
          raise "Invalid text criteria '#{@param[:criteria]} in conditional_formatting()"
        end
      end

      def handling_of_time_period_criteria
        case @param[:criteria]
        when 'yesterday'
          @param[:formula] = "FLOOR(#{@start_cell},1)=TODAY()-1"
        when 'today'
          @param[:formula] = "FLOOR(#{@start_cell},1)=TODAY()"
        when 'tomorrow'
          @param[:formula] = "FLOOR(#{@start_cell},1)=TODAY()+1"
        when 'last7Days'
          @param[:formula] =
            "AND(TODAY()-FLOOR(#{@start_cell},1)<=6,FLOOR(#{@start_cell},1)<=TODAY())"
        when 'lastWeek'
          @param[:formula] =
            "AND(TODAY()-ROUNDDOWN(#{@start_cell},0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(#{@start_cell},0)<(WEEKDAY(TODAY())+7))"
        when 'thisWeek'
          @param[:formula] =
            "AND(TODAY()-ROUNDDOWN(#{@start_cell},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(#{@start_cell},0)-TODAY()<=7-WEEKDAY(TODAY()))"
        when 'nextWeek'
          @param[:formula] =
            "AND(ROUNDDOWN(#{@start_cell},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(#{@start_cell},0)-TODAY()<(15-WEEKDAY(TODAY())))"
        when 'lastMonth'
          @param[:formula] =
            "AND(MONTH(#{@start_cell})=MONTH(TODAY())-1,OR(YEAR(#{@start_cell})=YEAR(TODAY()),AND(MONTH(#{@start_cell})=1,YEAR(A1)=YEAR(TODAY())-1)))"
        when 'thisMonth'
          @param[:formula] =
            "AND(MONTH(#{@start_cell})=MONTH(TODAY()),YEAR(#{@start_cell})=YEAR(TODAY()))"
        when 'nextMonth'
          @param[:formula] =
            "AND(MONTH(#{@start_cell})=MONTH(TODAY())+1,OR(YEAR(#{@start_cell})=YEAR(TODAY()),AND(MONTH(#{@start_cell})=12,YEAR(#{@start_cell})=YEAR(TODAY())+1)))"
        else
          raise "Invalid time_period criteria '#{@param[:criteria]}' in conditional_formatting()"
        end
      end

      def handling_of_blanks_error_types
        # Special handling of blanks/error types.
        case @param[:type]
        when 'containsBlanks'
          @param[:formula] = "LEN(TRIM(#{@start_cell}))=0"
        when 'notContainsBlanks'
          @param[:formula] = "LEN(TRIM(#{@start_cell}))>0"
        when 'containsErrors'
          @param[:formula] = "ISERROR(#{@start_cell})"
        when 'notContainsErrors'
          @param[:formula] = "NOT(ISERROR(#{@start_cell}))"
        when '2_color_scale'
          @param[:type] = 'colorScale'

          # Color scales don't use any additional formatting.
          @param[:format] = nil

          # Turn off 3 color parameters.
          @param[:mid_type]  = nil
          @param[:mid_color] = nil

          @param[:min_type]  ||= 'min'
          @param[:max_type]  ||= 'max'
          @param[:min_value] ||= 0
          @param[:max_value] ||= 0
          @param[:min_color] ||= '#FF7128'
          @param[:max_color] ||= '#FFEF9C'

          @param[:max_color] = palette_color(@param[:max_color])
          @param[:min_color] = palette_color(@param[:min_color])
        when '3_color_scale'
          @param[:type] = 'colorScale'

          # Color scales don't use any additional formatting.
          @param[:format] = nil

          @param[:min_type]  ||= 'min'
          @param[:mid_type]  ||= 'percentile'
          @param[:max_type]  ||= 'max'
          @param[:min_value] ||= 0
          @param[:mid_value] ||= 50
          @param[:max_value] ||= 0
          @param[:min_color] ||= '#F8696B'
          @param[:mid_color] ||= '#FFEB84'
          @param[:max_color] ||= '#63BE7B'

          @param[:max_color] = palette_color(@param[:max_color])
          @param[:mid_color] = palette_color(@param[:mid_color])
          @param[:min_color] = palette_color(@param[:min_color])
        when 'dataBar'
          # Excel 2007 data bars don't use any additional formatting.
          @param[:format] = nil

          if @param[:min_type]
            @param[:x14_min_type] = @param[:min_type]
          else
            @param[:min_type]     = 'min'
            @param[:x14_min_type] = 'autoMin'
          end
          if @param[:max_type]
            @param[:x14_max_type] = @param[:max_type]
          else
            @param[:max_type]     = 'max'
            @param[:x14_max_type] = 'autoMax'
          end

          @param[:min_value]                      ||= 0
          @param[:max_value]                      ||= 0
          @param[:bar_color]                      ||= '#638EC6'
          @param[:bar_border_color]               ||= @param[:bar_color]
          @param[:bar_only]                       ||= 0
          @param[:bar_no_border]                  ||= 0
          @param[:bar_solid]                      ||= 0
          @param[:bar_direction]                  ||= ''
          @param[:bar_negative_color]             ||= '#FF0000'
          @param[:bar_negative_border_color]      ||= '#FF0000'
          @param[:bar_negative_color_same]        ||= 0
          @param[:bar_negative_border_color_same] ||= 0
          @param[:bar_axis_position]              ||= ''
          @param[:bar_axis_color]                 ||= '#000000'

          @param[:bar_color] =
            palette_color(@param[:bar_color])
          @param[:bar_border_color] =
            palette_color(@param[:bar_border_color])
          @param[:bar_negative_color] =
            palette_color(@param[:bar_negative_color])
          @param[:bar_negative_border_color] =
            palette_color(@param[:bar_negative_border_color])
          @param[:bar_axis_color] =
            palette_color(@param[:bar_axis_color])
        end

        # Adjust for 2010 style data_bar parameters.
        if ptrue?(@param[:is_data_bar_2010])
          @worksheet.excel_version = 2010

          @param[:min_value] = nil if @param[:min_type] == 'min' && @param[:min_value] == 0
          @param[:max_value] = nil if @param[:max_type] == 'max' && @param[:max_value] == 0

          # Store range for Excel 2010 data bars.
          @param[:range] = range
        end

        # Strip the leading = from formulas.
        @param[:min_value] = @param[:min_value].to_s.sub(/^=/, '') if @param[:min_value]
        @param[:mid_value] = @param[:mid_value].to_s.sub(/^=/, '') if @param[:mid_value]
        @param[:max_value] = @param[:max_value].to_s.sub(/^=/, '') if @param[:max_value]
      end

      def palette_color(index)
        @worksheet.palette_color(index)
      end

      def range_start_cell_for_conditional_formatting(*args)  # :nodoc:
        row1, row2, col1, col2, user_range, _param =
          row_col_param_for_conditional_formatting(*args)
        range       = xl_range(row1, row2, col1, col2)
        @start_cell = xl_rowcol_to_cell(row1, col1)

        # Override with user defined multiple range if provided.
        range = user_range if user_range

        @range = range
      end

      def row_col_param_for_conditional_formatting(*args)
        # Check for a cell reference in A1 notation and substitute row and column
        user_range = if args[0].to_s =~ (/^\D/) && (args[0] =~ /,/)
                       # Check for a user defined multiple range like B3:K6,B8:K11.
                       args[0].sub(/^=/, '').gsub(/\s*,\s*/, ' ').gsub("$", '')
                     end

        if (row_col_array = row_col_notation(args.first))
          if row_col_array.size == 2
            row1, col1 = row_col_array
            row2 = args[1]
          elsif row_col_array.size == 4
            row1, col1, row2, col2 = row_col_array
            param = args[1]
          end
        else
          row1, col1, row2, col2, param = args
        end

        if row2.respond_to?(:keys)
          param = row2
          row2 = row1
          col2 = col1
        end
        raise WriteXLSXInsufficientArgumentError if [row1, col1, row2, col2, param].include?(nil)

        # Check that row and col are valid without storing the values.
        check_dimensions(row1, col1)
        check_dimensions(row2, col2)

        # Swap last row/col for first row/col as necessary
        row1, row2 = row2, row1 if row1 > row2
        col1, col2 = col2, col1 if col1 > col2

        [row1, row2, col1, col2, user_range, param.dup]
      end

      def param_for_conditional_formatting(*args)  # :nodoc:
        _dummy, _dummy, _dummy, _dummy, _dummy, @param =
          row_col_param_for_conditional_formatting(*args)
        check_conditional_formatting_parameters(@param)

        @param[:format] = @param[:format].get_dxf_index if @param[:format]
        @param[:priority] = @worksheet.dxf_priority

        # Check for 2010 style data_bar parameters.
        %i[data_bar_2010 bar_solid bar_border_color bar_negative_color
           bar_negative_color_same bar_negative_border_color
           bar_negative_border_color_same bar_no_border
           bar_axis_position bar_axis_color bar_direction].each do |key|
          if @param[key]
            @param[:is_data_bar_2010] = 1
            break
          end
        end

        @worksheet.dxf_priority += 1
      end

      def check_conditional_formatting_parameters(param)  # :nodoc:
        # Check for valid input parameters.
        if !(param.keys.uniq - valid_parameter_for_conditional_formatting).empty? ||
           !param.has_key?(:type) ||
           !valid_type_for_conditional_formatting.has_key?(param[:type].downcase)
          raise WriteXLSXOptionParameterError, "Invalid type : #{param[:type]}"
        end

        param[:direction] = 'bottom' if param[:type] == 'bottom'
        param[:type] = valid_type_for_conditional_formatting[param[:type].downcase]

        # Check for valid criteria types.
        param[:criteria] = valid_criteria_type_for_conditional_formatting[param[:criteria].downcase] if param.has_key?(:criteria) && valid_criteria_type_for_conditional_formatting.has_key?(param[:criteria].downcase)

        # Convert date/times value if required.
        if %w[date time cellIs].include?(param[:type])
          param[:type] = 'cellIs'

          param[:value]   = convert_date_time_if_required(param[:value])
          param[:minimum] = convert_date_time_if_required(param[:minimum])
          param[:maximum] = convert_date_time_if_required(param[:maximum])
        end

        # Set properties for icon sets.
        if param[:type] == 'iconSet'
          unless param[:icon_style]
            raise "The 'icon_style' parameter must be specified when " +
                  "'type' == 'icon_set' in conditional_formatting()"
          end

          # Check for valid icon styles.
          if icon_set_styles[param[:icon_style]]
            param[:icon_style] = icon_set_styles[param[:icon_style]]
          else
            raise "Unknown icon style '$param->{icon_style}' for parameter " +
                  "'icon_style' in conditional_formatting()"
          end

          # Set the number of icons for the icon style.
          param[:total_icons] = 3
          if param[:icon_style] =~ /^4/
            param[:total_icons] = 4
          elsif param[:icon_style] =~ /^5/
            param[:total_icons] = 5
          end

          param[:icons] = set_icon_properties(param[:total_icons], param[:icons])
        end

        # 'Between' and 'Not between' criteria require 2 values.
        if param[:criteria] == 'between' || param[:criteria] == 'notBetween'
          raise WriteXLSXOptionParameterError, "Invalid criteria : #{param[:criteria]}" unless param.has_key?(:minimum) || param.has_key?(:maximum)
        else
          param[:minimum] = nil
          param[:maximum] = nil
        end

        # Convert date/times value if required.
        raise WriteXLSXOptionParameterError if (param[:type] == 'date' || param[:type] == 'time') && !(convert_date_time_value(param, :value) || convert_date_time_value(param, :maximum))
      end

      def convert_date_time_if_required(val)
        if val.to_s =~ /T/
          date_time = convert_date_time(val)
          raise "Invalid date/time value '#{val}' in conditional_formatting()" unless date_time

          date_time
        else
          val
        end
      end

      # List of valid input parameters for conditional_formatting.
      def valid_parameter_for_conditional_formatting
        %i[
          type
          format
          criteria
          value
          minimum
          maximum
          stop_if_true
          min_type
          mid_type
          max_type
          min_value
          mid_value
          max_value
          min_color
          mid_color
          max_color
          bar_color
          bar_negative_color
          bar_negative_color_same
          bar_solid
          bar_border_color
          bar_negative_border_color
          bar_negative_border_color_same
          bar_no_border
          bar_direction
          bar_axis_position
          bar_axis_color
          bar_only
          icon_style
          reverse_icons
          icons_only
          icons
          data_bar_2010
        ]
      end

      # List of  valid validation types for conditional_formatting.
      def valid_type_for_conditional_formatting
        {
          'cell'          => 'cellIs',
          'date'          => 'date',
          'time'          => 'time',
          'average'       => 'aboveAverage',
          'duplicate'     => 'duplicateValues',
          'unique'        => 'uniqueValues',
          'top'           => 'top10',
          'bottom'        => 'top10',
          'text'          => 'text',
          'time_period'   => 'timePeriod',
          'blanks'        => 'containsBlanks',
          'no_blanks'     => 'notContainsBlanks',
          'errors'        => 'containsErrors',
          'no_errors'     => 'notContainsErrors',
          '2_color_scale' => '2_color_scale',
          '3_color_scale' => '3_color_scale',
          'data_bar'      => 'dataBar',
          'formula'       => 'expression',
          'icon_set'      => 'iconSet'
        }
      end

      # List of valid criteria types for conditional_formatting.
      def valid_criteria_type_for_conditional_formatting
        {
          'between'                  => 'between',
          'not between'              => 'notBetween',
          'equal to'                 => 'equal',
          '='                        => 'equal',
          '=='                       => 'equal',
          'not equal to'             => 'notEqual',
          '!='                       => 'notEqual',
          '<>'                       => 'notEqual',
          'greater than'             => 'greaterThan',
          '>'                        => 'greaterThan',
          'less than'                => 'lessThan',
          '<'                        => 'lessThan',
          'greater than or equal to' => 'greaterThanOrEqual',
          '>='                       => 'greaterThanOrEqual',
          'less than or equal to'    => 'lessThanOrEqual',
          '<='                       => 'lessThanOrEqual',
          'containing'               => 'containsText',
          'not containing'           => 'notContains',
          'begins with'              => 'beginsWith',
          'ends with'                => 'endsWith',
          'yesterday'                => 'yesterday',
          'today'                    => 'today',
          'last 7 days'              => 'last7Days',
          'last week'                => 'lastWeek',
          'this week'                => 'thisWeek',
          'next week'                => 'nextWeek',
          'last month'               => 'lastMonth',
          'this month'               => 'thisMonth',
          'next month'               => 'nextMonth'
        }
      end

      # List of valid icon styles.
      def icon_set_styles
        {
          "3_arrows"                => "3Arrows",            # 1
          "3_flags"                 => "3Flags",             # 2
          "3_traffic_lights_rimmed" => "3TrafficLights2",    # 3
          "3_symbols_circled"       => "3Symbols",           # 4
          "4_arrows"                => "4Arrows",            # 5
          "4_red_to_black"          => "4RedToBlack",        # 6
          "4_traffic_lights"        => "4TrafficLights",     # 7
          "5_arrows_gray"           => "5ArrowsGray",        # 8
          "5_quarters"              => "5Quarters",          # 9
          "3_arrows_gray"           => "3ArrowsGray",        # 10
          "3_traffic_lights"        => "3TrafficLights",     # 11
          "3_signs"                 => "3Signs",             # 12
          "3_symbols"               => "3Symbols2",          # 13
          "4_arrows_gray"           => "4ArrowsGray",        # 14
          "4_ratings"               => "4Rating",            # 15
          "5_arrows"                => "5Arrows",            # 16
          "5_ratings"               => "5Rating"            # 17
        }
      end

      #
      # Set the sub-properites for icons.
      #
      def set_icon_properties(total_icons, user_props)
        props       = []

        # Set the default icon properties.
        total_icons.times do
          props << {
            criteria: 0,
            value:    0,
            type:     'percent'
          }
        end

        # Set the default icon values based on the number of icons.
        if total_icons == 3
          props[0][:value] = 67
          props[1][:value] = 33
        elsif total_icons == 4
          props[0][:value] = 75
          props[1][:value] = 50
          props[2][:value] = 25
        elsif total_icons == 5
          props[0][:value] = 80
          props[1][:value] = 60
          props[2][:value] = 40
          props[3][:value] = 20
        end

        # Overwrite default properties with user defined properties.
        if user_props

          # Ensure we don't set user properties for lowest icon.
          max_data = user_props.size
          max_data = total_icons - 1 if max_data >= total_icons

          (0..max_data - 1).each do |i|
            # Set the user defined 'value' property.
            props[i][:value] = user_props[i][:value].to_s.sub(/^=/, '') if user_props[i][:value]

            # Set the user defined 'type' property.
            if user_props[i][:type]

              type = user_props[i][:type]

              if type != 'percent' && type != 'percentile' &&
                 type != 'number'  && type != 'formula'
                raise "Unknown icon property type '$props->{type}' for sub-" +
                      "property 'type' in conditional_formatting()"
              else
                props[i][:type] = type

                props[i][:type] = 'num' if props[i][:type] == 'number'
              end
            end

            # Set the user defined 'criteria' property.
            props[i][:criteria] = 1 if user_props[i][:criteria] && user_props[i][:criteria] == '>'
          end
        end
        props
      end

      def date_1904?
        @worksheet.date_1904?
      end
    end
    class CellIsFormat < ConditionalFormat
      def attributes
        super << ['operator', criteria]
      end

      def write_cf_rule
        if minimum && maximum
          @writer.tag_elements('cfRule', attributes) do
            write_formula_tag(minimum)
            write_formula_tag(maximum)
          end
        else
          quoted_value = value.to_s
          numeric_regex = /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/
          # String "Cell" values must be quoted, apart from ranges.
          if !(quoted_value =~ /(\$?)([A-Z]{1,3})(\$?)(\d+)/) &&
             !(quoted_value =~ numeric_regex) &&
             !(quoted_value =~ /^".*"$/)
            quoted_value = %("#{value}")
          end
          write_cf_rule_formula_tag(quoted_value)
        end
      end
    end
    class AboveAverageFormat < ConditionalFormat
      def attributes
        attr = super
        attr << ['aboveAverage', 0] if criteria =~ /below/
        attr << ['equalAverage', 1] if criteria =~ /equal/
        attr << ['stdDev', $~[1]] if criteria =~ /([123]) std dev/
        attr
      end
    end
    class Top10Format < ConditionalFormat
      def attributes
        attr = super
        attr << ['percent', 1]             if criteria == '%'
        attr << ['bottom',  1]             if direction
        attr << ['rank',    value || 10]
        attr
      end
    end
    class TextOrWithFormat < ConditionalFormat
      def attributes
        attr = super
        attr << ['operator', criteria]
        attr << ['text',     value]
        attr
      end

      def write_cf_rule
        write_cf_rule_formula_tag
      end
    end
    class TimePeriodFormat < ConditionalFormat
      def attributes
        super << ['timePeriod', criteria]
      end

      def write_cf_rule
        write_cf_rule_formula_tag
      end
    end
    class BlanksOrErrorsFormat < ConditionalFormat
      def write_cf_rule
        write_cf_rule_formula_tag
      end
    end
    class ColorScaleFormat < ConditionalFormat
      def write_cf_rule
        @writer.tag_elements('cfRule', attributes) do
          write_color_scale
        end
      end

      #
      # Write the <colorScale> element.
      #
      def write_color_scale
        @writer.tag_elements('colorScale') do
          write_cfvo(min_type, min_value)
          write_cfvo(mid_type, mid_value) if mid_type
          write_cfvo(max_type, max_value)
          write_color('rgb', min_color)
          write_color('rgb', mid_color)  if mid_color
          write_color('rgb', max_color)
        end
      end
    end
    class DataBarFormat < ConditionalFormat
      def write_cf_rule
        @writer.tag_elements('cfRule', attributes) do
          write_data_bar
          write_data_bar_ext(@param) if ptrue?(@param[:is_data_bar_2010])
        end
      end

      #
      # Write the <dataBar> element.
      #
      def write_data_bar
        attributes = []

        attributes << ['showValue', 0] if ptrue?(bar_only)
        @writer.tag_elements('dataBar', attributes) do
          write_cfvo(min_type, min_value)
          write_cfvo(max_type, max_value)

          write_color('rgb', bar_color)
        end
      end

      #
      # Write the <extLst> dataBar extension element.
      #
      def write_data_bar_ext(param)
        # Create a pseudo GUID for each unique Excel 2010 data bar.
        worksheet_count = @worksheet.index + 1
        data_bar_count  = @worksheet.data_bars_2010.size + 1

        guid = sprintf(
          "{DA7ABA51-AAAA-BBBB-%04X-%012X}",
          worksheet_count, data_bar_count
        )

        # Store the 2010 data bar parameters to write the extLst elements.
        param[:guid] = guid
        @worksheet.data_bars_2010 << param

        @writer.tag_elements('extLst') do
          @worksheet.write_ext('{B025F937-C7B1-47D3-B67F-A62EFF666E3E}') do
            @writer.data_element('x14:id', guid)
          end
        end
      end
    end
    class ExpressionFormat < ConditionalFormat
      def write_cf_rule
        write_cf_rule_formula_tag(criteria)
      end
    end
    class IconSetFormat < ConditionalFormat
      def write_cf_rule
        @writer.tag_elements('cfRule', attributes) do
          write_icon_set
        end
      end

      #
      # Write the <iconSet> element.
      #
      def write_icon_set
        attributes = []
        # Don't set attribute for default style.
        attributes = [['iconSet', icon_style]] if icon_style != '3TrafficLights'
        attributes << ['showValue', 0]           if icons_only
        attributes << ['reverse', 1]             if reverse_icons

        @writer.tag_elements('iconSet', attributes) do
          # Write the properties for different icon styles.
          if icons
            icons.reverse.each do |icon|
              write_cfvo(icon[:type], icon[:value], icon[:criteria])
            end
          end
        end
      end
    end
	class ContentTypes
      include Writexlsx_kot::Utility

      App_package  = 'application/vnd.openxmlformats-package.'
      App_document = 'application/vnd.openxmlformats-officedocument.'

      def initialize(workbook)
        @writer = Package::XMLWriterSimple.new
        @workbook  = workbook
        @defaults  = [
          ['rels', "#{App_package}relationships+xml"],
          ['xml', 'application/xml']
        ]
        @overrides = [
          ['/docProps/app.xml',    "#{App_document}extended-properties+xml"],
          ['/docProps/core.xml',   "#{App_package}core-properties+xml"],
          ['/xl/styles.xml',       "#{App_document}spreadsheetml.styles+xml"],
          ['/xl/theme/theme1.xml', "#{App_document}theme+xml"],
          ['/xl/workbook.xml',     "#{App_document}spreadsheetml.sheet.main+xml"]
        ]
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_types do
            write_defaults
            write_overrides
          end
        end
      end

      #
      # Add elements to the ContentTypes defaults.
      #
      def add_default(part_name, content_type)
        @defaults.push([part_name, content_type])
      end

      #
      # Add elements to the ContentTypes overrides.
      #
      def add_override(part_name, content_type)
        @overrides.push([part_name, content_type])
      end

      def add_worksheet_names
        @workbook.non_chartsheet_count.times do |index|
          add_worksheet_name("sheet#{index + 1}")
        end
      end

      #
      # Add the name of a worksheet to the ContentTypes overrides.
      #
      def add_worksheet_name(name)
        worksheet_name = "/xl/worksheets/#{name}.xml"

        add_override(worksheet_name, "#{App_document}spreadsheetml.worksheet+xml")
      end

      def add_chartsheet_names
        @workbook.chartsheet_count.times do |index|
          add_chartsheet_name("sheet#{index + 1}")
        end
      end

      #
      # Add the name of a chartsheet to the ContentTypes overrides.
      #
      def add_chartsheet_name(name)
        chartsheet_name = "/xl/chartsheets/#{name}.xml"

        add_override(chartsheet_name, "#{App_document}spreadsheetml.chartsheet+xml")
      end

      def add_chart_names
        (1..@workbook.charts.size).each { |i| add_chart_name("chart#{i}") }
      end

      #
      # Add the name of a chart to the ContentTypes overrides.
      #
      def add_chart_name(name)
        chart_name = "/xl/charts/#{name}.xml"

        add_override(chart_name, "#{App_document}drawingml.chart+xml")
      end

      def add_drawing_names
        (1..@workbook.drawings.size).each do |i|
          add_drawing_name("drawing#{i}")
        end
      end

      #
      # Add the name of a drawing to the ContentTypes overrides.
      #
      def add_drawing_name(name)
        drawing_name = "/xl/drawings/#{name}.xml"

        add_override(drawing_name, "#{App_document}drawing+xml")
      end

      #
      # Add the name of a VML drawing to the ContentTypes defaults.
      #
      def add_vml_name
        add_default('vml', "#{App_document}vmlDrawing")
      end

      def add_comment_names
        (1..@workbook.num_comment_files).each do |i|
          add_comment_name("comments#{i}")
        end
      end

      #
      # Add the name of a comment to the ContentTypes overrides.
      #
      def add_comment_name(name)
        comment_name = "/xl/#{name}.xml"

        add_override(comment_name, "#{App_document}spreadsheetml.comments+xml")
      end

      #
      # Add the sharedStrings link to the ContentTypes overrides.
      #
      def add_shared_strings
        add_override('/xl/sharedStrings.xml', "#{App_document}spreadsheetml.sharedStrings+xml")
      end

      #
      # Add the calcChain link to the ContentTypes overrides.
      #
      def add_calc_chain
        add_override('/xl/calcChain.xml', "#{App_document}spreadsheetml.calcChain+xml")
      end

      #
      # Add the image default types.
      #
      def add_image_types
        @workbook.image_types.each_key do |type|
          add_default(type, "image/#{type}")
        end
      end

      def add_table_names(table_count)
        (1..table_count).each { |i| add_table_name("table#{i}") }
      end

      #
      # Add the name of a table to the ContentTypes overrides.
      #
      def add_table_name(table_name)
        add_override(
          "/xl/tables/#{table_name}.xml",
          "#{App_document}spreadsheetml.table+xml"
        )
      end

      #
      # Add a vbaProject to the ContentTypes defaults.
      #
      def add_vba_project
        change_the_workbook_xml_content_type_from_xlsx_to_xlsm
        add_default('bin', 'application/vnd.ms-office.vbaProject')
      end

      #
      # Add the name of a table to the ContentTypes overrides.
      #
      def add_custom_properties
        custom = "/docProps/custom.xml"

        add_override(custom, "#{App_document}custom-properties+xml")
      end

      #
      # Add the metadata file to the ContentTypes overrides.
      #
      def add_metadata
        add_override(
          "/xl/metadata.xml",
          "#{App_document}spreadsheetml.sheetMetadata+xml"
        )
      end

      private

      def change_the_workbook_xml_content_type_from_xlsx_to_xlsm
        @overrides.collect! do |arr|
          arr[1] = 'application/vnd.ms-excel.sheet.macroEnabled.main+xml' if arr[0] == '/xl/workbook.xml'
          arr
        end
      end

      #
      # Write out all of the <Default> types.
      #
      def write_defaults
        @defaults.each do |a|
          write_default_or_override('Default', 'Extension', a)
        end
      end

      #
      # Write out all of the <Override> types.
      #
      def write_overrides
        @overrides.each do |a|
          write_default_or_override('Override', 'PartName', a)
        end
      end

      def write_default_or_override(tag, param0, a)
        @writer.empty_tag(tag,
                          [
                            [param0, a[0]],
                            ['ContentType', a[1]]
                          ])
      end

      #
      # Write the <Types> element.
      #
      def write_types(&block)
        xmlns = 'http://schemas.openxmlformats.org/package/2006/content-types'
        attributes = [
          ['xmlns', xmlns]
        ]

        @writer.tag_elements('Types', attributes, &block)
      end

      #
      # Write the <Default> element.
      #
      def write_default(extension, content_type)
        attributes = [
          ['Extension',   extension],
          ['ContentType', content_type]
        ]

        @writer.empty_tag('Default', attributes)
      end

      #
      # Write the <Override> element.
      #
      def write_override(part_name, content_type)
        attributes = [
          ['PartName',    part_name],
          ['ContentType', content_type]
        ]

        @writer.empty_tag('Override', attributes)
      end
    end
	class Core
      include Writexlsx_kot::Utility

      App_package  = 'application/vnd.openxmlformats-package.'
      App_document = 'application/vnd.openxmlformats-officedocument.'

      def initialize
        @writer = Package::XMLWriterSimple.new
        @properties = {}
        @createtime  = [Time.now.gmtime]
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_cp_core_properties { write_cp_core_properties_base }
        end
      end

      def set_properties(properties)
        @properties = properties
      end

      private

      def write_cp_core_properties_base
        write_dc_title
        write_dc_subject
        write_dc_creator
        write_cp_keywords
        write_dc_description
        write_cp_last_modified_by
        write_dcterms_created
        write_dcterms_modified
        write_cp_category
        write_cp_content_status
      end

      #
      # Convert a gmtime/localtime() date to a ISO 8601 style
      # "2010-01-01T00:00:00Z" date. Excel always treats this as
      # a utc date/time.
      #
      def datetime_to_iso8601_date(gm_time = nil)
        gm_time ||= Time.now.gmtime

        gm_time.strftime('%Y-%m-%dT%H:%M:%SZ')
      end

      #
      # Write the <cp:coreProperties> element.
      #
      def write_cp_core_properties(&block)
        xmlns_cp       = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'
        xmlns_dc       = 'http://purl.org/dc/elements/1.1/'
        xmlns_dcterms  = 'http://purl.org/dc/terms/'
        xmlns_dcmitype = 'http://purl.org/dc/dcmitype/'
        xmlns_xsi      = 'http://www.w3.org/2001/XMLSchema-instance'

        attributes = [
          ['xmlns:cp',       xmlns_cp],
          ['xmlns:dc',       xmlns_dc],
          ['xmlns:dcterms',  xmlns_dcterms],
          ['xmlns:dcmitype', xmlns_dcmitype],
          ['xmlns:xsi',      xmlns_xsi]
        ]

        @writer.tag_elements('cp:coreProperties', attributes, &block)
      end

      #
      # Write the <dc:creator> element.
      #
      def write_dc_creator
        write_base(:author, 'dc:creator', '')
      end

      #
      # Write the <cp:lastModifiedBy> element.
      #
      def write_cp_last_modified_by
        write_base(:author, 'cp:lastModifiedBy', '')
      end

      #
      # Write the <dcterms:created> element.
      #
      def write_dcterms_created
        write_dcterms('dcterms:created')
      end

      #
      # Write the <dcterms:modified> element.
      #
      def write_dcterms_modified
        write_dcterms('dcterms:modified')
      end

      def write_dcterms(tag)
        @writer.data_element(tag, dcterms_date, [['xsi:type', 'dcterms:W3CDTF']])
      end

      def dcterms_date
        datetime_to_iso8601_date(@properties[:created])
      end

      #
      # Write the <dc:title> element.
      #
      def write_dc_title
        write_base(:title, 'dc:title')
      end

      #
      # Write the <dc:subject> element.
      #
      def write_dc_subject
        write_base(:subject, 'dc:subject')
      end

      #
      # Write the <cp:keywords> element.
      #
      def write_cp_keywords
        write_base(:keywords, 'cp:keywords')
      end

      #
      # Write the <dc:description> element.
      #
      def write_dc_description
        write_base(:comments, 'dc:description')
      end

      #
      # Write the <cp:category> element.
      #
      def write_cp_category
        write_base(:category, 'cp:category')
      end

      #
      # Write the <cp:contentStatus> element.
      #
      def write_cp_content_status
        write_base(:status, 'cp:contentStatus')
      end

      def write_base(key, tag, default = nil)
        data = @properties[key] || default
        return unless data

        @writer.data_element(tag, data)
      end
    end
	class Custom
      include Writexlsx_kot::Utility

      def initialize
        @writer     = Package::XMLWriterSimple.new
        @properties = []
        @pid        = 1
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_properties
        end
      end

      #
      # Set the document properties.
      #
      def set_properties(properties)
        @properties = properties
      end

      private

      def write_properties
        schema   = 'http://schemas.openxmlformats.org/officeDocument/2006/'
        xmlns    = "#{schema}custom-properties"
        xmlns_vt = "#{schema}docPropsVTypes"

        attributes = [
          ['xmlns',    xmlns],
          ['xmlns:vt', xmlns_vt]
        ]

        @writer.tag_elements('Properties', attributes) do
          @properties.each do |property|
            # Write the property element.
            write_property(property)
          end
        end
      end

      def write_property(property)
        fmtid = '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'

        @pid += 1
        name, value, type  = property

        attributes = [
          ['fmtid', fmtid],
          ['pid',   @pid],
          ['name',  name]
        ]

        @writer.tag_elements('property', attributes) do
          if type == 'date'
            # Write the vt:filetime element.
            write_vt_filetime(value)
          elsif type == 'number'
            # Write the vt:r8 element.
            write_vt_r8(value)
          elsif type == 'number_int'
            # Write the vt:i4 element.
            write_vt_i4(value)
          elsif type == 'bool'
            # Write the vt:bool element.
            write_vt_bool(value)
          else
            # Write the vt:lpwstr element.
            write_vt_lpwstr(value)
          end
        end
      end

      def write_vt_lpwstr(data)
        @writer.data_element('vt:lpwstr', data)
      end

      #
      # Write the <vt:i4> element.
      #
      def write_vt_i4(data)
        @writer.data_element('vt:i4', data)
      end

      #
      # Write the <vt:r8> element.
      #
      def write_vt_r8(data)
        @writer.data_element('vt:r8', data)
      end

      #
      # Write the <vt:bool> element.
      #
      def write_vt_bool(data)
        data = if ptrue?(data)
                 'true'
               else
                 'false'
               end

        @writer.data_element('vt:bool', data)
      end

      #
      # Write the <vt:filetime> element.
      #
      def write_vt_filetime(data)
        @writer.data_element('vt:filetime', data)
      end
    end
	class Metadata
      include Writexlsx_kot::Utility

      def initialize(workbook)
        @writer = Package::XMLWriterSimple.new
        @workbook      = workbook
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          # Write the metadata element.
          write_metadata
          # Write the metadataTypes element.
          write_metadata_types
          # Write the futureMetadata element.
          write_future_metadata
          # Write the cellMetadata element.
          write_cell_metadata
          @writer.end_tag('metadata')
        end
      end

      private

      #
      # Write the <metadata> element.
      #
      def write_metadata
        xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
        xmlns_xda =
          'http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray'

        attributes = [
          ['xmlns',     xmlns],
          ['xmlns:xda', xmlns_xda]
        ]

        @writer.start_tag('metadata', attributes)
      end

      #
      # Write the <metadataTypes> element.
      #
      def write_metadata_types
        attributes = [['count', 1]]

        @writer.tag_elements('metadataTypes', attributes) do
          # Write the metadataType element.
          write_metadata_type
        end
      end

      #
      # Write the <metadataType> element.
      #
      def write_metadata_type
        attributes = [
          %w[name XLDAPR],
          ['minSupportedVersion', 120000],
          ['copy',                1],
          ['pasteAll',            1],
          ['pasteValues',         1],
          ['merge',               1],
          ['splitFirst',          1],
          ['rowColShift',         1],
          ['clearFormats',        1],
          ['clearComments',       1],
          ['assign',              1],
          ['coerce',              1],
          ['cellMeta',            1]
        ]

        @writer.empty_tag('metadataType', attributes)
      end

      #
      # Write the <futureMetadata> element.
      #
      def write_future_metadata
        attributes = [
          %w[name XLDAPR],
          ['count', 1]
        ]

        @writer.tag_elements('futureMetadata', attributes) do
          @writer.tag_elements('bk') do
            @writer.tag_elements('extLst') do
              # Write the ext element.
              write_ext
            end
          end
        end
      end

      #
      # Write the <ext> element.
      #
      def write_ext
        attributes = [['uri', '{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}']]
        @writer.tag_elements('ext', attributes) do
          # Write the xda:dynamicArrayProperties element.
          write_xda_dynamic_array_properties
        end
      end

      #
      # Write the <xda:dynamicArrayProperties> element.
      #
      def write_xda_dynamic_array_properties
        attributes = [
          ['fDynamic',   1],
          ['fCollapsed', 0]
        ]

        @writer.empty_tag('xda:dynamicArrayProperties', attributes)
      end

      #
      # Write the <cellMetadata> element.
      #
      def write_cell_metadata
        count = 1

        attributes = [['count', count]]

        @writer.tag_elements('cellMetadata', attributes) do
          @writer.tag_elements('bk') do
            # Write the rc element.
            write_rc
          end
        end
      end

      #
      # Write the <rc> element.
      #
      def write_rc
        attributes = [
          ['t', 1],
          ['v', 0]
        ]
        @writer.empty_tag('rc', attributes)
      end
    end
    class Packager
      include Writexlsx_kot::Utility

      def initialize(workbook)
        @workbook     = workbook
        @package_dir  = ''
        @table_count  = @workbook.worksheets.tables_count
        @named_ranges = []
      end

      def set_package_dir(package_dir)
        @package_dir = package_dir
      end

      #
      # Write the xml files that make up the XLXS OPC package.
      #
      def create_package
        write_worksheet_files
        write_chartsheet_files
        write_workbook_file
        write_chart_files
        write_drawing_files
        write_vml_files
        write_comment_files
        write_table_files
        write_shared_strings_file
        write_app_file
        write_core_file
        write_custom_file
        write_content_types_file
        write_styles_file
        write_theme_file
        write_root_rels_file
        write_workbook_rels_file
        write_worksheet_rels_files
        write_chartsheet_rels_files
        write_drawing_rels_files
        add_image_files
        add_vba_project
        write_metadata_file
      end

      private

      #
      # Write the workbook.xml file.
      #
      def write_workbook_file
        FileUtils.mkdir_p("#{@package_dir}/xl")

        @workbook.set_xml_writer("#{@package_dir}/xl/workbook.xml")
        @workbook.assemble_xml_file
      end

      #
      # Write the worksheet files.
      #
      def write_worksheet_files
        @workbook.worksheets.write_worksheet_files(@package_dir)
      end

      def write_chartsheet_files
        @workbook.worksheets.write_chartsheet_files(@package_dir)
      end

      #
      # Write the chart files.
      #
      def write_chart_files
        write_chart_or_drawing_files(@workbook.charts, 'chart')
      end

      #
      # Write the drawing files.
      #
      def write_drawing_files
        write_chart_or_drawing_files(@workbook.drawings, 'drawing')
      end

      def write_chart_or_drawing_files(objects, filename)
        dir = "#{@package_dir}/xl/#{filename}s"

        objects.each_with_index do |object, index|
          FileUtils.mkdir_p(dir)
          object.set_xml_writer("#{dir}/#{filename}#{index + 1}.xml")
          object.assemble_xml_file
        end
      end

      #
      # Write the comment VML files.
      #
      def write_vml_files
        @workbook.worksheets.write_vml_files(@package_dir)
      end

      #
      # Write the comment files.
      #
      def write_comment_files
        @workbook.worksheets.write_comment_files(@package_dir)
      end

      #
      # Write the sharedStrings.xml file.
      #
      def write_shared_strings_file
        sst  = @workbook.shared_strings

        FileUtils.mkdir_p("#{@package_dir}/xl")

        return if @workbook.shared_strings_empty?

        sst.set_xml_writer("#{@package_dir}/xl/sharedStrings.xml")
        sst.assemble_xml_file
      end

      #
      # Write the app.xml file.
      #
      def write_app_file
        app = Package::App.new(@workbook)

        # Add the Worksheet parts.
        app.add_worksheet_part_names
        # Add the Worksheet heading pairs.
        app.add_worksheet_heading_pairs
        # Add the Chartsheet heading pairs.
        app.add_chartsheet_heading_pairs
        # Add the Chartsheet parts.
        app.add_chartsheet_part_names
        # Add the Named Range heading pairs.
        app.add_named_range_heading_pairs
        # Add the Named Ranges parts.
        app.add_named_ranges_parts

        app.set_properties(@workbook.doc_properties)
        app.doc_security = @workbook.read_only

        FileUtils.mkdir_p("#{@package_dir}/docProps")
        app.set_xml_writer("#{@package_dir}/docProps/app.xml")
        app.assemble_xml_file
      end

      #
      # Write the core.xml file.
      #
      def write_core_file
        core       = Package::Core.new

        FileUtils.mkdir_p("#{@package_dir}/docProps")

        core.set_properties(@workbook.doc_properties)
        core.set_xml_writer("#{@package_dir}/docProps/core.xml")
        core.assemble_xml_file
      end

      #
      # Write the metadata.xml file.
      #
      def write_metadata_file
        metadata = Package::Metadata.new(@workbook)

        return unless @workbook.has_metadata?

        FileUtils.mkdir_p("#{@package_dir}/xl")

        metadata.set_xml_writer("#{@package_dir}/xl/metadata.xml")
        metadata.assemble_xml_file
      end

      #
      # Write the custom.xml file.
      #
      def write_custom_file
        properties = @workbook.custom_properties
        custom     = Package::Custom.new

        return if properties.empty?

        FileUtils.mkdir_p("#{@package_dir}/docProps")

        custom.set_properties(properties)
        custom.set_xml_writer("#{@package_dir}/docProps/custom.xml")
        custom.assemble_xml_file
      end

      #
      # Write the ContentTypes.xml file.
      #
      def write_content_types_file
        content = Package::ContentTypes.new(@workbook)

        content.add_image_types
        content.add_worksheet_names
        content.add_chartsheet_names
        content.add_chart_names
        content.add_drawing_names
        content.add_vml_name if @workbook.num_vml_files > 0
        content.add_table_names(@table_count)
        content.add_comment_names
        # Add the sharedString rel if there is string data in the workbook.
        content.add_shared_strings unless @workbook.shared_strings_empty?
        # Add vbaProject if present.
        content.add_vba_project if @workbook.vba_project
        # Add the custom properties if present.
        content.add_custom_properties unless @workbook.custom_properties.empty?
        # Add the metadata file if present.
        content.add_metadata if @workbook.has_metadata?

        content.set_xml_writer("#{@package_dir}/[Content_Types].xml")
        content.assemble_xml_file
      end

      #
      # Write the style xml file.
      #
      def write_styles_file
        dir              = "#{@package_dir}/xl"

        rels = Package::Styles.new

        FileUtils.mkdir_p(dir)

        rels.set_style_properties(*@workbook.style_properties)

        rels.set_xml_writer("#{dir}/styles.xml")
        rels.assemble_xml_file
      end

      #
      # Write the style xml file.
      #
      def write_theme_file
        rels = Package::Theme.new

        FileUtils.mkdir_p("#{@package_dir}/xl/theme")

        rels.set_xml_writer("#{@package_dir}/xl/theme/theme1.xml")
        rels.assemble_xml_file
      end

      #
      # Write the table files.
      #
      def write_table_files
        @workbook.worksheets.write_table_files(@package_dir)
      end

      #
      # Write the _rels/.rels xml file.
      #
      def write_root_rels_file
        rels = Package::Relationships.new

        FileUtils.mkdir_p("#{@package_dir}/_rels")

        rels.add_document_relationship('/officeDocument', 'xl/workbook.xml')

        rels.add_package_relationship(
          '/metadata/core-properties', 'docProps/core.xml'
        )

        rels.add_document_relationship(
          '/extended-properties', 'docProps/app.xml'
        )

        unless @workbook.custom_properties.empty?
          rels.add_document_relationship(
            '/custom-properties', 'docProps/custom.xml'
          )
        end
        rels.set_xml_writer("#{@package_dir}/_rels/.rels")
        rels.assemble_xml_file
      end

      #
      # Write the _rels/.rels xml file.
      #
      def write_workbook_rels_file
        rels = Package::Relationships.new

        FileUtils.mkdir_p("#{@package_dir}/xl/_rels")

        worksheet_index  = 1
        chartsheet_index = 1

        @workbook.worksheets.each do |worksheet|
          if worksheet.is_chartsheet?
            rels.add_document_relationship(
              '/chartsheet', "chartsheets/sheet#{chartsheet_index}.xml"
            )
            chartsheet_index += 1
          else
            rels.add_document_relationship(
              '/worksheet', "worksheets/sheet#{worksheet_index}.xml"
            )
            worksheet_index += 1
          end
        end

        rels.add_document_relationship('/theme',  'theme/theme1.xml')
        rels.add_document_relationship('/styles', 'styles.xml')

        # Add the sharedString rel if there is string data in the workbook.
        unless @workbook.shared_strings_empty?
          rels.add_document_relationship(
            '/sharedStrings', 'sharedStrings.xml'
          )
        end

        # Add vbaProject if present.
        rels.add_ms_package_relationship('/vbaProject', 'vbaProject.bin') if @workbook.vba_project

        # Add the metadata file if required.
        if @workbook.has_metadata?
          rels.add_document_relationship(
            '/sheetMetadata', 'metadata.xml'
          )
        end

        rels.set_xml_writer("#{@package_dir}/xl/_rels/workbook.xml.rels")
        rels.assemble_xml_file
      end

      #
      # Write the worksheet .rels files for worksheets that contain links to
      # external data such as hyperlinks or drawings.
      #
      def write_worksheet_rels_files
        @workbook.worksheets.write_worksheet_rels_files(@package_dir)
      end

      #
      # Write the chartsheet .rels files for links to drawing files.
      #
      def write_chartsheet_rels_files
        @workbook.worksheets.write_chartsheet_rels_files(@package_dir)
      end

      #
      # Write the drawing .rels files for worksheets that contain charts or
      # drawings.
      #
      def write_drawing_rels_files
        @workbook.worksheets.write_drawing_rels_files(@package_dir)
      end

      #
      # Write the /xl/media/image?.xml files.
      #
      def add_image_files
        dir = "#{@package_dir}/xl/media"

        @workbook.images.each_with_index do |image, index|
          FileUtils.mkdir_p(dir)
          FileUtils.cp(image[0], "#{dir}/image#{index + 1}.#{image[1]}")
        end
      end

      #
      # Write the vbaProject.bin file.
      #
      def add_vba_project
        dir = @package_dir
        vba_project = @workbook.vba_project

        return unless vba_project

        FileUtils.mkdir_p("#{dir}/xl")
        FileUtils.copy(vba_project, "#{dir}/xl/vbaProject.bin")
      end
    end
	class Relationships
      include Writexlsx_kot::Utility

      Schema_root     = 'http://schemas.openxmlformats.org'
      Package_schema  = Schema_root + '/package/2006/relationships'
      Document_schema = Schema_root + '/officeDocument/2006/relationships'

      def initialize
        @writer = Package::XMLWriterSimple.new
        @rels   = []
        @id     = 1
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_relationships
        end
      end

      #
      # Add container relationship to XLSX .rels xml files.
      #
      def add_document_relationship(type, target, target_mode = nil)
        @rels.push([Document_schema + type, target, target_mode])
      end

      #
      # Add container relationship to XLSX .rels xml files.
      #
      def add_package_relationship(type, target)
        @rels.push([Package_schema + type, target])
      end

      #
      # Add container relationship to XLSX .rels xml files. Uses MS schema.
      #
      def add_ms_package_relationship(type, target)
        schema = 'http://schemas.microsoft.com/office/2006/relationships'
        @rels.push([schema + type, target])
      end

      #
      # Add worksheet relationship to sheet.rels xml files.
      #
      def add_worksheet_relationship(type, target, target_mode = nil)
        @rels.push([Document_schema + type, target, target_mode])
      end

      private

      #
      # Write the <Relationships> element.
      #
      def write_relationships
        attributes = [
          ['xmlns', Package_schema]
        ]

        @writer.tag_elements('Relationships', attributes) do
          @rels.each { |rel| write_relationship(*rel) }
        end
      end

      #
      # Write the <Relationship> element.
      #
      def write_relationship(type, target, target_mode = nil)
        attributes = [
          ['Id',     "rId#{@id}"],
          ['Type',   type],
          ['Target', target]
        ]
        @id += 1

        attributes << ['TargetMode', target_mode] if target_mode

        @writer.empty_tag('Relationship', attributes)
      end
    end
	class SharedStrings
      include Writexlsx_kot::Utility

      PRESERVE_SPACE_ATTRIBUTES = ['xml:space', 'preserve'].freeze

      attr_reader :strings

      def initialize
        @writer        = Package::XMLWriterSimple.new
        @strings       = [] # string table
        @strings_index = {} # string table index
        @count         = 0 # count
        @str_unique    = 0
      end

      def index(string, params = {})
        add(string) unless params[:only_query]
        @strings_index[string]
      end

      def add(string)
        unless @strings_index[string]
          str = string.frozen? ? string : string.freeze
          @strings << str
          @str_unique += 1
          @strings_index[str] = @strings.size - 1
        end
        @count += 1
      end

      def string(index)
        @strings[index]
      end

      def empty?
        @strings.empty?
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          # Write the sst table.
          write_sst { write_sst_strings }
        end
      end

      private

      #
      # Write the <sst> element.
      #
      def write_sst(&block)
        schema       = 'http://schemas.openxmlformats.org'

        attributes =
          [
            ['xmlns',       schema + '/spreadsheetml/2006/main'],
            ['count',       total_count],
            ['uniqueCount', unique_count]
          ]

        @writer.tag_elements('sst', attributes, &block)
      end

      #
      # Write the sst string elements.
      #
      def write_sst_strings
        @strings.each { |string| write_si(string) }
      end

      #
      # Write the <si> element.
      #
      def write_si(string)
        attributes = []

        # Excel escapes control characters with _xHHHH_ and also escapes any
        # literal strings of that type by encoding the leading underscore. So
        # "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
        # The following substitutions deal with those cases.

        # Escape the escape.
        string = string.gsub(/(_x[0-9a-fA-F]{4}_)/, '_x005F\1')

        # Convert control character to the _xHHHH_ escape.
        if string =~ /([\x00-\x08\x0B-\x1F])/
          string = string.gsub(
            /([\x00-\x08\x0B-\x1F])/,
            sprintf("_x%04X_", ::Regexp.last_match(1).ord)
          )
        end

        # Convert character to \xC2\xxx or \xC3\xxx
        string = add_c2_c3(string) if string.bytesize == 1 && 0x80 <= string.ord && string.ord <= 0xFF

        # Add attribute to preserve leading or trailing whitespace.
        attributes << PRESERVE_SPACE_ATTRIBUTES if string =~ /\A\s|\s\Z/

        # Write any rich strings without further tags.
        if string =~ /^<r>/ && string =~ %r{</r>$}
          @writer.si_rich_element(string)
        else
          @writer.si_element(string, attributes)
        end
      end

      def add_c2_c3(string)
        num = string.ord
        if 0x80 <= num && num < 0xC0
          0xC2.chr + num.chr
        else
          0xC3.chr + (num - 0x40).chr
        end
      end

      def total_count
        @count
      end

      def unique_count
        @str_unique
      end
    end
	class Styles
      include Writexlsx_kot::Utility

      def initialize
        @writer = Package::XMLWriterSimple.new
        @xf_formats        = nil
        @palette           = []
        @font_count        = 0
        @num_formats       = []
        @border_count      = 0
        @fill_count        = 0
        @custom_colors     = []
        @dxf_formats       = []
        @has_hyperlink     = 0
        @hyperlink_font_id = 0
        @has_comments      = false
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file
        write_xml_declaration do
          write_style_sheet { write_style_sheet_base }
        end
      end

      #
      # Pass in the Format objects and other properties used to set the styles.
      #
      def set_style_properties(
            xf_formats, palette, font_count, num_formats, border_count,
            fill_count, custom_colors, dxf_formats, has_comments
          )
        @xf_formats    = xf_formats
        @palette       = palette
        @font_count    = font_count
        @num_formats   = num_formats
        @border_count  = border_count
        @fill_count    = fill_count
        @custom_colors = custom_colors
        @dxf_formats   = dxf_formats
        @has_comments  = has_comments
      end

      #
      # Convert from an Excel internal colour index to a XML style #RRGGBB index
      # based on the default or user defined values in the Workbook palette.
      #
      def palette_color(index)
        if index.to_s =~ /^#([0-9A-F]{6})$/i
          "FF#{::Regexp.last_match(1).upcase}"
        elsif index == 0x40
          "Automatic"
        else
          "FF#{super(index)}"
        end
      end

      #
      # Write the <styleSheet> element.
      #
      def write_style_sheet(&block)
        attributes = [['xmlns', XMLWriterSimple::XMLNS]]

        @writer.tag_elements('styleSheet', attributes, &block)
      end

      #
      # Write the <numFmts> element.
      #
      def write_num_fmts
        count = @num_formats.size

        return if count == 0

        attributes = [['count', count]]

        @writer.tag_elements('numFmts', attributes) do
          # Write the numFmts elements.
          index = 164
          @num_formats.each do |num_format|
            write_num_fmt(index, num_format)
            index += 1
          end
        end
      end

      FORMAT_CODES = {
        0  => 'General',
        1  => '0',
        2  => '0.00',
        3  => '#,##0',
        4  => '#,##0.00',
        5  => '($#,##0_);($#,##0)',
        6  => '($#,##0_);[Red]($#,##0)',
        7  => '($#,##0.00_);($#,##0.00)',
        8  => '($#,##0.00_);[Red]($#,##0.00)',
        9  => '0%',
        10 => '0.00%',
        11 => '0.00E+00',
        12 => '# ?/?',
        13 => '# ??/??',
        14 => 'm/d/yy',
        15 => 'd-mmm-yy',
        16 => 'd-mmm',
        17 => 'mmm-yy',
        18 => 'h:mm AM/PM',
        19 => 'h:mm:ss AM/PM',
        20 => 'h:mm',
        21 => 'h:mm:ss',
        22 => 'm/d/yy h:mm',
        37 => '(#,##0_);(#,##0)',
        38 => '(#,##0_);[Red](#,##0)',
        39 => '(#,##0.00_);(#,##0.00)',
        40 => '(#,##0.00_);[Red](#,##0.00)',
        41 => '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)',
        42 => '_($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)',
        43 => '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)',
        44 => '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)',
        45 => 'mm:ss',
        46 => '[h]:mm:ss',
        47 => 'mm:ss.0',
        48 => '##0.0E+0',
        49 => '@'
      }

      #
      # Write the <numFmt> element.
      #
      def write_num_fmt(num_fmt_id, format_code)
        # Set the format code for built-in number formats.
        format_code = FORMAT_CODES[num_fmt_id] || 'General' if num_fmt_id < 164

        attributes = [
          ['numFmtId',   num_fmt_id],
          ['formatCode', format_code]
        ]

        @writer.empty_tag('numFmt', attributes)
      end

      #
      # Write the <fonts> element.
      #
      def write_fonts
        count = @font_count

        if @has_comments
          # Add an extra font for comments.
          count += 1
        end

        write_format_elements('fonts', count) do
          write_font_base
        end
      end

      def write_font_base
        @xf_formats.each do |format|
          next unless format.has_font?

          format.write_font(@writer, self)
          @has_hyperlink     = 1 if ptrue?(format.hyperlink)
          @hyperlink_font_id = format.font_index unless ptrue?(@hyperlink_font_id)
        end
        write_comment_font if @has_comments
      end

      #
      # Write the <font> element used for comments.
      #
      def write_comment_font
        @writer.tag_elements('font') do
          @writer.empty_tag('sz', [['val', 8]])
          write_color('indexed', 81)
          @writer.empty_tag('name',   [%w[val Tahoma]])
          @writer.empty_tag('family', [['val', 2]])
        end
      end

      #
      # Write the <fills> element.
      #
      def write_fills
        attributes = [['count', @fill_count]]

        @writer.tag_elements('fills', attributes) do
          write_fills_base
        end
      end

      def write_fills_base
        # Write the default fill element.
        write_default_fill('none')
        write_default_fill('gray125')

        # Write the fill elements for format objects that have them.
        @xf_formats.each do |format|
          write_fill(format) if format.has_fill?
        end
      end

      #
      # Write the <fill> element for the default fills.
      #
      def write_default_fill(pattern_type)
        @writer.tag_elements('fill') do
          @writer.empty_tag('patternFill', [['patternType', pattern_type]])
        end
      end

      PATTERNS = %w[
        none
        solid
        mediumGray
        darkGray
        lightGray
        darkHorizontal
        darkVertical
        darkDown
        darkUp
        darkGrid
        darkTrellis
        lightHorizontal
        lightVertical
        lightDown
        lightUp
        lightGrid
        lightTrellis
        gray125
        gray0625
      ]

      #
      # Write the <fill> element.
      #
      def write_fill(format, dxf_format = nil)
        # Special handling for pattern only case.
        if pattern_only_case?(format, dxf_format)
          write_default_fill(PATTERNS[format.pattern])
        else
          @writer.tag_elements('fill') do
            write_fill_base(format, dxf_format)
          end
        end
      end

      def pattern_only_case?(format, dxf_format)
        bg_color, fg_color = bg_and_fg_color(format, dxf_format)

        !ptrue?(fg_color) && !ptrue?(bg_color) && ptrue?(format.pattern)
      end

      def write_fill_base(format, dxf_format)
        # The "none" pattern is handled differently for dxf formats.
        attributes = if dxf_format && format.pattern <= 1
                       []
                     else
                       [['patternType', PATTERNS[format.pattern]]]
                     end

        @writer.tag_elements('patternFill', attributes) do
          write_pattern_fill(format, dxf_format)
        end
      end

      def write_pattern_fill(format, dxf_format)
        bg_color, fg_color = bg_and_fg_color(format, dxf_format)

        if fg_color && fg_color != 0 && fg_color != 0x40  # 'Automatic'
          @writer.empty_tag('fgColor', [['rgb', palette_color(fg_color)]])
        end

        if bg_color && bg_color != 0
          if bg_color != 0x40 # 'Automatic'
            @writer.empty_tag('bgColor', [['rgb', palette_color(bg_color)]])
          end
        elsif !dxf_format && format.pattern <= 1
          @writer.empty_tag('bgColor', [['indexed', 64]])
        end
      end

      def bg_and_fg_color(format, dxf_format)
        bg_color   = format.bg_color
        fg_color   = format.fg_color

        # Colors for dxf formats are handled differently from normal formats since
        # the normal format reverses the meaning of BG and FG for solid fills.
        if dxf_format && dxf_format != 0
          bg_color = format.dxf_bg_color
          fg_color = format.dxf_fg_color
        end

        [bg_color, fg_color]
      end

      #
      # Write the <borders> element.
      #
      def write_borders
        write_format_elements('borders', @border_count) do
          write_borders_base
        end
      end

      def write_borders_base
        @xf_formats.each do |format|
          write_border(format) if format.has_border?
        end
      end

      def write_format_elements(elements, count, &block)
        attributes = [['count', count]]

        @writer.tag_elements(elements, attributes, &block)
      end

      #
      # Write the <border> element.
      #
      def write_border(format, dxf_format = nil)
        # Write the start border tag.
        @writer.tag_elements('border', format.border_attributes) do
          write_border_base(format, dxf_format)
        end
      end

      def write_border_base(format, dxf_format)
        # Write the <border> sub elements.
        write_border_sub_elements(format)

        # Condition DXF formats don't allow diagonal borders
        if dxf_format
          write_sub_border('vertical')
          write_sub_border('horizontal')
        else
          # Ensure that a default diag border is set if the diag type is set.
          format.diag_border = 1 if format.diag_type != 0 && format.diag_border == 0

          write_sub_border('diagonal', format.diag_border, format.diag_color)
        end
      end

      def write_border_sub_elements(format)
        write_sub_border('left',   format.left,   format.left_color)
        write_sub_border('right',  format.right,  format.right_color)
        write_sub_border('top',    format.top,    format.top_color)
        write_sub_border('bottom', format.bottom, format.bottom_color)
      end

      BORDER_STYLES = %w[
        none
        thin
        medium
        dashed
        dotted
        thick
        double
        hair
        mediumDashed
        dashDot
        mediumDashDot
        dashDotDot
        mediumDashDotDot
        slantDashDot
      ]

      #
      # Write the <border> sub elements such as <right>, <top>, etc.
      #
      def write_sub_border(type, style = 0, color = nil)
        if style == 0
          @writer.empty_tag(type)
          return
        end

        attributes = [[:style, BORDER_STYLES[style]]]

        @writer.tag_elements(type, attributes) do
          if [0, 0x40].include?(color) # 'Automatic'
            @writer.empty_tag('color', [['auto', 1]])
          elsif color != 0 && color != 0x40 # 'Automatic'
            color = palette_color(color)

            @writer.empty_tag('color', [['rgb', color]])
          end
        end
      end

      #
      # Write the <cellStyleXfs> element.
      #
      def write_cell_style_xfs
        count = ptrue?(@has_hyperlink) ? 2 : 1

        attributes = [['count', count]]

        @writer.tag_elements('cellStyleXfs', attributes) do
          # Write the style_xf element.
          write_style_xf(0, 0)

          write_style_xf(1, @hyperlink_font_id) if ptrue?(@has_hyperlink)
        end
      end

      #
      # Write the <cellXfs> element.
      #
      def write_cell_xfs
        formats = @xf_formats

        attributes = [['count', formats.size]]

        @writer.tag_elements('cellXfs', attributes) do
          # Write the xf elements.
          formats.each { |format| write_xf(format) }
        end
      end

      #
      # Write the style <xf> element.
      #
      def write_style_xf(has_hyperlink, font_id)
        attributes = [
          ['numFmtId', 0],
          ['fontId',   font_id],
          ['fillId',   0],
          ['borderId', 0]
        ]

        if ptrue?(has_hyperlink)
          attributes << ['applyNumberFormat', 0]
          attributes << ['applyFill',         0]
          attributes << ['applyBorder',       0]
          attributes << ['applyAlignment',    0]
          attributes << ['applyProtection',   0]
          @writer.tag_elements('xf', attributes) do
            @writer.empty_tag('alignment',  [%w[vertical top]])
            @writer.empty_tag('protection', [['locked',   0]])
          end
        else
          @writer.empty_tag('xf', attributes)
        end
      end

      private

      def write_style_sheet_base
        write_num_fmts
        write_fonts
        write_fills
        write_borders
        write_cell_style_xfs
        write_cell_xfs
        write_cell_styles
        write_dxfs
        write_table_styles
        write_colors
      end

      #
      # Write the <xf> element.
      #
      def write_xf(format)
        # Check if XF format has alignment properties set.
        apply_align, align = format.get_align_properties

        # Check for cell protection properties.
        protection = format.get_protection_properties

        # Check if an alignment sub-element should be written.
        has_align = apply_align && !align.empty?

        # Write XF with sub-elements if required.
        if has_align || protection
          @writer.tag_elements('xf', format.xf_attributes) do
            @writer.empty_tag('alignment',  align)      if has_align
            @writer.empty_tag('protection', protection) if protection
          end
        else
          @writer.empty_tag('xf', format.xf_attributes)
        end
      end

      #
      # Write the <cellStyles> element.
      #
      def write_cell_styles
        count = ptrue?(@has_hyperlink) ? 2 : 1

        attributes = [['count', count]]

        @writer.tag_elements('cellStyles', attributes) do
          # Write the cellStyle element.
          write_cell_style('Hyperlink', 1, 8) if ptrue?(@has_hyperlink)
          write_cell_style('Normal', 0, 0)
        end
      end

      #
      # Write the <cellStyle> element.
      #
      def write_cell_style(name, xf_id, builtin_id)
        attributes = [
          ['name',      name],
          ['xfId',      xf_id],
          ['builtinId', builtin_id]
        ]

        @writer.empty_tag('cellStyle', attributes)
      end

      #
      # Write the <dxfs> element.
      #
      def write_dxfs
        attributes = [['count', @dxf_formats.count]]

        if @dxf_formats.empty?
          @writer.empty_tag('dxfs', attributes)
        else
          @writer.tag_elements('dxfs', attributes) do
            # Write the font elements for format objects that have them.
            @dxf_formats.each do |format|
              write_dxf(format)
            end
          end
        end
      end

      def write_dxf(format)
        @writer.tag_elements('dxf') do
          format.write_font(@writer, self, 1) if format.has_dxf_font?

          write_num_fmt(format.num_format_index, format.num_format) if format.num_format_index != 0

          write_fill(format, 1)    if format.has_dxf_fill?
          write_border(format, 1)  if format.has_dxf_border?
        end
      end

      #
      # Write the <tableStyles> element.
      #
      def write_table_styles
        attributes = [
          ['count',             0],
          %w[defaultTableStyle TableStyleMedium9],
          %w[defaultPivotStyle PivotStyleLight16]
        ]

        @writer.empty_tag('tableStyles', attributes)
      end

      #
      # Write the <colors> element.
      #
      def write_colors
        return if @custom_colors.empty?

        @writer.tag_elements('colors') do
          write_mru_colors(@custom_colors)
        end
      end

      #
      # Write the <mruColors> element for the most recently used colours.
      #
      def write_mru_colors(custom_colors)
        # Limit the mruColors to the last 10.
        count = custom_colors.size
        # array[-10, 10] returns array which contains last 10 items.
        custom_colors = custom_colors[-10, 10] if count > 10

        @writer.tag_elements('mruColors') do
          # Write the custom colors in reverse order.
          custom_colors.reverse.each do |color|
            write_color('rgb', color)
          end
        end
      end

      #
      # Write the <condense> element.
      #
      def write_condense
        attributes = [['val', 0]]

        @writer.empty_tag('condense', attributes)
      end

      #
      # Write the <extend> element.
      #
      def write_extend
        attributes = [['val', 0]]

        @writer.empty_tag('extend', attributes)
      end
    end
	class Table
      include Writexlsx_kot::Utility

      class ColumnData
        attr_reader :id
        attr_accessor :name, :format, :formula, :name_format
        attr_accessor :total_string, :total_function, :custom_total

        def initialize(id, param = {})
          @id             = id
          @name           = "Column#{id}"
          @total_string   = ''
          @total_function = ''
          @custom_total   = ''
          @formula        = ''
          @format         = nil
          @name_format    = nil
          @user_data      = param[id - 1] if param
        end
      end

      attr_reader :id, :name

      def initialize(worksheet, *args)
        @worksheet = worksheet
        @writer  = Package::XMLWriterSimple.new

        @row1, @row2, @col1, @col2, @param = handle_args(*args)
        @columns     = []
        @col_formats = []
        @seen_name   = {}

        # Set the data range rows (without the header and footer).
        @first_data_row = @row1
        @first_data_row += 1 if ptrue?(@param[:header_row])
        @last_data_row = @row2
        @last_data_row -= 1 if @param[:total_row]

        set_the_table_options
        set_the_table_style
        set_the_table_name
        set_the_table_and_autofilter_ranges
        set_the_autofilter_range

        add_the_table_columns
        write_the_cell_data_if_supplied
        write_any_columns_formulas_after_the_user_supplied_table_data
        store_filter_cell_positions
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      #
      # Assemble and writes the XML file.
      #
      def assemble_xml_file
        write_xml_declaration do
          # Write the table element.
          @writer.tag_elements('table', write_table_attributes) do
            write_auto_filter
            write_table_columns
            write_table_style_info
          end
        end
      end

      def add_the_table_columns
        col_id = 0
        (@col1..@col2).each do |col_num|
          # Set up the default column data.
          col_data = Package::Table::ColumnData.new(col_id + 1, @param[:columns])

          overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)

          # Store the column data.
          @columns << col_data

          write_the_column_headers_to_the_worksheet(col_num, col_data)

          col_id += 1
        end    # Table columns.
      end

      def overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
        # Check if there are user defined values for this column.
        if @param[:columns] && (user_data = @param[:columns][col_id])
          # Map user defined values to internal values.
          col_data.name = user_data[:header] if user_data[:header] && !user_data[:header].empty?

          # Excel requires unique case insensitive header names.
          if @seen_name[col_data.name.downcase]
            raise "add_table() contains duplicate name: '#{col_data.name}'"
          else
            @seen_name[col_data.name.downcase] = true
          end

          # Get the header format if defined.
          col_data.name_format = user_data[:header_format]

          # Handle the column formula.
          handle_the_column_formula(
            col_data, col_num, user_data[:formula], user_data[:format]
          )

          # Handle the function for the total row.
          if user_data[:total_function]
            handle_the_function_for_the_table_row(
              @row2, col_data, col_num, user_data
            )
          elsif user_data[:total_string]
            total_label_only(
              @row2, col_num, col_data, user_data[:total_string], user_data[:format]
            )
          end

          # Get the dxf format index.
          col_data.format = user_data[:format].get_dxf_index if user_data[:format]

          # Store the column format for writing the cell data.
          # It doesn't matter if it is undefined.
          @col_formats[col_id] = user_data[:format]
        end
      end

      def write_the_column_headers_to_the_worksheet(col_num, col_data)
        if @param[:header_row] != 0
          @worksheet.write_string(
            @row1, col_num, col_data.name, col_data.name_format
          )
        end
      end

      def write_the_cell_data_if_supplied
        return unless @param[:data]

        data = @param[:data]
        i = 0    # For indexing the row data.
        (@first_data_row..@last_data_row).each do |row|
          next unless data[i]

          j = 0    # For indexing the col data.
          (@col1..@col2).each do |col|
            token = data[i][j]
            @worksheet.write(row, col, token, @col_formats[j]) if token
            j += 1
          end
          i += 1
        end
      end

      def write_any_columns_formulas_after_the_user_supplied_table_data
        col_id = 0

        (@col1..@col2).each do |col|
          column_data = @columns[col_id]
          if ptrue?(column_data) && ptrue?(column_data.formula)
            formula_format = @col_formats[col_id]
            formula        = column_data.formula

            (@first_data_row..@last_data_row).each do |row|
              @worksheet.write_formula(row, col, formula, formula_format)
            end
          end
          col_id += 1
        end
      end

      def store_filter_cell_positions
        if ptrue?(@param[:autofilter])
          (@col1..@col2).each do |col|
            @worksheet.filter_cells["#{@row1}:#{col}"] = 1
          end
        end
      end

      def prepare(id)
        @id = id
        @name ||= "Table#{id}"
      end

      private

      def handle_args(row1, col1 = nil, row2 = nil, col2 = nil, param = nil)
        # Check for a cell reference in A1 notation and substitute row and column
        if (row_col_array = row_col_notation(row1))
          _row1, _col1, _row2, _col2 = row_col_array
          _param = col1
        else
          _row1 = row1
          _col1 = col1
          _row2 = row2
          _col2 = col2
          _param = param
        end

        # Check for a valid number of args.
        raise "Not enough parameters to add_table()" if [_row1, _col1, _row2, _col2].include?(nil)

        # Check that row and col are valid without storing the values.
        check_dimensions_and_update_max_min_values(_row1, _col1, 1, 1)
        check_dimensions_and_update_max_min_values(_row2, _col2, 1, 1)

        # Swap last row/col for first row/col as necessary.
        _row1, _row2 = _row2, _row1 if _row1 > _row2
        _col1, _col2 = _col2, _col1 if _col1 > _col2

        # The final hash contains the validation parameters.
        _param ||= {}

        # Turn on Excel's defaults.
        _param[:banded_rows] ||= 1
        _param[:header_row]  ||= 1
        _param[:autofilter]  ||= 1

        # Check that there are enough rows.
        num_rows = _row2 - _row1
        num_rows -= 1 if ptrue?(_param[:header_row])

        raise "Must have at least one data row in in add_table()" if num_rows < 0

        # If the header row if off the default is to turn autofilter off.
        _param[:autofilter] = 0 if _param[:header_row] == 0

        check_parameter(_param, valid_table_parameter, 'add_table')

        [_row1, _row2, _col1, _col2, _param]
      end

      # List of valid input parameters.
      def valid_table_parameter
        %i[
          autofilter
          banded_columns
          banded_rows
          columns
          data
          first_column
          header_row
          last_column
          name
          style
          total_row
        ]
      end

      def handle_the_column_formula(col_data, _col_num, formula, _format)
        return unless formula

        formula = formula.sub(/^=/, '').gsub(/@/, '[#This Row],')

        # Escape any future functions.
        col_data.formula = @worksheet.prepare_formula(formula, 1)

        # (@first_data_row..@last_data_row).each do |row|
        #   @worksheet.write_formula(row, col_num, col_data.formula, format)
        # end
      end

      def handle_the_function_for_the_table_row(row2, col_data, col_num, user_data)
        formula = ''
        function = user_data[:total_function]
        function = 'countNums' if function == 'count_nums'
        function = 'stdDev'    if function == 'std_dev'

        subtotals = {
          average:   101,
          countNums: 102,
          count:     103,
          max:       104,
          min:       105,
          stdDev:    106,
          sum:       109,
          var:       110
        }

        if subtotals[function.to_sym]
          formula = table_function_to_formula(function, col_data.name)
        else
          formula = @worksheet.prepare_formula(function, 1)
          col_data.custom_total = formula
          function = 'custom'
        end

        col_data.total_function = function
        @worksheet.write_formula(row2, col_num, formula, user_data[:format], user_data[:total_value])
      end

      #
      # Convert a table total function to a worksheet formula.
      #
      def table_function_to_formula(function, col_name)
        col_name = col_name.gsub("'", "''")
                           .gsub("#", "'#")
                           .gsub("[", "'[")
                           .gsub("]", "']")

        subtotals = {
          average:   101,
          countNums: 102,
          count:     103,
          max:       104,
          min:       105,
          stdDev:    107,
          sum:       109,
          var:       110
        }

        unless (func_num = subtotals[function.to_sym])
          raise "Unsupported function '#{function}' in add_table()"
        end

        "SUBTOTAL(#{func_num},[#{col_name}])"
      end

      # Total label only (not a function).
      def total_label_only(row2, col_num, col_data, total_string, format)
        col_data.total_string = total_string

        @worksheet.write_string(row2, col_num, total_string, format)
      end

      def set_the_table_options
        @show_first_col   = ptrue?(@param[:first_column])   ? 1 : 0
        @show_last_col    = ptrue?(@param[:last_column])    ? 1 : 0
        @show_row_stripes = ptrue?(@param[:banded_rows])    ? 1 : 0
        @show_col_stripes = ptrue?(@param[:banded_columns]) ? 1 : 0
        @header_row_count = ptrue?(@param[:header_row])     ? 1 : 0
        @totals_row_shown = ptrue?(@param[:total_row])      ? 1 : 0
      end

      def set_the_table_style
        @style = if @param[:style]
                   @param[:style].gsub(/\s/, '')
                 else
                   "TableStyleMedium9"
                 end
      end

      def set_the_table_name
        if @param[:name]
          name = @param[:name]

          # Raise if the name contains invalid chars as defined by Excel help.
          raise "Invalid character in name '#{name} used in add_table()" if name !~ /^[\w\\][\w\\.]*$/ || name =~ /^\d/

          # Raise if the name looks like a cell name.
          ralse "Invalid name '#{name}' looks like a cell name in add_table()" if name =~ /^[a-zA-Z][a-zA-Z]?[a-dA-D]?[0-9]+$/

          # Raise if the name looks like a R1C1.
          raise "Invalid name '#{name}' like a RC cell ref in add_table()" if name =~ /^[rcRC]$/ || name =~ /^[rcRC]\d+[rcRC]\d+$/

          @name = @param[:name]
        end
      end

      def set_the_table_and_autofilter_ranges
        @range   = xl_range(@row1, @row2,          @col1, @col2)
        @a_range = xl_range(@row1, @last_data_row, @col1, @col2)
      end

      def set_the_autofilter_range
        @autofilter = @a_range if ptrue?(@param[:autofilter])
      end

      def write_table_attributes
        schema           = 'http://schemas.openxmlformats.org/'
        xmlns            = "#{schema}spreadsheetml/2006/main"

        attributes = [
          ['xmlns',       xmlns],
          ['id',          id],
          ['name',        @name],
          ['displayName', @name],
          ['ref',         @range]
        ]

        attributes << ['headerRowCount', 0] unless ptrue?(@header_row_count)

        attributes << if ptrue?(@totals_row_shown)
                        ['totalsRowCount', 1]
                      else
                        ['totalsRowShown', 0]
                      end
      end

      #
      # Write the <autoFilter> element.
      #
      def write_auto_filter
        return unless ptrue?(@autofilter)

        attributes = [['ref', @autofilter]]

        @writer.empty_tag('autoFilter', attributes)
      end

      #
      # Write the <tableColumns> element.
      #
      def write_table_columns
        count = @columns.size

        attributes = [['count', count]]

        @writer.tag_elements('tableColumns', attributes) do
          @columns.each { |col_data| write_table_column(col_data) }
        end
      end

      #
      # Write the <tableColumn> element.
      #
      def write_table_column(col_data)
        attributes = [
          ['id',   col_data.id],
          ['name', col_data.name]
        ]

        if ptrue?(col_data.total_string)
          attributes << [:totalsRowLabel, col_data.total_string]
        elsif ptrue?(col_data.total_function)
          attributes << [:totalsRowFunction, col_data.total_function]
        end

        attributes << [:dataDxfId, col_data.format] if col_data.format

        if ptrue?(col_data.formula) || ptrue?(col_data.custom_total)
          @writer.tag_elements('tableColumn', attributes) do
            if ptrue?(col_data.formula)
              # Write the calculatedColumnFormula element.
              write_calculated_column_formula(col_data.formula)
            end
            if ptrue?(col_data.custom_total)
              # Write the totalsRowFormula element.
              write_totals_row_formula(col_data.custom_total)
            end
          end
        else
          @writer.empty_tag('tableColumn', attributes)
        end
      end

      #
      # Write the <tableStyleInfo> element.
      #
      def write_table_style_info
        attributes = []
        attributes << ['name', @style] if @style && @style != '' && @style != 'None'
        attributes << ['showFirstColumn',   @show_first_col]
        attributes << ['showLastColumn',    @show_last_col]
        attributes << ['showRowStripes',    @show_row_stripes]
        attributes << ['showColumnStripes', @show_col_stripes]

        @writer.empty_tag('tableStyleInfo', attributes)
      end

      #
      # Write the <calculatedColumnFormula> element.
      #
      def write_calculated_column_formula(formula)
        @writer.data_element('calculatedColumnFormula', formula)
      end

      #
      # _write_totals_row_formula()
      #
      # Write the <totalsRowFormula> element.
      #
      def write_totals_row_formula(formula)
        @writer.data_element('totalsRowFormula', formula)
      end
    end
	class Theme
      include Writexlsx_kot::Utility

      def initialize
        @writer = nil
      end

      def assemble_xml_file
        return unless @writer

        write_theme_file
        @writer.write("\n")
        @writer.close
      end

      #
      # Set the filehandle only. This class doesn't use a real XML writer class.
      #
      def set_xml_writer(filename)
        fh = open(filename, 'wb')

        @writer = fh
      end

      private

      #
      # Write a default theme.xml file.
      #
      def write_theme_file
        theme  =
          %(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="EEECE1"/></a:lt2><a:accent1><a:srgbClr val="4F81BD"/></a:accent1><a:accent2><a:srgbClr val="C0504D"/></a:accent2><a:accent3><a:srgbClr val="9BBB59"/></a:accent3><a:accent4><a:srgbClr val="8064A2"/></a:accent4><a:accent5><a:srgbClr val="4BACC6"/></a:accent5><a:accent6><a:srgbClr val="F79646"/></a:accent6><a:hlink><a:srgbClr val="0000FF"/></a:hlink><a:folHlink><a:srgbClr val="800080"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Cambria"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="ＭＳ Ｐゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Times New Roman"/><a:font script="Hebr" typeface="Times New Roman"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="MoolBoran"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Times New Roman"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/><a:font script="Jpan" typeface="ＭＳ Ｐゴシック"/><a:font script="Hang" typeface="맑은 고딕"/><a:font script="Hans" typeface="宋体"/><a:font script="Hant" typeface="新細明體"/><a:font script="Arab" typeface="Arial"/><a:font script="Hebr" typeface="Arial"/><a:font script="Thai" typeface="Tahoma"/><a:font script="Ethi" typeface="Nyala"/><a:font script="Beng" typeface="Vrinda"/><a:font script="Gujr" typeface="Shruti"/><a:font script="Khmr" typeface="DaunPenh"/><a:font script="Knda" typeface="Tunga"/><a:font script="Guru" typeface="Raavi"/><a:font script="Cans" typeface="Euphemia"/><a:font script="Cher" typeface="Plantagenet Cherokee"/><a:font script="Yiii" typeface="Microsoft Yi Baiti"/><a:font script="Tibt" typeface="Microsoft Himalaya"/><a:font script="Thaa" typeface="MV Boli"/><a:font script="Deva" typeface="Mangal"/><a:font script="Telu" typeface="Gautami"/><a:font script="Taml" typeface="Latha"/><a:font script="Syrc" typeface="Estrangelo Edessa"/><a:font script="Orya" typeface="Kalinga"/><a:font script="Mlym" typeface="Kartika"/><a:font script="Laoo" typeface="DokChampa"/><a:font script="Sinh" typeface="Iskoola Pota"/><a:font script="Mong" typeface="Mongolian Baiti"/><a:font script="Viet" typeface="Arial"/><a:font script="Uigh" typeface="Microsoft Uighur"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="1"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst></a:effectStyle><a:effectStyle><a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst><a:scene3d><a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera><a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig></a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs></a:gsLst><a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>)
        @writer.write(theme)
      end
    end
	class Vml
      include Writexlsx_kot::Utility

      def initialize
        @writer = Package::XMLWriterSimple.new
      end

      def set_xml_writer(filename)
        @writer.set_xml_writer(filename)
      end

      def assemble_xml_file(
        data_id, vml_shape_id, comments_data,
        buttons_data, header_images_data = []
      )
        return unless @writer

        write_xml_namespace do
          # Write the o:shapelayout element.
          write_shapelayout(data_id)

          z_index = 1
          unless buttons_data.empty?
            vml_shape_id, z_index =
              write_shape_type_and_shape(
                buttons_data,
                vml_shape_id, z_index
              ) do
              write_button_shapetype
            end
          end
          unless comments_data.empty?
            write_shape_type_and_shape(
              comments_data,
              vml_shape_id, z_index
            ) do
              write_comment_shapetype
            end
          end
          unless header_images_data.empty?
            write_image_shapetype
            index = 1
            header_images_data.each do |image|
              # Write the v:shape element.
              vml_shape_id += 1
              write_image_shape(vml_shape_id, index, image)
              index += 1
            end
          end
        end
        @writer.crlf
        @writer.close
      end

      private

      def write_shape_type_and_shape(data, vml_shape_id, z_index)
        # Write the v:shapetype element.
        yield
        data.each do |obj|
          # Write the v:shape element.
          vml_shape_id += 1
          obj.write_shape(@writer, vml_shape_id, z_index)
          z_index += 1
        end
        [vml_shape_id, z_index]
      end

      #
      # Write the <xml> element. This is the root element of VML.
      #
      def write_xml_namespace(&block)
        @writer.tag_elements('xml', xml_attributes, &block)
      end

      # for <xml> elements.
      def xml_attributes
        schema  = 'urn:schemas-microsoft-com:'
        [
          ['xmlns:v', "#{schema}vml"],
          ['xmlns:o', "#{schema}office:office"],
          ['xmlns:x', "#{schema}office:excel"]
        ]
      end

      #
      # Write the <o:shapelayout> element.
      #
      def write_shapelayout(data_id)
        attributes = [
          ['v:ext', 'edit']
        ]

        @writer.tag_elements('o:shapelayout', attributes) do
          # Write the o:idmap element.
          write_idmap(data_id)
        end
      end

      #
      # Write the <o:idmap> element.
      #
      def write_idmap(data_id)
        attributes = [
          ['v:ext', 'edit'],
          ['data',  data_id]
        ]

        @writer.empty_tag('o:idmap', attributes)
      end

      #
      # Write the <v:shapetype> element.
      #
      def write_comment_shapetype
        attributes = [
          %w[id _x0000_t202],
          ['coordsize', '21600,21600'],
          ['o:spt',     202],
          ['path',      'm,l,21600r21600,l21600,xe']
        ]

        @writer.tag_elements('v:shapetype', attributes) do
          # Write the v:stroke element.
          write_stroke
          # Write the v:path element.
          write_comment_path('t', 'rect')
        end
      end

      #
      # Write the <v:shapetype> element.
      #
      def write_button_shapetype
        attributes = [
          %w[id _x0000_t201],
          ['coordsize', '21600,21600'],
          ['o:spt',     201],
          ['path',      'm,l,21600r21600,l21600,xe']
        ]

        @writer.tag_elements('v:shapetype', attributes) do
          # Write the v:stroke element.
          write_stroke
          # Write the v:path element.
          write_button_path
          # Write the o:lock element.
          write_shapetype_lock
        end
      end

      #
      # Write the <v:shapetype> element.
      #
      def write_image_shapetype
        id               = '_x0000_t75'
        coordsize        = '21600,21600'
        spt              = 75
        o_preferrelative = 't'
        path             = 'm@4@5l@4@11@9@11@9@5xe'
        filled           = 'f'
        stroked          = 'f'

        attributes = [
          ['id',               id],
          ['coordsize',        coordsize],
          ['o:spt',            spt],
          ['o:preferrelative', o_preferrelative],
          ['path',             path],
          ['filled',           filled],
          ['stroked',          stroked]
        ]

        @writer.tag_elements('v:shapetype', attributes) do
          # Write the v:stroke element.
          write_stroke

          # Write the v:formulas element.
          write_formulas

          # Write the v:path element.
          write_image_path

          # Write the o:lock element.
          write_aspect_ratio_lock
        end
      end

      #
      # Write the <v:path> element.
      #
      def write_button_path
        attributes = [
          %w[shadowok f],
          ['o:extrusionok', 'f'],
          %w[strokeok f],
          %w[fillok f],
          ['o:connecttype', 'rect']
        ]
        @writer.empty_tag('v:path', attributes)
      end

      #
      # Write the <v:path> element.
      #
      def write_image_path
        extrusionok     = 'f'
        gradientshapeok = 't'
        connecttype     = 'rect'

        attributes = [
          ['o:extrusionok',   extrusionok],
          ['gradientshapeok', gradientshapeok],
          ['o:connecttype',   connecttype]
        ]

        @writer.empty_tag('v:path', attributes)
      end

      #
      # Write the <o:lock> element.
      #
      def write_shapetype_lock
        attributes = [
          ['v:ext',     'edit'],
          %w[shapetype t]
        ]
        @writer.empty_tag('o:lock', attributes)
      end

      #
      # Write the <o:lock> element.
      #
      def write_rotation_lock
        attributes = [
          ['v:ext',    'edit'],
          %w[rotation t]
        ]
        @writer.empty_tag('o:lock', attributes)
      end

      #
      # Write the <o:lock> element.
      #
      def write_aspect_ratio_lock
        ext         = 'edit'
        aspectratio = 't'

        attributes = [
          ['v:ext',       ext],
          ['aspectratio', aspectratio]
        ]

        @writer.empty_tag('o:lock', attributes)
      end

      #
      # Write the <v:shape> element.
      #
      def write_image_shape(id, index, image_data)
        type       = '#_x0000_t75'

        # Get the image parameters
        width    = image_data[0]
        height   = image_data[1]
        name     = image_data[2]
        position = image_data[3]
        x_dpi    = image_data[4]
        y_dpi    = image_data[5]
        ref_id   = image_data[6]

        # Scale the height/width by the resolution, relative to 72dpi.
        width  = width  * 72.0 / x_dpi
        height = height * 72.0 / y_dpi

        # Excel uses a rounding based around 72 and 96 dpi.
        width  = 72 / 96.0 * ((width  * 96 / 72.0) + 0.25).to_i
        height = 72 / 96.0 * ((height * 96 / 72.0) + 0.25).to_i

        width = width.to_i if (width - width.to_i).abs < 0.1
        height = height.to_i if (height - height.to_i).abs < 0.1

        style = [
          "position:absolute", "margin-left:0", "margin-top:0",
          "width:#{width}pt", "height:#{height}pt",
          "z-index:#{index}"
        ].join(';')
        attributes = [
          ['id',     position],
          ['o:spid', "_x0000_s#{id}"],
          ['type',   type],
          ['style',  style]
        ]

        @writer.tag_elements('v:shape', attributes) do
          # Write the v:imagedata element.
          write_imagedata(ref_id, name)

          # Write the o:lock element.
          write_rotation_lock
        end
      end

      #
      # Write the <v:imagedata> element.
      #
      def write_imagedata(index, o_title)
        attributes = [
          ['o:relid', "rId#{index}"],
          ['o:title', o_title]
        ]

        @writer.empty_tag('v:imagedata', attributes)
      end

      #
      # Write the <v:formulas> element.
      #
      def write_formulas
        @writer.tag_elements('v:formulas') do
          # Write the v:f elements.
          write_f('if lineDrawn pixelLineWidth 0')
          write_f('sum @0 1 0')
          write_f('sum 0 0 @1')
          write_f('prod @2 1 2')
          write_f('prod @3 21600 pixelWidth')
          write_f('prod @3 21600 pixelHeight')
          write_f('sum @0 0 1')
          write_f('prod @6 1 2')
          write_f('prod @7 21600 pixelWidth')
          write_f('sum @8 21600 0')
          write_f('prod @7 21600 pixelHeight')
          write_f('sum @10 21600 0')
        end
      end

      #
      # Write the <v:f> element.
      #
      def write_f(eqn)
        attributes = [['eqn', eqn]]

        @writer.empty_tag('v:f', attributes)
      end
    end
 
  end

	class Workbook
    include Writexlsx_kot::Utility

    attr_writer :firstsheet                     # :nodoc:
    attr_reader :palette                        # :nodoc:
    attr_reader :worksheets, :charts, :drawings # :nodoc:
    attr_reader :named_ranges                   # :nodoc:
    attr_reader :doc_properties                 # :nodoc:
    attr_reader :custom_properties              # :nodoc:
    attr_reader :image_types, :images           # :nodoc:
    attr_reader :shared_strings                 # :nodoc:
    attr_reader :vba_project                    # :nodoc:
    attr_reader :excel2003_style                # :nodoc:
    attr_reader :max_url_length                 # :nodoc:
    attr_reader :strings_to_urls                # :nodoc:
    attr_reader :read_only                      # :nodoc:

    def initialize(file, *option_params)
      options, default_formats = process_workbook_options(*option_params)
      @writer = Package::XMLWriterSimple.new

      @file = file
      @tempdir = options[:tempdir] ||
                 File.join(
                   Dir.tmpdir,
                   Digest::MD5.hexdigest("#{Time.now.to_f}-#{Process.pid}")
                 )
      @date_1904           = options[:date_1904] || false
      @activesheet         = 0
      @firstsheet          = 0
      @selected            = 0
      @fileclosed          = false
      @worksheets          = Sheets.new
      @charts              = []
      @drawings            = []
      @formats             = Formats.new
      @xf_formats          = []
      @dxf_formats         = []
      @font_count          = 0
      @num_formats         = []
      @defined_names       = []
      @named_ranges        = []
      @custom_colors       = []
      @doc_properties      = {}
      @custom_properties   = []
      @optimization        = options[:optimization] || 0
      @x_window            = 240
      @y_window            = 15
      @window_width        = 16095
      @window_height       = 9660
      @tab_ratio           = 600
      @excel2003_style     = options[:excel2003_style] || false
      @table_count         = 0
      @image_types         = {}
      @images              = []
      @strings_to_urls     = options[:strings_to_urls].nil? || options[:strings_to_urls] ? true : false

      @max_url_length      = 2079
      @has_comments        = false
      @read_only           = 0
      @has_metadata        = false
      if options[:max_url_length]
        @max_url_length = options[:max_url_length]

        @max_url_length = 2079 if @max_url_length < 250
      end
      # Structures for the shared strings data.
      @shared_strings = Package::SharedStrings.new

      # Formula calculation default settings.
      @calc_id             = 124519
      @calc_mode           = 'auto'
      @calc_on_load        = true

      if @excel2003_style
        add_format(default_formats.merge(
                     xf_index:    0,
                     font_family: 0,
                     font:        'Arial',
                     size:        10,
                     theme:       -1
                   ))
      else
        add_format(default_formats.merge(xf_index: 0))
      end

      # Add a default URL format.
      @default_url_format = add_format(hyperlink: 1)

      set_color_palette
    end

    #
    # The close method is used to close an Excel file.
    #
    def close
      # In case close() is called twice.
      return if @fileclosed

      @fileclosed = true
      store_workbook
    end

    #
    # get array of Worksheet objects
    #
    # :call-seq:
    #   sheets              -> array of all Wordsheet object
    #   sheets(1, 3, 4)     -> array of spcified Worksheet object.
    #
    def sheets(*args)
      if args.empty?
        @worksheets
      else
        args.collect { |i| @worksheets[i] }
      end
    end

    #
    # Return a worksheet object in the workbook using the sheetname.
    #
    def worksheet_by_name(sheetname = nil)
      sheets.select { |s| s.name == sheetname }.first
    end
    alias get_worksheet_by_name worksheet_by_name

    #
    # Set the date system: false = 1900 (the default), true = 1904
    #
    def set_1904(mode = true)
      raise "set_1904() must be called before add_worksheet()" unless sheets.empty?

      @date_1904 = ptrue?(mode)
    end

    #
    # return date system. false = 1900, true = 1904
    #
    def get_1904
      @date_1904
    end

    def set_tempdir(dir)
      @tempdir = dir.dup
    end

    #
    # user must not use. it is internal method.
    #
    def set_xml_writer(filename)  # :nodoc:
      @writer.set_xml_writer(filename)
    end

    #
    # user must not use. it is internal method.
    #
    def xml_str  # :nodoc:
      @writer.string
    end

    #
    # user must not use. it is internal method.
    #
    def assemble_xml_file  # :nodoc:
      return unless @writer

      # Prepare format object for passing to Style.rb.
      prepare_format_properties

      write_xml_declaration do
        # Write the root workbook element.
        write_workbook do
          # Write the XLSX file version.
          write_file_version

          # Write the fileSharing element.
          write_file_sharing

          # Write the workbook properties.
          write_workbook_pr

          # Write the workbook view properties.
          write_book_views

          # Write the worksheet names and ids.
          @worksheets.write_sheets(@writer)

          # Write the workbook defined names.
          write_defined_names

          # Write the workbook calculation properties.
          write_calc_pr

          # Write the workbook extension storage.
          # write_ext_lst
        end
      end
    end

    #
    # At least one worksheet should be added to a new workbook. A worksheet is used to write data into cells:
    #
    def add_worksheet(name = '')
      name = check_sheetname(name)
      worksheet = Worksheet.new(self, @worksheets.size, name)
      @worksheets << worksheet
      worksheet
    end

    #
    # This method is use to create a new chart either as a standalone worksheet
    # (the default) or as an embeddable object that can be inserted into
    # a worksheet via the insert_chart method.
    #
    def add_chart(params = {})
      # Type must be specified so we can create the required chart instance.
      type     = params[:type]
      embedded = params[:embedded]
      name     = params[:name]
      raise "Must define chart type in add_chart()" unless type

      chart = Chart.factory(type, params[:subtype])
      chart.palette = @palette

      # If the chart isn't embedded let the workbook control it.
      if ptrue?(embedded)
        chart.name = name if name

        # Set index to 0 so that the activate() and set_first_sheet() methods
        # point back to the first worksheet if used for embedded charts.
        chart.index = 0
        chart.set_embedded_config_data
      else
        # Check the worksheet name for non-embedded charts.
        sheetname  = check_chart_sheetname(name)
        chartsheet = Chartsheet.new(self, @worksheets.size, sheetname)
        chartsheet.chart = chart
        @worksheets << chartsheet
      end
      @charts << chart
      ptrue?(embedded) ? chart : chartsheet
    end

    #
    # The add_format method can be used to create new Format objects
    # which are used to apply formatting to a cell. You can either define
    # the properties at creation time via a hash of property values
    # or later via method calls.
    #
    #     format1 = workbook.add_format(property_hash) # Set properties at creation
    #     format2 = workbook.add_format                # Set properties later
    #
    def add_format(property_hash = {})
      properties = {}
      properties.update(font: 'Arial', size: 10, theme: -1) if @excel2003_style
      properties.update(property_hash)

      format = Format.new(@formats, properties)

      @formats.formats.push(format)    # Store format reference

      format
    end

    #
    # The +add_shape+ method can be used to create new shapes that may be
    # inserted into a worksheet.
    #
    def add_shape(properties = {})
      shape = Shape.new(properties)
      shape.palette = @palette

      @shapes ||= []
      @shapes << shape  # Store shape reference.
      shape
    end

    #
    # Create a defined name in Excel. We handle global/workbook level names and
    # local/worksheet names.
    #
    def define_name(name, formula)
      sheet_index = nil
      sheetname   = ''

      # Local defined names are formatted like "Sheet1!name".
      if name =~ /^(.*)!(.*)$/
        sheetname   = ::Regexp.last_match(1)
        name        = ::Regexp.last_match(2)
        sheet_index = @worksheets.index_by_name(sheetname)
      else
        sheet_index = -1   # Use -1 to indicate global names.
      end

      # Raise if the sheet index wasn't found.
      raise "Unknown sheet name #{sheetname} in defined_name()" unless sheet_index

      # Raise if the name contains invalid chars as defined by Excel help.
      # Refer to the following to see Excel's syntax rules for defined names:
      # http://office.microsoft.com/en-001/excel-help/define-and-use-names-in-formulas-HA010147120.aspx#BMsyntax_rules_for_names
      #
      raise "Invalid characters in name '#{name}' used in defined_name()" if name =~ /\A[-0-9 !"#$%&'()*+,.:;<=>?@\[\]\^`{}~]/ || name =~ /.+[- !"#$%&'()*+,\\:;<=>?@\[\]\^`{}~]/

      # Raise if the name looks like a cell name.
      raise "Invalid name '#{name}' looks like a cell name in defined_name()" if name =~ /^[a-zA-Z][a-zA-Z]?[a-dA-D]?[0-9]+$/

      # Raise if the name looks like a R1C1
      raise "Invalid name '#{name}' like a RC cell ref in defined_name()" if name =~ /\A[rcRC]\Z/ || name =~ /\A[rcRC]\d+[rcRC]\d+\Z/

      @defined_names.push([name, sheet_index, formula.sub(/^=/, '')])
    end

    #
    # Set the workbook size.
    #
    def set_size(width = nil, height = nil)
      @window_width = if ptrue?(width)
                        # Convert to twips at 96 dpi.
                        width.to_i * 1440 / 96
                      else
                        16095
                      end

      @window_height = if ptrue?(height)
                         # Convert to twips at 96 dpi.
                         height.to_i * 1440 / 96
                       else
                         9660
                       end
    end

    #
    # Set the ratio of space for worksheet tabs.
    #
    def set_tab_ratio(tab_ratio = nil)
      return unless tab_ratio

      if tab_ratio < 0 || tab_ratio > 100
        raise "Tab ratio outside range: 0 <= zoom <= 100"
      else
        @tab_ratio = (tab_ratio * 10).to_i
      end
    end

    #
    # The set_properties method can be used to set the document properties
    # of the Excel file created by WriteXLSX. These properties are visible
    # when you use the Office Button -> Prepare -> Properties option in Excel
    # and are also available to external applications that read or index windows
    # files.
    #
    def set_properties(params)
      # Ignore if no args were passed.
      return -1 if params.empty?

      # List of valid input parameters.
      valid = {
        title:          1,
        subject:        1,
        author:         1,
        keywords:       1,
        comments:       1,
        last_author:    1,
        created:        1,
        category:       1,
        manager:        1,
        company:        1,
        status:         1,
        hyperlink_base: 1
      }

      # Check for valid input parameters.
      params.each_key do |key|
        return -1 unless valid.has_key?(key)
      end

      # Set the creation time unless specified by the user.
      params[:created] = @createtime unless params.has_key?(:created)

      @doc_properties = params.dup
    end

    #
    # Set a user defined custom document property.
    #
    def set_custom_property(name, value, type = nil)
      # Valid types.
      valid_type = {
        'text'       => 1,
        'date'       => 1,
        'number'     => 1,
        'number_int' => 1,
        'bool'       => 1
      }

      raise "The name and value parameters must be defined in set_custom_property()" if !name || (type != 'bool' && !value)

      # Determine the type for strings and numbers if it hasn't been specified.
      unless ptrue?(type)
        type = if value =~ /^\d+$/
                 'number_int'
               elsif value =~
                     /^([+-]?)(?=[0-9]|\.[0-9])[0-9]*(\.[0-9]*)?([Ee]([+-]?[0-9]+))?$/
                 'number'
               else
                 'text'
               end
      end

      # Check for valid validation types.
      raise "Unknown custom type '$type' in set_custom_property()" unless valid_type[type]

      #  Check for strings longer than Excel's limit of 255 chars.
      raise "Length of text custom value '$value' exceeds Excel's limit of 255 in set_custom_property()" if type == 'text' && value.length > 255

      if type == 'bool'
        value = value ? 1 : 0
      end

      @custom_properties << [name, value, type]
    end

    #
    # The add_vba_project method can be used to add macros or functions to an
    # WriteXLSX file using a binary VBA project file that has been extracted
    # from an existing Excel xlsm file.
    #
    def add_vba_project(vba_project)
      @vba_project = vba_project
    end

    #
    # Set the VBA name for the workbook.
    #
    def set_vba_name(vba_codename = nil)
      @vba_codename = vba_codename || 'ThisWorkbook'
    end

    #
    # Set the Excel "Read-only recommended" save option.
    #
    def read_only_recommended
      @read_only = 2
    end

    #
    # set_calc_mode()
    #
    # Set the Excel caclcuation mode for the workbook.
    #
    def set_calc_mode(mode, calc_id = nil)
      @calc_mode = mode || 'auto'

      if mode == 'manual'
        @calc_on_load = false
      elsif mode == 'auto_except_tables'
        @calc_mode = 'autoNoTable'
      end

      @calc_id = calc_id if calc_id
    end

    #
    # Get the default url format used when a user defined format isn't specified
    # with write_url(). The format is the hyperlink style defined by Excel for the
    # default theme.
    #
    attr_reader :default_url_format
    alias get_default_url_format default_url_format

    #
    # Change the RGB components of the elements in the colour palette.
    #
    def set_custom_color(index, red = 0, green = 0, blue = 0)
      # Match a HTML #xxyyzz style parameter
      if red.to_s =~ /^#(\w\w)(\w\w)(\w\w)/
        red   = ::Regexp.last_match(1).hex
        green = ::Regexp.last_match(2).hex
        blue  = ::Regexp.last_match(3).hex
      end

      # Check that the colour index is the right range
      raise "Color index #{index} outside range: 8 <= index <= 64" if index < 8 || index > 64

      # Check that the colour components are in the right range
      if (red   < 0 || red   > 255) ||
         (green < 0 || green > 255) ||
         (blue  < 0 || blue  > 255)
        raise "Color component outside range: 0 <= color <= 255"
      end

      index -= 8       # Adjust colour index (wingless dragonfly)

      # Set the RGB value
      @palette[index] = [red, green, blue]

      # Store the custome colors for the style.xml file.
      @custom_colors << sprintf("FF%02X%02X%02X", red, green, blue)

      index + 8
    end

    attr_writer :activesheet

    attr_reader :writer

    def date_1904? # :nodoc:
      @date_1904 ||= false
      !!@date_1904
    end

    #
    # Add a string to the shared string table, if it isn't already there, and
    # return the string index.
    #
    EMPTY_HASH = {}.freeze
    def shared_string_index(str) # :nodoc:
      @shared_strings.index(str, EMPTY_HASH)
    end

    def str_unique   # :nodoc:
      @shared_strings.unique_count
    end

    def shared_strings_empty?  # :nodoc:
      @shared_strings.empty?
    end

    def chartsheet_count
      @worksheets.chartsheet_count
    end

    def non_chartsheet_count
      @worksheets.worksheets.count
    end

    def style_properties
      [
        @xf_formats,
        @palette,
        @font_count,
        @num_formats,
        @border_count,
        @fill_count,
        @custom_colors,
        @dxf_formats,
        @has_comments
      ]
    end

    def num_vml_files
      @worksheets.select { |sheet| sheet.has_vml? || sheet.has_header_vml? }.count
    end

    def num_comment_files
      @worksheets.select { |sheet| sheet.has_comments? }.count
    end

    def chartsheets
      @worksheets.chartsheets
    end

    def non_chartsheets
      @worksheets.worksheets
    end

    def firstsheet # :nodoc:
      @firstsheet ||= 0
    end

    def activesheet # :nodoc:
      @activesheet ||= 0
    end

    def has_metadata?
      @has_metadata
    end

    private

    def filename
      setup_filename unless @filename
      @filename
    end

    def fileobj
      setup_filename unless @fileobj
      @fileobj
    end

    def setup_filename # :nodoc:
      if @file.respond_to?(:to_str) && @file != ''
        @filename = @file
        @fileobj  = nil
      elsif @file.respond_to?(:write)
        @filename = File.join(tempdir, Digest::MD5.hexdigest(Time.now.to_s) + '.xlsx.tmp')
        @fileobj  = @file
      else
        raise "'#{@file}' must be valid filename String of IO object."
      end
    end

    attr_reader :tempdir

    #
    # Sets the colour palette to the Excel defaults.
    #
    def set_color_palette # :nodoc:
      @palette = [
        [0x00, 0x00, 0x00, 0x00],    # 8
        [0xff, 0xff, 0xff, 0x00],    # 9
        [0xff, 0x00, 0x00, 0x00],    # 10
        [0x00, 0xff, 0x00, 0x00],    # 11
        [0x00, 0x00, 0xff, 0x00],    # 12
        [0xff, 0xff, 0x00, 0x00],    # 13
        [0xff, 0x00, 0xff, 0x00],    # 14
        [0x00, 0xff, 0xff, 0x00],    # 15
        [0x80, 0x00, 0x00, 0x00],    # 16
        [0x00, 0x80, 0x00, 0x00],    # 17
        [0x00, 0x00, 0x80, 0x00],    # 18
        [0x80, 0x80, 0x00, 0x00],    # 19
        [0x80, 0x00, 0x80, 0x00],    # 20
        [0x00, 0x80, 0x80, 0x00],    # 21
        [0xc0, 0xc0, 0xc0, 0x00],    # 22
        [0x80, 0x80, 0x80, 0x00],    # 23
        [0x99, 0x99, 0xff, 0x00],    # 24
        [0x99, 0x33, 0x66, 0x00],    # 25
        [0xff, 0xff, 0xcc, 0x00],    # 26
        [0xcc, 0xff, 0xff, 0x00],    # 27
        [0x66, 0x00, 0x66, 0x00],    # 28
        [0xff, 0x80, 0x80, 0x00],    # 29
        [0x00, 0x66, 0xcc, 0x00],    # 30
        [0xcc, 0xcc, 0xff, 0x00],    # 31
        [0x00, 0x00, 0x80, 0x00],    # 32
        [0xff, 0x00, 0xff, 0x00],    # 33
        [0xff, 0xff, 0x00, 0x00],    # 34
        [0x00, 0xff, 0xff, 0x00],    # 35
        [0x80, 0x00, 0x80, 0x00],    # 36
        [0x80, 0x00, 0x00, 0x00],    # 37
        [0x00, 0x80, 0x80, 0x00],    # 38
        [0x00, 0x00, 0xff, 0x00],    # 39
        [0x00, 0xcc, 0xff, 0x00],    # 40
        [0xcc, 0xff, 0xff, 0x00],    # 41
        [0xcc, 0xff, 0xcc, 0x00],    # 42
        [0xff, 0xff, 0x99, 0x00],    # 43
        [0x99, 0xcc, 0xff, 0x00],    # 44
        [0xff, 0x99, 0xcc, 0x00],    # 45
        [0xcc, 0x99, 0xff, 0x00],    # 46
        [0xff, 0xcc, 0x99, 0x00],    # 47
        [0x33, 0x66, 0xff, 0x00],    # 48
        [0x33, 0xcc, 0xcc, 0x00],    # 49
        [0x99, 0xcc, 0x00, 0x00],    # 50
        [0xff, 0xcc, 0x00, 0x00],    # 51
        [0xff, 0x99, 0x00, 0x00],    # 52
        [0xff, 0x66, 0x00, 0x00],    # 53
        [0x66, 0x66, 0x99, 0x00],    # 54
        [0x96, 0x96, 0x96, 0x00],    # 55
        [0x00, 0x33, 0x66, 0x00],    # 56
        [0x33, 0x99, 0x66, 0x00],    # 57
        [0x00, 0x33, 0x00, 0x00],    # 58
        [0x33, 0x33, 0x00, 0x00],    # 59
        [0x99, 0x33, 0x00, 0x00],    # 60
        [0x99, 0x33, 0x66, 0x00],    # 61
        [0x33, 0x33, 0x99, 0x00],    # 62
        [0x33, 0x33, 0x33, 0x00]    # 63
      ]
    end

    #
    # Check for valid worksheet names. We check the length, if it contains any
    # invalid characters and if the name is unique in the workbook.
    #
    def check_sheetname(name) # :nodoc:
      @worksheets.make_and_check_sheet_chart_name(:sheet, name)
    end

    def check_chart_sheetname(name)
      @worksheets.make_and_check_sheet_chart_name(:chart, name)
    end

    #
    # Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name and cell
    # range such as ( 'Sheet1', 0, 1, 4, 1 ).
    #
    def get_chart_range(range) # :nodoc:
      # Split the range formula into sheetname and cells at the last '!'.
      pos = range.rindex('!')
      return nil unless pos

      if pos > 0
        sheetname = range[0, pos]
        cells = range[pos + 1..-1]
      end

      # Split the cell range into 2 cells or else use single cell for both.
      if cells =~ /:/
        cell_1, cell_2 = cells.split(":")
      else
        cell_1 = cells
        cell_2 = cells
      end

      # Remove leading/trailing apostrophes and convert escaped quotes to single.
      sheetname.sub!(/^'/, '')
      sheetname.sub!(/'$/, '')
      sheetname.gsub!("''", "'")

      row_start, col_start = xl_cell_to_rowcol(cell_1)
      row_end,   col_end   = xl_cell_to_rowcol(cell_2)

      # Check that we have a 1D range only.
      return nil if row_start != row_end && col_start != col_end

      [sheetname, row_start, col_start, row_end, col_end]
    end

    def write_workbook(&block) # :nodoc:
      schema = 'http://schemas.openxmlformats.org'
      attributes = [
        ['xmlns',
         schema + '/spreadsheetml/2006/main'],
        ['xmlns:r',
         schema + '/officeDocument/2006/relationships']
      ]
      @writer.tag_elements('workbook', attributes, &block)
    end

    def write_file_version # :nodoc:
      attributes = [
        %w[appName xl],
        ['lastEdited', 4],
        ['lowestEdited', 4],
        ['rupBuild', 4505]
      ]

      attributes << [:codeName, '{37E998C4-C9E5-D4B9-71C8-EB1FF731991C}'] if @vba_project

      @writer.empty_tag('fileVersion', attributes)
    end

    #
    # Write the <fileSharing> element.
    #
    def write_file_sharing
      return unless ptrue?(@read_only)

      attributes = []
      attributes << ['readOnlyRecommended', 1]
      @writer.empty_tag('fileSharing', attributes)
    end

    def write_workbook_pr # :nodoc:
      attributes = []
      attributes << ['codeName', @vba_codename]  if ptrue?(@vba_codename)
      attributes << ['date1904', 1]              if date_1904?
      attributes << ['defaultThemeVersion', 124226]
      @writer.empty_tag('workbookPr', attributes)
    end

    def write_book_views # :nodoc:
      @writer.tag_elements('bookViews') { write_workbook_view }
    end

    def write_workbook_view # :nodoc:
      attributes = [
        ['xWindow',       @x_window],
        ['yWindow',       @y_window],
        ['windowWidth',   @window_width],
        ['windowHeight',  @window_height]
      ]
      attributes << ['tabRatio', @tab_ratio] if @tab_ratio != 600
      attributes << ['firstSheet', @firstsheet + 1] if @firstsheet > 0
      attributes << ['activeTab', @activesheet] if @activesheet > 0
      @writer.empty_tag('workbookView', attributes)
    end

    def write_calc_pr # :nodoc:
      attributes = [['calcId', @calc_id]]

      case @calc_mode
      when 'manual'
        attributes << %w[calcMode manual]
        attributes << ['calcOnSave', 0]
      when 'autoNoTable'
        attributes << %w[calcMode autoNoTable]
      end

      attributes << ['fullCalcOnLoad', 1] if @calc_on_load

      @writer.empty_tag('calcPr', attributes)
    end

    def write_ext_lst # :nodoc:
      @writer.tag_elements('extLst') { write_ext }
    end

    def write_ext # :nodoc:
      attributes = [
        ['xmlns:mx', "#{OFFICE_URL}mac/excel/2008/main"],
        ['uri', uri]
      ]
      @writer.tag_elements('ext', attributes) { write_mx_arch_id }
    end

    def write_mx_arch_id # :nodoc:
      @writer.empty_tag('mx:ArchID', ['Flags', 2])
    end

    def write_defined_names # :nodoc:
      return unless ptrue?(@defined_names)

      @writer.tag_elements('definedNames') do
        @defined_names.each { |defined_name| write_defined_name(defined_name) }
      end
    end

    def write_defined_name(defined_name) # :nodoc:
      name, id, range, hidden = defined_name

      attributes = [['name', name]]
      attributes << ['localSheetId', "#{id}"] unless id == -1
      attributes << %w[hidden 1]     if hidden

      @writer.data_element('definedName', range, attributes)
    end

    def write_io(str) # :nodoc:
      @writer << str
      str
    end

    # for test
    def defined_names # :nodoc:
      @defined_names ||= []
    end

    #
    # Assemble worksheets into a workbook.
    #
    def store_workbook # :nodoc:
      # Add a default worksheet if non have been added.
      add_worksheet if @worksheets.empty?

      # Ensure that at least one worksheet has been selected.
      @worksheets.visible_first.select if @activesheet == 0

      # Set the active sheet.
      @activesheet = @worksheets.visible_first.index if @activesheet == 0
      @worksheets[@activesheet].activate

      # Convert the SST strings data structure.
      prepare_sst_string_data

      # Prepare the worksheet VML elements such as comments and buttons.
      prepare_vml_objects
      # Set the defined names for the worksheets such as Print Titles.
      prepare_defined_names
      # Prepare the drawings, charts and images.
      prepare_drawings
      # Add cached data to charts.
      add_chart_data

      # Prepare the worksheet tables.
      prepare_tables

      # Prepare the metadata file links.
      prepare_metadata

      # Package the workbook.
      packager = Package::Packager.new(self)
      packager.set_package_dir(tempdir)
      packager.create_package

      # Free up the Packager object.
      packager = nil

      # Store the xlsx component files with the temp dir name removed.
      ZipFileUtils.zip("#{tempdir}", filename)

      IO.copy_stream(filename, fileobj) if fileobj
      Writexlsx_kot::Utility.delete_files(tempdir)
    end

    def write_parts(zip)
      parts.each do |part|
        zip.put_next_entry(zip_entry_for_part(part.sub(Regexp.new("#{tempdir}/?"), '')))
        zip.puts(File.read(part))
      end
    end

    def zip_entry_for_part(part)
      Zip_kot::Entry.new("", part)
    end

    #
    # files
    #
    def parts
      Dir.glob(File.join(tempdir, "**", "*"), File::FNM_DOTMATCH).select { |f| File.file?(f) }
    end

    #
    # prepare_sst_string_data
    #
    def prepare_sst_string_data; end

    #
    # Prepare all of the format properties prior to passing them to Styles.rb.
    #
    def prepare_format_properties # :nodoc:
      # Separate format objects into XF and DXF formats.
      prepare_formats

      # Set the font index for the format objects.
      prepare_fonts

      # Set the number format index for the format objects.
      prepare_num_formats

      # Set the border index for the format objects.
      prepare_borders

      # Set the fill index for the format objects.
      prepare_fills
    end

    #
    # Iterate through the XF Format objects and separate them into XF and DXF
    # formats.
    #
    def prepare_formats # :nodoc:
      @formats.formats.each do |format|
        xf_index  = format.xf_index
        dxf_index = format.dxf_index

        @xf_formats[xf_index] = format   if xf_index
        @dxf_formats[dxf_index] = format if dxf_index
      end
    end

    #
    # Iterate through the XF Format objects and give them an index to non-default
    # font elements.
    #
    def prepare_fonts # :nodoc:
      fonts = {}

      @xf_formats.each { |format| format.set_font_info(fonts) }

      @font_count = fonts.size

      # For the DXF formats we only need to check if the properties have changed.
      @dxf_formats.each do |format|
        # The only font properties that can change for a DXF format are: color,
        # bold, italic, underline and strikethrough.
        format.has_dxf_font(true) if format.color? || format.bold? || format.italic? || format.underline? || format.strikeout?
      end
    end

    #
    # Iterate through the XF Format objects and give them an index to non-default
    # number format elements.
    #
    # User defined records start from index 0xA4.
    #
    def prepare_num_formats # :nodoc:
      num_formats        = []
      unique_num_formats = {}
      index              = 164

      (@xf_formats + @dxf_formats).each do |format|
        num_format = format.num_format

        # Check if num_format is an index to a built-in number format.
        # Also check for a string of zeros, which is a valid number format
        # string but would evaluate to zero.
        #
        if num_format.to_s =~ /^\d+$/ && num_format.to_s !~ /^0+\d/
          # Number format '0' is indexed as 1 in Excel.
          num_format = 1 if num_format == 0
          # Index to a built-in number format.
          format.num_format_index = num_format
          next
        elsif num_format.to_s == 'General'
          # The 'General' format has an number format index of 0.
          format.num_format_index = 0
          next
        end

        if unique_num_formats[num_format]
          # Number format has already been used.
          format.num_format_index = unique_num_formats[num_format]
        else
          # Add a new number format.
          unique_num_formats[num_format] = index
          format.num_format_index = index
          index += 1

          # Only store/increase number format count for XF formats
          # (not for DXF formats).
          num_formats << num_format if ptrue?(format.xf_index)
        end
      end

      @num_formats = num_formats
    end

    #
    # Iterate through the XF Format objects and give them an index to non-default
    # border elements.
    #
    def prepare_borders # :nodoc:
      borders = {}

      @xf_formats.each { |format| format.set_border_info(borders) }

      @border_count = borders.size

      # For the DXF formats we only need to check if the properties have changed.
      @dxf_formats.each do |format|
        key = format.get_border_key
        format.has_dxf_border(true) if key =~ /[^0:]/
      end
    end

    #
    # Iterate through the XF Format objects and give them an index to non-default
    # fill elements.
    #
    # The user defined fill properties start from 2 since there are 2 default
    # fills: patternType="none" and patternType="gray125".
    #
    def prepare_fills # :nodoc:
      fills = {}
      index = 2    # Start from 2. See above.

      # Add the default fills.
      fills['0:0:0']  = 0
      fills['17:0:0'] = 1

      # Store the DXF colors separately since them may be reversed below.
      @dxf_formats.each do |format|
        next unless format.pattern != 0 || format.bg_color != 0 || format.fg_color != 0

        format.has_dxf_fill(true)
        format.dxf_bg_color = format.bg_color
        format.dxf_fg_color = format.fg_color
      end

      @xf_formats.each do |format|
        # The following logical statements jointly take care of special cases
        # in relation to cell colours and patterns:
        # 1. For a solid fill (_pattern == 1) Excel reverses the role of
        #    foreground and background colours, and
        # 2. If the user specifies a foreground or background colour without
        #    a pattern they probably wanted a solid fill, so we fill in the
        #    defaults.
        #
        if format.pattern == 1 && ne_0?(format.bg_color) && ne_0?(format.fg_color)
          format.fg_color, format.bg_color = format.bg_color, format.fg_color
        elsif format.pattern <= 1 && ne_0?(format.bg_color) && eq_0?(format.fg_color)
          format.fg_color = format.bg_color
          format.bg_color = 0
          format.pattern  = 1
        elsif format.pattern <= 1 && eq_0?(format.bg_color) && ne_0?(format.fg_color)
          format.bg_color = 0
          format.pattern  = 1
        end

        key = format.get_fill_key

        if fills[key]
          # Fill has already been used.
          format.fill_index = fills[key]
          format.has_fill(false)
        else
          # This is a new fill.
          fills[key]        = index
          format.fill_index = index
          format.has_fill(true)
          index += 1
        end
      end

      @fill_count = index
    end

    def eq_0?(val)
      ptrue?(val) ? false : true
    end

    def ne_0?(val)
      !eq_0?(val)
    end

    #
    # Iterate through the worksheets and store any defined names in addition to
    # any user defined names. Stores the defined names for the Workbook.xml and
    # the named ranges for App.xml.
    #
    def prepare_defined_names # :nodoc:
      @worksheets.each do |sheet|
        # Check for Print Area settings.
        if sheet.autofilter_area
          @defined_names << [
            '_xlnm._FilterDatabase',
            sheet.index,
            sheet.autofilter_area,
            1
          ]
        end

        # Check for Print Area settings.
        unless sheet.print_area.empty?
          @defined_names << [
            '_xlnm.Print_Area',
            sheet.index,
            sheet.print_area
          ]
        end

        # Check for repeat rows/cols. aka, Print Titles.
        next unless !sheet.print_repeat_cols.empty? || !sheet.print_repeat_rows.empty?

        range = if !sheet.print_repeat_cols.empty? && !sheet.print_repeat_rows.empty?
                  sheet.print_repeat_cols + ',' + sheet.print_repeat_rows
                else
                  sheet.print_repeat_cols + sheet.print_repeat_rows
                end

        # Store the defined names.
        @defined_names << ['_xlnm.Print_Titles', sheet.index, range]
      end

      @defined_names = sort_defined_names(@defined_names)
      @named_ranges  = extract_named_ranges(@defined_names)
    end

    #
    # Iterate through the worksheets and set up the VML objects.
    #
    def prepare_vml_objects  # :nodoc:
      comment_id     = 0
      vml_drawing_id = 0
      vml_data_id    = 1
      vml_header_id  = 0
      vml_shape_id   = 1024
      comment_files  = 0
      has_button     = false

      @worksheets.each do |sheet|
        next if !sheet.has_vml? && !sheet.has_header_vml?

        if sheet.has_vml?
          if sheet.has_comments?
            comment_files += 1
            comment_id    += 1
            @has_comments = true
          end
          vml_drawing_id += 1

          sheet.prepare_vml_objects(
            vml_data_id, vml_shape_id,
            vml_drawing_id, comment_id
          )

          # Each VML file should start with a shape id incremented by 1024.
          vml_data_id += 1 * (1 + sheet.num_comments_block)
          vml_shape_id += 1024 * (1 + sheet.num_comments_block)
        end

        if sheet.has_header_vml?
          vml_header_id  += 1
          vml_drawing_id += 1
          sheet.prepare_header_vml_objects(vml_header_id, vml_drawing_id)
        end

        # Set the sheet vba_codename if it has a button and the workbook
        # has a vbaProject binary.
        unless sheet.buttons_data.empty?
          has_button = true
          sheet.set_vba_name if @vba_project && !sheet.vba_codename
        end
      end

      # Set the workbook vba_codename if one of the sheets has a button and
      # the workbook has a vbaProject binary.
      set_vba_name if has_button && @vba_project && !@vba_codename
    end

    #
    # Set the table ids for the worksheet tables.
    #
    def prepare_tables
      table_id = 0
      seen     = {}

      sheets.each do |sheet|
        table_id += sheet.prepare_tables(table_id + 1, seen)
      end
    end

    #
    # Set the metadata rel link.
    #
    def prepare_metadata
      @worksheets.each do |sheet|
        if sheet.has_dynamic_arrays?
          @has_metadata = true
          break
        end
      end
    end

    #
    # Add "cached" data to charts to provide the numCache and strCache data for
    # series and title/axis ranges.
    #
    def add_chart_data # :nodoc:
      worksheets = {}
      seen_ranges = {}

      # Map worksheet names to worksheet objects.
      @worksheets.each { |worksheet| worksheets[worksheet.name] = worksheet }

      # Build an array of the worksheet charts including any combined charts.
      @charts.collect { |chart| [chart, chart.combined] }.flatten.compact
             .each do |chart|
        chart.formula_ids.each do |range, id|
          # Skip if the series has user defined data.
          if chart.formula_data[id]
            seen_ranges[range] = chart.formula_data[id] unless seen_ranges[range]
            next
          # Check to see if the data is already cached locally.
          elsif seen_ranges[range]
            chart.formula_data[id] = seen_ranges[range]
            next
          end

          # Convert the range formula to a sheet name and cell range.
          sheetname, *cells = get_chart_range(range)

          # Skip if we couldn't parse the formula.
          next unless sheetname

          # Handle non-contiguous ranges: (Sheet1!$A$1:$A$2,Sheet1!$A$4:$A$5).
          # We don't try to parse the ranges. We just return an empty list.
          if sheetname =~ /^\([^,]+,/
            chart.formula_data[id] = []
            seen_ranges[range] = []
            next
          end

          # Raise if the name is unknown since it indicates a user error in
          # a chart series formula.
          raise "Unknown worksheet reference '#{sheetname} in range '#{range}' passed to add_series()\n" unless worksheets[sheetname]

          # Add the data to the chart.
          # And store range data locally to avoid lookup if seen agein.
          chart.formula_data[id] =
            seen_ranges[range] = chart_data(worksheets[sheetname], cells)
        end
      end
    end

    def chart_data(worksheet, cells)
      # Get the data from the worksheet table.
      data = worksheet.get_range_data(*cells)

      # Convert shared string indexes to strings.
      data.collect do |token|
        if token.is_a?(Hash)
          string = @shared_strings.string(token[:sst_id])

          # Ignore rich strings for now. Deparse later if necessary.
          if string =~ /^<r>/ && string =~ %r{</r>$}
            ''
          else
            string
          end
        else
          token
        end
      end
    end

    #
    # Sort internal and user defined names in the same order as used by Excel.
    # This may not be strictly necessary but unsorted elements caused a lot of
    # issues in the the Spreadsheet::WriteExcel binary version. Also makes
    # comparison testing easier.
    #
    def sort_defined_names(names) # :nodoc:
      names.sort do |a, b|
        name_a  = normalise_defined_name(a[0])
        name_b  = normalise_defined_name(b[0])
        sheet_a = normalise_sheet_name(a[2])
        sheet_b = normalise_sheet_name(b[2])
        # Primary sort based on the defined name.
        if name_a > name_b
          1
        elsif name_a < name_b
          -1
        elsif sheet_a >= sheet_b  # name_a == name_b
          # Secondary sort based on the sheet name.
          1
        else
          -1
        end
      end
    end

    # Used in the above sort routine to normalise the defined names. Removes any
    # leading '_xmln.' from internal names and lowercases the strings.
    def normalise_defined_name(name) # :nodoc:
      name.sub(/^_xlnm./, '').downcase
    end

    # Used in the above sort routine to normalise the worksheet names for the
    # secondary sort. Removes leading quote and lowercases the strings.
    def normalise_sheet_name(name) # :nodoc:
      name.sub(/^'/, '').downcase
    end

    #
    # Extract the named ranges from the sorted list of defined names. These are
    # used in the App.xml file.
    #
    def extract_named_ranges(defined_names) # :nodoc:
      named_ranges = []

      defined_names.each do |defined_name|
        name, index, range = defined_name

        # Skip autoFilter ranges.
        next if name == '_xlnm._FilterDatabase'

        # We are only interested in defined names with ranges.
        next unless range =~ /^([^!]+)!/

        sheet_name = ::Regexp.last_match(1)

        # Match Print_Area and Print_Titles xlnm types.
        if name =~ /^_xlnm\.(.*)$/
          xlnm_type = ::Regexp.last_match(1)
          name = "#{sheet_name}!#{xlnm_type}"
        elsif index != -1
          name = "#{sheet_name}!#{name}"
        end

        named_ranges << name
      end

      named_ranges
    end

    #
    # Iterate through the worksheets and set up any chart or image drawings.
    #
    def prepare_drawings # :nodoc:
      chart_ref_id     = 0
      image_ref_id     = 0
      drawing_id       = 0
      ref_id           = 0
      image_ids        = {}
      header_image_ids = {}
      background_ids   = {}
      @worksheets.each do |sheet|
        chart_count = sheet.charts.size
        image_count = sheet.images.size
        shape_count = sheet.shapes.size
        header_image_count = sheet.header_images.size
        footer_image_count = sheet.footer_images.size
        has_background     = sheet.background_image.size
        has_drawings       = false

        # Check that some image or drawing needs to be processed.
        next if chart_count + image_count + shape_count + header_image_count + footer_image_count + has_background == 0

        # Don't increase the drawing_id header/footer images.
        if chart_count + image_count + shape_count > 0
          drawing_id += 1
          has_drawings = true
        end

        # Prepare the background images.
        if ptrue?(has_background)
          filename = sheet.background_image
          type, width, height, name, x_dpi, y_dpi, md5 = get_image_properties(filename)

          if background_ids[md5]
            ref_id = background_ids[md5]
          else
            image_ref_id += 1
            ref_id = image_ref_id
            background_ids[md5] = ref_id
            @images << [filename, type]
          end

          sheet.prepare_background(ref_id, type)
        end

        # Prepare the worksheet images.
        sheet.images.each_with_index do |image, index|
          filename = image[2]
          type, width, height, name, x_dpi, y_dpi, md5 = get_image_properties(image[2])
          if image_ids[md5]
            ref_id = image_ids[md5]
          else
            image_ref_id += 1
            image_ids[md5] = ref_id = image_ref_id
            @images << [filename, type]
          end
          sheet.prepare_image(
            index, ref_id, drawing_id, width, height,
            name,  type,   x_dpi,      y_dpi, md5
          )
        end

        # Prepare the worksheet charts.
        sheet.charts.each_with_index do |_chart, index|
          chart_ref_id += 1
          sheet.prepare_chart(index, chart_ref_id, drawing_id)
        end

        # Prepare the worksheet shapes.
        sheet.shapes.each_with_index do |_shape, index|
          sheet.prepare_shape(index, drawing_id)
        end

        # Prepare the header images.
        header_image_count.times do |index|
          filename = sheet.header_images[index][0]
          position = sheet.header_images[index][1]

          type, width, height, name, x_dpi, y_dpi, md5 =
            get_image_properties(filename)

          if header_image_ids[md5]
            ref_id = header_image_ids[md5]
          else
            image_ref_id += 1
            header_image_ids[md5] = ref_id = image_ref_id
            @images << [filename, type]
          end

          sheet.prepare_header_image(
            ref_id,   width, height, name, type,
            position, x_dpi, y_dpi,  md5
          )
        end

        # Prepare the footer images.
        footer_image_count.times do |index|
          filename = sheet.footer_images[index][0]
          position = sheet.footer_images[index][1]

          type, width, height, name, x_dpi, y_dpi, md5 =
            get_image_properties(filename)

          if header_image_ids[md5]
            ref_id = header_image_ids[md5]
          else
            image_ref_id += 1
            header_image_ids[md5] = ref_id = image_ref_id
            @images << [filename, type]
          end

          sheet.prepare_header_image(
            ref_id,   width, height, name, type,
            position, x_dpi, y_dpi,  md5
          )
        end

        if has_drawings
          drawings = sheet.drawings
          @drawings << drawings
        end
      end

      # Sort the workbook charts references into the order that the were
      # written from the worksheets above.
      @charts = @charts.select { |chart| chart.id != -1 }
                       .sort_by { |chart| chart.id }

      @drawing_count = drawing_id
    end

    #
    # Extract information from the image file such as dimension, type, filename,
    # and extension. Also keep track of previously seen images to optimise out
    # any duplicates.
    #
    def get_image_properties(filename)
      # Note the image_id, and previous_images mechanism isn't currently used.
      x_dpi = 96
      y_dpi = 96

      # Open the image file and import the data.
      data = File.binread(filename)
      md5  = Digest::MD5.hexdigest(data)
      if data.unpack1('x A3') == 'PNG'
        # Test for PNGs.
        type, width, height, x_dpi, y_dpi = process_png(data)
        @image_types[:png] = 1
      elsif data.unpack1('n') == 0xFFD8
        # Test for JPEG files.
        type, width, height, x_dpi, y_dpi = process_jpg(data, filename)
        @image_types[:jpeg] = 1
      elsif data.unpack1('A4') == 'GIF8'
        # Test for GIFs.
        type, width, height, x_dpi, y_dpi = process_gif(data, filename)
        @image_types[:gif] = 1
      elsif data.unpack1('A2') == 'BM'
        # Test for BMPs.
        type, width, height = process_bmp(data, filename)
        @image_types[:bmp] = 1
      else
        # TODO. Add Image::Size to support other types.
        raise "Unsupported image format for file: #{filename}\n"
      end

      # Set a default dpi for images with 0 dpi.
      x_dpi = 96 if x_dpi == 0
      y_dpi = 96 if y_dpi == 0

      [type, width, height, File.basename(filename), x_dpi, y_dpi, md5]
    end

    #
    # Extract width and height information from a PNG file.
    #
    def process_png(data)
      type   = 'png'
      width  = 0
      height = 0
      x_dpi  = 96
      y_dpi  = 96

      offset = 8
      data_length = data.size

      # Search through the image data to read the height and width in th the
      # IHDR element. Also read the DPI in the pHYs element.
      while offset < data_length

        length = data[offset + 0, 4].unpack1("N")
        png_type   = data[offset + 4, 4].unpack1("A4")

        case png_type
        when "IHDR"
          width  = data[offset + 8, 4].unpack1("N")
          height = data[offset + 12, 4].unpack1("N")
        when "pHYs"
          x_ppu = data[offset +  8, 4].unpack1("N")
          y_ppu = data[offset + 12, 4].unpack1("N")
          units = data[offset + 16, 1].unpack1("C")

          if units == 1
            x_dpi = x_ppu * 0.0254
            y_dpi = y_ppu * 0.0254
          end
        end

        offset = offset + length + 12

        break if png_type == "IEND"
      end
      raise "#{filename}: no size data found in png image.\n" unless height

      [type, width, height, x_dpi, y_dpi]
    end

    def process_jpg(data, filename)
      type     = 'jpeg'
      x_dpi    = 96
      y_dpi    = 96

      offset = 2
      data_length = data.bytesize

      # Search through the image data to read the JPEG markers.
      while offset < data_length
        marker  = data[offset + 0, 2].unpack1("n")
        length  = data[offset + 2, 2].unpack1("n")

        # Read the height and width in the 0xFFCn elements
        # (Except C4, C8 and CC which aren't SOF markers).
        if (marker & 0xFFF0) == 0xFFC0 &&
           marker != 0xFFC4 && marker != 0xFFCC
          height = data[offset + 5, 2].unpack1("n")
          width  = data[offset + 7, 2].unpack1("n")
        end

        # Read the DPI in the 0xFFE0 element.
        if marker == 0xFFE0
          units     = data[offset + 11, 1].unpack1("C")
          x_density = data[offset + 12, 2].unpack1("n")
          y_density = data[offset + 14, 2].unpack1("n")

          if units == 1
            x_dpi = x_density
            y_dpi = y_density
          elsif units == 2
            x_dpi = x_density * 2.54
            y_dpi = y_density * 2.54
          end
        end

        offset += length + 2
        break if marker == 0xFFDA
      end

      raise "#{filename}: no size data found in jpeg image.\n" unless height

      [type, width, height, x_dpi, y_dpi]
    end

    #
    # Extract width and height information from a GIF file.
    #
    def process_gif(data, filename)
      type  = 'gif'
      x_dpi = 96
      y_dpi = 96

      width  = data[6, 2].unpack1("v")
      height = data[8, 2].unpack1("v")

      raise "#{filename}: no size data found in gif image.\n" if height.nil?

      [type, width, height, x_dpi, y_dpi]
    end

    # Extract width and height information from a BMP file.
    def process_bmp(data, filename)       # :nodoc:
      type     = 'bmp'

      # Check that the file is big enough to be a bitmap.
      raise "#{filename} doesn't contain enough data." if data.bytesize <= 0x36

      # Read the bitmap width and height. Verify the sizes.
      width, height = data.unpack("x18 V2")
      raise "#{filename}: largest image width #{width} supported is 65k." if width > 0xFFFF
      raise "#{filename}: largest image height supported is 65k." if height > 0xFFFF

      # Read the bitmap planes and bpp data. Verify them.
      planes, bitcount = data.unpack("x26 v2")
      raise "#{filename} isn't a 24bit true color bitmap." unless bitcount == 24
      raise "#{filename}: only 1 plane supported in bitmap image." unless planes == 1

      # Read the bitmap compression. Verify compression.
      compression = data.unpack1("x30 V")
      raise "#{filename}: compression not supported in bitmap image." unless compression == 0

      [type, width, height]
    end
  end
	class Sheets < DelegateClass(Array)
    include Writexlsx_kot::Utility

    BASE_NAME = { sheet: 'Sheet', chart: 'Chart' }  # :nodoc:

    def initialize
      super([])
    end

    def chartsheet_count
      chartsheets.count
    end

    def sheetname_count
      count - chartname_count
    end

    def chartname_count
      chartsheet_count
    end

    def make_and_check_sheet_chart_name(type, name)
      count = sheet_chart_count(type)
      name = "#{BASE_NAME[type]}#{count + 1}" unless ptrue?(name)

      check_valid_sheetname(name)
      name
    end

    def write_sheets(writer)
      writer.tag_elements('sheets') do
        id_num = 1
        each do |sheet|
          write_sheet(writer, sheet, id_num)
          id_num += 1
        end
      end
    end

    def write_worksheet_files(package_dir)
      dir = "#{package_dir}/xl/worksheets"
      worksheets.each_with_index do |sheet, index|
        write_sheet_files(dir, sheet, index)
      end
    end

    def write_chartsheet_files(package_dir)
      dir = "#{package_dir}/xl/chartsheets"
      chartsheets.each_with_index do |sheet, index|
        write_sheet_files(dir, sheet, index)
      end
    end

    def write_vml_files(package_dir)
      dir = "#{package_dir}/xl/drawings"
      index = 1
      each do |sheet|
        next if !sheet.has_vml? and !sheet.has_header_vml?

        FileUtils.mkdir_p(dir)

        if sheet.has_vml?
          vml = Package::Vml.new
          vml.set_xml_writer("#{dir}/vmlDrawing#{index}.vml")
          vml.assemble_xml_file(
            sheet.vml_data_id, sheet.vml_shape_id,
            sheet.sorted_comments, sheet.buttons_data
          )
          index += 1
        end
        next unless sheet.has_header_vml?

        vml = Package::Vml.new
        vml.set_xml_writer("#{dir}/vmlDrawing#{index}.vml")
        vml.assemble_xml_file(
          sheet.vml_header_id, sheet.vml_header_id * 1024,
          [], [], sheet.header_images_data
        )
        write_vml_drawing_rels_files(package_dir, sheet, index)
        index += 1
      end
    end

    def write_comment_files(package_dir)
      self.select { |sheet| sheet.has_comments? }
          .each_with_index do |sheet, index|
        FileUtils.mkdir_p("#{package_dir}/xl/drawings")
        sheet.comments.set_xml_writer("#{package_dir}/xl/comments#{index + 1}.xml")
        sheet.comments.assemble_xml_file
      end
    end

    def write_table_files(package_dir)
      unless tables.empty?
        dir = "#{package_dir}/xl/tables"
        FileUtils.mkdir_p(dir)
        tables.each_with_index do |table, index|
          table.set_xml_writer("#{dir}/table#{index + 1}.xml")
          table.assemble_xml_file
        end
      end
    end

    def write_chartsheet_rels_files(package_dir)
      write_sheet_rels_files_base(
        chartsheets, "#{package_dir}/xl/chartsheets/_rels", 'sheet'
      )
    end

    def write_drawing_rels_files(package_dir)
      dir = "#{package_dir}/xl/drawings/_rels"

      index = 0
      each do |sheet|
        index += 1 if !sheet.drawing_links[0].empty? || sheet.has_shapes?

        next if sheet.drawing_links[0].empty?

        FileUtils.mkdir_p(dir)

        rels = Package::Relationships.new

        sheet.drawing_links.each do |drawing_datas|
          drawing_datas.each do |drawing_data|
            rels.add_document_relationship(*drawing_data)
          end
        end

        # Create the .rels file such as /xl/drawings/_rels/sheet1.xml.rels.
        rels.set_xml_writer("#{dir}/drawing#{index}.xml.rels")
        rels.assemble_xml_file
      end
    end

    def write_vml_drawing_rels_files(package_dir, worksheet, index)
      # Create the drawing .rels dir.
      dir = "#{package_dir}/xl/drawings/_rels"
      FileUtils.mkdir_p(dir)

      rels = Package::Relationships.new

      worksheet.vml_drawing_links.each do |drawing_data|
        rels.add_document_relationship(*drawing_data)
      end

      # Create the .rels file such as /xl/drawings/_rels/vmlDrawing1.vml.rels.
      rels.set_xml_writer("#{dir}/vmlDrawing#{index}.vml.rels")
      rels.assemble_xml_file
    end

    def write_worksheet_rels_files(package_dir)
      write_sheet_rels_files_base(
        worksheets, "#{package_dir}/xl/worksheets/_rels", 'sheet'
      )
    end

    def write_sheet_rels_files_base(sheets, dir, body)
      sheets.each_with_index do |sheet, index|
        next if sheet.external_links.empty?

        FileUtils.mkdir_p(dir)

        rels = Package::Relationships.new

        sheet.external_links.each do |link_datas|
          link_datas.each do |link_data|
            rels.add_worksheet_relationship(*link_data)
          end
        end

        # Create the .rels file such as /xl/worksheets/_rels/sheet1.xml.rels.
        rels.set_xml_writer("#{dir}/#{body}#{index + 1}.xml.rels")
        rels.assemble_xml_file
      end
    end

    def tables
      inject([]) { |tables, sheet| tables + sheet.tables }.flatten
    end

    def tables_count
      tables.count
    end

    def index_by_name(sheetname)
      name = sheetname.sub(/^'/, '').sub(/'$/, '')
      collect { |sheet| sheet.name }.index(name)
    end

    def worksheets
      reject { |worksheet| worksheet.is_chartsheet? }
    end

    def chartsheets
      self.select { |worksheet| worksheet.is_chartsheet? }
    end

    def visible_first
      reject { |worksheet| worksheet.hidden? }.first
    end

    private

    def sheet_chart_count(type)
      case type
      when :sheet
        sheetname_count
      when :chart
        chartname_count
      end
    end

    def check_valid_sheetname(name)
      # Check that sheet name is <= 31. Excel limit.
      raise "Sheetname #{name} must be <= #{SHEETNAME_MAX} chars" if name.length > SHEETNAME_MAX

      # Check that sheetname doesn't contain any invalid characters
      invalid_char = %r{[\[\]:*?/\\]}
      raise 'Invalid character []:*?/\\ in worksheet name: ' + name if name =~ invalid_char

      # Check that sheetname doesn't start or end with an apostrophe.
      raise "Worksheet name #{name} cannot start or end with an " if name =~ /^'/ || name =~ /'$/

      # Check that the worksheet name doesn't already exist since this is a fatal
      # error in Excel 97. The check must also exclude case insensitive matches.
      raise "apostropheWorksheet name '#{name}', with case ignored, is already used." unless is_sheetname_uniq?(name)
    end

    def is_sheetname_uniq?(name)
      each do |worksheet|
        return false if name.downcase == worksheet.name.downcase
      end
      true
    end

    def write_sheet_files(dir, sheet, index)
      FileUtils.mkdir_p(dir)
      sheet.set_xml_writer("#{dir}/sheet#{index + 1}.xml")
      sheet.assemble_xml_file
    end

    def write_sheet(writer, sheet, sheet_id) # :nodoc:
      attributes = [
        ['name',    sheet.name],
        ['sheetId', sheet_id]
      ]

      if sheet.hidden?
        attributes << %w[state hidden]
      elsif sheet.very_hidden?
        attributes << %w[state veryHidden]
      end
      attributes << r_id_attributes(sheet_id)
      writer.empty_tag('sheet', attributes)
    end
  end
	class Worksheet
    include Writexlsx_kot::Utility

    MAX_DIGIT_WIDTH = 7    # For Calabri 11.  # :nodoc:
    PADDING         = 5                       # :nodoc:
    COLINFO         = Struct.new('ColInfo', :width, :format, :hidden, :level, :collapsed, :autofit)

    attr_reader :index                                            # :nodoc:
    attr_reader :charts, :images, :tables, :shapes, :drawings     # :nodoc:
    attr_reader :header_images, :footer_images, :background_image # :nodoc:
    attr_reader :vml_drawing_links                                # :nodoc:
    attr_reader :vml_data_id                                      # :nodoc:
    attr_reader :vml_header_id                                    # :nodoc:
    attr_reader :autofilter_area                                  # :nodoc:
    attr_reader :writer, :set_rows, :col_info                     # :nodoc:
    attr_reader :vml_shape_id                                     # :nodoc:
    attr_reader :comments, :comments_author                       # :nodoc:
    attr_accessor :data_bars_2010, :dxf_priority                  # :nodoc:
    attr_reader :vba_codename                                     # :nodoc:
    attr_writer :excel_version                                    # :nodoc:
    attr_reader :filter_cells                                     # :nodoc:

    def initialize(workbook, index, name) # :nodoc:
      rowmax   = 1_048_576
      colmax   = 16_384
      strmax   = 32_767

      @writer = Package::XMLWriterSimple.new

      @workbook = workbook
      @index = index
      @name = name
      @col_info = {}
      @cell_data_table = []
      @excel_version = 2007
      @palette = workbook.palette
      @default_url_format = workbook.default_url_format
      @max_url_length = workbook.max_url_length

      @page_setup = PageSetup.new

      @screen_gridlines     = true
      @show_zeros           = true

      @xls_rowmax           = rowmax
      @xls_colmax           = colmax
      @xls_strmax           = strmax
      @dim_rowmin           = nil
      @dim_rowmax           = nil
      @dim_colmin           = nil
      @dim_colmax           = nil
      @selections           = []
      @panes                = []
      @hide_row_col_headers = 0
      @top_left_cell        = ''

      @tab_color  = 0

      @set_cols = {}
      @set_rows = {}
      @zoom = 100
      @zoom_scale_normal = true
      @right_to_left = false
      @leading_zeros = false

      @autofilter_area = nil
      @filter_on    = false
      @filter_range = []
      @filter_cols  = {}
      @filter_cells = {}
      @filter_type  = {}

      @row_sizes = {}

      @last_shape_id          = 1
      @rel_count              = 0
      @hlink_count            = 0
      @external_hyper_links   = []
      @external_drawing_links = []
      @external_comment_links = []
      @external_vml_links     = []
      @external_background_links = []
      @external_table_links   = []
      @drawing_links          = []
      @vml_drawing_links      = []
      @charts                 = []
      @images                 = []
      @tables                 = []
      @sparklines             = []
      @shapes                 = []
      @shape_hash             = {}
      @drawing_rels           = {}
      @drawing_rels_id        = 0
      @vml_drawing_rels       = {}
      @vml_drawing_rels_id    = 0
      @has_dynamic_arrays     = false

      @use_future_functions  = false

      @header_images          = []
      @footer_images          = []
      @background_image       = ''

      @outline_row_level = 0
      @outline_col_level = 0

      @original_row_height    = 15
      @default_row_height     = 15
      @default_row_pixels     = 20
      @default_col_width      = 8.43
      @default_col_pixels     = 64
      @default_row_rezoed     = 0
      @default_date_pixels    = 68

      @merge = []

      @has_vml        = false
      @has_header_vml = false
      @comments = Package::Comments.new(self)
      @buttons_array          = []
      @header_images_array    = []
      @ignore_errors          = nil

      @validations = []

      @cond_formats   = {}
      @data_bars_2010 = []
      @dxf_priority   = 1

      @protected_ranges     = []
      @num_protected_ranges = 0

      if excel2003_style?
        @original_row_height      = 12.75
        @default_row_height       = 12.75
        @default_row_pixels       = 17
        self.margins_left_right  = 0.75
        self.margins_top_bottom  = 1
        @page_setup.margin_header = 0.5
        @page_setup.margin_footer = 0.5
        @page_setup.header_footer_aligns = false
      end
    end

    def set_xml_writer(filename) # :nodoc:
      @writer.set_xml_writer(filename)
    end

    def assemble_xml_file # :nodoc:
      write_xml_declaration do
        @writer.tag_elements('worksheet', write_worksheet_attributes) do
          write_sheet_pr
          write_dimension
          write_sheet_views
          write_sheet_format_pr
          write_cols
          write_sheet_data
          write_sheet_protection
          write_protected_ranges
          # write_sheet_calc_pr
          write_phonetic_pr if excel2003_style?
          write_auto_filter
          write_merge_cells
          write_conditional_formats
          write_data_validations
          write_hyperlinks
          write_print_options
          write_page_margins
          write_page_setup
          write_header_footer
          write_row_breaks
          write_col_breaks
          write_ignored_errors
          write_drawings
          write_legacy_drawing
          write_legacy_drawing_hf
          write_picture
          write_table_parts
          write_ext_list
        end
      end
    end

    #
    # The name method is used to retrieve the name of a worksheet.
    #
    attr_reader :name

    #
    # Set this worksheet as a selected worksheet, i.e. the worksheet has its tab
    # highlighted.
    #
    def select
      @hidden   = false  # Selected worksheet can't be hidden.
      @selected = true
    end

    #
    # Set this worksheet as the active worksheet, i.e. the worksheet that is
    # displayed when the workbook is opened. Also set it as selected.
    #
    def activate
      @hidden = false
      @selected = true
      @workbook.activesheet = @index
    end

    #
    # Hide this worksheet.
    #
    def hide(hidden = :hidden)
      @hidden = hidden
      @selected = false
      @workbook.activesheet = 0 if @workbook.activesheet == @index
      @workbook.firstsheet  = 0 if @workbook.firstsheet  == @index
    end

    #
    # Hide this worksheet. This can only be unhidden from VBA.
    #
    def very_hidden
      hide(:very_hidden)
    end

    def hidden? # :nodoc:
      @hidden == :hidden
    end

    def very_hidden? # :nodoc:
      @hidden == :very_hidden
    end

    #
    # Set this worksheet as the first visible sheet. This is necessary
    # when there are a large number of worksheets and the activated
    # worksheet is not visible on the screen.
    #
    def set_first_sheet
      @hidden = false
      @workbook.firstsheet = @index
    end

    #
    # Set the worksheet protection flags to prevent modification of worksheet
    # objects.
    #
    def protect(password = nil, options = {})
      check_parameter(options, protect_default_settings.keys, 'protect')
      @protect = protect_default_settings.merge(options)

      # Set the password after the user defined values.
      if password && password != ''
        @protect[:password] =
          encode_password(password)
      end
    end

    #
    # Unprotect ranges within a protected worksheet.
    #
    def unprotect_range(range, range_name = nil, password = nil)
      if range.nil?
        raise "The range must be defined in unprotect_range())\n"
      else
        range = range.gsub("$", "")
        range = range.sub(/^=/, "")
        @num_protected_ranges += 1
      end

      range_name ||= "Range#{@num_protected_ranges}"
      password   &&= encode_password(password)

      @protected_ranges << [range, range_name, password]
    end

    def protect_default_settings  # :nodoc:
      {
        sheet:                 true,
        content:               false,
        objects:               false,
        scenarios:             false,
        format_cells:          false,
        format_columns:        false,
        format_rows:           false,
        insert_columns:        false,
        insert_rows:           false,
        insert_hyperlinks:     false,
        delete_columns:        false,
        delete_rows:           false,
        select_locked_cells:   true,
        sort:                  false,
        autofilter:            false,
        pivot_tables:          false,
        select_unlocked_cells: true
      }
    end
    private :protect_default_settings

    #
    # :call-seq:
    #   set_column(firstcol, lastcol, width, format, hidden, level, collapsed)
    #
    # This method can be used to change the default properties of a single
    # column or a range of columns. All parameters apart from +first_col+
    # and +last_col+ are optional.
    #
    def set_column(*args)
      # Check for a cell reference in A1 notation and substitute row and column
      # ruby 3.2 no longer handles =~ for various types
      if args[0].respond_to?(:=~) && args[0].to_s =~ /^\D/
        _row1, firstcol, _row2, lastcol, *data = substitute_cellref(*args)
      else
        firstcol, lastcol, *data = args
      end

      # Ensure at least firstcol, lastcol and width
      return unless firstcol && lastcol && !data.empty?

      # Assume second column is the same as first if 0. Avoids KB918419 bug.
      lastcol = firstcol unless ptrue?(lastcol)

      # Ensure 2nd col is larger than first. Also for KB918419 bug.
      firstcol, lastcol = lastcol, firstcol if firstcol > lastcol

      width, format, hidden, level, collapsed = data
      autofit = 0

      # Check that cols are valid and store max and min values with default row.
      # NOTE: The check shouldn't modify the row dimensions and should only modify
      #       the column dimensions in certain cases.
      ignore_row = 1
      ignore_col = 1
      ignore_col = 0 if format.respond_to?(:xf_index)   # Column has a format.
      ignore_col = 0 if width && ptrue?(hidden)         # Column has a width but is hidden

      check_dimensions_and_update_max_min_values(0, firstcol, ignore_row, ignore_col)
      check_dimensions_and_update_max_min_values(0, lastcol,  ignore_row, ignore_col)

      # Set the limits for the outline levels (0 <= x <= 7).
      level ||= 0
      level = 0 if level < 0
      level = 7 if level > 7

      # Excel has a maximum column width of 255 characters.
      width = 255.0 if width && width > 255.0

      @outline_col_level = level if level > @outline_col_level

      # Store the column data based on the first column. Padded for sorting.
      (firstcol..lastcol).each do |col|
        @col_info[col] =
          COLINFO.new(width, format, hidden, level, collapsed, autofit)
      end

      # Store the column change to allow optimisations.
      @col_size_changed = 1
    end

    #
    # Set the width (and properties) of a single column or a range of columns in
    # pixels rather than character units.
    #
    def set_column_pixels(*data)
      cell = data[0]

      # Check for a cell reference in A1 notation and substitute row and column
      if cell =~ /^\D/
        data = substitute_cellref(*data)

        # Returned values row1 and row2 aren't required here. Remove them.
        data.shift         # $row1
        data.delete_at(1)  # $row2
      end

      # Ensure at least $first_col, $last_col and $width
      return if data.size < 3

      first_col = data[0]
      last_col  = data[1]
      pixels    = data[2]
      format    = data[3]
      hidden    = data[4] || 0
      level     = data[5]

      width = pixels_to_width(pixels) if ptrue?(pixels)

      set_column(first_col, last_col, width, format, hidden, level)
    end

    #
    # autofit()
    #
    # Simulate autofit based on the data, and datatypes in each column. We do this
    # by estimating a pixel width for each cell data.
    #
    def autofit
      col_width = {}

      # Iterate through all the data in the worksheet.
      (@dim_rowmin..@dim_rowmax).each do |row_num|
        # Skip row if it doesn't contain cell data.
        next unless @cell_data_table[row_num]

        (@dim_colmin..@dim_colmax).each do |col_num|
          length = 0
          case (cell_data = @cell_data_table[row_num][col_num])
          when StringCellData, RichStringCellData
            # Handle strings and rich strings.
            #
            # For standard shared strings we do a reverse lookup
            # from the shared string id to the actual string. For
            # rich strings we use the unformatted string. We also
            # split multiline strings and handle each part
            # separately.
            string = cell_data.raw_string

            if string =~ /\n/
              # Handle multiline strings.
              length = max = string.split("\n").collect do |str|
                xl_string_pixel_width(str)
              end.max
            else
              length = xl_string_pixel_width(string)
            end
          when DateTimeCellData

            # Handle dates.
            #
            # The following uses the default width for mm/dd/yyyy
            # dates. It isn't feasible to parse the number format
            # to get the actual string width for all format types.
            length = @default_date_pixels
          when NumberCellData

            # Handle numbers.
            #
            # We use a workaround/optimization for numbers since
            # digits all have a pixel width of 7. This gives a
            # slightly greater width for the decimal place and
            # minus sign but only by a few pixels and
            # over-estimation is okay.
            length = 7 * cell_data.token.to_s.length
          when BooleanCellData

            # Handle boolean values.
            #
            # Use the Excel standard widths for TRUE and FALSE.
            if ptrue?(cell_data.token)
              length = 31
            else
              length = 36
            end
          when FormulaCellData, FormulaArrayCellData, DynamicFormulaArrayCellData
            # Handle formulas.
            #
            # We only try to autofit a formula if it has a
            # non-zero value.
            if ptrue?(cell_data.data)
              length = xl_string_pixel_width(cell_data.data)
            end
          end

          # If the cell is in an autofilter header we add an
          # additional 16 pixels for the dropdown arrow.
          if length > 0 &&
             @filter_cells["#{row_num}:#{col_num}"]
            length += 16
          end

          # Add the string lenght to the lookup hash.
          max                = col_width[col_num] || 0
          col_width[col_num] = length if length > max
        end
      end

      # Apply the width to the column.
      col_width.each do |col_num, pixel_width|
        # Convert the string pixel width to a character width using an
        # additional padding of 7 pixels, like Excel.
        width = pixels_to_width(pixel_width + 7)

        # The max column character width in Excel is 255.
        width = 255.0 if width > 255.0

        # Add the width to an existing col info structure or add a new one.
        if @col_info[col_num]
          @col_info[col_num].width   = width
          @col_info[col_num].autofit = 1
        else
          @col_info[col_num] =
            COLINFO.new(width, nil, 0, 0, 0, 1)
        end
      end
    end

    #
    # :call-seq:
    #   set_selection(cell_or_cell_range)
    #
    # Set which cell or cells are selected in a worksheet.
    #
    def set_selection(*args)
      return if args.empty?

      if (row_col_array = row_col_notation(args.first))
        row_first, col_first, row_last, col_last = row_col_array
      else
        row_first, col_first, row_last, col_last = args
      end

      active_cell = xl_rowcol_to_cell(row_first, col_first)

      if row_last  # Range selection.
        # Swap last row/col for first row/col as necessary
        row_first, row_last = row_last, row_first if row_first > row_last
        col_first, col_last = col_last, col_first if col_first > col_last

        sqref = xl_range(row_first, row_last, col_first, col_last)
      else          # Single cell selection.
        sqref = active_cell
      end

      # Selection isn't set for cell A1.
      return if sqref == 'A1'

      @selections = [[nil, active_cell, sqref]]
    end

    ###############################################################################
    #
    # set_top_left_cell()
    #
    # Set the first visible cell at the top left of the worksheet.
    #
    def set_top_left_cell(row, col = nil)
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
      else
        _row = row
        _col = col
      end

      @top_left_cell = xl_rowcol_to_cell(_row, _col)
    end

    #
    # :call-seq:
    #   freeze_panes(row, col [ , top_row, left_col ] )
    #
    # This method can be used to divide a worksheet into horizontal or
    # vertical regions known as panes and to also "freeze" these panes so
    # that the splitter bars are not visible. This is the same as the
    # Window->Freeze Panes menu command in Excel
    #
    def freeze_panes(*args)
      return if args.empty?

      # Check for a cell reference in A1 notation and substitute row and column.
      if (row_col_array = row_col_notation(args.first))
        row, col, top_row, left_col = row_col_array
        type = args[1]
      else
        row, col, top_row, left_col, type = args
      end

      col      ||= 0
      top_row  ||= row
      left_col ||= col
      type     ||= 0

      @panes   = [row, col, top_row, left_col, type]
    end

    #
    # :call-seq:
    #   split_panes(y, x, top_row, left_col)
    #
    # Set panes and mark them as split.
    #
    def split_panes(*args)
      # Call freeze panes but add the type flag for split panes.
      freeze_panes(args[0], args[1], args[2], args[3], 2)
    end

    #
    # Set the page orientation as portrait.
    # The default worksheet orientation is portrait, so you won't generally
    # need to call this method.
    #
    def set_portrait
      @page_setup.orientation        = true
      @page_setup.page_setup_changed = true
    end

    #
    # Set the page orientation as landscape.
    #
    def set_landscape
      @page_setup.orientation         = false
      @page_setup.page_setup_changed  = true
    end

    #
    # This method is used to display the worksheet in "Page View/Layout" mode.
    #
    def set_page_view(flag = 1)
      @page_view = flag
    end

    #
    # set_pagebreak_view
    #
    # Set the page view mode.
    #
    def set_pagebreak_view
      @page_view = 2
    end

    #
    # Set the colour of the worksheet tab.
    #
    def tab_color=(color)
      @tab_color = Colors.new.color(color)
    end

    # This method is deprecated. use tab_color=().
    def set_tab_color(color)
      put_deprecate_message("#{self}.set_tab_color")
      self.tab_color = color
    end

    #
    # Set the paper type. Ex. 1 = US Letter, 9 = A4
    #
    def paper=(paper_size)
      @page_setup.paper = paper_size
    end

    def set_paper(paper_size)
      put_deprecate_message("#{self}.set_paper")
      self.paper = paper_size
    end

    #
    # Set the page header caption and optional margin.
    #
    def set_header(string = '', margin = 0.3, options = {})
      raise 'Header string must be less than 255 characters' if string.length > 255

      # Replace the Excel placeholder &[Picture] with the internal &G.
      @page_setup.header = string.gsub("&[Picture]", '&G')

      @page_setup.header_footer_aligns = options[:align_with_margins] if options[:align_with_margins]

      @page_setup.header_footer_scales = options[:scale_with_doc] if options[:scale_with_doc]

      # Reset the array in case the function is called more than once.
      @header_images = []

      [
        [:image_left, 'LH'], [:image_center, 'CH'], [:image_right, 'RH']
      ].each do |p|
        @header_images << [options[p.first], p.last] if options[p.first]
      end

      # placeholeder /&G/ の数
      placeholder_count = @page_setup.header.scan("&G").count

      image_count = @header_images.count

      raise "Number of header image (#{image_count}) doesn't match placeholder count (#{placeholder_count}) in string: #{@page_setup.header}" if image_count != placeholder_count

      @has_header_vml = true if image_count > 0

      @page_setup.margin_header         = margin || 0.3
      @page_setup.header_footer_changed = true
    end

    #
    # Set the page footer caption and optional margin.
    #
    def set_footer(string = '', margin = 0.3, options = {})
      raise 'Footer string must be less than 255 characters' if string.length > 255

      @page_setup.footer = string.dup

      # Replace the Excel placeholder &[Picture] with the internal &G.
      @page_setup.footer = string.gsub("&[Picture]", '&G')

      @page_setup.header_footer_aligns = options[:align_with_margins] if options[:align_with_margins]

      @page_setup.header_footer_scales = options[:scale_with_doc] if options[:scale_with_doc]

      # Reset the array in case the function is called more than once.
      @footer_images = []

      [
        [:image_left, 'LF'], [:image_center, 'CF'], [:image_right, 'RF']
      ].each do |p|
        @footer_images << [options[p.first], p.last] if options[p.first]
      end

      # placeholeder /&G/ の数
      placeholder_count = @page_setup.footer.scan("&G").count

      image_count = @footer_images.count

      raise "Number of footer image (#{image_count}) doesn't match placeholder count (#{placeholder_count}) in string: #{@page_setup.footer}" if image_count != placeholder_count

      @has_header_vml = true if image_count > 0

      @page_setup.margin_footer         = margin
      @page_setup.header_footer_changed = true
    end

    #
    # Center the worksheet data horizontally between the margins on the printed page:
    #
    def center_horizontally
      @page_setup.center_horizontally
    end

    #
    # Center the worksheet data vertically between the margins on the printed page:
    #
    def center_vertically
      @page_setup.center_vertically
    end

    #
    # Set all the page margins to the same value in inches.
    #
    def margins=(margin)
      self.margin_left   = margin
      self.margin_right  = margin
      self.margin_top    = margin
      self.margin_bottom = margin
    end

    #
    # Set the left and right margins to the same value in inches.
    # See set_margins
    #
    def margins_left_right=(margin)
      self.margin_left  = margin
      self.margin_right = margin
    end

    #
    # Set the top and bottom margins to the same value in inches.
    # See set_margins
    #
    def margins_top_bottom=(margin)
      self.margin_top    = margin
      self.margin_bottom = margin
    end

    #
    # Set the left margin in inches.
    # See margins=()
    #
    def margin_left=(margin)
      @page_setup.margin_left = remove_white_space(margin)
    end

    #
    # Set the right margin in inches.
    # See margins=()
    #
    def margin_right=(margin)
      @page_setup.margin_right = remove_white_space(margin)
    end

    #
    # Set the top margin in inches.
    # See margins=()
    #
    def margin_top=(margin)
      @page_setup.margin_top = remove_white_space(margin)
    end

    #
    # Set the bottom margin in inches.
    # See margins=()
    #
    def margin_bottom=(margin)
      @page_setup.margin_bottom = remove_white_space(margin)
    end

    #
    # set_margin_* methods are deprecated. use margin_*=().
    #
    def set_margins(margin)
      put_deprecate_message("#{self}.set_margins")
      self.margins = margin
    end

    #
    # this method is deprecated. use margin_left_right=().
    # Set the left and right margins to the same value in inches.
    #
    def set_margins_LR(margin)
      put_deprecate_message("#{self}.set_margins_LR")
      self.margins_left_right = margin
    end

    #
    # this method is deprecated. use margin_top_bottom=().
    # Set the top and bottom margins to the same value in inches.
    #
    def set_margins_TB(margin)
      put_deprecate_message("#{self}.set_margins_TB")
      self.margins_top_bottom = margin
    end

    #
    # this method is deprecated. use margin_left=()
    # Set the left margin in inches.
    #
    def set_margin_left(margin = 0.7)
      put_deprecate_message("#{self}.set_margin_left")
      self.margin_left = margin
    end

    #
    # this method is deprecated. use margin_right=()
    # Set the right margin in inches.
    #
    def set_margin_right(margin = 0.7)
      put_deprecate_message("#{self}.set_margin_right")
      self.margin_right = margin
    end

    #
    # this method is deprecated. use margin_top=()
    # Set the top margin in inches.
    #
    def set_margin_top(margin = 0.75)
      put_deprecate_message("#{self}.set_margin_top")
      self.margin_top = margin
    end

    #
    # this method is deprecated. use margin_bottom=()
    # Set the bottom margin in inches.
    #
    def set_margin_bottom(margin = 0.75)
      put_deprecate_message("#{self}.set_margin_bottom")
      self.margin_bottom = margin
    end

    #
    # Set the number of rows to repeat at the top of each printed page.
    #
    def repeat_rows(row_min, row_max = nil)
      row_max ||= row_min

      # Convert to 1 based.
      row_min += 1
      row_max += 1

      area = "$#{row_min}:$#{row_max}"

      # Build up the print titles "Sheet1!$1:$2"
      sheetname = quote_sheetname(@name)
      @page_setup.repeat_rows = "#{sheetname}!#{area}"
    end

    def print_repeat_rows   # :nodoc:
      @page_setup.repeat_rows
    end

    #
    # :call-seq:
    #   repeat_columns(first_col, last_col = nil)
    #
    # Set the columns to repeat at the left hand side of each printed page.
    #
    def repeat_columns(*args)
      if args[0] =~ /^\D/
        _dummy, first_col, _dummy, last_col = substitute_cellref(*args)
      else
        first_col, last_col = args
      end
      last_col ||= first_col

      area = "#{xl_col_to_name(first_col, 1)}:#{xl_col_to_name(last_col, 1)}"
      @page_setup.repeat_cols = "#{quote_sheetname(@name)}!#{area}"
    end

    def print_repeat_cols  # :nodoc:
      @page_setup.repeat_cols
    end

    #
    # :call-seq:
    #   print_area(first_row, first_col, last_row, last_col)
    #
    # This method is used to specify the area of the worksheet that will
    # be printed. All four parameters must be specified. You can also use
    # A1 notation.
    #
    def print_area(*args)
      return @page_setup.print_area.dup if args.empty?

      if (row_col_array = row_col_notation(args.first))
        row1, col1, row2, col2 = row_col_array
      else
        row1, col1, row2, col2 = args
      end

      return if [row1, col1, row2, col2].include?(nil)

      # Ignore max print area since this is the same as no print area for Excel.
      return if row1 == 0 && col1 == 0 && row2 == ROW_MAX - 1 && col2 == COL_MAX - 1

      # Build up the print area range "=Sheet2!R1C1:R2C1"
      @page_setup.print_area = convert_name_area(row1, col1, row2, col2)
    end

    #
    # Set the worksheet zoom factor in the range <tt>10 <= scale <= 400</tt>:
    #
    def zoom=(scale)
      # Confine the scale to Excel's range
      @zoom = if scale < 10 or scale > 400
                # carp "Zoom factor scale outside range: 10 <= zoom <= 400"
                100
              else
                scale.to_i
              end
    end

    # This method is deprecated. use zoom=().
    def set_zoom(scale)
      put_deprecate_message("#{self}.set_zoom")
      self.zoom = scale
    end

    #
    # Set the scale factor of the printed page.
    # Scale factors in the range 10 <= scale <= 400 are valid:
    #
    def print_scale=(scale = 100)
      scale_val = scale.to_i
      # Confine the scale to Excel's range
      scale_val = 100 if scale_val < 10 || scale_val > 400

      # Turn off "fit to page" option.
      @page_setup.fit_page = false

      @page_setup.scale              = scale_val
      @page_setup.page_setup_changed = true
    end

    #
    # This method is deprecated. use print_scale=().
    #
    def set_print_scale(scale = 100)
      put_deprecate_message("#{self}.set_print_scale")
      self.print_scale = (scale)
    end

    #
    # Set the option to print the worksheet in black and white.
    #
    def print_black_and_white
      @page_setup.black_white        = true
      @page_setup.page_setup_changed = true
    end

    #
    # Causes the write() method to treat integers with a leading zero as a string.
    # This ensures that any leading zeros such, as in zip codes, are maintained.
    #
    def keep_leading_zeros(flag = true)
      @leading_zeros = !!flag
    end

    #
    # Display the worksheet right to left for some eastern versions of Excel.
    #
    def right_to_left(flag = true)
      @right_to_left = !!flag
    end

    #
    # Hide cell zero values.
    #
    def hide_zero(flag = true)
      @show_zeros = !flag
    end

    #
    # Set the order in which pages are printed.
    #
    def print_across(across = true)
      if across
        @page_setup.across             = true
        @page_setup.page_setup_changed = true
      else
        @page_setup.across = false
      end
    end

    #
    # The start_page=() method is used to set the number of the
    # starting page when the worksheet is printed out.
    #
    def start_page=(page_start)
      @page_setup.page_start = page_start
    end

    def set_start_page(page_start)
      put_deprecate_message("#{self}.set_start_page")
      self.start_page = page_start
    end

    #
    # :call-seq:
    #  write(row, column [ , token [ , format ] ])
    #
    # Excel makes a distinction between data types such as strings, numbers,
    # blanks, formulas and hyperlinks. To simplify the process of writing
    # data the {#write()}[#method-i-write] method acts as a general alias for several more
    # specific methods:
    #
    def write(row, col, token = nil, format = nil, value1 = nil, value2 = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _token     = col
        _format    = token
        _value1    = format
        _value2    = value1
      else
        _row = row
        _col = col
        _token = token
        _format = format
        _value1 = value1
        _value2 = value2
      end
      _token ||= ''
      _token = _token.to_s if token.instance_of?(Time) || token.instance_of?(Date)

      if _format.respond_to?(:force_text_format?) && _format.force_text_format?
        write_string(_row, _col, _token, _format) # Force text format
      # Match an array ref.
      elsif _token.respond_to?(:to_ary)
        write_row(_row, _col, _token, _format, _value1, _value2)
      elsif _token.respond_to?(:coerce)  # Numeric
        write_number(_row, _col, _token, _format)
      elsif _token.respond_to?(:=~)  # String
        # Match integer with leading zero(s)
        if @leading_zeros && _token =~ /^0\d*$/
          write_string(_row, _col, _token, _format)
        elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
          write_number(_row, _col, _token, _format)
        # Match formula
        elsif _token =~ /^=/
          write_formula(_row, _col, _token, _format, _value1)
        # Match array formula
        elsif _token =~ /^\{=.*\}$/
          write_formula(_row, _col, _token, _format, _value1)
        # Match blank
        elsif _token == ''
          #        row_col_args.delete_at(2)     # remove the empty string from the parameter list
          write_blank(_row, _col, _format)
        elsif @workbook.strings_to_urls
          # Match http, https or ftp URL
          if _token =~ %r{\A[fh]tt?ps?://}
            write_url(_row, _col, _token, _format, _value1, _value2)
          # Match mailto:
          elsif _token =~ /\Amailto:/
            write_url(_row, _col, _token, _format, _value1, _value2)
          # Match internal or external sheet link
          elsif _token =~ /\A(?:in|ex)ternal:/
            write_url(_row, _col, _token, _format, _value1, _value2)
          else
            write_string(_row, _col, _token, _format)
          end
        else
          write_string(_row, _col, _token, _format)
        end
      else
        write_string(_row, _col, _token, _format)
      end
    end

    #
    # :call-seq:
    #   write_row(row, col, array [ , format ])
    #
    # Write a row of data starting from (row, col). Call write_col() if any of
    # the elements of the array are in turn array. This allows the writing
    # of 1D or 2D arrays of data in one go.
    #
    def write_row(row, col, tokens = nil, *options)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _tokens    = col
        _options   = [tokens] + options
      else
        _row = row
        _col = col
        _tokens = tokens
        _options = options
      end
      raise "Not an array ref in call to write_row()$!" unless _tokens.respond_to?(:to_ary)

      _tokens.each do |_token|
        # Check for nested arrays
        if _token.respond_to?(:to_ary)
          write_col(_row, _col, _token, *_options)
        else
          write(_row, _col, _token, *_options)
        end
        _col += 1
      end
    end

    #
    # :call-seq:
    #   write_col(row, col, array [ , format ])
    #
    # Write a column of data starting from (row, col). Call write_row() if any of
    # the elements of the array are in turn array. This allows the writing
    # of 1D or 2D arrays of data in one go.
    #
    def write_col(row, col, tokens = nil, *options)
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _tokens    = col
        _options   = [tokens] + options if options
      else
        _row = row
        _col = col
        _tokens = tokens
        _options = options
      end

      _tokens.each do |_token|
        # write() will deal with any nested arrays
        write(_row, _col, _token, *_options)
        _row += 1
      end
    end

    #
    # :call-seq:
    #   write_comment(row, column, string, options = {})
    #
    # Write a comment to the specified row and column (zero indexed).
    #
    def write_comment(row, col, string = nil, options = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _string    = col
        _options   = string
      else
        _row = row
        _col = col
        _string = string
        _options = options
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _string].include?(nil)

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      @has_vml = true

      # Process the properties of the cell comment.
      @comments.add(@workbook, self, _row, _col, _string, _options)
    end

    #
    # :call-seq:
    #   write_number(row, column, number [ , format ])
    #
    # Write an integer or a float to the cell specified by row and column:
    #
    def write_number(row, col, number, format = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _number = col
        _format = number
      else
        _row = row
        _col = col
        _number = number
        _format = format
      end
      raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _number.nil?

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      store_data_to_table(NumberCellData.new(_number, _format), _row, _col)
    end

    #
    # :call-seq:
    #   write_string(row, column, string [, format ])
    #
    # Write a string to the specified row and column (zero indexed).
    # +format+ is optional.
    #
    def write_string(row, col, string = nil, format = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _string = col
        _format = string
      else
        _row = row
        _col = col
        _string = string
        _format = format
      end
      _string &&= _string.to_s
      raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _string.nil?

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)

      store_data_to_table(StringCellData.new(index, _format, _string), _row, _col)
    end

    #
    # :call-seq:
    #    write_rich_string(row, column, (string | format, string)+,  [,cell_format])
    #
    # The write_rich_string() method is used to write strings with multiple formats.
    # The method receives string fragments prefixed by format objects. The final
    # format object is used as the cell format.
    #
    def write_rich_string(row, col, *rich_strings)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col    = row_col_array
        _rich_strings = [col] + rich_strings
      else
        _row = row
        _col = col
        _rich_strings = rich_strings
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _rich_strings[0]].include?(nil)

      _xf = cell_format_of_rich_string(_rich_strings)

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      _fragments, _raw_string = rich_strings_fragments(_rich_strings)
      # can't allow 2 formats in a row
      return -4 unless _fragments

      # Check that the string si < 32767 chars.
      return 3 if _raw_string.size > @xls_strmax

      index = shared_string_index(xml_str_of_rich_string(_fragments))

      store_data_to_table(RichStringCellData.new(index, _xf, _raw_string), _row, _col)
    end

    #
    # :call-seq:
    #   write_blank(row, col, format)
    #
    # Write a blank cell to the specified row and column (zero indexed).
    # A blank cell is used to specify formatting without adding a string
    # or a number.
    #
    def write_blank(row, col, format = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _format = col
      else
        _row = row
        _col = col
        _format = format
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)

      # Don't write a blank cell unless it has a format
      return unless _format

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      store_data_to_table(BlankCellData.new(_format), _row, _col)
    end

    def expand_formula(formula, function, addition = '')
      if formula =~ /\b(#{function})/
        formula.gsub(
          ::Regexp.last_match(1),
          "_xlfn#{addition}.#{::Regexp.last_match(1)}"
        )
      else
        formula
      end
    end
    private :expand_formula

    #
    # Utility method to strip equal sign and array braces from a formula
    # and also expand out future and dynamic array formulas.
    #
    def prepare_formula(given_formula, expand_future_functions = nil)
      # Ignore empty/null formulas.
      return given_formula unless ptrue?(given_formula)

      # Remove array formula braces and the leading =.
      formula = given_formula.sub(/^\{(.*)\}$/, '\1').sub(/^=/, '')

      # # Don't expand formulas that the user has already expanded.
      return formula if formula =~ /_xlfn\./

      # Expand dynamic array formulas.
      formula = expand_formula(formula, 'ANCHORARRAY\(')
      formula = expand_formula(formula, 'BYCOL\(')
      formula = expand_formula(formula, 'BYROW\(')
      formula = expand_formula(formula, 'CHOOSECOLS\(')
      formula = expand_formula(formula, 'CHOOSEROWS\(')
      formula = expand_formula(formula, 'DROP\(')
      formula = expand_formula(formula, 'EXPAND\(')
      formula = expand_formula(formula, 'FILTER\(', '._xlws')
      formula = expand_formula(formula, 'HSTACK\(')
      formula = expand_formula(formula, 'LAMBDA\(')
      formula = expand_formula(formula, 'MAKEARRAY\(')
      formula = expand_formula(formula, 'MAP\(')
      formula = expand_formula(formula, 'RANDARRAY\(')
      formula = expand_formula(formula, 'REDUCE\(')
      formula = expand_formula(formula, 'SCAN\(')
      formula = expand_formula(formula, 'SEQUENCE\(')
      formula = expand_formula(formula, 'SINGLE\(')
      formula = expand_formula(formula, 'SORT\(', '._xlws')
      formula = expand_formula(formula, 'SORTBY\(')
      formula = expand_formula(formula, 'SWITCH\(')
      formula = expand_formula(formula, 'TAKE\(')
      formula = expand_formula(formula, 'TEXTSPLIT\(')
      formula = expand_formula(formula, 'TOCOL\(')
      formula = expand_formula(formula, 'TOROW\(')
      formula = expand_formula(formula, 'UNIQUE\(')
      formula = expand_formula(formula, 'VSTACK\(')
      formula = expand_formula(formula, 'WRAPCOLS\(')
      formula = expand_formula(formula, 'WRAPROWS\(')
      formula = expand_formula(formula, 'XLOOKUP\(')

      if !@use_future_functions && !ptrue?(expand_future_functions)
        return formula
      end

      # Future functions.
      formula = expand_formula(formula, 'ACOTH\(')
      formula = expand_formula(formula, 'ACOT\(')
      formula = expand_formula(formula, 'AGGREGATE\(')
      formula = expand_formula(formula, 'ARABIC\(')
      formula = expand_formula(formula, 'ARRAYTOTEXT\(')
      formula = expand_formula(formula, 'BASE\(')
      formula = expand_formula(formula, 'BETA.DIST\(')
      formula = expand_formula(formula, 'BETA.INV\(')
      formula = expand_formula(formula, 'BINOM.DIST.RANGE\(')
      formula = expand_formula(formula, 'BINOM.DIST\(')
      formula = expand_formula(formula, 'BINOM.INV\(')
      formula = expand_formula(formula, 'BITAND\(')
      formula = expand_formula(formula, 'BITLSHIFT\(')
      formula = expand_formula(formula, 'BITOR\(')
      formula = expand_formula(formula, 'BITRSHIFT\(')
      formula = expand_formula(formula, 'BITXOR\(')
      formula = expand_formula(formula, 'CEILING.MATH\(')
      formula = expand_formula(formula, 'CEILING.PRECISE\(')
      formula = expand_formula(formula, 'CHISQ.DIST.RT\(')
      formula = expand_formula(formula, 'CHISQ.DIST\(')
      formula = expand_formula(formula, 'CHISQ.INV.RT\(')
      formula = expand_formula(formula, 'CHISQ.INV\(')
      formula = expand_formula(formula, 'CHISQ.TEST\(')
      formula = expand_formula(formula, 'COMBINA\(')
      formula = expand_formula(formula, 'CONCAT\(')
      formula = expand_formula(formula, 'CONFIDENCE.NORM\(')
      formula = expand_formula(formula, 'CONFIDENCE.T\(')
      formula = expand_formula(formula, 'COTH\(')
      formula = expand_formula(formula, 'COT\(')
      formula = expand_formula(formula, 'COVARIANCE.P\(')
      formula = expand_formula(formula, 'COVARIANCE.S\(')
      formula = expand_formula(formula, 'CSCH\(')
      formula = expand_formula(formula, 'CSC\(')
      formula = expand_formula(formula, 'DAYS\(')
      formula = expand_formula(formula, 'DECIMAL\(')
      formula = expand_formula(formula, 'ERF.PRECISE\(')
      formula = expand_formula(formula, 'ERFC.PRECISE\(')
      formula = expand_formula(formula, 'EXPON.DIST\(')
      formula = expand_formula(formula, 'F.DIST.RT\(')
      formula = expand_formula(formula, 'F.DIST\(')
      formula = expand_formula(formula, 'F.INV.RT\(')
      formula = expand_formula(formula, 'F.INV\(')
      formula = expand_formula(formula, 'F.TEST\(')
      formula = expand_formula(formula, 'FILTERXML\(')
      formula = expand_formula(formula, 'FLOOR.MATH\(')
      formula = expand_formula(formula, 'FLOOR.PRECISE\(')
      formula = expand_formula(formula, 'FORECAST.ETS.CONFINT\(')
      formula = expand_formula(formula, 'FORECAST.ETS.SEASONALITY\(')
      formula = expand_formula(formula, 'FORECAST.ETS.STAT\(')
      formula = expand_formula(formula, 'FORECAST.ETS\(')
      formula = expand_formula(formula, 'FORECAST.LINEAR\(')
      formula = expand_formula(formula, 'FORMULATEXT\(')
      formula = expand_formula(formula, 'GAMMA.DIST\(')
      formula = expand_formula(formula, 'GAMMA.INV\(')
      formula = expand_formula(formula, 'GAMMALN.PRECISE\(')
      formula = expand_formula(formula, 'GAMMA\(')
      formula = expand_formula(formula, 'GAUSS\(')
      formula = expand_formula(formula, 'HYPGEOM.DIST\(')
      formula = expand_formula(formula, 'IFNA\(')
      formula = expand_formula(formula, 'IFS\(')
      formula = expand_formula(formula, 'IMCOSH\(')
      formula = expand_formula(formula, 'IMCOT\(')
      formula = expand_formula(formula, 'IMCSCH\(')
      formula = expand_formula(formula, 'IMCSC\(')
      formula = expand_formula(formula, 'IMSECH\(')
      formula = expand_formula(formula, 'IMSEC\(')
      formula = expand_formula(formula, 'IMSINH\(')
      formula = expand_formula(formula, 'IMTAN\(')
      formula = expand_formula(formula, 'ISFORMULA\(')
      formula = expand_formula(formula, 'ISOMITTED\(')
      formula = expand_formula(formula, 'ISOWEEKNUM\(')
      formula = expand_formula(formula, 'LET\(')
      formula = expand_formula(formula, 'LOGNORM.DIST\(')
      formula = expand_formula(formula, 'LOGNORM.INV\(')
      formula = expand_formula(formula, 'MAXIFS\(')
      formula = expand_formula(formula, 'MINIFS\(')
      formula = expand_formula(formula, 'MODE.MULT\(')
      formula = expand_formula(formula, 'MODE.SNGL\(')
      formula = expand_formula(formula, 'MUNIT\(')
      formula = expand_formula(formula, 'NEGBINOM.DIST\(')
      formula = expand_formula(formula, 'NORM.DIST\(')
      formula = expand_formula(formula, 'NORM.INV\(')
      formula = expand_formula(formula, 'NORM.S.DIST\(')
      formula = expand_formula(formula, 'NORM.S.INV\(')
      formula = expand_formula(formula, 'NUMBERVALUE\(')
      formula = expand_formula(formula, 'PDURATION\(')
      formula = expand_formula(formula, 'PERCENTILE.EXC\(')
      formula = expand_formula(formula, 'PERCENTILE.INC\(')
      formula = expand_formula(formula, 'PERCENTRANK.EXC\(')
      formula = expand_formula(formula, 'PERCENTRANK.INC\(')
      formula = expand_formula(formula, 'PERMUTATIONA\(')
      formula = expand_formula(formula, 'PHI\(')
      formula = expand_formula(formula, 'POISSON.DIST\(')
      formula = expand_formula(formula, 'QUARTILE.EXC\(')
      formula = expand_formula(formula, 'QUARTILE.INC\(')
      formula = expand_formula(formula, 'QUERYSTRING\(')
      formula = expand_formula(formula, 'RANK.AVG\(')
      formula = expand_formula(formula, 'RANK.EQ\(')
      formula = expand_formula(formula, 'RRI\(')
      formula = expand_formula(formula, 'SECH\(')
      formula = expand_formula(formula, 'SEC\(')
      formula = expand_formula(formula, 'SHEETS\(')
      formula = expand_formula(formula, 'SHEET\(')
      formula = expand_formula(formula, 'SKEW.P\(')
      formula = expand_formula(formula, 'STDEV.P\(')
      formula = expand_formula(formula, 'STDEV.S\(')
      formula = expand_formula(formula, 'T.DIST.2T\(')
      formula = expand_formula(formula, 'T.DIST.RT\(')
      formula = expand_formula(formula, 'T.DIST\(')
      formula = expand_formula(formula, 'T.INV.2T\(')
      formula = expand_formula(formula, 'T.INV\(')
      formula = expand_formula(formula, 'T.TEST\(')
      formula = expand_formula(formula, 'TEXTAFTER\(')
      formula = expand_formula(formula, 'TEXTBEFORE\(')
      formula = expand_formula(formula, 'TEXTJOIN\(')
      formula = expand_formula(formula, 'UNICHAR\(')
      formula = expand_formula(formula, 'UNICODE\(')
      formula = expand_formula(formula, 'VALUETOTEXT\(')
      formula = expand_formula(formula, 'VAR.P\(')
      formula = expand_formula(formula, 'VAR.S\(')
      formula = expand_formula(formula, 'WEBSERVICE\(')
      formula = expand_formula(formula, 'WEIBULL.DIST\(')
      formula = expand_formula(formula, 'XMATCH\(')
      formula = expand_formula(formula, 'XOR\(')
      expand_formula(formula, 'Z.TEST\(')
    end

    #
    # :call-seq:
    #   write_formula(row, column, formula [ , format [ , value ] ])
    #
    # Write a formula or function to the cell specified by +row+ and +column+:
    #
    def write_formula(row, col, formula = nil, format = nil, value = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _formula   = col
        _format    = formula
        _value     = format
      else
        _row = row
        _col = col
        _formula = formula
        _format = format
        _value = value
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _formula].include?(nil)

      # Check for dynamic array functions.
      regex = /\bLET\(|\bSORT\(|\bLAMBDA\(|\bSINGLE\(|\bSORTBY\(|\bUNIQUE\(|\bXMATCH\(|\bFILTER\(|\bXLOOKUP\(|\bSEQUENCE\(|\bRANDARRAY\(|\bANCHORARRAY\(/
      if _formula =~ regex
        return write_dynamic_array_formula(
          _row, _col, _row, _col, _formula, _format, _value
        )
      end

      # Hand off array formulas.
      if _formula =~ /^\{=.*\}$/
        write_array_formula(_row, _col, _row, _col, _formula, _format, _value)
      else
        check_dimensions(_row, _col)
        store_row_col_max_min_values(_row, _col)
        _formula = _formula.sub(/^=/, '')

        store_data_to_table(FormulaCellData.new(_formula, _format, _value), _row, _col)
      end
    end

    #
    # Internal method shared by the write_array_formula() and
    # write_dynamic_array_formula() methods.
    #
    def write_array_formula_base(type, *args)
      # Check for a cell reference in A1 notation and substitute row and column
      # Convert single cell to range
      if args.first.to_s =~ /^([A-Za-z]+[0-9]+)$/
        range = "#{::Regexp.last_match(1)}:#{::Regexp.last_match(1)}"
        params = [range] + args[1..-1]
      else
        params = args
      end

      if (row_col_array = row_col_notation(params.first))
        row1, col1, row2, col2 = row_col_array
        formula, xf, value = params[1..-1]
      else
        row1, col1, row2, col2, formula, xf, value = params
      end
      raise WriteXLSXInsufficientArgumentError if [row1, col1, row2, col2, formula].include?(nil)

      # Swap last row/col with first row/col as necessary
      row1, row2 = row2, row1 if row1 > row2
      col1, col2 = col2, col1 if col1 > col2

      # Check that row and col are valid and store max and min values
      check_dimensions(row1, col1)
      check_dimensions(row2, col2)
      store_row_col_max_min_values(row1, col1)
      store_row_col_max_min_values(row2, col2)

      # Define array range
      range = if row1 == row2 && col1 == col2
                xl_rowcol_to_cell(row1, col1)
              else
                "#{xl_rowcol_to_cell(row1, col1)}:#{xl_rowcol_to_cell(row2, col2)}"
              end

      # Modify the formula string, as needed.
      formula = prepare_formula(formula, 1)

      store_data_to_table(
        if type == 'a'
          FormulaArrayCellData.new(formula, xf, range, value)
        elsif type == 'd'
          DynamicFormulaArrayCellData.new(formula, xf, range, value)
        else
          raise "invalid type in write_array_formula_base()."
        end,
        row1, col1
      )

      # Pad out the rest of the area with formatted zeroes.
      (row1..row2).each do |row|
        (col1..col2).each do |col|
          next if row == row1 && col == col1

          write_number(row, col, 0, xf)
        end
      end
    end

    #
    # write_array_formula(row1, col1, row2, col2, formula, format)
    #
    # Write an array formula to the specified row and column (zero indexed).
    #
    def write_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
      write_array_formula_base('a', row1, col1, row2, col2, formula, format, value)
    end

    #
    # write_dynamic_array_formula(row1, col1, row2, col2, formula, format)
    #
    # Write a dynamic formula to the specified row and column (zero indexed).
    #
    def write_dynamic_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
      write_array_formula_base('d', row1, col1, row2, col2, formula, format, value)
      @has_dynamic_arrays = true
    end

    #
    # write_boolean(row, col, val, format)
    #
    # Write a boolean value to the specified row and column (zero indexed).
    #
    def write_boolean(row, col, val = nil, format = nil)
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _val       = col
        _format    = val
      else
        _row = row
        _col = col
        _val = val
        _format = format
      end
      raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil?

      _val = _val ? 1 : 0  # Boolean value.
      # xf : cell format.

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      store_data_to_table(BooleanCellData.new(_val, _format), _row, _col)
    end

    #
    # :call-seq:
    #   update_format_with_params(row, col, format_params)
    #
    # Update formatting of the cell to the specified row and column (zero indexed).
    #
    def update_format_with_params(row, col, params = nil)
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _params = args[1]
      else
        _row = row
        _col = col
        _params = params
      end
      raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _params.nil?

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      format = nil
      cell_data = nil
      if @cell_data_table[_row].nil? || @cell_data_table[_row][_col].nil?
        format = @workbook.add_format(_params)
        write_blank(_row, _col, format)
      else
        if @cell_data_table[_row][_col].xf.nil?
          format = @workbook.add_format(_params)
          cell_data = @cell_data_table[_row][_col]
        else
          format = @workbook.add_format
          cell_data = @cell_data_table[_row][_col]
          format.copy(cell_data.xf)
          format.set_format_properties(_params)
        end
        # keep original value of cell
        value = if cell_data.is_a? FormulaCellData
                  "=#{cell_data.token}"
                elsif cell_data.is_a? FormulaArrayCellData
                  "{=#{cell_data.token}}"
                elsif cell_data.is_a? StringCellData
                  @workbook.shared_strings.string(cell_data.data[:sst_id])
                else
                  cell_data.data
                end
        write(_row, _col, value, format)
      end
    end

    #
    # :call-seq:
    #   update_range_format_with_params(row_first, col_first, row_last, col_last, format_params)
    #
    # Update formatting of cells in range to the specified row and column (zero indexed).
    #
    def update_range_format_with_params(row_first, col_first, row_last = nil, col_last = nil, params = nil)
      if (row_col_array = row_col_notation(row_first))
        _row_first, _col_first, _row_last, _col_last = row_col_array
        params = args[1..-1]
      else
        _row_first = row_first
        _col_first = col_first
        _row_last  = row_last
        _col_last  = col_last
        _params    = params
      end

      raise WriteXLSXInsufficientArgumentError if [_row_first, _col_first, _row_last, _col_last, _params].include?(nil)

      # Swap last row/col with first row/col as necessary
      _row_first, _row_last = _row_last, _row_first if _row_first > _row_last
      _col_first, _col_last = _col_last, _col_first if _col_first > _col_last

      # Check that column number is valid and store the max value
      check_dimensions(_row_last, _col_last)
      store_row_col_max_min_values(_row_last, _col_last)

      (_row_first.._row_last).each do |row|
        (_col_first.._col_last).each do |col|
          update_format_with_params(row, col, _params)
        end
      end
    end

    #
    # The outline_settings() method is used to control the appearance of
    # outlines in Excel.
    #
    def outline_settings(visible = 1, symbols_below = 1, symbols_right = 1, auto_style = 0)
      @outline_on    = visible
      @outline_below = symbols_below
      @outline_right = symbols_right
      @outline_style = auto_style

      @outline_changed = 1
    end

    #
    # Deprecated. This is a writeexcel method that is no longer required
    # by WriteXLSX. See below.
    #
    def store_formula(string)
      string.split(/(\$?[A-I]?[A-Z]\$?\d+)/)
    end

    #
    # :call-seq:
    #   write_url(row, column, url [ , format, label, tip ])
    #
    # Write a hyperlink to a URL in the cell specified by +row+ and +column+.
    # The hyperlink is comprised of two elements: the visible label and
    # the invisible link. The visible label is the same as the link unless
    # an alternative label is specified. The label parameter is optional.
    # The label is written using the {#write()}[#method-i-write] method. Therefore it is
    # possible to write strings, numbers or formulas as labels.
    #
    def write_url(row, col, url = nil, format = nil, str = nil, tip = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _url       = col
        _format    = url
        _str       = format
        _tip       = str
      else
        _row = row
        _col = col
        _url = url
        _format = format
        _str = str
        _tip = tip
      end
      _format, _str = _str, _format if _str.respond_to?(:xf_index) || !_format.respond_to?(:xf_index)
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _url].include?(nil)

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      hyperlink = Hyperlink.factory(_url, _str, _tip)
      store_hyperlink(_row, _col, hyperlink)

      raise "URL '#{url}' added but URL exceeds Excel's limit of 65,530 URLs per worksheet." if hyperlinks_count > 65_530

      # Add the default URL format.
      _format ||= @default_url_format

      # Write the hyperlink string.
      write_string(_row, _col, hyperlink.str, _format)
    end

    #
    # :call-seq:
    #   write_date_time (row, col, date_string [ , format ])
    #
    # Write a datetime string in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format as a
    # number representing an Excel date. format is optional.
    #
    def write_date_time(row, col, str, format = nil)
      # Check for a cell reference in A1 notation and substitute row and column
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _str       = col
        _format    = str
      else
        _row = row
        _col = col
        _str = str
        _format = format
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _str].include?(nil)

      # Check that row and col are valid and store max and min values
      check_dimensions(_row, _col)
      store_row_col_max_min_values(_row, _col)

      date_time = convert_date_time(_str)

      if date_time
        store_data_to_table(DateTimeCellData.new(date_time, _format), _row, _col)
      else
        # If the date isn't valid then write it as a string.
        write_string(_row, _col, _str, _format)
      end
    end

    #
    # :call-seq:
    #   insert_chart(row, column, chart [ , x, y, x_scale, y_scale ])
    #
    # This method can be used to insert a Chart object into a worksheet.
    # The Chart must be created by the add_chart() Workbook method and
    # it must have the embedded option set.
    #
    def insert_chart(row, col, chart = nil, *options)
      # Check for a cell reference in A1 notation and substitute row and column.
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _chart     = col
        _options   = [chart] + options
      else
        _row = row
        _col = col
        _chart = chart
        _options = options
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _chart].include?(nil)

      if _options.first.instance_of?(Hash)
        params = _options.first
        x_offset    = params[:x_offset]
        y_offset    = params[:y_offset]
        x_scale     = params[:x_scale]
        y_scale     = params[:y_scale]
        anchor      = params[:object_position]
        description = params[:description]
        decorative  = params[:decorative]
      else
        x_offset, y_offset, x_scale, y_scale, anchor = _options
      end
      x_offset ||= 0
      y_offset ||= 0
      x_scale  ||= 1
      y_scale  ||= 1
      anchor   ||= 1

      raise "Not a Chart object in insert_chart()" unless _chart.is_a?(Chart) || _chart.is_a?(Chartsheet)
      raise "Not a embedded style Chart object in insert_chart()" if _chart.respond_to?(:embedded) && _chart.embedded == 0

      if _chart.already_inserted? || (_chart.combined && _chart.combined.already_inserted?)
        raise "Chart cannot be inserted in a worksheet more than once"
      else
        _chart.already_inserted          = true
        _chart.combined.already_inserted = true if _chart.combined
      end

      # Use the values set with chart.set_size, if any.
      x_scale  = _chart.x_scale  if _chart.x_scale  != 1
      y_scale  = _chart.y_scale  if _chart.y_scale  != 1
      x_offset = _chart.x_offset if ptrue?(_chart.x_offset)
      y_offset = _chart.y_offset if ptrue?(_chart.y_offset)

      @charts << [
        _row,    _col,    _chart, x_offset,    y_offset,
        x_scale, y_scale, anchor, description, decorative
      ]
    end

    #
    # :call-seq:
    #   insert_image(row, column, filename, options)
    #
    def insert_image(row, col, image = nil, *options)
      # Check for a cell reference in A1 notation and substitute row and column.
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _image     = col
        _options   = [image] + options
      else
        _row = row
        _col = col
        _image = image
        _options = options
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col, _image].include?(nil)

      if _options.first.instance_of?(Hash)
        # Newer hash bashed options
        params      = _options.first
        x_offset    = params[:x_offset]
        y_offset    = params[:y_offset]
        x_scale     = params[:x_scale]
        y_scale     = params[:y_scale]
        anchor      = params[:object_position]
        url         = params[:url]
        tip         = params[:tip]
        description = params[:description]
        decorative  = params[:decorative]
      else
        x_offset, y_offset, x_scale, y_scale, anchor = _options
      end
      x_offset ||= 0
      y_offset ||= 0
      x_scale  ||= 1
      y_scale  ||= 1
      anchor   ||= 2

      @images << [
        _row, _col, _image, x_offset, y_offset,
        x_scale, y_scale, url, tip, anchor, description, decorative
      ]
    end

    #
    # :call-seq:
    #   repeat_formula(row, column, formula [ , format ])
    #
    # Deprecated. This is a writeexcel gem's method that is no longer
    # required by WriteXLSX.
    #
    def repeat_formula(row, col, formula, format, *pairs)
      # Check for a cell reference in A1 notation and substitute row and column.
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _formula   = col
        _format    = formula
        _pairs     = [format] + pairs
      else
        _row = row
        _col = col
        _formula = formula
        _format = format
        _pairs = pairs
      end
      raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)

      raise "Odd number of elements in pattern/replacement list" unless _pairs.size.even?
      raise "Not a valid formula" unless _formula.respond_to?(:to_ary)

      tokens  = _formula.join("\t").split("\t")
      raise "No tokens in formula" if tokens.empty?

      _value = nil
      if _pairs[-2] == 'result'
        _value = _pairs.pop
        _pairs.pop
      end
      until _pairs.empty?
        pattern = _pairs.shift
        replace = _pairs.shift

        tokens.each do |token|
          break if token.sub!(pattern, replace)
        end
      end
      _formula = tokens.join('')
      write_formula(_row, _col, _formula, _format, _value)
    end

    #
    # :call-seq:
    #   set_row(row [ , height, format, hidden, level, collapsed ])
    #
    # This method can be used to change the default properties of a row.
    # All parameters apart from +row+ are optional.
    #
    def set_row(*args)
      return unless args[0]

      row = args[0]
      height = args[1] || @default_height
      xf     = args[2]
      hidden = args[3] || 0
      level  = args[4] || 0
      collapsed = args[5] || 0

      # Get the default row height.
      default_height = @default_row_height

      # Use min col in check_dimensions. Default to 0 if undefined.
      min_col = @dim_colmin || 0

      # Check that row and col are valid and store max and min values.
      check_dimensions(row, min_col)
      store_row_col_max_min_values(row, min_col)

      height ||= default_height

      # If the height is 0 the row is hidden and the height is the default.
      if height == 0
        hidden = 1
        height = default_height
      end

      # Set the limits for the outline levels (0 <= x <= 7).
      level = 0 if level < 0
      level = 7 if level > 7

      @outline_row_level = level if level > @outline_row_level

      # Store the row properties.
      @set_rows[row] = [height, xf, hidden, level, collapsed]

      # Store the row change to allow optimisations.
      @row_size_changed = true

      # Store the row sizes for use when calculating image vertices.
      @row_sizes[row] = [height, hidden]
    end

    #
    # This method is used to set the height (in pixels) and the properties of the
    # row.
    #
    def set_row_pixels(*data)
      height = data[1]

      data[1] = pixels_to_height(height) if ptrue?(height)
      set_row(*data)
    end

    #
    # Set the default row properties
    #
    def set_default_row(height = nil, zero_height = nil)
      height      ||= @original_row_height
      zero_height ||= 0

      if height != @original_row_height
        @default_row_height = height

        # Store the row change to allow optimisations.
        @row_size_changed = 1
      end

      @default_row_zeroed = 1 if ptrue?(zero_height)
    end

    #
    # merge_range(first_row, first_col, last_row, last_col, string, format)
    #
    # Merge a range of cells. The first cell should contain the data and the
    # others should be blank. All cells should contain the same format.
    #
    def merge_range(*args)
      if (row_col_array = row_col_notation(args.first))
        row_first, col_first, row_last, col_last = row_col_array
        string, format, *extra_args = args[1..-1]
      else
        row_first, col_first, row_last, col_last,
        string, format, *extra_args = args
      end

      raise "Incorrect number of arguments" if [row_first, col_first, row_last, col_last, format].include?(nil)
      raise "Fifth parameter must be a format object" unless format.respond_to?(:xf_index)
      raise "Can't merge single cell" if row_first == row_last && col_first == col_last

      # Swap last row/col with first row/col as necessary
      row_first,  row_last = row_last,  row_first  if row_first > row_last
      col_first, col_last = col_last, col_first if col_first > col_last

      # Check that the data range is valid and store the max and min values.
      check_dimensions(row_first, col_first)
      check_dimensions(row_last,  col_last)
      store_row_col_max_min_values(row_first, col_first)
      store_row_col_max_min_values(row_last,  col_last)

      # Store the merge range.
      @merge << [row_first, col_first, row_last, col_last]

      # Write the first cell
      write(row_first, col_first, string, format, *extra_args)

      # Pad out the rest of the area with formatted blank cells.
      write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
    end

    #
    # Same as merge_range() above except the type of
    # {#write()}[#method-i-write] is specified.
    #
    def merge_range_type(type, *args)
      case type
      when 'array_formula', 'blank', 'rich_string'
        if (row_col_array = row_col_notation(args.first))
          row_first, col_first, row_last, col_last = row_col_array
          *others = args[1..-1]
        else
          row_first, col_first, row_last, col_last, *others = args
        end
        format = others.pop
      else
        if (row_col_array = row_col_notation(args.first))
          row_first, col_first, row_last, col_last = row_col_array
          token, format, *others = args[1..-1]
        else
          row_first, col_first, row_last, col_last,
          token, format, *others = args
        end
      end

      raise "Format object missing or in an incorrect position" unless format.respond_to?(:xf_index)
      raise "Can't merge single cell" if row_first == row_last && col_first == col_last

      # Swap last row/col with first row/col as necessary
      row_first, row_last = row_last, row_first if row_first > row_last
      col_first, col_last = col_last, col_first if col_first > col_last

      # Check that the data range is valid and store the max and min values.
      check_dimensions(row_first, col_first)
      check_dimensions(row_last,  col_last)
      store_row_col_max_min_values(row_first, col_first)
      store_row_col_max_min_values(row_last,  col_last)

      # Store the merge range.
      @merge << [row_first, col_first, row_last, col_last]

      # Write the first cell
      case type
      when 'blank', 'rich_string', 'array_formula'
        others << format
      end

      case type
      when 'string'
        write_string(row_first, col_first, token, format, *others)
      when 'number'
        write_number(row_first, col_first, token, format, *others)
      when 'blank'
        write_blank(row_first, col_first, *others)
      when 'date_time'
        write_date_time(row_first, col_first, token, format, *others)
      when 'rich_string'
        write_rich_string(row_first, col_first, *others)
      when 'url'
        write_url(row_first, col_first, token, format, *others)
      when 'formula'
        write_formula(row_first, col_first, token, format, *others)
      when 'array_formula'
        write_formula_array(row_first, col_first, *others)
      else
        raise "Unknown type '#{type}'"
      end

      # Pad out the rest of the area with formatted blank cells.
      write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
    end

    #
    # :call-seq:
    #   conditional_formatting(cell_or_cell_range, options)
    #
    # Conditional formatting is a feature of Excel which allows you to apply a
    # format to a cell or a range of cells based on a certain criteria.
    #
    def conditional_formatting(*args)
      cond_format = Package::ConditionalFormat.factory(self, *args)
      @cond_formats[cond_format.range] ||= []
      @cond_formats[cond_format.range] << cond_format
    end

    #
    # :call-seq:
    #    add_table(row1, col1, row2, col2, properties)
    #
    # Add an Excel table to a worksheet.
    #
    def add_table(*args)
      # Table count is a member of Workbook, global to all Worksheet.
      table = Package::Table.new(self, *args)
      @tables << table
      table
    end

    #
    # :call-seq:
    #    add_sparkline(properties)
    #
    # Add sparklines to the worksheet.
    #
    def add_sparkline(param)
      @sparklines << Sparkline.new(self, param, quote_sheetname(@name))
    end

    #
    # :call-seq:
    #   insert_button(row, col, properties)
    #
    # The insert_button() method can be used to insert an Excel form button
    # into a worksheet.
    #
    def insert_button(row, col, properties = nil)
      if (row_col_array = row_col_notation(row))
        _row, _col = row_col_array
        _properties = col
      else
        _row = row
        _col = col
        _properties = properties
      end
      @buttons_array << button_params(_row, _col, _properties)
      @has_vml = 1
    end

    #
    # :call-seq:
    #   data_validation(cell_or_cell_range, options)
    #
    # Data validation is a feature of Excel which allows you to restrict
    # the data that a users enters in a cell and to display help and
    # warning messages. It also allows you to restrict input to values
    # in a drop down list.
    #
    def data_validation(*args)
      validation = DataValidation.new(*args)
      @validations << validation unless validation.validate_none?
    end

    #
    # Set the option to hide gridlines on the screen and the printed page.
    #
    def hide_gridlines(option = 1)
      @screen_gridlines = (option != 2)

      @page_setup.hide_gridlines(option)
    end

    # Set the option to print the row and column headers on the printed page.
    #
    def print_row_col_headers(headers = true)
      @page_setup.print_row_col_headers(headers)
      # if headers
      #   @print_headers         = 1
      #   @page_setup.print_options_changed = 1
      # else
      #   @print_headers = 0
      # end
    end

    #
    # Set the option to hide the row and column headers in Excel.
    #
    def hide_row_col_headers
      @hide_row_col_headers = 1
    end

    #
    # The fit_to_pages() method is used to fit the printed area to a specific
    # number of pages both vertically and horizontally. If the printed area
    # exceeds the specified number of pages it will be scaled down to fit.
    # This guarantees that the printed area will always appear on the
    # specified number of pages even if the page size or margins change.
    #
    def fit_to_pages(width = 1, height = 1)
      @page_setup.fit_page   = true
      @page_setup.fit_width  = width
      @page_setup.fit_height = height
      @page_setup.page_setup_changed = true
    end

    #
    # :call-seq:
    #   autofilter(first_row, first_col, last_row, last_col)
    #
    # Set the autofilter area in the worksheet.
    #
    def autofilter(row1, col1 = nil, row2 = nil, col2 = nil)
      if (row_col_array = row_col_notation(row1))
        _row1, _col1, _row2, _col2 = row_col_array
      else
        _row1 = row1
        _col1 = col1
        _row2 = row2
        _col2 = col2
      end
      return if [_row1, _col1, _row2, _col2].include?(nil)

      # Reverse max and min values if necessary.
      _row1, _row2 = _row2, _row1 if _row2 < _row1
      _col1, _col2 = _col2, _col1 if _col2 < _col1

      @autofilter_area = convert_name_area(_row1, _col1, _row2, _col2)
      @autofilter_ref  = xl_range(_row1, _row2, _col1, _col2)
      @filter_range    = [_col1, _col2]

      # Store the filter cell positions for use in the autofit calculation.
      (_col1.._col2).each do |col|
        @filter_cells["#{_row1}:#{col}"] = 1
      end
    end

    #
    # Set the column filter criteria.
    #
    # The filter_column method can be used to filter columns in a autofilter
    # range based on simple conditions.
    #
    def filter_column(col, expression)
      raise "Must call autofilter before filter_column" unless @autofilter_area

      col = prepare_filter_column(col)

      tokens = extract_filter_tokens(expression)

      raise "Incorrect number of tokens in expression '#{expression}'" unless tokens.size == 3 || tokens.size == 7

      tokens = parse_filter_expression(expression, tokens)

      # Excel handles single or double custom filters as default filters. We need
      # to check for them and handle them accordingly.
      if tokens.size == 2 && tokens[0] == 2
        # Single equality.
        filter_column_list(col, tokens[1])
      elsif tokens.size == 5 && tokens[0] == 2 && tokens[2] == 1 && tokens[3] == 2
        # Double equality with "or" operator.
        filter_column_list(col, tokens[1], tokens[4])
      else
        # Non default custom filter.
        @filter_cols[col] = Array.new(tokens)
        @filter_type[col] = 0
      end

      @filter_on = 1
    end

    #
    # Set the column filter criteria in Excel 2007 list style.
    #
    def filter_column_list(col, *tokens)
      tokens.flatten!
      raise "Incorrect number of arguments to filter_column_list" if tokens.empty?
      raise "Must call autofilter before filter_column_list" unless @autofilter_area

      col = prepare_filter_column(col)

      @filter_cols[col] = tokens
      @filter_type[col] = 1           # Default style.
      @filter_on        = 1
    end

    #
    # Store the horizontal page breaks on a worksheet.
    #
    def set_h_pagebreaks(*args)
      breaks = args.collect do |brk|
        Array(brk)
      end.flatten
      @page_setup.hbreaks += breaks
    end

    #
    # Store the vertical page breaks on a worksheet.
    #
    def set_v_pagebreaks(*args)
      @page_setup.vbreaks += args
    end

    #
    # This method is used to make all cell comments visible when a worksheet
    # is opened.
    #
    def show_comments(visible = true)
      @comments_visible = visible
    end

    #
    # This method is used to set the default author of all cell comments.
    #
    def comments_author=(author)
      @comments_author = author || ''
    end

    # This method is deprecated. use comments_author=().
    def set_comments_author(author)
      put_deprecate_message("#{self}.set_comments_author")
      self.comments_author = author
    end

    def has_vml?  # :nodoc:
      @has_vml
    end

    def has_header_vml?  # :nodoc:
      @has_header_vml
    end

    def has_comments? # :nodoc:
      !@comments.empty?
    end

    def has_shapes?
      @has_shapes
    end

    def is_chartsheet? # :nodoc:
      !!@is_chartsheet
    end

    def set_external_vml_links(vml_drawing_id) # :nodoc:
      @external_vml_links <<
        ['/vmlDrawing', "../drawings/vmlDrawing#{vml_drawing_id}.vml"]
    end

    def set_external_comment_links(comment_id) # :nodoc:
      @external_comment_links <<
        ['/comments',   "../comments#{comment_id}.xml"]
    end

    #
    # Set up chart/drawings.
    #
    def prepare_chart(index, chart_id, drawing_id) # :nodoc:
      drawing_type = 1

      row,     col,     chart,  x_offset,    y_offset,
      x_scale, y_scale, anchor, description, decorative = @charts[index]

      chart.id = chart_id - 1
      x_scale ||= 0
      y_scale ||= 0

      # Use user specified dimensions, if any.
      width  = chart.width  if ptrue?(chart.width)
      height = chart.height if ptrue?(chart.height)

      width  = (0.5 + (width  * x_scale)).to_i
      height = (0.5 + (height * y_scale)).to_i

      dimensions = position_object_emus(col, row, x_offset, y_offset, width, height, anchor)

      # Set the chart name for the embedded object if it has been specified.
      name = chart.name

      # Create a Drawing object to use with worksheet unless one already exists.
      drawing = Drawing.new(drawing_type, dimensions, 0, 0, nil, anchor, drawing_rel_index, 0, nil, name, description, decorative)
      if drawings?
        @drawings.add_drawing_object(drawing)
      else
        @drawings = Drawings.new
        @drawings.add_drawing_object(drawing)
        @drawings.embedded = 1

        @external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
      end
      @drawing_links << ['/chart', "../charts/chart#{chart_id}.xml"]
    end

    #
    # Returns a range of data from the worksheet _table to be used in chart
    # cached data. Strings are returned as SST ids and decoded in the workbook.
    # Return nils for data that doesn't exist since Excel can chart series
    # with data missing.
    #
    def get_range_data(row_start, col_start, row_end, col_end) # :nodoc:
      # TODO. Check for worksheet limits.

      # Iterate through the table data.
      data = []
      (row_start..row_end).each do |row_num|
        # Store nil if row doesn't exist.
        unless @cell_data_table[row_num]
          data << nil
          next
        end

        (col_start..col_end).each do |col_num|
          cell = @cell_data_table[row_num][col_num]
          if cell
            data << cell.data
          else
            data << nil
          end
        end
      end

      data
    end

    #
    # Calculate the vertices that define the position of a graphical object within
    # the worksheet in pixels.
    #
    def position_object_pixels(col_start, row_start, x1, y1, width, height, anchor = nil) # :nodoc:
      # Adjust start column for negative offsets.
      while x1 < 0 && col_start > 0
        x1 += size_col(col_start - 1)
        col_start -= 1
      end

      # Adjust start row for negative offsets.
      while y1 < 0 && row_start > 0
        y1 += size_row(row_start - 1)
        row_start -= 1
      end

      # Ensure that the image isn't shifted off the page at top left.
      x1 = 0 if x1 < 0
      y1 = 0 if y1 < 0

      # Calculate the absolute x offset of the top-left vertex.
      x_abs = if @col_size_changed
                (0..col_start - 1).inject(0) { |sum, col| sum += size_col(col, anchor) }
              else
                # Optimisation for when the column widths haven't changed.
                @default_col_pixels * col_start
              end
      x_abs += x1

      # Calculate the absolute y offset of the top-left vertex.
      # Store the column change to allow optimisations.
      y_abs = if @row_size_changed
                (0..row_start - 1).inject(0) { |sum, row| sum += size_row(row, anchor) }
              else
                # Optimisation for when the row heights haven't changed.
                @default_row_pixels * row_start
              end
      y_abs += y1

      # Adjust start column for offsets that are greater than the col width.
      while x1 >= size_col(col_start, anchor)
        x1 -= size_col(col_start)
        col_start += 1
      end

      # Adjust start row for offsets that are greater than the row height.
      while y1 >= size_row(row_start, anchor)
        y1 -= size_row(row_start)
        row_start += 1
      end

      # Initialise end cell to the same as the start cell.
      col_end = col_start
      row_end = row_start

      # Only offset the image in the cell if the row/col isn't hidden.
      width  += x1 if size_col(col_start, anchor) > 0
      height += y1 if size_row(row_start, anchor) > 0

      # Subtract the underlying cell widths to find the end cell of the object.
      while width >= size_col(col_end, anchor)
        width -= size_col(col_end, anchor)
        col_end += 1
      end

      # Subtract the underlying cell heights to find the end cell of the object.
      while height >= size_row(row_end, anchor)
        height -= size_row(row_end, anchor)
        row_end += 1
      end

      # The end vertices are whatever is left from the width and height.
      x2 = width
      y2 = height

      [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs]
    end

    def comments_visible? # :nodoc:
      !!@comments_visible
    end

    def sorted_comments # :nodoc:
      @comments.sorted_comments
    end

    #
    # Write the cell value <v> element.
    #
    def write_cell_value(value = '') # :nodoc:
      return write_cell_formula('=NA()') if value.is_a?(Float) && value.nan?

      value ||= ''

      int_value = value.to_i
      value = int_value if value == int_value
      @writer.data_element('v', value)
    end

    #
    # Write the cell formula <f> element.
    #
    def write_cell_formula(formula = '') # :nodoc:
      @writer.data_element('f', formula)
    end

    #
    # Write the cell array formula <f> element.
    #
    def write_cell_array_formula(formula, range) # :nodoc:
      @writer.data_element(
        'f', formula,
        [
          %w[t array],
          ['ref', range]
        ]
      )
    end

    def date_1904? # :nodoc:
      @workbook.date_1904?
    end

    def excel2003_style? # :nodoc:
      @workbook.excel2003_style
    end

    #
    # Convert from an Excel internal colour index to a XML style #RRGGBB index
    # based on the default or user defined values in the Workbook palette.
    #
    def palette_color(index) # :nodoc:
      if index.to_s =~ /^#([0-9A-F]{6})$/i
        "FF#{::Regexp.last_match(1).upcase}"
      else
        "FF#{super(index)}"
      end
    end

    def buttons_data  # :nodoc:
      @buttons_array
    end

    def header_images_data  # :nodoc:
      @header_images_array
    end

    def external_links
      [
        @external_hyper_links,
        @external_drawing_links,
        @external_vml_links,
        @external_background_links,
        @external_table_links,
        @external_comment_links
      ].reject { |a| a.empty? }
    end

    def drawing_links
      [@drawing_links]
    end

    #
    # Turn the HoH that stores the comments into an array for easier handling
    # and set the external links for comments and buttons.
    #
    def prepare_vml_objects(vml_data_id, vml_shape_id, vml_drawing_id, comment_id)
      set_external_vml_links(vml_drawing_id)
      set_external_comment_links(comment_id) if has_comments?

      # The VML o:idmap data id contains a comma separated range when there is
      # more than one 1024 block of comments, like this: data="1,2".
      data = "#{vml_data_id}"
      (1..num_comments_block).each do |i|
        data += ",#{vml_data_id + i}"
      end
      @vml_data_id = data
      @vml_shape_id = vml_shape_id
    end

    #
    # Setup external linkage for VML header/footer images.
    #
    def prepare_header_vml_objects(vml_header_id, vml_drawing_id)
      @vml_header_id = vml_header_id
      @external_vml_links << ['/vmlDrawing', "../drawings/vmlDrawing#{vml_drawing_id}.vml"]
    end

    #
    # Set the table ids for the worksheet tables.
    #
    def prepare_tables(table_id, seen)
      if tables_count > 0
        id = table_id
        tables.each do |table|
          table.prepare(id)

          if seen[table.name]
            raise "error: invalid duplicate table name '#{table.name}' found."
          else
            seen[table.name] = 1
          end

          # Store the link used for the rels file.
          @external_table_links << ['/table', "../tables/table#{id}.xml"]
          id += 1
        end
      end
      tables_count || 0
    end

    def num_comments_block
      @comments.size / 1024
    end

    def tables_count
      @tables.size
    end

    def horizontal_dpi=(val)
      @page_setup.horizontal_dpi = val
    end

    def vertical_dpi=(val)
      @page_setup.vertical_dpi = val
    end

    #
    # set the vba name for the worksheet
    #
    def set_vba_name(vba_codename = nil)
      @vba_codename = vba_codename || @name
    end

    #
    # Ignore worksheet errors/warnings in user defined ranges.
    #
    def ignore_errors(ignores)
      # List of valid input parameters.
      valid_parameter_keys = %i[
        number_stored_as_text
        eval_error
        formula_differs
        formula_range
        formula_unlocked
        empty_cell_reference
        list_data_validation
        calculated_column
        two_digit_text_year
      ]

      raise "Unknown parameter '#{ignores.key - valid_parameter_keys}' in ignore_errors()." unless (ignores.keys - valid_parameter_keys).empty?

      @ignore_errors = ignores
    end

    def write_ext(url, &block)
      attributes = [
        ['xmlns:x14', "#{OFFICE_URL}spreadsheetml/2009/9/main"],
        ['uri',       url]
      ]
      @writer.tag_elements('ext', attributes, &block)
    end

    def write_sparkline_groups
      # Write the x14:sparklineGroups element.
      @writer.tag_elements('x14:sparklineGroups', sparkline_groups_attributes) do
        # Write the sparkline elements.
        @sparklines.reverse.each do |sparkline|
          sparkline.write_sparkline_group(@writer)
        end
      end
    end

    def has_dynamic_arrays?
      @has_dynamic_arrays
    end

    private

    #
    # Compare adjacent column information structures.
    #
    def compare_col_info(col_options, previous_options)
      if !col_options.width.nil? != !previous_options.width.nil?
        return nil
      end
      if col_options.width && previous_options.width &&
         col_options.width != previous_options.width
        return nil
      end

      if !col_options.format.nil? != !previous_options.format.nil?
        return nil
      end
      if col_options.format && previous_options.format &&
         col_options.format != previous_options.format
        return nil
      end

      return nil if col_options.hidden    != previous_options.hidden
      return nil if col_options.level     != previous_options.level
      return nil if col_options.collapsed != previous_options.collapsed

      true
    end

    #
    # Get the index used to address a drawing rel link.
    #
    def drawing_rel_index(target = nil)
      if !target
        # Undefined values for drawings like charts will always be unique.
        @drawing_rels_id += 1
      elsif ptrue?(@drawing_rels[target])
        @drawing_rels[target]
      else
        @drawing_rels_id += 1
        @drawing_rels[target] = @drawing_rels_id
      end
    end

    #
    # Get the index used to address a vml_drawing rel link.
    #
    def get_vml_drawing_rel_index(target)
      if @vml_drawing_rels[target]
        @vml_drawing_rels[target]
      else
        @vml_drawing_rels_id += 1
        @vml_drawing_rels[target] = @vml_drawing_rels_id
      end
    end

    def hyperlinks_count
      @hyperlinks.keys.inject(0) { |s, n| s += @hyperlinks[n].keys.size }
    end

    def store_hyperlink(row, col, hyperlink)
      @hyperlinks      ||= {}
      @hyperlinks[row] ||= {}
      @hyperlinks[row][col] = hyperlink
    end

    def cell_format_of_rich_string(rich_strings)
      # If the last arg is a format we use it as the cell format.
      rich_strings.pop if rich_strings[-1].respond_to?(:xf_index)
    end

    #
    # Convert the list of format, string tokens to pairs of (format, string)
    # except for the first string fragment which doesn't require a default
    # formatting run. Use the default for strings without a leading format.
    #
    def rich_strings_fragments(rich_strings) # :nodoc:
      # Create a temp format with the default font for unformatted fragments.
      default = Format.new(0)

      last = 'format'
      pos  = 0
      raw_string = ''

      fragments = []
      rich_strings.each do |token|
        if token.respond_to?(:xf_index)
          # Can't allow 2 formats in a row
          return nil if last == 'format' && pos > 0

          # Token is a format object. Add it to the fragment list.
          fragments << token
          last = 'format'
        else
          # Token is a string.
          if last == 'format'
            # If previous token was a format just add the string.
            fragments << token
          else
            # If previous token wasn't a format add one before the string.
            fragments << default << token
          end

          raw_string += token    # Keep track of actual string length.
          last = 'string'
        end
        pos += 1
      end
      [fragments, raw_string]
    end

    def xml_str_of_rich_string(fragments)
      # Create a temp XML::Writer object and use it to write the rich string
      # XML to a string.
      writer = Package::XMLWriterSimple.new

      # If the first token is a string start the <r> element.
      writer.start_tag('r') unless fragments[0].respond_to?(:xf_index)

      # Write the XML elements for the format string fragments.
      fragments.each do |token|
        if token.respond_to?(:xf_index)
          # Write the font run.
          writer.start_tag('r')
          token.write_font_rpr(writer, self)
        else
          # Write the string fragment part, with whitespace handling.
          attributes = []

          attributes << ['xml:space', 'preserve'] if token =~ /^\s/ || token =~ /\s$/
          writer.data_element('t', token, attributes)
          writer.end_tag('r')
        end
      end
      writer.string
    end

    # Pad out the rest of the area with formatted blank cells.
    def write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
      (row_first..row_last).each do |row|
        (col_first..col_last).each do |col|
          next if row == row_first && col == col_first

          write_blank(row, col, format)
        end
      end
    end

    #
    # Extract the tokens from the filter expression. The tokens are mainly non-
    # whitespace groups. The only tricky part is to extract string tokens that
    # contain whitespace and/or quoted double quotes (Excel's escaped quotes).
    #
    def extract_filter_tokens(expression = nil) # :nodoc:
      return [] unless expression

      tokens = []
      str = expression
      while str =~ /"(?:[^"]|"")*"|\S+/
        tokens << ::Regexp.last_match(0)
        str = $~.post_match
      end

      # Remove leading and trailing quotes and unescape other quotes
      tokens.map! do |token|
        token.sub!(/^"/, '')
        token.sub!(/"$/, '')
        token.gsub!('""', '"')

        # if token is number, convert to numeric.
        if token =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/
          token.to_f == token.to_i ? token.to_i : token.to_f
        else
          token
        end
      end

      tokens
    end

    #
    # Converts the tokens of a possibly conditional expression into 1 or 2
    # sub expressions for further parsing.
    #
    def parse_filter_expression(expression, tokens) # :nodoc:
      # The number of tokens will be either 3 (for 1 expression)
      # or 7 (for 2  expressions).
      #
      if tokens.size == 7
        conditional = tokens[3]
        if conditional =~ /^(and|&&)$/
          conditional = 0
        elsif conditional =~ /^(or|\|\|)$/
          conditional = 1
        else
          raise "Token '#{conditional}' is not a valid conditional " +
                "in filter expression '#{expression}'"
        end
        expression_1 = parse_filter_tokens(expression, tokens[0..2])
        expression_2 = parse_filter_tokens(expression, tokens[4..6])
        [expression_1, conditional, expression_2].flatten
      else
        parse_filter_tokens(expression, tokens)
      end
    end

    #
    # Parse the 3 tokens of a filter expression and return the operator and token.
    #
    def parse_filter_tokens(expression, tokens)     # :nodoc:
      operators = {
        '==' => 2,
        '='  => 2,
        '=~' => 2,
        'eq' => 2,

        '!=' => 5,
        '!~' => 5,
        'ne' => 5,
        '<>' => 5,

        '<'  => 1,
        '<=' => 3,
        '>'  => 4,
        '>=' => 6
      }

      operator = operators[tokens[1]]
      token    = tokens[2]

      # Special handling of "Top" filter expressions.
      if tokens[0] =~ /^top|bottom$/i
        value = tokens[1]
        if value.to_s =~ /\D/ or value.to_i < 1 or value.to_i > 500
          raise "The value '#{value}' in expression '#{expression}' " +
                "must be in the range 1 to 500"
        end
        token.downcase!
        if token != 'items' and token != '%'
          raise "The type '#{token}' in expression '#{expression}' " +
                "must be either 'items' or '%'"
        end

        operator = if tokens[0] =~ /^top$/i
                     30
                   else
                     32
                   end

        operator += 1 if tokens[2] == '%'

        token    = value
      end

      if !operator and tokens[0]
        raise "Token '#{tokens[1]}' is not a valid operator " +
              "in filter expression '#{expression}'"
      end

      # Special handling for Blanks/NonBlanks.
      if token.to_s =~ /^blanks|nonblanks$/i
        # Only allow Equals or NotEqual in this context.
        if operator != 2 and operator != 5
          raise "The operator '#{tokens[1]}' in expression '#{expression}' " +
                "is not valid in relation to Blanks/NonBlanks'"
        end

        token.downcase!

        # The operator should always be 2 (=) to flag a "simple" equality in
        # the binary record. Therefore we convert <> to =.
        if token == 'blanks'
          token = ' ' if operator == 5
        elsif operator == 5
          operator = 2
          token    = 'blanks'
        else
          operator = 5
          token    = ' '
        end
      end

      # if the string token contains an Excel match character then change the
      # operator type to indicate a non "simple" equality.
      operator = 22 if operator == 2 and token.to_s =~ /[*?]/

      [operator, token]
    end

    #
    # This is an internal method that is used to filter elements of the array of
    # pagebreaks used in the _store_hbreak() and _store_vbreak() methods. It:
    #   1. Removes duplicate entries from the list.
    #   2. Sorts the list.
    #   3. Removes 0 from the list if present.
    #
    def sort_pagebreaks(*args) # :nodoc:
      return [] if args.empty?

      breaks = args.uniq.sort
      breaks.delete(0)

      # The Excel 2007 specification says that the maximum number of page breaks
      # is 1026. However, in practice it is actually 1023.
      max_num_breaks = 1023
      if breaks.size > max_num_breaks
        breaks[0, max_num_breaks]
      else
        breaks
      end
    end

    #
    # Calculate the vertices that define the position of a graphical object within
    # the worksheet in EMUs.
    #
    def position_object_emus(col_start, row_start, x1, y1, width, height, anchor = nil) # :nodoc:
      col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs =
        position_object_pixels(col_start, row_start, x1, y1, width, height, anchor)

      # Convert the pixel values to EMUs. See above.
      x1    = (0.5 + (9_525 * x1)).to_i
      y1    = (0.5 + (9_525 * y1)).to_i
      x2    = (0.5 + (9_525 * x2)).to_i
      y2    = (0.5 + (9_525 * y2)).to_i
      x_abs = (0.5 + (9_525 * x_abs)).to_i
      y_abs = (0.5 + (9_525 * y_abs)).to_i

      [col_start, row_start, x1, y1, col_end, row_end, x2, y2, x_abs, y_abs]
    end

    #
    # Convert the width of a cell from user's units to pixels. Excel rounds the
    # column width to the nearest pixel. If the width hasn't been set by the user
    # we use the default value. A hidden column is treated as having a width of
    # zero unless it has the special "object_position" of 4 (size with cells).
    #
    def size_col(col, anchor = 0) # :nodoc:
      # Look up the cell value to see if it has been changed.
      if @col_info[col]
        width  = @col_info[col].width || @default_col_width
        hidden = @col_info[col].hidden

        # Convert to pixels.
        pixels = if hidden == 1 && anchor != 4
                   0
                 elsif width < 1
                   ((width * (MAX_DIGIT_WIDTH + PADDING)) + 0.5).to_i
                 else
                   ((width * MAX_DIGIT_WIDTH) + 0.5).to_i + PADDING
                 end
      else
        pixels = @default_col_pixels
      end
      pixels
    end

    #
    # Convert the height of a cell from user's units to pixels. If the height
    # hasn't been set by the user we use the default value. A hidden row is
    # treated as having a height of zero unless it has the special
    # "object_position" of 4 (size with cells).
    #
    def size_row(row, anchor = 0) # :nodoc:
      # Look up the cell value to see if it has been changed
      if @row_sizes[row]
        height, hidden = @row_sizes[row]

        pixels = if hidden == 1 && anchor != 4
                   0
                 else
                   (4 / 3.0 * height).to_i
                 end
      else
        pixels = (4 / 3.0 * @default_row_height).to_i
      end
      pixels
    end

    #
    # Convert the width of a cell from pixels to character units.
    #
    def pixels_to_width(pixels)
      max_digit_width = 7.0
      padding         = 5.0

      if pixels <= 12
        pixels / (max_digit_width + padding)
      else
        (pixels - padding) / max_digit_width
      end
    end

    #
    # Convert the height of a cell from pixels to character units.
    #
    def pixels_to_height(pixels)
      height = 0.75 * pixels
      height = height.to_i if (height - height.to_i).abs < 0.1
      height
    end

    #
    # Set up image/drawings.
    #
    def prepare_image(index, image_id, drawing_id, width, height, name, image_type, x_dpi = 96, y_dpi = 96, md5 = nil) # :nodoc:
      x_dpi ||= 96
      y_dpi ||= 96
      drawing_type = 2

      row, col, _image, x_offset, y_offset,
      x_scale, y_scale, url, tip, anchor, description, decorative = @images[index]

      width  *= x_scale
      height *= y_scale

      width  *= 96.0 / x_dpi
      height *= 96.0 / y_dpi

      dimensions = position_object_emus(col, row, x_offset, y_offset, width, height, anchor)

      # Convert from pixels to emus.
      width  = (0.5 + (width  * 9_525)).to_i
      height = (0.5 + (height * 9_525)).to_i

      # Create a Drawing object to use with worksheet unless one already exists.
      drawing = Drawing.new(drawing_type, dimensions, width, height, nil, anchor, 0, 0, tip, name, description, decorative)
      if drawings?
        drawings = @drawings
      else
        drawings = Drawings.new
        drawings.embedded = 1

        @drawings = drawings

        @external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
      end
      drawings.add_drawing_object(drawing)

      drawing.description = name unless description

      if url
        rel_type = '/hyperlink'
        target_mode = 'External'
        target = escape_url(url) if url =~ %r{^[fh]tt?ps?://} || url =~ /^mailto:/
        if url =~ /^external:/
          target = escape_url(url.sub(/^external:/, ''))

          # Additional escape not required in worksheet hyperlinks
          target = target.gsub("#", '%23')

          # Prefix absolute paths (not relative) with file:///
          target = if target =~ /^\w:/ || target =~ /^\\\\/
                     "file:///#{target}"
                   else
                     target.gsub("\\", '/')
                   end
        end

        if url =~ /^internal:/
          target      = url.sub(/^internal:/, '#')
          target_mode = nil
        end

        if target.length > 255
          raise <<"EOS"
Ignoring URL #{target} where link or anchor > 255 characters since it exceeds Excel's limit for URLS. See LIMITATIONS section of the WriteXLSX documentation.
EOS
        end

        @drawing_links << [rel_type, target, target_mode] if target && !@drawing_rels[url]
        drawing.url_rel_index = drawing_rel_index(url)
      end

      @drawing_links << ['/image', "../media/image#{image_id}.#{image_type}"] unless @drawing_rels[md5]
      drawing.rel_index = drawing_rel_index(md5)
    end
    public :prepare_image

    def prepare_header_image(image_id, width, height, name, image_type, position, x_dpi, y_dpi, md5)
      # Strip the extension from the filename.
      body = name.dup
      body[/\.[^.]+$/, 0] = ''

      @vml_drawing_links << ['/image', "../media/image#{image_id}.#{image_type}"] unless @vml_drawing_rels[md5]

      ref_id = get_vml_drawing_rel_index(md5)
      @header_images_array << [width, height, body, position, x_dpi, y_dpi, ref_id]
    end
    public :prepare_header_image

    #
    # Set the background image for the worksheet.
    #
    def set_background(image)
      raise "Couldn't locate #{image}: $!" unless File.exist?(image)

      @background_image = image
    end
    public :set_background

    #
    # Set up an image without a drawing object for the background image.
    #
    def prepare_background(image_id, image_type)
      @external_background_links <<
        ['/image', "../media/image#{image_id}.#{image_type}"]
    end
    public :prepare_background

    #
    # :call-seq:
    #   insert_shape(row, col, shape [ , x, y, x_scale, y_scale ])
    #
    # Insert a shape into the worksheet.
    #
    def insert_shape(
          row_start, column_start, shape = nil, x_offset = nil, y_offset = nil,
          x_scale = nil, y_scale = nil, anchor = nil
        )
      # Check for a cell reference in A1 notation and substitute row and column.
      if (row_col_array = row_col_notation(row_start))
        _row_start, _column_start = row_col_array
        _shape    = column_start
        _x_offset = shape
        _y_offset = x_offset
        _x_scale  = y_offset
        _y_scale  = x_scale
        _anchor   = y_scale
      else
        _row_start = row_start
        _column_start = column_start
        _shape = shape
        _x_offset = x_offset
        _y_offset = y_offset
        _x_scale = x_scale
        _y_scale = y_scale
        _anchor = anchor
      end
      raise "Insufficient arguments in insert_shape()" if [_row_start, _column_start, _shape].include?(nil)

      _shape.set_position(
        _row_start, _column_start, _x_offset, _y_offset,
        _x_scale, _y_scale, _anchor
      )
      # Assign a shape ID.
      while true
        id = _shape.id || 0
        used = @shape_hash[id]

        # Test if shape ID is already used. Otherwise assign a new one.
        if !used && id != 0
          break
        else
          @last_shape_id += 1
          _shape.id = @last_shape_id
        end
      end

      # Allow lookup of entry into shape array by shape ID.
      @shape_hash[_shape.id] = _shape.element = @shapes.size

      insert = if ptrue?(_shape.stencil)
                 # Insert a copy of the shape, not a reference so that the shape is
                 # used as a stencil. Previously stamped copies don't get modified
                 # if the stencil is modified.
                 _shape.dup
               else
                 _shape
               end

      # For connectors change x/y coords based on location of connected shapes.
      insert.auto_locate_connectors(@shapes, @shape_hash)

      # Insert a link to the shape on the list of shapes. Connection to
      # the parent shape is maintained.
      @shapes << insert
      insert
    end
    public :insert_shape

    #
    # Set up drawing shapes
    #
    def prepare_shape(index, drawing_id)
      shape = @shapes[index]

      # Create a Drawing object to use with worksheet unless one already exists.
      unless drawings?
        @drawings = Drawings.new
        @drawings.embedded = 1
        @external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
        @has_shapes = true
      end

      # Validate the he shape against various rules.
      shape.validate(index)
      shape.calc_position_emus(self)

      drawing_type = 3
      drawing = Drawing.new(
        drawing_type, shape.dimensions, shape.width_emu, shape.height_emu,
        shape, shape.anchor, drawing_rel_index, 0, shape.name, nil, 0
      )
      drawings.add_drawing_object(drawing)
    end
    public :prepare_shape

    #
    # This method handles the parameters passed to insert_button as well as
    # calculating the button object position and vertices.
    #
    def button_params(row, col, params)
      button = Writexlsx_kot::Package::Button.new

      button_number = 1 + @buttons_array.size

      # Set the button caption.
      caption = params[:caption] || "Button #{button_number}"

      button.font = { _caption: caption }

      # Set the macro name.
      button.macro = if params[:macro]
                       "[0]!#{params[:macro]}"
                     else
                       "[0]!Button#{button_number}_Click"
                     end

      # Set the alt text for the button.
      button.description = params[:description]

      # Ensure that a width and height have been set.
      default_width  = @default_col_pixels
      default_height = @default_row_pixels
      params[:width]  = default_width  unless params[:width]
      params[:height] = default_height unless params[:height]

      # Set the x/y offsets.
      params[:x_offset] = 0 unless params[:x_offset]
      params[:y_offset] = 0 unless params[:y_offset]

      # Scale the size of the button box if required.
      params[:width] = params[:width] * params[:x_scale] if params[:x_scale]
      params[:height] = params[:height] * params[:y_scale] if params[:y_scale]

      # Round the dimensions to the nearest pixel.
      params[:width]  = (0.5 + params[:width]).to_i
      params[:height] = (0.5 + params[:height]).to_i

      params[:start_row] = row
      params[:start_col] = col

      # Calculate the positions of button object.
      vertices = position_object_pixels(
        params[:start_col],
        params[:start_row],
        params[:x_offset],
        params[:y_offset],
        params[:width],
        params[:height]
      )

      # Add the width and height for VML.
      vertices << [params[:width], params[:height]]

      button.vertices = vertices

      button
    end

    #
    # Hash a worksheet password. Based on the algorithm in ECMA-376-4:2016,
    # Office Open XML File Foemats -- Transitional Migration Features,
    # Additional attributes for workbookProtection element (Part 1, §18.2.29).   #
    def encode_password(password) # :nodoc:
      hash = 0

      password.reverse.split("").each do |char|
        hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
        hash ^= char.ord
      end

      hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
      hash ^= password.length
      hash ^= 0xCE4B

      sprintf("%X", hash)
    end

    #
    # Write the <worksheet> element. This is the root element of Worksheet.
    #
    def write_worksheet_attributes # :nodoc:
      schema = 'http://schemas.openxmlformats.org/'
      attributes = [
        ['xmlns',    "#{schema}spreadsheetml/2006/main"],
        ['xmlns:r',  "#{schema}officeDocument/2006/relationships"]
      ]

      if @excel_version == 2010
        attributes << ['xmlns:mc',     "#{schema}markup-compatibility/2006"]
        attributes << ['xmlns:x14ac',  "#{OFFICE_URL}spreadsheetml/2009/9/ac"]
        attributes << ['mc:Ignorable', 'x14ac']
      end
      attributes
    end

    #
    # Write the <sheetPr> element for Sheet level properties.
    #
    def write_sheet_pr # :nodoc:
      return unless tab_outline_fit? || vba_codename? || filter_on?

      attributes = []
      attributes << ['codeName',   @vba_codename] if vba_codename?
      attributes << ['filterMode', 1]             if filter_on?

      if tab_outline_fit?
        @writer.tag_elements('sheetPr', attributes) do
          write_tab_color
          write_outline_pr
          write_page_set_up_pr
        end
      else
        @writer.empty_tag('sheetPr', attributes)
      end
    end

    def tab_outline_fit?
      tab_color? || outline_changed? || fit_page?
    end

    #
    # Write the <pageSetUpPr> element.
    #
    def write_page_set_up_pr # :nodoc:
      @writer.empty_tag('pageSetUpPr', [['fitToPage', 1]]) if fit_page?
    end

    # Write the <dimension> element. This specifies the range of cells in the
    # worksheet. As a special case, empty spreadsheets use 'A1' as a range.
    #
    def write_dimension # :nodoc:
      if !@dim_rowmin && !@dim_colmin
        # If the min dims are undefined then no dimensions have been set
        # and we use the default 'A1'.
        ref = 'A1'
      elsif !@dim_rowmin && @dim_colmin
        # If the row dims aren't set but the column dims are then they
        # have been changed via set_column().
        if @dim_colmin == @dim_colmax
          # The dimensions are a single cell and not a range.
          ref = xl_rowcol_to_cell(0, @dim_colmin)
        else
          # The dimensions are a cell range.
          cell_1 = xl_rowcol_to_cell(0, @dim_colmin)
          cell_2 = xl_rowcol_to_cell(0, @dim_colmax)
          ref = cell_1 + ':' + cell_2
        end
      elsif @dim_rowmin == @dim_rowmax && @dim_colmin == @dim_colmax
        # The dimensions are a single cell and not a range.
        ref = xl_rowcol_to_cell(@dim_rowmin, @dim_colmin)
      else
        # The dimensions are a cell range.
        cell_1 = xl_rowcol_to_cell(@dim_rowmin, @dim_colmin)
        cell_2 = xl_rowcol_to_cell(@dim_rowmax, @dim_colmax)
        ref = cell_1 + ':' + cell_2
      end
      @writer.empty_tag('dimension', [['ref', ref]])
    end

    #
    # Write the <sheetViews> element.
    #
    def write_sheet_views # :nodoc:
      @writer.tag_elements('sheetViews', []) { write_sheet_view }
    end

    def write_sheet_view # :nodoc:
      attributes = []
      # Hide screen gridlines if required.
      attributes << ['showGridLines', 0] unless @screen_gridlines

      # Hide the row/column headers.
      attributes << ['showRowColHeaders', 0] if ptrue?(@hide_row_col_headers)

      # Hide zeroes in cells.
      attributes << ['showZeros', 0] unless show_zeros?

      # Display worksheet right to left for Hebrew, Arabic and others.
      attributes << ['rightToLeft', 1] if @right_to_left

      # Show that the sheet tab is selected.
      attributes << ['tabSelected', 1] if @selected

      # Turn outlines off. Also required in the outlinePr element.
      attributes << ["showOutlineSymbols", 0] if @outline_on

      # Set the page view/layout mode if required.
      case @page_view
      when 1
        attributes << %w[view pageLayout]
      when 2
        attributes << %w[view pageBreakPreview]
      end

      # Set the first visible cell.
      attributes << ['topLeftCell', @top_left_cell] if ptrue?(@top_left_cell)

      # Set the zoom level.
      if @zoom != 100
        attributes << ['zoomScale', @zoom]

        if @page_view == 1
          attributes << ['zoomScalePageLayoutView', @zoom]
        elsif @page_view == 2
          attributes << ['zoomScaleSheetLayoutView', @zoom]
        elsif ptrue?(@zoom_scale_normal)
          attributes << ['zoomScaleNormal', @zoom]
        end
      end

      attributes << ['workbookViewId', 0]

      if @panes.empty? && @selections.empty?
        @writer.empty_tag('sheetView', attributes)
      else
        @writer.tag_elements('sheetView', attributes) do
          write_panes
          write_selections
        end
      end
    end

    #
    # Write the <selection> elements.
    #
    def write_selections # :nodoc:
      @selections.each { |selection| write_selection(*selection) }
    end

    #
    # Write the <selection> element.
    #
    def write_selection(pane, active_cell, sqref) # :nodoc:
      attributes  = []
      attributes << ['pane', pane]              if pane
      attributes << ['activeCell', active_cell] if active_cell
      attributes << ['sqref', sqref]            if sqref

      @writer.empty_tag('selection', attributes)
    end

    #
    # Write the <sheetFormatPr> element.
    #
    def write_sheet_format_pr # :nodoc:
      attributes = [
        ['defaultRowHeight', @default_row_height]
      ]
      attributes << ['customHeight', 1] if @default_row_height != @original_row_height

      attributes << ['zeroHeight', 1] if ptrue?(@default_row_zeroed)

      attributes << ['outlineLevelRow', @outline_row_level] if @outline_row_level > 0
      attributes << ['outlineLevelCol', @outline_col_level] if @outline_col_level > 0
      attributes << ['x14ac:dyDescent', '0.25'] if @excel_version == 2010
      @writer.empty_tag('sheetFormatPr', attributes)
    end

    #
    # Write the <cols> element and <col> sub elements.
    #
    def write_cols # :nodoc:
      # Exit unless some column have been formatted.
      return if @col_info.empty?

      @writer.tag_elements('cols') do
        # Use the first element of the column informatin structure to set
        # the initial/previous properties.
        first_col           = @col_info.keys.min
        last_col            = first_col
        previous_options    = @col_info[first_col]
        deleted_col         = first_col
        deleted_col_options = previous_options

        @col_info.delete(first_col)

        @col_info.keys.sort.each do |col|
          col_options = @col_info[col]

          # Check if the column number is contiguous with the previous
          # column and if the properties are the same.
          if (col == last_col + 1) &&
             compare_col_info(col_options, previous_options)
            last_col = col
          else
            # If not contiguous/equal then we write out the current range
            # of columns and start again.
            write_col_info([first_col, last_col, previous_options])
            first_col = col
            last_col  = first_col
            previous_options = col_options
          end
        end

        # We will exit the previous loop with one unhandled column range.
        write_col_info([first_col, last_col, previous_options])

        # Put back the deleted first column information structure:
        @col_info[deleted_col] = deleted_col_options
      end
    end

    #
    # Write the <col> element.
    #
    def write_col_info(args) # :nodoc:
      @writer.empty_tag('col', col_info_attributes(args))
    end

    def col_info_attributes(args)
      min       = args[0]           || 0 # First formatted column.
      max       = args[1]           || 0 # Last formatted column.
      width     = args[2].width          # Col width in user units.
      format    = args[2].format         # Format index.
      hidden    = args[2].hidden    || 0 # Hidden flag.
      level     = args[2].level     || 0 # Outline level.
      collapsed = args[2].collapsed || 0 # Outline Collapsed
      autofit   = args[2].autofit   || 0 # Best fit for autofit numbers.
      xf_index = format ? format.get_xf_index : 0

      custom_width = true
      custom_width = false if width.nil? && hidden == 0
      custom_width = false if width == 8.43

      width ||= hidden == 0 ? @default_col_width : 0

      # Convert column width from user units to character width.
      width = if width && width < 1
                (((width * (MAX_DIGIT_WIDTH + PADDING)) + 0.5).to_i / MAX_DIGIT_WIDTH.to_f * 256).to_i / 256.0
              else
                ((((width * MAX_DIGIT_WIDTH) + 0.5).to_i + PADDING).to_i / MAX_DIGIT_WIDTH.to_f * 256).to_i / 256.0
              end
      width = width.to_i if width - width.to_i == 0

      attributes = [
        ['min',   min + 1],
        ['max',   max + 1],
        ['width', width]
      ]

      attributes << ['style',        xf_index] if xf_index  != 0
      attributes << ['hidden',       1]        if hidden    != 0
      attributes << ['bestFit',      1]        if autofit   != 0
      attributes << ['customWidth',  1]        if custom_width
      attributes << ['outlineLevel', level]    if level     != 0
      attributes << ['collapsed',    1]        if collapsed != 0
      attributes
    end

    #
    # Write the <sheetData> element.
    #
    def write_sheet_data # :nodoc:
      if @dim_rowmin
        @writer.tag_elements('sheetData') { write_rows }
      else
        # If the dimensions aren't defined then there is no data to write.
        @writer.empty_tag('sheetData')
      end
    end

    #
    # Write out the worksheet data as a series of rows and cells.
    #
    def write_rows # :nodoc:
      calculate_spans

      (@dim_rowmin..@dim_rowmax).each do |row_num|
        # Skip row if it doesn't contain row formatting or cell data.
        next if not_contain_formatting_or_data?(row_num)

        span_index = row_num / 16
        span       = @row_spans[span_index]

        # Write the cells if the row contains data.
        if @cell_data_table[row_num]
          args = @set_rows[row_num] || []
          write_row_element(row_num, span, *args) do
            write_cell_column_dimension(row_num)
          end
        else
          # Row attributes only.
          write_empty_row(row_num, span, *(@set_rows[row_num]))
        end
      end
    end

    def not_contain_formatting_or_data?(row_num) # :nodoc:
      !@set_rows[row_num] && !@cell_data_table[row_num] && !@comments.has_comment_in_row?(row_num)
    end

    def write_cell_column_dimension(row_num)  # :nodoc:
      row = @cell_data_table[row_num]
      row_name = (row_num + 1).to_s
      (@dim_colmin..@dim_colmax).each do |col_num|
        if (cell = row[col_num])
          cell.write_cell(self, row_num, row_name, col_num)
        end
      end
    end

    #
    # Write the <row> element.
    #
    def write_row_element(*args, &block)  # :nodoc:
      @writer.tag_elements('row', row_attributes(args), &block)
    end

    #
    # Write and empty <row> element, i.e., attributes only, no cell data.
    #
    def write_empty_row(*args) # :nodoc:
      @writer.empty_tag('row', row_attributes(args))
    end

    def row_attributes(args)
      r, spans, height, format, hidden, level, collapsed, _empty_row = args
      height    ||= @default_row_height
      hidden    ||= 0
      level     ||= 0
      xf_index = format ? format.get_xf_index : 0

      attributes = [['r',  r + 1]]

      attributes << ['spans',        spans]    if spans
      attributes << ['s',            xf_index] if ptrue?(xf_index)
      attributes << ['customFormat', 1]        if ptrue?(format)
      attributes << ['ht',           height]   if height != @original_row_height
      attributes << ['hidden',       1]        if ptrue?(hidden)
      attributes << ['customHeight', 1]        if height != @original_row_height
      attributes << ['outlineLevel', level]    if ptrue?(level)
      attributes << ['collapsed',    1]        if ptrue?(collapsed)

      attributes << ['x14ac:dyDescent', '0.25'] if @excel_version == 2010
      attributes
    end

    #
    # Write the frozen or split <pane> elements.
    #
    def write_panes # :nodoc:
      return if @panes.empty?

      if @panes[4] == 2
        write_split_panes
      else
        write_freeze_panes(*@panes)
      end
    end

    #
    # Write the <pane> element for freeze panes.
    #
    def write_freeze_panes(row, col, top_row, left_col, type) # :nodoc:
      y_split       = row
      x_split       = col
      top_left_cell = xl_rowcol_to_cell(top_row, left_col)

      # Move user cell selection to the panes.
      unless @selections.empty?
        _dummy, active_cell, sqref = @selections[0]
        @selections = []
      end

      active_cell ||= nil
      sqref       ||= nil
      active_pane = set_active_pane_and_cell_selections(row, col, row, col, active_cell, sqref)

      # Set the pane type.
      state = if type == 0
                'frozen'
              elsif type == 1
                'frozenSplit'
              else
                'split'
              end

      attributes = []
      attributes << ['xSplit',      x_split] if x_split > 0
      attributes << ['ySplit',      y_split] if y_split > 0
      attributes << ['topLeftCell', top_left_cell]
      attributes << ['activePane',  active_pane]
      attributes << ['state',       state]

      @writer.empty_tag('pane', attributes)
    end

    #
    # Write the <pane> element for split panes.
    #
    # See also, implementers note for split_panes().
    #
    def write_split_panes # :nodoc:
      row, col, top_row, left_col = @panes
      has_selection = false
      y_split = row
      x_split = col

      # Move user cell selection to the panes.
      unless @selections.empty?
        _dummy, active_cell, sqref = @selections[0]
        @selections = []
        has_selection = true
      end

      # Convert the row and col to 1/20 twip units with padding.
      y_split = ((20 * y_split) + 300).to_i if y_split > 0
      x_split = calculate_x_split_width(x_split) if x_split > 0

      # For non-explicit topLeft definitions, estimate the cell offset based
      # on the pixels dimensions. This is only a workaround and doesn't take
      # adjusted cell dimensions into account.
      if top_row == row && left_col == col
        top_row  = (0.5 + ((y_split - 300) / 20 / 15)).to_i
        left_col = (0.5 + ((x_split - 390) / 20 / 3 * 4 / 64)).to_i
      end

      top_left_cell = xl_rowcol_to_cell(top_row, left_col)

      # If there is no selection set the active cell to the top left cell.
      unless has_selection
        active_cell = top_left_cell
        sqref       = top_left_cell
      end
      active_pane = set_active_pane_and_cell_selections(
        row, col, top_row, left_col, active_cell, sqref
      )

      attributes = []
      attributes << ['xSplit', x_split] if x_split > 0
      attributes << ['ySplit', y_split] if y_split > 0
      attributes << ['topLeftCell', top_left_cell]
      attributes << ['activePane', active_pane] if has_selection

      @writer.empty_tag('pane', attributes)
    end

    #
    # Convert column width from user units to pane split width.
    #
    def calculate_x_split_width(width) # :nodoc:
      # Convert to pixels.
      pixels = if width < 1
                 int((width * 12) + 0.5)
               else
                 ((width * MAX_DIGIT_WIDTH) + 0.5).to_i + PADDING
               end

      # Convert to points.
      points = pixels * 3 / 4

      # Convert to twips (twentieths of a point).
      twips = points * 20

      # Add offset/padding.
      twips + 390
    end

    #
    # Write the <sheetCalcPr> element for the worksheet calculation properties.
    #
    def write_sheet_calc_pr # :nodoc:
      @writer.empty_tag('sheetCalcPr', [['fullCalcOnLoad', 1]])
    end

    #
    # Write the <phoneticPr> element.
    #
    def write_phonetic_pr # :nodoc:
      attributes = [
        ['fontId', 0],
        %w[type noConversion]
      ]

      @writer.empty_tag('phoneticPr', attributes)
    end

    #
    # Write the <pageMargins> element.
    #
    def write_page_margins # :nodoc:
      @page_setup.write_page_margins(@writer)
    end

    #
    # Write the <pageSetup> element.
    #
    def write_page_setup # :nodoc:
      @page_setup.write_page_setup(@writer)
    end

    #
    # Write the <mergeCells> element.
    #
    def write_merge_cells # :nodoc:
      write_some_elements('mergeCells', @merge) do
        @merge.each { |merged_range| write_merge_cell(merged_range) }
      end
    end

    def write_some_elements(tag, container, &block)
      return if container.empty?

      @writer.tag_elements(tag, [['count', container.size]], &block)
    end

    #
    # Write the <mergeCell> element.
    #
    def write_merge_cell(merged_range) # :nodoc:
      row_min, col_min, row_max, col_max = merged_range

      # Convert the merge dimensions to a cell range.
      cell_1 = xl_rowcol_to_cell(row_min, col_min)
      cell_2 = xl_rowcol_to_cell(row_max, col_max)

      @writer.empty_tag('mergeCell', [['ref', "#{cell_1}:#{cell_2}"]])
    end

    #
    # Write the <printOptions> element.
    #
    def write_print_options # :nodoc:
      @page_setup.write_print_options(@writer)
    end

    #
    # Write the <headerFooter> element.
    #
    def write_header_footer # :nodoc:
      @page_setup.write_header_footer(@writer, excel2003_style?)
    end

    #
    # Write the <rowBreaks> element.
    #
    def write_row_breaks # :nodoc:
      write_breaks('rowBreaks')
    end

    #
    # Write the <colBreaks> element.
    #
    def write_col_breaks # :nodoc:
      write_breaks('colBreaks')
    end

    def write_breaks(tag) # :nodoc:
      case tag
      when 'rowBreaks'
        page_breaks = sort_pagebreaks(*@page_setup.hbreaks)
        max = 16383
      when 'colBreaks'
        page_breaks = sort_pagebreaks(*@page_setup.vbreaks)
        max = 1048575
      else
        raise "Invalid parameter '#{tag}' in write_breaks."
      end
      count = page_breaks.size

      return if page_breaks.empty?

      attributes = [
        ['count', count],
        ['manualBreakCount', count]
      ]

      @writer.tag_elements(tag, attributes) do
        page_breaks.each { |num| write_brk(num, max) }
      end
    end

    #
    # Write the <brk> element.
    #
    def write_brk(id, max) # :nodoc:
      attributes = [
        ['id',  id],
        ['max', max],
        ['man', 1]
      ]

      @writer.empty_tag('brk', attributes)
    end

    #
    # Write the <autoFilter> element.
    #
    def write_auto_filter # :nodoc:
      return unless autofilter_ref?

      attributes = [
        ['ref', @autofilter_ref]
      ]

      if filter_on?
        # Autofilter defined active filters.
        @writer.tag_elements('autoFilter', attributes) do
          write_autofilters
        end
      else
        # Autofilter defined without active filters.
        @writer.empty_tag('autoFilter', attributes)
      end
    end

    #
    # Function to iterate through the columns that form part of an autofilter
    # range and write the appropriate filters.
    #
    def write_autofilters # :nodoc:
      col1, col2 = @filter_range

      (col1..col2).each do |col|
        # Skip if column doesn't have an active filter.
        next unless @filter_cols[col]

        # Retrieve the filter tokens and write the autofilter records.
        tokens = @filter_cols[col]
        type   = @filter_type[col]

        # Filters are relative to first column in the autofilter.
        write_filter_column(col - col1, type, *tokens)
      end
    end

    #
    # Write the <filterColumn> element.
    #
    def write_filter_column(col_id, type, *filters) # :nodoc:
      @writer.tag_elements('filterColumn', [['colId', col_id]]) do
        if type == 1
          # Type == 1 is the new XLSX style filter.
          write_filters(*filters)
        else
          # Type == 0 is the classic "custom" filter.
          write_custom_filters(*filters)
        end
      end
    end

    #
    # Write the <filters> element.
    #
    def write_filters(*filters) # :nodoc:
      non_blanks = filters.reject { |filter| filter.to_s =~ /^blanks$/i }
      attributes = []

      attributes = [['blank', 1]] if filters != non_blanks

      if filters.size == 1 && non_blanks.empty?
        # Special case for blank cells only.
        @writer.empty_tag('filters', attributes)
      else
        # General case.
        @writer.tag_elements('filters', attributes) do
          non_blanks.sort.each { |filter| write_filter(filter) }
        end
      end
    end

    #
    # Write the <filter> element.
    #
    def write_filter(val) # :nodoc:
      @writer.empty_tag('filter', [['val', val]])
    end

    #
    # Write the <customFilters> element.
    #
    def write_custom_filters(*tokens) # :nodoc:
      if tokens.size == 2
        # One filter expression only.
        @writer.tag_elements('customFilters') { write_custom_filter(*tokens) }
      else
        # Two filter expressions.

        # Check if the "join" operand is "and" or "or".
        attributes = if tokens[2] == 0
                       [['and', 1]]
                     else
                       [['and', 0]]
                     end

        # Write the two custom filters.
        @writer.tag_elements('customFilters', attributes) do
          write_custom_filter(tokens[0], tokens[1])
          write_custom_filter(tokens[3], tokens[4])
        end
      end
    end

    #
    # Write the <customFilter> element.
    #
    def write_custom_filter(operator, val) # :nodoc:
      operators = {
        1  => 'lessThan',
        2  => 'equal',
        3  => 'lessThanOrEqual',
        4  => 'greaterThan',
        5  => 'notEqual',
        6  => 'greaterThanOrEqual',
        22 => 'equal'
      }

      # Convert the operator from a number to a descriptive string.
      if operators[operator]
        operator = operators[operator]
      else
        raise "Unknown operator = #{operator}\n"
      end

      # The 'equal' operator is the default attribute and isn't stored.
      attributes = []
      attributes << ['operator', operator] unless operator == 'equal'
      attributes << ['val', val]

      @writer.empty_tag('customFilter', attributes)
    end

    #
    # Process any sored hyperlinks in row/col order and write the <hyperlinks>
    # element. The attributes are different for internal and external links.
    #
    def write_hyperlinks # :nodoc:
      return unless @hyperlinks

      hlink_attributes = []
      @hyperlinks.keys.sort.each do |row_num|
        # Sort the hyperlinks into column order.
        col_nums = @hyperlinks[row_num].keys.sort
        # Iterate over the columns.
        col_nums.each do |col_num|
          # Get the link data for this cell.
          link = @hyperlinks[row_num][col_num]

          # If the cell isn't a string then we have to add the url as
          # the string to display
          if ptrue?(@cell_data_table)          &&
             ptrue?(@cell_data_table[row_num]) &&
             ptrue?(@cell_data_table[row_num][col_num]) && @cell_data_table[row_num][col_num].display_url_string?
            link.display_on
          end

          if link.respond_to?(:external_hyper_link)
            # External link with rel file relationship.
            @rel_count += 1
            # Links for use by the packager.
            @external_hyper_links << link.external_hyper_link
          end
          hlink_attributes << link.attributes(row_num, col_num, @rel_count)
        end
      end

      return if hlink_attributes.empty?

      # Write the hyperlink elements.
      @writer.tag_elements('hyperlinks') do
        hlink_attributes.each do |attributes|
          @writer.empty_tag('hyperlink', attributes)
        end
      end
    end

    #
    # Write the <tabColor> element.
    #
    def write_tab_color # :nodoc:
      return unless tab_color?

      @writer.empty_tag(
        'tabColor',
        [
          ['rgb', palette_color(@tab_color)]
        ]
      )
    end

    #
    # Write the <outlinePr> element.
    #
    def write_outline_pr
      return unless outline_changed?

      attributes = []
      attributes << ["applyStyles",  1] if @outline_style != 0
      attributes << ["summaryBelow", 0] if @outline_below == 0
      attributes << ["summaryRight", 0] if @outline_right == 0
      attributes << ["showOutlineSymbols", 0] if @outline_on == 0

      @writer.empty_tag('outlinePr', attributes)
    end

    #
    # Write the <sheetProtection> element.
    #
    def write_sheet_protection # :nodoc:
      return unless protect?

      attributes = []
      attributes << ["password",         @protect[:password]] if ptrue?(@protect[:password])
      attributes << ["sheet",            1] if ptrue?(@protect[:sheet])
      attributes << ["content",          1] if ptrue?(@protect[:content])
      attributes << ["objects",          1] unless ptrue?(@protect[:objects])
      attributes << ["scenarios",        1] unless ptrue?(@protect[:scenarios])
      attributes << ["formatCells",      0] if ptrue?(@protect[:format_cells])
      attributes << ["formatColumns",    0] if ptrue?(@protect[:format_columns])
      attributes << ["formatRows",       0] if ptrue?(@protect[:format_rows])
      attributes << ["insertColumns",    0] if ptrue?(@protect[:insert_columns])
      attributes << ["insertRows",       0] if ptrue?(@protect[:insert_rows])
      attributes << ["insertHyperlinks", 0] if ptrue?(@protect[:insert_hyperlinks])
      attributes << ["deleteColumns",    0] if ptrue?(@protect[:delete_columns])
      attributes << ["deleteRows",       0] if ptrue?(@protect[:delete_rows])

      attributes << ["selectLockedCells", 1] unless ptrue?(@protect[:select_locked_cells])

      attributes << ["sort",        0] if ptrue?(@protect[:sort])
      attributes << ["autoFilter",  0] if ptrue?(@protect[:autofilter])
      attributes << ["pivotTables", 0] if ptrue?(@protect[:pivot_tables])

      attributes << ["selectUnlockedCells", 1] unless ptrue?(@protect[:select_unlocked_cells])

      @writer.empty_tag('sheetProtection', attributes)
    end

    #
    # Write the <protectedRanges> element.
    #
    def write_protected_ranges
      return if @num_protected_ranges == 0

      @writer.tag_elements('protectedRanges') do
        @protected_ranges.each do |protected_range|
          write_protected_range(*protected_range)
        end
      end
    end

    #
    # Write the <protectedRange> element.
    #
    def write_protected_range(sqref, name, password)
      attributes = []

      attributes << ['password', password] if password
      attributes << ['sqref',    sqref]
      attributes << ['name',     name]

      @writer.empty_tag('protectedRange', attributes)
    end

    #
    # Write the <drawing> elements.
    #
    def write_drawings # :nodoc:
      increment_rel_id_and_write_r_id('drawing') if drawings?
    end

    #
    # Write the <legacyDrawing> element.
    #
    def write_legacy_drawing # :nodoc:
      increment_rel_id_and_write_r_id('legacyDrawing') if has_vml?
    end

    #
    # Write the <legacyDrawingHF> element.
    #
    def write_legacy_drawing_hf # :nodoc:
      return unless has_header_vml?

      # Increment the relationship id for any drawings or comments.
      @rel_count += 1

      attributes = [['r:id', "rId#{@rel_count}"]]
      @writer.empty_tag('legacyDrawingHF', attributes)
    end

    #
    # Write the <picture> element.
    #
    def write_picture
      return unless ptrue?(@background_image)

      # Increment the relationship id.
      @rel_count += 1
      id = @rel_count

      attributes = [['r:id', "rId#{id}"]]

      @writer.empty_tag('picture', attributes)
    end

    #
    # Write the underline font element.
    #
    def write_underline(writer, underline) # :nodoc:
      writer.empty_tag('u', underline_attributes(underline))
    end

    #
    # Write the <tableParts> element.
    #
    def write_table_parts
      return if @tables.empty?

      @writer.tag_elements('tableParts', [['count', tables_count]]) do
        tables_count.times { increment_rel_id_and_write_r_id('tablePart') }
      end
    end

    #
    # Write the <tablePart> element.
    #
    def write_table_part(id)
      @writer.empty_tag('tablePart', [r_id_attributes(id)])
    end

    def increment_rel_id_and_write_r_id(tag)
      @rel_count += 1
      write_r_id(tag, @rel_count)
    end

    def write_r_id(tag, id)
      @writer.empty_tag(tag, [r_id_attributes(id)])
    end

    #
    # Write the <extLst> element for data bars and sparklines.
    #
    def write_ext_list  # :nodoc:
      return if @data_bars_2010.empty? && @sparklines.empty?

      @writer.tag_elements('extLst') do
        write_ext_list_data_bars  if @data_bars_2010.size > 0
        write_ext_list_sparklines if @sparklines.size > 0
      end
    end

    #
    # Write the Excel 2010 data_bar subelements.
    #
    def write_ext_list_data_bars
      # Write the ext element.
      write_ext('{78C0D931-6437-407d-A8EE-F0AAD7539E65}') do
        @writer.tag_elements('x14:conditionalFormattings') do
          # Write each of the Excel 2010 conditional formatting data bar elements.
          @data_bars_2010.each do |data_bar|
            # Write the x14:conditionalFormatting element.
            write_conditional_formatting_2010(data_bar)
          end
        end
      end
    end

    #
    # Write the <x14:conditionalFormatting> element.
    #
    def write_conditional_formatting_2010(data_bar)
      xmlns_xm = 'http://schemas.microsoft.com/office/excel/2006/main'

      attributes = [['xmlns:xm', xmlns_xm]]

      @writer.tag_elements('x14:conditionalFormatting', attributes) do
        # Write the '<x14:cfRule element.
        write_x14_cf_rule(data_bar)

        # Write the x14:dataBar element.
        write_x14_data_bar(data_bar)

        # Write the x14 max and min data bars.
        write_x14_cfvo(data_bar[:x14_min_type], data_bar[:min_value])
        write_x14_cfvo(data_bar[:x14_max_type], data_bar[:max_value])

        # Write the x14:borderColor element.
        write_x14_border_color(data_bar[:bar_border_color]) unless ptrue?(data_bar[:bar_no_border])

        # Write the x14:negativeFillColor element.
        write_x14_negative_fill_color(data_bar[:bar_negative_color]) unless ptrue?(data_bar[:bar_negative_color_same])

        # Write the x14:negativeBorderColor element.
        if !ptrue?(data_bar[:bar_no_border]) &&
           !ptrue?(data_bar[:bar_negative_border_color_same])
          write_x14_negative_border_color(
            data_bar[:bar_negative_border_color]
          )
        end

        # Write the x14:axisColor element.
        write_x14_axis_color(data_bar[:bar_axis_color]) if data_bar[:bar_axis_position] != 'none'

        # Write closing elements.
        @writer.end_tag('x14:dataBar')
        @writer.end_tag('x14:cfRule')

        # Add the conditional format range.
        @writer.data_element('xm:sqref', data_bar[:range])
      end
    end

    #
    # Write the <cfvo> element.
    #
    def write_x14_cfvo(type, value)
      attributes = [['type', type]]

      if %w[min max autoMin autoMax].include?(type)
        @writer.empty_tag('x14:cfvo', attributes)
      else
        @writer.tag_elements('x14:cfvo', attributes) do
          @writer.data_element('xm:f', value)
        end
      end
    end

    #
    # Write the <'<x14:cfRule> element.
    #
    def write_x14_cf_rule(data_bar)
      type = 'dataBar'
      id   = data_bar[:guid]

      attributes = [
        ['type', type],
        ['id',   id]
      ]

      @writer.start_tag('x14:cfRule', attributes)
    end

    #
    # Write the <x14:dataBar> element.
    #
    def write_x14_data_bar(data_bar)
      min_length = 0
      max_length = 100

      attributes = [
        ['minLength', min_length],
        ['maxLength', max_length]
      ]

      attributes << ['border',   1] unless ptrue?(data_bar[:bar_no_border])
      attributes << ['gradient', 0] if ptrue?(data_bar[:bar_solid])

      attributes << %w[direction leftToRight] if data_bar[:bar_direction] == 'left'
      attributes << %w[direction rightToLeft] if data_bar[:bar_direction] == 'right'

      attributes << ['negativeBarColorSameAsPositive', 1] if ptrue?(data_bar[:bar_negative_color_same])

      if !ptrue?(data_bar[:bar_no_border]) &&
         !ptrue?(data_bar[:bar_negative_border_color_same])
        attributes << ['negativeBarBorderColorSameAsPositive', 0]
      end

      attributes << %w[axisPosition middle] if data_bar[:bar_axis_position] == 'middle'

      attributes << %w[axisPosition none] if data_bar[:bar_axis_position] == 'none'

      @writer.start_tag('x14:dataBar', attributes)
    end

    #
    # Write the <x14:borderColor> element.
    #
    def write_x14_border_color(rgb)
      attributes = [['rgb', rgb]]

      @writer.empty_tag('x14:borderColor', attributes)
    end

    #
    # Write the <x14:negativeFillColor> element.
    #
    def write_x14_negative_fill_color(rgb)
      attributes = [['rgb', rgb]]

      @writer.empty_tag('x14:negativeFillColor', attributes)
    end

    #
    # Write the <x14:negativeBorderColor> element.
    #
    def write_x14_negative_border_color(rgb)
      attributes = [['rgb', rgb]]

      @writer.empty_tag('x14:negativeBorderColor', attributes)
    end

    #
    # Write the <x14:axisColor> element.
    #
    def write_x14_axis_color(rgb)
      attributes = [['rgb', rgb]]

      @writer.empty_tag('x14:axisColor', attributes)
    end

    #
    # Write the sparkline subelements.
    #
    def write_ext_list_sparklines
      # Write the ext element.
      write_ext('{05C60535-1F16-4fd2-B633-F4F36F0B64E0}') do
        # Write the x14:sparklineGroups element.
        write_sparkline_groups
      end
    end

    #
    # Write the <x14:sparklines> element and <x14:sparkline> subelements.
    #
    def write_sparklines(sparkline)
      # Write the sparkline elements.
      @writer.tag_elements('x14:sparklines') do
        (0..sparkline[:count] - 1).each do |i|
          range    = sparkline[:ranges][i]
          location = sparkline[:locations][i]

          @writer.tag_elements('x14:sparkline') do
            @writer.data_element('xm:f', range)
            @writer.data_element('xm:sqref', location)
          end
        end
      end
    end

    def sparkline_groups_attributes  # :nodoc:
      [
        ['xmlns:xm', "#{OFFICE_URL}excel/2006/main"]
      ]
    end

    #
    # Write the <dataValidations> element.
    #
    def write_data_validations # :nodoc:
      write_some_elements('dataValidations', @validations) do
        @validations.each { |validation| validation.write_data_validation(@writer) }
      end
    end

    #
    # Write the Worksheet conditional formats.
    #
    def write_conditional_formats  # :nodoc:
      @cond_formats.keys.sort.each do |range|
        write_conditional_formatting(range, @cond_formats[range])
      end
    end

    #
    # Write the <conditionalFormatting> element.
    #
    def write_conditional_formatting(range, cond_formats) # :nodoc:
      @writer.tag_elements('conditionalFormatting', [['sqref', range]]) do
        cond_formats.each { |cond_format| cond_format.write_cf_rule }
      end
    end

    def store_data_to_table(cell_data, row, col) # :nodoc:
      if @cell_data_table[row]
        @cell_data_table[row][col] = cell_data
      else
        @cell_data_table[row] = []
        @cell_data_table[row][col] = cell_data
      end
    end

    def store_row_col_max_min_values(row, col)
      store_row_max_min_values(row)
      store_col_max_min_values(col)
    end

    #
    # Calculate the "spans" attribute of the <row> tag. This is an XLSX
    # optimisation and isn't strictly required. However, it makes comparing
    # files easier.
    #
    def calculate_spans # :nodoc:
      span_min = nil
      span_max = 0
      spans = []

      (@dim_rowmin..@dim_rowmax).each do |row_num|
        span_min, span_max = calc_spans(@cell_data_table, row_num, span_min, span_max) if @cell_data_table[row_num]

        # Calculate spans for comments.
        span_min, span_max = calc_spans(@comments, row_num, span_min, span_max) if @comments[row_num]

        next unless ((row_num + 1) % 16 == 0) || (row_num == @dim_rowmax)

        span_index = row_num / 16
        next unless span_min

        span_min += 1
        span_max += 1
        spans[span_index] = "#{span_min}:#{span_max}"
        span_min = nil
      end

      @row_spans = spans
    end

    def calc_spans(data, row_num, span_min, span_max)
      (@dim_colmin..@dim_colmax).each do |col_num|
        if data[row_num][col_num]
          if span_min
            span_min = col_num if col_num < span_min
            span_max = col_num if col_num > span_max
          else
            span_min = col_num
            span_max = col_num
          end
        end
      end
      [span_min, span_max]
    end

    #
    # Add a string to the shared string table, if it isn't already there, and
    # return the string index.
    #
    def shared_string_index(str) # :nodoc:
      @workbook.shared_string_index(str)
    end

    #
    # convert_name_area(first_row, first_col, last_row, last_col)
    #
    # Convert zero indexed rows and columns to the format required by worksheet
    # named ranges, eg, "Sheet1!$A$1:$C$13".
    #
    def convert_name_area(row_num_1, col_num_1, row_num_2, col_num_2) # :nodoc:
      range1       = ''
      range2       = ''
      row_col_only = false

      # Convert to A1 notation.
      col_char_1 = xl_col_to_name(col_num_1, 1)
      col_char_2 = xl_col_to_name(col_num_2, 1)
      row_char_1 = "$#{row_num_1 + 1}"
      row_char_2 = "$#{row_num_2 + 1}"

      # We need to handle some special cases that refer to rows or columns only.
      if row_num_1 == 0 and row_num_2 == ROW_MAX - 1
        range1       = col_char_1
        range2       = col_char_2
        row_col_only = true
      elsif col_num_1 == 0 and col_num_2 == COL_MAX - 1
        range1       = row_char_1
        range2       = row_char_2
        row_col_only = true
      else
        range1 = col_char_1 + row_char_1
        range2 = col_char_2 + row_char_2
      end

      # A repeated range is only written once (if it isn't a special case).
      area = if range1 == range2 && !row_col_only
               range1
             else
               "#{range1}:#{range2}"
             end

      # Build up the print area range "Sheet1!$A$1:$C$13".
      "#{quote_sheetname(@name)}!#{area}"
    end

    def fit_page? # :nodoc:
      @page_setup.fit_page
    end

    def filter_on? # :nodoc:
      ptrue?(@filter_on)
    end

    def tab_color? # :nodoc:
      ptrue?(@tab_color)
    end

    def outline_changed?
      ptrue?(@outline_changed)
    end

    def vba_codename?
      ptrue?(@vba_codename)
    end

    def zoom_scale_normal? # :nodoc:
      ptrue?(@zoom_scale_normal)
    end

    def right_to_left? # :nodoc:
      !!@right_to_left
    end

    def show_zeros? # :nodoc:
      !!@show_zeros
    end

    def protect? # :nodoc:
      !!@protect
    end

    def autofilter_ref? # :nodoc:
      !!@autofilter_ref
    end

    def drawings? # :nodoc:
      !!@drawings
    end

    def remove_white_space(margin) # :nodoc:
      if margin.respond_to?(:gsub)
        margin.gsub(/[^\d.]/, '')
      else
        margin
      end
    end

    def set_active_pane_and_cell_selections(row, col, top_row, left_col, active_cell, sqref) # :nodoc:
      if row > 0 && col > 0
        active_pane = 'bottomRight'
        row_cell = xl_rowcol_to_cell(top_row, 0)
        col_cell = xl_rowcol_to_cell(0, left_col)

        @selections <<
          ['topRight',    col_cell,    col_cell] <<
          ['bottomLeft',  row_cell,    row_cell] <<
          ['bottomRight', active_cell, sqref]
      elsif col > 0
        active_pane = 'topRight'
        @selections << ['topRight', active_cell, sqref]
      else
        active_pane = 'bottomLeft'
        @selections << ['bottomLeft', active_cell, sqref]
      end
      active_pane
    end

    def prepare_filter_column(col) # :nodoc:
      # Check for a column reference in A1 notation and substitute.
      if col.to_s =~ /^\D/
        col_letter = col

        # Convert col ref to a cell ref and then to a col number.
        _dummy, col = substitute_cellref("#{col}1")
        raise "Invalid column '#{col_letter}'" if col >= COL_MAX
      end

      col_first, col_last = @filter_range

      # Reject column if it is outside filter range.
      raise "Column '#{col}' outside autofilter column range (#{col_first} .. #{col_last})" if col < col_first or col > col_last

      col
    end

    #
    # Write the <ignoredErrors> element.
    #
    def write_ignored_errors
      return unless @ignore_errors

      ignore = @ignore_errors

      @writer.tag_elements('ignoredErrors') do
        {
          number_stored_as_text: 'numberStoredAsText',
          eval_error:            'evalError',
          formula_differs:       'formula',
          formula_range:         'formulaRange',
          formula_unlocked:      'unlockedFormula',
          empty_cell_reference:  'emptyCellReference',
          list_data_validation:  'listDataValidation',
          calculated_column:     'calculatedColumn',
          two_digit_text_year:   'twoDigitTextYear'
        }.each do |key, value|
          write_ignored_error(value, ignore[key]) if ignore[key]
        end
      end
    end

    #
    # Write the <ignoredError> element.
    #
    def write_ignored_error(type, sqref)
      attributes = [
        ['sqref', sqref],
        [type, 1]
      ]

      @writer.empty_tag('ignoredError', attributes)
    end
  end
	class Colors
    COLORS = {
      aqua:      0x0F,
      cyan:      0x0F,
      black:     0x08,
      blue:      0x0C,
      brown:     0x10,
      magenta:   0x0E,
      fuchsia:   0x0E,
      gray:      0x17,
      grey:      0x17,
      green:     0x11,
      lime:      0x0B,
      navy:      0x12,
      orange:    0x35,
      pink:      0x21,
      purple:    0x14,
      red:       0x0A,
      silver:    0x16,
      white:     0x09,
      yellow:    0x0D,
      automatic: 0x40
    }   # :nodoc:

    ###############################################################################
    #
    # get_color(colour)
    #
    # Used in conjunction with the set_xxx_color methods to convert a color
    # string into a number. Color range is 0..63 but we will restrict it
    # to 8..63 to comply with Gnumeric. Colors 0..7 are repeated in 8..15.
    #
    def color(color_code = nil) # :nodoc:
      if color_code.respond_to?(:to_int) && color_code.respond_to?(:+)
        # the default color if arg is outside range,
        if color_code < 0 || 63 < color_code
          0x7FFF
        # or an index < 8 mapped into the correct range,
        elsif color_code < 8
          (color_code + 8).to_i
        # or an integer in the valid range
        else
          color_code.to_i
        end
      elsif color_code.respond_to?(:to_sym)
        color_code = color_code.downcase.to_sym if color_code.respond_to?(:to_str)
        COLORS[color_code] || 0x7FFF
      else
        0x7FFF
      end
    end

    def inspect
      to_s
    end
  end  # class Colors
	class Format
    include Writexlsx_kot::Utility

    attr_reader :xf_index, :dxf_index, :num_format                                                 # :nodoc:
    attr_reader :underline, :font_script, :size, :theme, :font, :font_family, :hyperlink, :xf_id   # :nodoc:
    attr_reader :diag_type, :diag_color, :font_only, :color_indexed                        # :nodoc:
    attr_reader :left, :left_color, :right, :right_color, :top, :top_color, :bottom, :bottom_color # :nodoc:
    attr_reader :font_scheme                                                                       # :nodoc:
    attr_accessor :quote_prefix, :num_format_index, :border_index, :font_index                                    # :nodoc:
    attr_accessor :fill_index, :font_condense, :font_extend, :diag_border                          # :nodoc:
    attr_accessor :bg_color, :fg_color, :pattern                                                   # :nodoc:

    attr_accessor :dxf_bg_color, :dxf_fg_color                                                     # :nodoc:
    attr_reader :rotation, :bold, :italic, :font_strikeout                                         # :nodoc:

    def initialize(formats, params = {})   # :nodoc:
      @formats = formats

      @xf_index       = nil
      @dxf_index      = nil

      @num_format = 'General'
      @num_format_index = 0
      @font_index     = 0
      @font           = 'Calibri'
      @size           = 11
      @bold           = 0
      @italic         = 0
      @color          = 0x0
      @underline      = 0
      @font_strikeout = 0
      @font_outline   = 0
      @font_shadow    = 0
      @font_script    = 0
      @font_family    = 2
      @font_charset   = 0
      @font_scheme    = 'minor'
      @font_condense  = 0
      @font_extend    = 0
      @theme          = 0
      @hyperlink      = 0
      @xf_id          = 0

      @hidden         = 0
      @locked         = 1

      @text_h_align   = 0
      @text_wrap      = 0
      @text_v_align   = 0
      @text_justlast  = 0
      @rotation       = 0

      @fg_color       = 0x00
      @bg_color       = 0x00
      @pattern        = 0
      @fill_index     = 0
      @fill_count     = 0

      @border_index   = 0
      @border_count   = 0

      @bottom         = 0
      @bottom_color   = 0x0
      @diag_border    = 0
      @diag_color     = 0x0
      @diag_type      = 0
      @left           = 0
      @left_color     = 0x0
      @right          = 0
      @right_color    = 0x0
      @top            = 0
      @top_color      = 0x0

      @indent         = 0
      @shrink         = 0
      @merge_range    = 0
      @reading_order  = 0
      @just_distrib   = 0
      @color_indexed  = 0
      @font_only      = 0
      @quote_prefix   = 0

      set_format_properties(params) unless params.empty?
    end

    #
    # Copy the attributes of another Format object.
    #
    def copy(other)
      reserve = %i[
        xf_index
        dxf_index
        xdf_format_indices
        palette
      ]
      (instance_variables - reserve).each do |v|
        instance_variable_set(v, other.instance_variable_get(v))
      end
    end

    #
    # :call-seq:
    #    set_format_properties( :bold => 1 [, :color => 'red'..] )
    #    set_format_properties( font [, shade, ..])
    #    set_format_properties( :bold => 1, font, ...)
    #      *) font  = { :color => 'red', :bold => 1 }
    #         shade = { :bg_color => 'green', :pattern => 1 }
    #
    # Convert hashes of properties to method calls.
    #
    def set_format_properties(*properties)   # :nodoc:
      return if properties.empty?

      properties.each do |property|
        property.each do |key, value|
          # Strip leading "-" from Tk style properties e.g. "-color" => 'red'.
          key = key.sub(/^-/, '') if key.respond_to?(:to_str)

          # Create a sub to set the property.
          if value.respond_to?(:to_str) || !value.respond_to?(:+)
            send("set_#{key}", value.to_s)
          else
            send("set_#{key}", value)
          end
        end
      end
    end

    #
    # Return properties for an Style xf <alignment> sub-element.
    #
    def get_align_properties
      align = []    # Attributes to return

      # Check if any alignment options in the format have been changed.
      if @text_h_align != 0 || @text_v_align != 0 || @indent != 0 ||
         @rotation != 0 || @text_wrap != 0 || @shrink != 0 || @reading_order != 0
        changed = 1
      else
        return
      end

      # Indent is only allowed for horizontal left, right and distributed. If it
      # is defined for any other alignment or no alignment has been set then
      # default to left alignment.
      @text_h_align = 1 if @indent != 0 && ![1, 3, 7].include?(@text_h_align)

      # Check for properties that are mutually exclusive.
      @shrink       = 0 if @text_wrap != 0
      @shrink       = 0 if @text_h_align == 4    # Fill
      @shrink       = 0 if @text_h_align == 5    # Justify
      @shrink       = 0 if @text_h_align == 7    # Distributed
      @just_distrib = 0 if @text_h_align != 7    # Distributed
      @just_distrib = 0 if @indent != 0

      continuous = 'centerContinuous'

      align << %w[horizontal left]        if @text_h_align == 1
      align << %w[horizontal center]      if @text_h_align == 2
      align << %w[horizontal right]       if @text_h_align == 3
      align << %w[horizontal fill]        if @text_h_align == 4
      align << %w[horizontal justify]     if @text_h_align == 5
      align << ['horizontal', continuous]    if @text_h_align == 6
      align << %w[horizontal distributed] if @text_h_align == 7

      align << ['justifyLastLine', 1] if @just_distrib != 0

      # Property 'vertical' => 'bottom' is a default. It sets applyAlignment
      # without an alignment sub-element.
      align << %w[vertical top]         if @text_v_align == 1
      align << %w[vertical center]      if @text_v_align == 2
      align << %w[vertical justify]     if @text_v_align == 4
      align << %w[vertical distributed] if @text_v_align == 5

      align << ['indent',       @indent]   if @indent   != 0
      align << ['textRotation', @rotation] if @rotation != 0

      align << ['wrapText',     1] if @text_wrap != 0
      align << ['shrinkToFit',  1] if @shrink    != 0

      align << ['readingOrder', 1] if @reading_order == 1
      align << ['readingOrder', 2] if @reading_order == 2

      [changed, align]
    end

    #
    # Return properties for an Excel XML <Protection> element.
    #
    def get_protection_properties
      attributes = []

      attributes << ['locked', 0] unless ptrue?(@locked)
      attributes << ['hidden', 1] if     ptrue?(@hidden)

      attributes.empty? ? nil : attributes
    end

    def set_bold(bold = 1)
      @bold = ptrue?(bold) ? 1 : 0
    end

    def inspect
      to_s
    end

    #
    # Returns a unique hash key for the Format object.
    #
    def get_format_key
      [get_font_key, get_border_key, get_fill_key, get_alignment_key, @num_format, @locked, @hidden, @quote_prefix].join(':')
    end

    #
    # Returns a unique hash key for a font. Used by Workbook.
    #
    def get_font_key
      [
        @bold,
        @color,
        @font_charset,
        @font_family,
        @font_outline,
        @font_script,
        @font_shadow,
        @font_strikeout,
        @font,
        @italic,
        @size,
        @underline,
        @theme
      ].join(':')
    end

    #
    # Returns a unique hash key for a border style. Used by Workbook.
    #
    def get_border_key
      [
        @bottom,
        @bottom_color,
        @diag_border,
        @diag_color,
        @diag_type,
        @left,
        @left_color,
        @right,
        @right_color,
        @top,
        @top_color
      ].join(':')
    end

    #
    # Returns a unique hash key for a fill style. Used by Workbook.
    #
    def get_fill_key
      [
        @pattern,
        @bg_color,
        @fg_color
      ].join(':')
    end

    #
    # Returns a unique hash key for alignment formats.
    #
    def get_alignment_key
      [@text_h_align, @text_v_align, @indent, @rotation, @text_wrap, @shrink, @reading_order].join(':')
    end

    #
    # Returns the index used by Worksheet->_XF()
    #
    def get_xf_index
      if @xf_index
        @xf_index
      elsif @formats.xf_index_by_key(get_format_key)
        @formats.xf_index_by_key(get_format_key)
      else
        @xf_index = @formats.set_xf_index_by_key(get_format_key)
      end
    end

    #
    # Returns the index used by Worksheet->_XF()
    #
    def get_dxf_index
      if @dxf_index
        @dxf_index
      elsif @formats.dxf_index_by_key(get_format_key)
        @formats.dxf_index_by_key(get_format_key)
      else
        @dxf_index = @formats.set_dxf_index_by_key(get_format_key)
      end
    end

    def color(color_code)
      Format.color(color_code)
    end

    #
    # Used in conjunction with the set_xxx_color methods to convert a color
    # string into a number. Color range is 0..63 but we will restrict it
    # to 8..63 to comply with Gnumeric. Colors 0..7 are repeated in 8..15.
    #
    def self.color(color_code)
      colors = Colors::COLORS

      # Return the default color if nil,
      return 0x00 unless color_code

      if color_code.respond_to?(:to_str)
        # Return RGB style colors for processing later.
        return color_code if color_code =~ /^#[0-9A-F]{6}$/i

        # or the color string converted to an integer,
        return colors[color_code.downcase.to_sym] if colors[color_code.downcase.to_sym]

        # or the default color if string is unrecognised,
        0x00 if color_code =~ /\D/
      else
        # or an index < 8 mapped into the correct range,
        return color_code + 8 if color_code < 8

        # or the default color if arg is outside range,
        return 0x00 if color_code > 63

        # or an integer in the valid range
        color_code
      end
    end

    #
    # Set cell alignment.
    #
    def set_align(location)
      return unless location             # No default

      location = location.downcase

      set_text_h_align(1) if location == 'left'
      set_text_h_align(2) if location == 'centre'
      set_text_h_align(2) if location == 'center'
      set_text_h_align(3) if location == 'right'
      set_text_h_align(4) if location == 'fill'
      set_text_h_align(5) if location == 'justify'
      set_text_h_align(6) if location == 'center_across'
      set_text_h_align(6) if location == 'centre_across'
      set_text_h_align(6) if location == 'merge'              # Legacy.
      set_text_h_align(7) if location == 'distributed'
      set_text_h_align(7) if location == 'equal_space'        # S::PE.
      set_text_h_align(7) if location == 'justify_distributed'

      @just_distrib = 1 if location == 'justify_distributed'

      set_text_v_align(1) if location == 'top'
      set_text_v_align(2) if location == 'vcentre'
      set_text_v_align(2) if location == 'vcenter'
      set_text_v_align(3) if location == 'bottom'
      set_text_v_align(4) if location == 'vjustify'
      set_text_v_align(5) if location == 'vdistributed'
      set_text_v_align(5) if location == 'vequal_space'    # S::PE.
    end

    #
    # Set vertical cell alignment. This is required by the set_properties() method
    # to differentiate between the vertical and horizontal properties.
    #
    def set_valign(location)
      set_align(location)
    end

    #
    # Implements the Excel5 style "merge".
    #
    def set_center_across(_flag = 1)
      set_text_h_align(6)
    end

    #
    # This was the way to implement a merge in Excel5. However it should have been
    # called "center_across" and not "merge".
    # This is now deprecated. Use set_center_across() or better merge_range().
    #
    def set_merge(_merge = 1)
      set_text_h_align(6)
    end

    #
    # Set cells borders to the same style
    #
    def set_border(style)
      set_bottom(style)
      set_top(style)
      set_left(style)
      set_right(style)
    end

    #
    # Set cells border to the same color
    #
    def set_border_color(color)
      set_bottom_color(color)
      set_top_color(color)
      set_left_color(color)
      set_right_color(color)
    end

    #
    # Set the rotation angle of the text. An alignment property.
    #
    def set_rotation(rotation)
      if rotation == 270
        rotation = 255
      elsif rotation >= -90 && rotation <= 90
        rotation = -rotation + 90 if rotation < 0
      else
        raise "Rotation #{rotation} outside range: -90 <= angle <= 90"
        rotation = 0
      end

      @rotation = rotation
    end

    #
    # Set the properties for the hyperlink style. This isn't a public method. To
    # be fixed when styles are supported.
    #
    def set_hyperlink(hyperlink)
      @xf_id = 1

      set_underline(1)
      set_theme(10)
      @hyperlink = hyperlink
    end

    def set_font_info(fonts)
      key = get_font_key

      if fonts[key]
        # Font has already been used.
        @font_index = fonts[key]
        @has_font   = false
      else
        # This is a new font.
        @font_index = fonts.size
        fonts[key]  = fonts.size
        @has_font   = true
      end
    end

    def set_border_info(borders)
      key = get_border_key

      if borders[key]
        # Border has already been used.
        @border_index = borders[key]
        @has_border   = false
      else
        # This is a new border.
        @border_index = borders.size
        borders[key]  = borders.size
        @has_border   = true
      end
    end

    def method_missing(name, *args)  # :nodoc:
      method = "#{name}"

      # Check for a valid method names, i.e. "set_xxx_yyy".
      method =~ /set_(\w+)/ or raise "Unknown method: #{method}\n"

      # Match the attribute, i.e. "@xxx_yyy".
      attribute = "@#{::Regexp.last_match(1)}"

      # Check that the attribute exists
      # ........
      value = if method =~ /set\w+color$/    # for "set_property_color" methods
                color(args[0])
              else                            # for "set_xxx" methods
                args[0] || 1
              end

      instance_variable_set(attribute, value)
    end

    def color?
      ptrue?(@color)
    end

    def bold?
      ptrue?(@bold)
    end

    def italic?
      ptrue?(@italic)
    end

    def strikeout?
      ptrue?(@font_strikeout)
    end

    def outline?
      ptrue?(@font_outline)
    end

    def shadow?
      ptrue?(@font_shadow)
    end

    def underline?
      ptrue?(@underline)
    end

    def has_border(flag)
      @has_border = flag
    end

    def has_border? # :nodoc:
      @has_border
    end

    def has_dxf_border(flag)
      @has_dxf_border = flag
    end

    def has_dxf_border?
      @has_dxf_border
    end

    def has_font(flag)
      @has_font = flag
    end

    def has_font?
      @has_font
    end

    def has_dxf_font(flag)
      @has_dxf_font = flag
    end

    def has_dxf_font?
      @has_dxf_font
    end

    def has_fill(flag)
      @has_fill = flag
    end

    def has_fill?
      @has_fill
    end

    def has_dxf_fill(flag)
      @has_dxf_fill = flag
    end

    def has_dxf_fill?
      @has_dxf_fill
    end

    def [](attr)
      instance_variable_get("@#{attr}")
    end

    def write_font(writer, worksheet, dxf_format = nil) # :nodoc:
      writer.tag_elements('font') do
        # The condense and extend elements are mainly used in dxf formats.
        write_condense(writer) if ptrue?(@font_condense)
        write_extend(writer)   if ptrue?(@font_extend)

        write_font_shapes(writer)

        writer.empty_tag('sz', [['val', size]]) unless dxf_format

        if theme == -1
        # Ignore for excel2003_style
        elsif ptrue?(theme)
          write_color('theme', theme, writer)
        elsif ptrue?(@color_indexed)
          write_color('indexed', @color_indexed, writer)
        elsif ptrue?(@color)
          color = worksheet.palette_color(@color)
          if color != 'Automatic'
            write_color('rgb', color, writer)
          end
        elsif !ptrue?(dxf_format)
          write_color('theme', 1, writer)
        end

        unless ptrue?(dxf_format)
          writer.empty_tag('name', [['val', @font]])
          write_font_family_scheme(writer)
        end
      end
    end

    def write_font_rpr(writer, worksheet) # :nodoc:
      writer.tag_elements('rPr') do
        write_font_shapes(writer)
        writer.empty_tag('sz', [['val', size]])

        if ptrue?(theme)
          write_color('theme', theme, writer)
        elsif ptrue?(@color)
          color = worksheet.palette_color(@color)
          write_color('rgb', color, writer)
        else
          write_color('theme', 1, writer)
        end

        writer.empty_tag('rFont', [['val', @font]])
        write_font_family_scheme(writer)
      end
    end

    def border_attributes
      attributes = []

      # Diagonal borders add attributes to the <border> element.
      if diag_type == 1
        attributes << ['diagonalUp',   1]
      elsif diag_type == 2
        attributes << ['diagonalDown', 1]
      elsif diag_type == 3
        attributes << ['diagonalUp',   1]
        attributes << ['diagonalDown', 1]
      end
      attributes
    end

    def xf_attributes
      attributes = [
        ['numFmtId', num_format_index],
        ['fontId', font_index],
        ['fillId', fill_index],
        ['borderId', border_index],
        ['xfId', xf_id]
      ]
      attributes << ['quotePrefix', 1] if ptrue?(quote_prefix)
      attributes << ['applyNumberFormat', 1] if num_format_index > 0
      # Add applyFont attribute if XF format uses a font element.
      attributes << ['applyFont', 1] if font_index > 0 && !ptrue?(@hyperlink)
      # Add applyFill attribute if XF format uses a fill element.
      attributes << ['applyFill', 1] if fill_index > 0
      # Add applyBorder attribute if XF format uses a border element.
      attributes << ['applyBorder', 1] if border_index > 0

      # Check if XF format has alignment properties set.
      apply_align, _align = get_align_properties
      # We can also have applyAlignment without a sub-element.
      attributes << ['applyAlignment', 1] if apply_align || ptrue?(@hyperlink)
      attributes << ['applyProtection', 1] if get_protection_properties || ptrue?(hyperlink)

      attributes
    end

    def force_text_format?
      @num_format == 49 # Text format ('@')
    end

    private

    def write_font_shapes(writer)
      writer.empty_tag('b')       if bold?
      writer.empty_tag('i')       if italic?
      writer.empty_tag('strike')  if strikeout?
      writer.empty_tag('outline') if outline?
      writer.empty_tag('shadow')  if shadow?

      # Handle the underline variants.
      write_underline(writer, underline) if underline?

      write_vert_align(writer, 'superscript') if font_script == 1
      write_vert_align(writer, 'subscript')   if font_script == 2
    end

    def write_font_family_scheme(writer)
      writer.empty_tag('family', [['val', @font_family]]) if ptrue?(@font_family)

      writer.empty_tag('charset', [['val', @font_charset]]) if ptrue?(@font_charset)

      writer.empty_tag('scheme', [['val', @font_scheme]]) if @font == 'Calibri' && !ptrue?(@hyperlink)
    end

    #
    # Write the underline font element.
    #
    def write_underline(writer, underline)
      writer.empty_tag('u', write_underline_attributes(underline))
    end

    #
    # Write the underline font element.
    #
    def write_underline_attributes(underline)
      val = 'val'
      # Handle the underline variants.
      case underline
      when 2
        [[val, 'double']]
      when 33
        [[val, 'singleAccounting']]
      when 34
        [[val, 'doubleAccounting']]
      else
        []
      end
    end

    #
    # Write the <vertAlign> font sub-element.
    #
    def write_vert_align(writer, val) # :nodoc:
      writer.empty_tag('vertAlign', [['val', val]])
    end

    #
    # Write the <condense> element.
    #
    def write_condense(writer)
      writer.empty_tag('condense', [['val', 0]])
    end

    #
    # Write the <extend> element.
    #
    def write_extend(writer)
      writer.empty_tag('extend', [['val', 0]])
    end
  end
	class Drawing
    attr_accessor :type, :dimensions, :width, :height, :shape, :anchor, :rel_index, :url_rel_index, :name, :description
    attr_reader :tip, :decorative

    def initialize(type, dimensions, width, height, shape, anchor, rel_index = nil, url_rel_index = nil, tip = nil, name = nil, description = nil, decorative = nil)
      @type = type
      @dimensions = dimensions
      @width = width
      @height = height
      @shape = shape
      @anchor = anchor
      @rel_index = rel_index
      @url_rel_index = url_rel_index
      @tip = tip
      @name = name
      @description = description
      @decorative = decorative
    end
  end
	class Drawings
    include Writexlsx_kot::Utility

    attr_writer :embedded, :orientation

    def initialize
      @writer = Package::XMLWriterSimple.new
      @drawings    = []
      @embedded    = false
      @orientation = false
    end

    def xml_str
      @writer.string
    end

    def set_xml_writer(filename)
      @writer.set_xml_writer(filename)
    end

    #
    # Assemble and write the XML file.
    #
    def assemble_xml_file
      write_xml_declaration do
        # Write the xdr:wsDr element.
        write_drawing_workspace do
          if @embedded
            index = 0
            @drawings.each do |drawing|
              # Write the xdr:twoCellAnchor element.
              index += 1
              write_two_cell_anchor(index, drawing)
            end
          else
            # Write the xdr:absoluteAnchor element.
            write_absolute_anchor(1)
          end
        end
      end
    end

    #
    # Add a chart, image or shape sub object to the drawing.
    #
    def add_drawing_object(drawing)
      @drawings << drawing
    end

    private

    #
    # Write the <xdr:wsDr> element.
    #
    def write_drawing_workspace(&block)
      schema = 'http://schemas.openxmlformats.org/drawingml/'
      attributes = [
        ['xmlns:xdr', "#{schema}2006/spreadsheetDrawing"],
        ['xmlns:a',    "#{schema}2006/main"]
      ]

      @writer.tag_elements('xdr:wsDr', attributes, &block)
    end

    #
    # Write the <xdr:twoCellAnchor> element.
    #
    def write_two_cell_anchor(*args)
      index, drawing = args

      type          = drawing.type
      width         = drawing.width
      height        = drawing.height
      shape         = drawing.shape
      anchor        = drawing.anchor
      rel_index     = drawing.rel_index
      url_rel_index = drawing.url_rel_index
      tip           = drawing.tip
      name          = drawing.name
      description   = drawing.description
      decorative    = drawing.decorative

      col_from, row_from, col_from_offset, row_from_offset,
      col_to, row_to, col_to_offset, row_to_offset, col_absolute, row_absolute = drawing.dimensions

      attributes      = []

      # Add attribute for images.
      if anchor == 2
        attributes << [:editAs, 'oneCell']
      elsif anchor == 3
        attributes << [:editAs, 'absolute']
      end

      # Add attribute for shapes.
      attributes << [:editAs, shape.edit_as] if shape && shape.edit_as

      @writer.tag_elements('xdr:twoCellAnchor', attributes) do
        # Write the xdr:from element.
        write_from(col_from, row_from, col_from_offset, row_from_offset)
        # Write the xdr:to element.
        write_to(col_to, row_to, col_to_offset, row_to_offset)

        if type == 1
          # Graphic frame.

          # Write the xdr:graphicFrame element for charts.
          write_graphic_frame(index, rel_index, name, description, decorative)
        elsif type == 2
          # Write the xdr:pic element.
          write_pic(
            index,        rel_index,      col_absolute,
            row_absolute, width,          height,
            description,  url_rel_index, tip, decorative
          )
        else
          # Write the xdr:sp element for shapes.
          write_sp(index, col_absolute, row_absolute, width, height, shape)
        end

        # Write the xdr:clientData element.
        write_client_data
      end
    end

    #
    # Write the <xdr:absoluteAnchor> element.
    #
    def write_absolute_anchor(index)
      @writer.tag_elements('xdr:absoluteAnchor') do
        # Different co-ordinates for horizonatal (= 0) and vertical (= 1).
        if ptrue?(@orientation)
          # Write the xdr:pos element.
          write_pos(0, -47625)

          # Write the xdr:ext element.
          write_xdr_ext(6162675, 6124575)
        else

          # Write the xdr:pos element.
          write_pos(0, 0)

          # Write the xdr:ext element.
          write_xdr_ext(9308969, 6078325)
        end

        # Write the xdr:graphicFrame element.
        write_graphic_frame(index, index)

        # Write the xdr:clientData element.
        write_client_data
      end
    end

    #
    # Write the <xdr:from> element.
    #
    def write_from(col, row, col_offset, row_offset)
      @writer.tag_elements('xdr:from') do
        # Write the xdr:col element.
        write_col(col)
        # Write the xdr:colOff element.
        write_col_off(col_offset)
        # Write the xdr:row element.
        write_row(row)
        # Write the xdr:rowOff element.
        write_row_off(row_offset)
      end
    end

    #
    # Write the <xdr:to> element.
    #
    def write_to(col, row, col_offset, row_offset)
      @writer.tag_elements('xdr:to') do
        # Write the xdr:col element.
        write_col(col)
        # Write the xdr:colOff element.
        write_col_off(col_offset)
        # Write the xdr:row element.
        write_row(row)
        # Write the xdr:rowOff element.
        write_row_off(row_offset)
      end
    end

    #
    # Write the <xdr:col> element.
    #
    def write_col(data)
      @writer.data_element('xdr:col', data)
    end

    #
    # Write the <xdr:colOff> element.
    #
    def write_col_off(data)
      @writer.data_element('xdr:colOff', data)
    end

    #
    # Write the <xdr:row> element.
    #
    def write_row(data)
      @writer.data_element('xdr:row', data)
    end

    #
    # Write the <xdr:rowOff> element.
    #
    def write_row_off(data)
      @writer.data_element('xdr:rowOff', data)
    end

    #
    # Write the <xdr:pos> element.
    #
    def write_pos(x, y)
      attributes = [
        ['x', x],
        ['y', y]
      ]

      @writer.empty_tag('xdr:pos', attributes)
    end

    #
    # Write the <xdr:ext> element.
    #
    def write_xdr_ext(cx, cy)
      attributes = [
        ['cx', cx],
        ['cy', cy]
      ]

      @writer.empty_tag('xdr:ext', attributes)
    end

    #
    # Write the <xdr:graphicFrame> element.
    #
    def write_graphic_frame(
          index, rel_index, name = nil,
          description = nil, decorative = nil, macro = nil
        )
      macro  ||= ''

      attributes = [['macro', macro]]

      @writer.tag_elements('xdr:graphicFrame', attributes) do
        # Write the xdr:nvGraphicFramePr element.
        write_nv_graphic_frame_pr(index, name, description, decorative)
        # Write the xdr:xfrm element.
        write_xfrm
        # Write the a:graphic element.
        write_atag_graphic(rel_index)
      end
    end

    #
    # Write the <xdr:nvGraphicFramePr> element.
    #
    def write_nv_graphic_frame_pr(
          index, name = nil, description = nil, decorative = nil
        )

      name = "Chart #{index}" unless ptrue?(name)

      @writer.tag_elements('xdr:nvGraphicFramePr') do
        # Write the xdr:cNvPr element.
        write_c_nv_pr(index + 1, name, description,
                      nil, nil, decorative)
        # Write the xdr:cNvGraphicFramePr element.
        write_c_nv_graphic_frame_pr
      end
    end

    #
    # Write the <xdr:cNvPr> element.
    #
    def write_c_nv_pr(index, name, description = nil, url_rel_index = nil, tip = nil, decorative = nil)
      attributes = [
        ['id',   index],
        ['name', name]
      ]

      # Add description attribute for images.
      attributes << ['descr', description] if ptrue?(description) && !ptrue?(decorative)

      if ptrue?(url_rel_index) || ptrue?(decorative)
        @writer.tag_elements('xdr:cNvPr', attributes) do
          if ptrue?(url_rel_index)
            # Write the a:hlinkClick element.
            write_a_hlink_click(url_rel_index, tip)
          end
          if ptrue?(decorative)
            # Write the adec:decorative element.
            write_decorative
          end
        end
      else
        @writer.empty_tag('xdr:cNvPr', attributes)
      end
    end

    #
    # Write the <a:hlinkClick> element.
    #
    def write_a_hlink_click(index, tip)
      schema  = 'http://schemas.openxmlformats.org/officeDocument/'
      xmlns_r = "#{schema}2006/relationships"
      r_id    = "rId#{index}"

      attributes = [
        ['xmlns:r', xmlns_r],
        ['r:id', r_id]
      ]

      attributes << ['tooltip', tip] if tip

      @writer.empty_tag('a:hlinkClick', attributes)
    end

    #
    # Write the <adec:decorative> element.
    #
    def write_decorative
      @writer.tag_elements('a:extLst') do
        write_a_uri_ext('{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}')
        write_a16_creation_id
        @writer.end_tag('a:ext')

        write_a_uri_ext('{C183D7F6-B498-43B3-948B-1728B52AA6E4}')
        write_adec_decorative
        @writer.end_tag('a:ext')
      end
    end

    #
    # Write the <a:ext> element.
    #
    def write_a_uri_ext(uri)
      attributes = [
        ['uri', uri]
      ]

      @writer.start_tag('a:ext', attributes)
    end

    #
    # Write the <adec:decorative> element.
    #
    def write_adec_decorative
      xmlns_adec = 'http://schemas.microsoft.com/office/drawing/2017/decorative'
      val        = 1

      attributes = [
        ['xmlns:adec', xmlns_adec],
        ['val',        val]
      ]

      @writer.empty_tag('adec:decorative', attributes)
    end

    #
    # Write the <a16:creationId> element.
    #
    def write_a16_creation_id
      xmlns_a_16 = 'http://schemas.microsoft.com/office/drawing/2014/main'
      id         = '{00000000-0008-0000-0000-000002000000}'

      attributes = [
        ['xmlns:a16', xmlns_a_16],
        ['id',        id]
      ]

      @writer.empty_tag('a16:creationId', attributes)
    end

    #
    # Write the <xdr:cNvGraphicFramePr> element.
    #
    def write_c_nv_graphic_frame_pr
      if @embedded
        @writer.empty_tag('xdr:cNvGraphicFramePr')
      else
        @writer.tag_elements('xdr:cNvGraphicFramePr') do
          # Write the a:graphicFrameLocks element.
          write_a_graphic_frame_locks
        end
      end
    end

    #
    # Write the <a:graphicFrameLocks> element.
    #
    def write_a_graphic_frame_locks
      no_grp = 1

      attributes = [['noGrp', no_grp]]

      @writer.empty_tag('a:graphicFrameLocks', attributes)
    end

    #
    # Write the <xdr:xfrm> element.
    #
    def write_xfrm
      @writer.tag_elements('xdr:xfrm') do
        # Write the xfrmOffset element.
        write_xfrm_offset
        # Write the xfrmOffset element.
        write_xfrm_extension
      end
    end

    #
    # Write the <a:off> xfrm sub-element.
    #
    def write_xfrm_offset
      x    = 0
      y    = 0

      attributes = [
        ['x', x],
        ['y', y]
      ]

      @writer.empty_tag('a:off', attributes)
    end

    #
    # Write the <a:ext> xfrm sub-element.
    #
    def write_xfrm_extension
      x    = 0
      y    = 0

      attributes = [
        ['cx', x],
        ['cy', y]
      ]

      @writer.empty_tag('a:ext', attributes)
    end

    #
    # Write the <a:graphic> element.
    #
    def write_atag_graphic(index)
      @writer.tag_elements('a:graphic') do
        # Write the a:graphicData element.
        write_atag_graphic_data(index)
      end
    end

    #
    # Write the <a:graphicData> element.
    #
    def write_atag_graphic_data(index)
      uri = 'http://schemas.openxmlformats.org/drawingml/2006/chart'

      attributes = [['uri', uri]]

      @writer.tag_elements('a:graphicData', attributes) do
        # Write the c:chart element.
        write_c_chart(index)
      end
    end

    #
    # Write the <c:chart> element.
    #
    def write_c_chart(id)
      schema  = 'http://schemas.openxmlformats.org/'
      xmlns_c = "#{schema}drawingml/2006/chart"
      xmlns_r = "#{schema}officeDocument/2006/relationships"

      attributes = [
        ['xmlns:c', xmlns_c],
        ['xmlns:r', xmlns_r]
      ]
      attributes << r_id_attributes(id)

      @writer.empty_tag('c:chart', attributes)
    end

    #
    # Write the <xdr:clientData> element.
    #
    def write_client_data
      @writer.empty_tag('xdr:clientData')
    end

    #
    # Write the <xdr:sp> element.
    #
    def write_sp(index, col_absolute, row_absolute, width, height, shape)
      if shape.connect == 0
        # Add attribute for shapes.
        attributes = [
          [:macro, ''],
          [:textlink, '']
        ]
        @writer.tag_elements('xdr:sp', attributes) do
          # Write the xdr:nvSpPr element.
          write_nv_sp_pr(index, shape)

          # Write the xdr:spPr element.
          write_xdr_sp_pr(col_absolute, row_absolute, width, height, shape)

          # Write the xdr:txBody element.
          write_tx_body(shape) if shape.text != 0
        end
      else
        attributes = [[:macro,  '']]
        @writer.tag_elements('xdr:cxnSp', attributes) do
          # Write the xdr:nvCxnSpPr element.
          write_nv_cxn_sp_pr(index, shape)

          # Write the xdr:spPr element.
          write_xdr_sp_pr(col_absolute, row_absolute, width, height, shape)
        end
      end
    end

    #
    # Write the <xdr:nvCxnSpPr> element.
    #
    def write_nv_cxn_sp_pr(index, shape)
      @writer.tag_elements('xdr:nvCxnSpPr') do
        shape.name = [shape.type, index].join(' ') unless shape.name
        write_c_nv_pr(shape.id, shape.name)

        @writer.tag_elements('xdr:cNvCxnSpPr') do
          attributes = [[:noChangeShapeType, '1']]
          @writer.empty_tag('a:cxnSpLocks', attributes)

          if shape.start
            attributes = [
              ['id', shape.start],
              ['idx', shape.start_index]
            ]
            @writer.empty_tag('a:stCxn', attributes)
          end

          if shape.end
            attributes = [
              ['id', shape.end],
              ['idx', shape.end_index]
            ]
            @writer.empty_tag('a:endCxn', attributes)
          end
        end
      end
    end

    #
    # Write the <xdr:NvSpPr> element.
    #
    def write_nv_sp_pr(index, shape)
      attributes = []
      attributes << ['txBox', 1] if shape.tx_box

      @writer.tag_elements('xdr:nvSpPr') do
        write_c_nv_pr(shape.id, "#{shape.type} #{index}")

        @writer.tag_elements('xdr:cNvSpPr', attributes) do
          @writer.empty_tag('a:spLocks', [[:noChangeArrowheads, '1']])
        end
      end
    end

    #
    # Write the <xdr:pic> element.
    #
    def write_pic(index, rel_index, col_absolute, row_absolute, width, height, description, url_rel_index, tip, decorative)
      @writer.tag_elements('xdr:pic') do
        # Write the xdr:nvPicPr element.
        write_nv_pic_pr(index, rel_index, description, url_rel_index, tip, decorative)
        # Write the xdr:blipFill element.
        write_blip_fill(rel_index)

        # Pictures are rectangle shapes by default.
        shape = Shape.new
        shape.type = 'rect'

        # Write the xdr:spPr element.
        write_sp_pr(col_absolute, row_absolute, width, height, shape)
      end
    end

    #
    # Write the <xdr:nvPicPr> element.
    #
    def write_nv_pic_pr(index, _rel_index, description, url_rel_index, tip, decorative)
      @writer.tag_elements('xdr:nvPicPr') do
        # Write the xdr:cNvPr element.
        write_c_nv_pr(
          index + 1, "Picture #{index}", description,
          url_rel_index, tip, decorative
        )
        # Write the xdr:cNvPicPr element.
        write_c_nv_pic_pr
      end
    end

    #
    # Write the <xdr:cNvPicPr> element.
    #
    def write_c_nv_pic_pr
      @writer.tag_elements('xdr:cNvPicPr') do
        # Write the a:picLocks element.
        write_a_pic_locks
      end
    end

    #
    # Write the <a:picLocks> element.
    #
    def write_a_pic_locks
      no_change_aspect = 1

      attributes = [['noChangeAspect', no_change_aspect]]

      @writer.empty_tag('a:picLocks', attributes)
    end

    #
    # Write the <xdr:blipFill> element.
    #
    def write_blip_fill(index)
      @writer.tag_elements('xdr:blipFill') do
        # Write the a:blip element.
        write_a_blip(index)
        # Write the a:stretch element.
        write_a_stretch
      end
    end

    #
    # Write the <a:blip> element.
    #
    def write_a_blip(index)
      schema  = 'http://schemas.openxmlformats.org/officeDocument/'
      xmlns_r = "#{schema}2006/relationships"
      r_embed = "rId#{index}"

      attributes = [
        ['xmlns:r', xmlns_r],
        ['r:embed', r_embed]
      ]

      @writer.empty_tag('a:blip', attributes)
    end

    #
    # Write the <a:stretch> element.
    #
    def write_a_stretch
      @writer.tag_elements('a:stretch') do
        # Write the a:fillRect element.
        write_a_fill_rect
      end
    end

    #
    # Write the <a:fillRect> element.
    #
    def write_a_fill_rect
      @writer.empty_tag('a:fillRect')
    end

    #
    # Write the <xdr:spPr> element, for charts.
    #
    def write_sp_pr(col_absolute, row_absolute, width, height, shape = {})
      @writer.tag_elements('xdr:spPr') do
        # Write the a:xfrm element.
        write_a_xfrm(col_absolute, row_absolute, width, height)
        # Write the a:prstGeom element.
        write_a_prst_geom(shape)
      end
    end

    #
    # Write the <xdr:spPr> element for shapes.
    #
    def write_xdr_sp_pr(col_absolute, row_absolute, width, height, shape)
      attributes = [%w[bwMode auto]]

      @writer.tag_elements('xdr:spPr', attributes) do
        # Write the a:xfrm element.
        write_a_xfrm(col_absolute, row_absolute, width, height, shape)

        # Write the a:prstGeom element.
        write_a_prst_geom(shape)

        if shape.fill.to_s.bytesize > 1
          # Write the a:solidFill element.
          write_a_solid_fill(shape.fill)
        else
          @writer.empty_tag('a:noFill')
        end

        # Write the a:ln element.
        write_a_ln(shape)
      end
    end

    #
    # Write the <a:xfrm> element.
    #
    def write_a_xfrm(col_absolute, row_absolute, width, height, shape = nil)
      attributes = []

      rotation = shape ? shape.rotation : 0
      rotation *= 60000

      attributes << ['rot', rotation] if rotation != 0
      attributes << ['flipH', 1]      if shape && ptrue?(shape.flip_h)
      attributes << ['flipV', 1]      if shape && ptrue?(shape.flip_v)

      @writer.tag_elements('a:xfrm', attributes) do
        # Write the a:off element.
        write_a_off(col_absolute, row_absolute)
        # Write the a:ext element.
        write_a_ext(width, height)
      end
    end

    #
    # Write the <a:off> element.
    #
    def write_a_off(x, y)
      attributes = [
        ['x', x],
        ['y', y]
      ]

      @writer.empty_tag('a:off', attributes)
    end

    #
    # Write the <a:ext> element.
    #
    def write_a_ext(cx, cy)
      attributes = [
        ['cx', cx],
        ['cy', cy]
      ]

      @writer.empty_tag('a:ext', attributes)
    end

    #
    # Write the <a:prstGeom> element.
    #
    def write_a_prst_geom(shape = {})
      attributes = []
      attributes << ['prst', shape.type] if shape.type

      @writer.tag_elements('a:prstGeom', attributes) do
        # Write the a:avLst element.
        write_a_av_lst(shape)
      end
    end

    #
    # Write the <a:avLst> element.
    #
    def write_a_av_lst(shape = {})
      if shape.adjustments.respond_to?(:empty?)
        adjustments = shape.adjustments
      elsif shape.adjustments.respond_to?(:coerce)
        adjustments = [shape.adjustments]
      elsif !shape.adjustments
        adjustments = []
      end

      if adjustments.respond_to?(:empty?) && !adjustments.empty?
        @writer.tag_elements('a:avLst') do
          i = 0
          adjustments.each do |adj|
            i += 1
            # Only connectors have multiple adjustments.
            suffix = shape.connect == 0 ? '' : i

            # Scale Adjustments: 100,000 = 100%.
            adj_int = (adj * 1000).to_i

            attributes = [
              [:name, "adj#{suffix}"],
              [:fmla, "val #{adj_int}"]
            ]
            @writer.empty_tag('a:gd', attributes)
          end
        end
      else
        @writer.empty_tag('a:avLst')
      end
    end

    #
    # Write the <a:solidFill> element.
    #
    def write_a_solid_fill(rgb = '000000')
      attributes = [['val', rgb]]

      @writer.tag_elements('a:solidFill') do
        @writer.empty_tag('a:srgbClr', attributes)
      end
    end

    #
    # Write the <a:ln> elements.
    #
    def write_a_ln(shape = {})
      weight = shape.line_weight || 0
      attributes = [['w', weight * 9525]]
      @writer.tag_elements('a:ln', attributes) do
        line = shape.line || 0
        if line.to_s.bytesize > 1
          # Write the a:solidFill element.
          write_a_solid_fill(line)
        else
          @writer.empty_tag('a:noFill')
        end

        if shape.line_type != ''
          attributes = [['val', shape.line_type]]
          @writer.empty_tag('a:prstDash', attributes)
        end

        if shape.connect == 0
          attributes = [['lim', 800000]]
          @writer.empty_tag('a:miter', attributes)
        else
          @writer.empty_tag('a:round')
        end

        @writer.empty_tag('a:headEnd')
        @writer.empty_tag('a:tailEnd')
      end
    end

    #
    # Write the <xdr:txBody> element.
    #
    def write_tx_body(shape)
      attributes = [
        [:vertOverflow, "clip"],
        [:wrap,         "square"],
        [:lIns,         "27432"],
        [:tIns,         "22860"],
        [:rIns,         "27432"],
        [:bIns,         "22860"],
        [:anchor,       shape.valign],
        [:upright,      "1"]
      ]
      @writer.tag_elements('xdr:txBody') do
        @writer.empty_tag('a:bodyPr', attributes)
        @writer.empty_tag('a:lstStyle')

        @writer.tag_elements('a:p') do
          rotation = shape.format[:rotation] || 0
          rotation *= 60000

          attributes = [
            [:algn, shape.align],
            [:rtl, rotation]
          ]
          @writer.tag_elements('a:pPr', attributes) do
            attributes = [[:sz, "1000"]]
            @writer.empty_tag('a:defRPr', attributes)
          end

          @writer.tag_elements('a:r') do
            size = shape.format[:size] || 8
            size *= 100

            bold      = shape.format[:bold]      || 0
            italic    = shape.format[:italic]    || 0
            underline = ptrue?(shape.format[:underline]) ? 'sng' : 'none'
            strike    = ptrue?(shape.format[:font_strikeout]) ? 'Strike' : 'noStrike'

            attributes = [
              [:lang,     "en-US"],
              [:sz,       size],
              [:b,        bold],
              [:i,        italic],
              [:u,        underline],
              [:strike,   strike],
              [:baseline, 0]
            ]
            @writer.tag_elements('a:rPr', attributes) do
              color = shape.format[:color]
              if color
                color = shape.palette_color(color)
                color = color.sub(/^FF/, '')  # Remove leading FF from rgb for shape color.
              else
                color = '000000'
              end

              write_a_solid_fill(color)

              font = shape.format[:font] || 'Calibri'
              attributes = [[:typeface, font]]
              @writer.empty_tag('a:latin', attributes)
              @writer.empty_tag('a:cs', attributes)
            end
            @writer.tag_elements('a:t') do
              @writer.characters(shape.text)
            end
          end
        end
      end
    end
  end
	class Sparkline
    include Writexlsx_kot::Utility

    def initialize(ws, param, sheetname)
      @color = {}

      # Check for valid input parameters.
      param.each_key do |k|
        raise "Unknown parameter '#{k}' in add_sparkline()" unless valid_sparkline_parameter[k]
      end
      %i[location range].each do |required_key|
        raise "Parameter '#{required_key}' is required in add_sparkline()" unless param[required_key]
      end

      # Handle the sparkline type.
      type = param[:type] || 'line'
      raise "Parameter ':type' must be 'line', 'column' or 'win_loss' in add_sparkline()" unless %w[line column win_loss].include?(type)

      type  = 'stacked' if type == 'win_loss'
      @type = type

      # We handle single location/range values or array refs of values.
      @locations = [param[:location]].flatten
      @ranges    = [param[:range]].flatten

      raise "Must have the same number of location and range parameters in add_sparkline()" if @ranges.size != @locations.size

      # Cleanup the input ranges.
      @ranges.collect! do |range|
        # Remove the absolute reference $ symbols.
        range = range.gsub("$", '')
        # Convert a simple range into a full Sheet1!A1:D1 range.
        range = "#{sheetname}!#{range}" unless range =~ /!/
        range
      end
      # Cleanup the input locations.
      @locations.collect! { |location| location.gsub("$", '') }

      # Map options.
      @high      = param[:high_point]
      @low       = param[:low_point]
      @negative  = param[:negative_points]
      @first     = param[:first_point]
      @last      = param[:last_point]
      @markers   = param[:markers]
      @min       = param[:min]
      @max       = param[:max]
      @axis      = param[:axis]
      @reverse   = param[:reverse]
      @hidden    = param[:show_hidden]
      @weight    = param[:weight]

      # Map empty cells options.
      @empty = case param[:empty_cells] || ''
               when 'zero'
                 0
               when 'connect'
                 'span'
               else
                 'gap'
               end

      # Map the date axis range.
      date_range = param[:date_axis]
      date_range = "#{sheetname}!#{date_range}" if ptrue?(date_range) && !(date_range =~ /!/)
      @date_axis = date_range

      # Set the sparkline styles.
      style = spark_styles[param[:style] || 0]

      @series_color   = style[:series]
      @negative_color = style[:negative]
      @markers_color  = style[:markers]
      @first_color    = style[:first]
      @last_color     = style[:last]
      @high_color     = style[:high]
      @low_color      = style[:low]

      # Override the style colours with user defined colors.
      %i[series_color negative_color markers_color first_color last_color high_color low_color].each do |user_color|
        set_spark_color(user_color, ptrue?(param[user_color]) ? ws.palette_color(param[user_color]) : nil)
      end
    end

    def count
      @locations.size
    end

    def group_attributes
      cust_max = cust_max_min(@max) if @max
      cust_min = cust_max_min(@min) if @min

      a = []
      a << ['manualMax', @max] if @max && @max != 'group'
      a << ['manualMin', @min] if @min && @min != 'group'

      # Ignore the default type attribute (line).
      a << ['type',          @type]        if @type != 'line'

      a << ['lineWeight',    @weight]      if @weight
      a << ['dateAxis',      1]            if @date_axis
      a << ['displayEmptyCellsAs', @empty] if ptrue?(@empty)

      a << ['markers',       1]         if @markers
      a << ['high',          1]         if @high
      a << ['low',           1]         if @low
      a << ['first',         1]         if @first
      a << ['last',          1]         if @last
      a << ['negative',      1]         if @negative
      a << ['displayXAxis',  1]         if @axis
      a << ['displayHidden', 1]         if @hidden
      a << ['minAxisType',   cust_min]  if cust_min
      a << ['maxAxisType',   cust_max]  if cust_max
      a << ['rightToLeft',   1]         if @reverse
      a
    end

    #
    # Write the <x14:sparklineGroup> element.
    #
    # Example for order.
    #
    # <x14:sparklineGroup
    #     manualMax="0"
    #     manualMin="0"
    #     lineWeight="2.25"
    #     type="column"
    #     dateAxis="1"
    #     displayEmptyCellsAs="span"
    #     markers="1"
    #     high="1"
    #     low="1"
    #     first="1"
    #     last="1"
    #     negative="1"
    #     displayXAxis="1"
    #     displayHidden="1"
    #     minAxisType="custom"
    #     maxAxisType="custom"
    #     rightToLeft="1">
    #
    def write_sparkline_group(writer)
      @writer = writer

      @writer.tag_elements('x14:sparklineGroup', group_attributes) do
        write
      end
    end

    private

    def write
      write_color_series
      write_color_negative
      write_color_axis
      write_color_markers
      write_color_first
      write_color_last
      write_color_high
      write_color_low
      write_xmf_date_axis if @date_axis
      write_sparklines
    end

    #
    # Write the <x14:colorSeries> element.
    #
    def write_color_series
      write_spark_color('x14:colorSeries', @series_color)
    end

    #
    # Write the <x14:colorNegative> element.
    #
    def write_color_negative
      write_spark_color('x14:colorNegative', @negative_color)
    end

    #
    # Write the <x14:colorAxis> element.
    #
    def write_color_axis  # :nodoc:
      write_spark_color('x14:colorAxis', { _rgb: 'FF000000' })
    end

    #
    # Write the <x14:colorMarkers> element.
    #
    def write_color_markers  # :nodoc:
      write_spark_color('x14:colorMarkers', @markers_color)
    end

    #
    # Write the <x14:colorFirst> element.
    #
    def write_color_first  # :nodoc:
      write_spark_color('x14:colorFirst', @first_color)
    end

    #
    # Write the <x14:colorLast> element.
    #
    def write_color_last  # :nodoc:
      write_spark_color('x14:colorLast', @last_color)
    end

    #
    # Write the <x14:colorHigh> element.
    #
    def write_color_high  # :nodoc:
      write_spark_color('x14:colorHigh', @high_color)
    end

    #
    # Write the <x14:colorLow> element.
    #
    def write_color_low  # :nodoc:
      write_spark_color('x14:colorLow', @low_color)
    end

    #
    # Write the <xm:f> element.
    #
    def write_xmf_date_axis
      @writer.data_element('xm:f', @date_axis)
    end

    #
    # Write the <x14:sparklines> element and <x14:sparkline> subelements.
    #
    def write_sparklines  # :nodoc:
      # Write the sparkline elements.
      @writer.tag_elements('x14:sparklines') do
        (0..count - 1).each do |i|
          range    = @ranges[i]
          location = @locations[i]

          @writer.tag_elements('x14:sparkline') do
            @writer.data_element('xm:f',     range)
            @writer.data_element('xm:sqref', location)
          end
        end
      end
    end

    #
    # Helper function for the sparkline color functions below.
    #
    def write_spark_color(element, color)  # :nodoc:
      attr = []

      attr << ['rgb',   color[:_rgb]]   if color[:_rgb]
      attr << ['theme', color[:_theme]] if color[:_theme]
      attr << ['tint',  color[:_tint]]  if color[:_tint]

      @writer.empty_tag(element, attr)
    end

    def set_spark_color(user_color, palette_color)
      return unless palette_color

      instance_variable_set("@#{user_color}", { _rgb: palette_color })
    end

    def cust_max_min(max_min)  # :nodoc:
      max_min == 'group' ? 'group' : 'custom'
    end

    def valid_sparkline_parameter  # :nodoc:
      {
        location:        1,
        range:           1,
        type:            1,
        high_point:      1,
        low_point:       1,
        negative_points: 1,
        first_point:     1,
        last_point:      1,
        markers:         1,
        style:           1,
        series_color:    1,
        negative_color:  1,
        markers_color:   1,
        first_color:     1,
        last_color:      1,
        high_color:      1,
        low_color:       1,
        max:             1,
        min:             1,
        axis:            1,
        reverse:         1,
        empty_cells:     1,
        show_hidden:     1,
        date_axis:       1,
        weight:          1
      }
    end

    def spark_styles  # :nodoc:
      [
        {   # 0
          series:   { _theme: "4", _tint: "-0.499984740745262" },
          negative: { _theme: "5" },
          markers:  { _theme: "4", _tint: "-0.499984740745262" },
          first:    { _theme: "4", _tint: "0.39997558519241921" },
          last:     { _theme: "4", _tint: "0.39997558519241921" },
          high:     { _theme: "4" },
          low:      { _theme: "4" }
        },
        {   # 1
          series:   { _theme: "4", _tint: "-0.499984740745262" },
          negative: { _theme: "5" },
          markers:  { _theme: "4", _tint: "-0.499984740745262" },
          first:    { _theme: "4", _tint: "0.39997558519241921" },
          last:     { _theme: "4", _tint: "0.39997558519241921" },
          high:     { _theme: "4" },
          low:      { _theme: "4" }
        },
        {   # 2
          series:   { _theme: "5", _tint: "-0.499984740745262" },
          negative: { _theme: "6" },
          markers:  { _theme: "5", _tint: "-0.499984740745262" },
          first:    { _theme: "5", _tint: "0.39997558519241921" },
          last:     { _theme: "5", _tint: "0.39997558519241921" },
          high:     { _theme: "5" },
          low:      { _theme: "5" }
        },
        {   # 3
          series:   { _theme: "6", _tint: "-0.499984740745262" },
          negative: { _theme: "7" },
          markers:  { _theme: "6", _tint: "-0.499984740745262" },
          first:    { _theme: "6", _tint: "0.39997558519241921" },
          last:     { _theme: "6", _tint: "0.39997558519241921" },
          high:     { _theme: "6" },
          low:      { _theme: "6" }
        },
        {   # 4
          series:   { _theme: "7", _tint: "-0.499984740745262" },
          negative: { _theme: "8" },
          markers:  { _theme: "7", _tint: "-0.499984740745262" },
          first:    { _theme: "7", _tint: "0.39997558519241921" },
          last:     { _theme: "7", _tint: "0.39997558519241921" },
          high:     { _theme: "7" },
          low:      { _theme: "7" }
        },
        {   # 5
          series:   { _theme: "8", _tint: "-0.499984740745262" },
          negative: { _theme: "9" },
          markers:  { _theme: "8", _tint: "-0.499984740745262" },
          first:    { _theme: "8", _tint: "0.39997558519241921" },
          last:     { _theme: "8", _tint: "0.39997558519241921" },
          high:     { _theme: "8" },
          low:      { _theme: "8" }
        },
        {   # 6
          series:   { _theme: "9", _tint: "-0.499984740745262" },
          negative: { _theme: "4" },
          markers:  { _theme: "9", _tint: "-0.499984740745262" },
          first:    { _theme: "9", _tint: "0.39997558519241921" },
          last:     { _theme: "9", _tint: "0.39997558519241921" },
          high:     { _theme: "9" },
          low:      { _theme: "9" }
        },
        {   # 7
          series:   { _theme: "4", _tint: "-0.249977111117893" },
          negative: { _theme: "5" },
          markers:  { _theme: "5", _tint: "-0.249977111117893" },
          first:    { _theme: "5", _tint: "-0.249977111117893" },
          last:     { _theme: "5", _tint: "-0.249977111117893" },
          high:     { _theme: "5", _tint: "-0.249977111117893" },
          low:      { _theme: "5", _tint: "-0.249977111117893" }
        },
        {   # 8
          series:   { _theme: "5", _tint: "-0.249977111117893" },
          negative: { _theme: "6" },
          markers:  { _theme: "6", _tint: "-0.249977111117893" },
          first:    { _theme: "6", _tint: "-0.249977111117893" },
          last:     { _theme: "6", _tint: "-0.249977111117893" },
          high:     { _theme: "6", _tint: "-0.249977111117893" },
          low:      { _theme: "6", _tint: "-0.249977111117893" }
        },
        {   # 9
          series:   { _theme: "6", _tint: "-0.249977111117893" },
          negative: { _theme: "7" },
          markers:  { _theme: "7", _tint: "-0.249977111117893" },
          first:    { _theme: "7", _tint: "-0.249977111117893" },
          last:     { _theme: "7", _tint: "-0.249977111117893" },
          high:     { _theme: "7", _tint: "-0.249977111117893" },
          low:      { _theme: "7", _tint: "-0.249977111117893" }
        },
        {   # 10
          series:   { _theme: "7", _tint: "-0.249977111117893" },
          negative: { _theme: "8" },
          markers:  { _theme: "8", _tint: "-0.249977111117893" },
          first:    { _theme: "8", _tint: "-0.249977111117893" },
          last:     { _theme: "8", _tint: "-0.249977111117893" },
          high:     { _theme: "8", _tint: "-0.249977111117893" },
          low:      { _theme: "8", _tint: "-0.249977111117893" }
        },
        {   # 11
          series:   { _theme: "8", _tint: "-0.249977111117893" },
          negative: { _theme: "9" },
          markers:  { _theme: "9", _tint: "-0.249977111117893" },
          first:    { _theme: "9", _tint: "-0.249977111117893" },
          last:     { _theme: "9", _tint: "-0.249977111117893" },
          high:     { _theme: "9", _tint: "-0.249977111117893" },
          low:      { _theme: "9", _tint: "-0.249977111117893" }
        },
        {   # 12
          series:   { _theme: "9", _tint: "-0.249977111117893" },
          negative: { _theme: "4" },
          markers:  { _theme: "4", _tint: "-0.249977111117893" },
          first:    { _theme: "4", _tint: "-0.249977111117893" },
          last:     { _theme: "4", _tint: "-0.249977111117893" },
          high:     { _theme: "4", _tint: "-0.249977111117893" },
          low:      { _theme: "4", _tint: "-0.249977111117893" }
        },
        {   # 13
          series:   { _theme: "4" },
          negative: { _theme: "5" },
          markers:  { _theme: "4", _tint: "-0.249977111117893" },
          first:    { _theme: "4", _tint: "-0.249977111117893" },
          last:     { _theme: "4", _tint: "-0.249977111117893" },
          high:     { _theme: "4", _tint: "-0.249977111117893" },
          low:      { _theme: "4", _tint: "-0.249977111117893" }
        },
        {   # 14
          series:   { _theme: "5" },
          negative: { _theme: "6" },
          markers:  { _theme: "5", _tint: "-0.249977111117893" },
          first:    { _theme: "5", _tint: "-0.249977111117893" },
          last:     { _theme: "5", _tint: "-0.249977111117893" },
          high:     { _theme: "5", _tint: "-0.249977111117893" },
          low:      { _theme: "5", _tint: "-0.249977111117893" }
        },
        {   # 15
          series:   { _theme: "6" },
          negative: { _theme: "7" },
          markers:  { _theme: "6", _tint: "-0.249977111117893" },
          first:    { _theme: "6", _tint: "-0.249977111117893" },
          last:     { _theme: "6", _tint: "-0.249977111117893" },
          high:     { _theme: "6", _tint: "-0.249977111117893" },
          low:      { _theme: "6", _tint: "-0.249977111117893" }
        },
        {   # 16
          series:   { _theme: "7" },
          negative: { _theme: "8" },
          markers:  { _theme: "7", _tint: "-0.249977111117893" },
          first:    { _theme: "7", _tint: "-0.249977111117893" },
          last:     { _theme: "7", _tint: "-0.249977111117893" },
          high:     { _theme: "7", _tint: "-0.249977111117893" },
          low:      { _theme: "7", _tint: "-0.249977111117893" }
        },
        {   # 17
          series:   { _theme: "8" },
          negative: { _theme: "9" },
          markers:  { _theme: "8", _tint: "-0.249977111117893" },
          first:    { _theme: "8", _tint: "-0.249977111117893" },
          last:     { _theme: "8", _tint: "-0.249977111117893" },
          high:     { _theme: "8", _tint: "-0.249977111117893" },
          low:      { _theme: "8", _tint: "-0.249977111117893" }
        },
        {   # 18
          series:   { _theme: "9" },
          negative: { _theme: "4" },
          markers:  { _theme: "9", _tint: "-0.249977111117893" },
          first:    { _theme: "9", _tint: "-0.249977111117893" },
          last:     { _theme: "9", _tint: "-0.249977111117893" },
          high:     { _theme: "9", _tint: "-0.249977111117893" },
          low:      { _theme: "9", _tint: "-0.249977111117893" }
        },
        {   # 19
          series:   { _theme: "4", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "4", _tint: "0.79998168889431442" },
          first:    { _theme: "4", _tint: "-0.249977111117893" },
          last:     { _theme: "4", _tint: "-0.249977111117893" },
          high:     { _theme: "4", _tint: "-0.499984740745262" },
          low:      { _theme: "4", _tint: "-0.499984740745262" }
        },
        {   # 20
          series:   { _theme: "5", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "5", _tint: "0.79998168889431442" },
          first:    { _theme: "5", _tint: "-0.249977111117893" },
          last:     { _theme: "5", _tint: "-0.249977111117893" },
          high:     { _theme: "5", _tint: "-0.499984740745262" },
          low:      { _theme: "5", _tint: "-0.499984740745262" }
        },
        {   # 21
          series:   { _theme: "6", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "6", _tint: "0.79998168889431442" },
          first:    { _theme: "6", _tint: "-0.249977111117893" },
          last:     { _theme: "6", _tint: "-0.249977111117893" },
          high:     { _theme: "6", _tint: "-0.499984740745262" },
          low:      { _theme: "6", _tint: "-0.499984740745262" }
        },
        {   # 22
          series:   { _theme: "7", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "7", _tint: "0.79998168889431442" },
          first:    { _theme: "7", _tint: "-0.249977111117893" },
          last:     { _theme: "7", _tint: "-0.249977111117893" },
          high:     { _theme: "7", _tint: "-0.499984740745262" },
          low:      { _theme: "7", _tint: "-0.499984740745262" }
        },
        {   # 23
          series:   { _theme: "8", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "8", _tint: "0.79998168889431442" },
          first:    { _theme: "8", _tint: "-0.249977111117893" },
          last:     { _theme: "8", _tint: "-0.249977111117893" },
          high:     { _theme: "8", _tint: "-0.499984740745262" },
          low:      { _theme: "8", _tint: "-0.499984740745262" }
        },
        {   # 24
          series:   { _theme: "9", _tint: "0.39997558519241921" },
          negative: { _theme: "0", _tint: "-0.499984740745262" },
          markers:  { _theme: "9", _tint: "0.79998168889431442" },
          first:    { _theme: "9", _tint: "-0.249977111117893" },
          last:     { _theme: "9", _tint: "-0.249977111117893" },
          high:     { _theme: "9", _tint: "-0.499984740745262" },
          low:      { _theme: "9", _tint: "-0.499984740745262" }
        },
        {   # 25
          series:   { _theme: "1", _tint: "0.499984740745262" },
          negative: { _theme: "1", _tint: "0.249977111117893" },
          markers:  { _theme: "1", _tint: "0.249977111117893" },
          first:    { _theme: "1", _tint: "0.249977111117893" },
          last:     { _theme: "1", _tint: "0.249977111117893" },
          high:     { _theme: "1", _tint: "0.249977111117893" },
          low:      { _theme: "1", _tint: "0.249977111117893" }
        },
        {   # 26
          series:   { _theme: "1", _tint: "0.34998626667073579" },
          negative: { _theme: "0", _tint: "-0.249977111117893" },
          markers:  { _theme: "0", _tint: "-0.249977111117893" },
          first:    { _theme: "0", _tint: "-0.249977111117893" },
          last:     { _theme: "0", _tint: "-0.249977111117893" },
          high:     { _theme: "0", _tint: "-0.249977111117893" },
          low:      { _theme: "0", _tint: "-0.249977111117893" }
        },
        {   # 27
          series:   { _rgb: "FF323232" },
          negative: { _rgb: "FFD00000" },
          markers:  { _rgb: "FFD00000" },
          first:    { _rgb: "FFD00000" },
          last:     { _rgb: "FFD00000" },
          high:     { _rgb: "FFD00000" },
          low:      { _rgb: "FFD00000" }
        },
        {   # 28
          series:   { _rgb: "FF000000" },
          negative: { _rgb: "FF0070C0" },
          markers:  { _rgb: "FF0070C0" },
          first:    { _rgb: "FF0070C0" },
          last:     { _rgb: "FF0070C0" },
          high:     { _rgb: "FF0070C0" },
          low:      { _rgb: "FF0070C0" }
        },
        {   # 29
          series:   { _rgb: "FF376092" },
          negative: { _rgb: "FFD00000" },
          markers:  { _rgb: "FFD00000" },
          first:    { _rgb: "FFD00000" },
          last:     { _rgb: "FFD00000" },
          high:     { _rgb: "FFD00000" },
          low:      { _rgb: "FFD00000" }
        },
        {   # 30
          series:   { _rgb: "FF0070C0" },
          negative: { _rgb: "FF000000" },
          markers:  { _rgb: "FF000000" },
          first:    { _rgb: "FF000000" },
          last:     { _rgb: "FF000000" },
          high:     { _rgb: "FF000000" },
          low:      { _rgb: "FF000000" }
        },
        {   # 31
          series:   { _rgb: "FF5F5F5F" },
          negative: { _rgb: "FFFFB620" },
          markers:  { _rgb: "FFD70077" },
          first:    { _rgb: "FF5687C2" },
          last:     { _rgb: "FF359CEB" },
          high:     { _rgb: "FF56BE79" },
          low:      { _rgb: "FFFF5055" }
        },
        {   # 32
          series:   { _rgb: "FF5687C2" },
          negative: { _rgb: "FFFFB620" },
          markers:  { _rgb: "FFD70077" },
          first:    { _rgb: "FF777777" },
          last:     { _rgb: "FF359CEB" },
          high:     { _rgb: "FF56BE79" },
          low:      { _rgb: "FFFF5055" }
        },
        {   # 33
          series:   { _rgb: "FFC6EFCE" },
          negative: { _rgb: "FFFFC7CE" },
          markers:  { _rgb: "FF8CADD6" },
          first:    { _rgb: "FFFFDC47" },
          last:     { _rgb: "FFFFEB9C" },
          high:     { _rgb: "FF60D276" },
          low:      { _rgb: "FFFF5367" }
        },
        {   # 34
          series:   { _rgb: "FF00B050" },
          negative: { _rgb: "FFFF0000" },
          markers:  { _rgb: "FF0070C0" },
          first:    { _rgb: "FFFFC000" },
          last:     { _rgb: "FFFFC000" },
          high:     { _rgb: "FF00B050" },
          low:      { _rgb: "FFFF0000" }
        },
        {   # 35
          series:   { _theme: "3" },
          negative: { _theme: "9" },
          markers:  { _theme: "8" },
          first:    { _theme: "4" },
          last:     { _theme: "5" },
          high:     { _theme: "6" },
          low:      { _theme: "7" }
        },
        {   # 36
          series:   { _theme: "1" },
          negative: { _theme: "9" },
          markers:  { _theme: "8" },
          first:    { _theme: "4" },
          last:     { _theme: "5" },
          high:     { _theme: "6" },
          low:      { _theme: "7" }
        }
      ]
    end
  end
	class Worksheet
		class CellData   # :nodoc:
      include Writexlsx_kot::Utility

      attr_reader :xf

      #
      # attributes for the <cell> element. This is the innermost loop so efficiency is
      # important where possible.
      #
      def cell_attributes(worksheet, row, row_name, col) # :nodoc:
        xf_index = xf ? xf.get_xf_index : 0
        attributes = [
          ['r', xl_rowcol_to_cell(row_name, col)]
        ]

        # Add the cell format index.
        if xf_index != 0
          attributes << ['s', xf_index]
        elsif worksheet.set_rows[row] && worksheet.set_rows[row][1]
          row_xf = worksheet.set_rows[row][1]
          attributes << ['s', row_xf.get_xf_index]
        elsif worksheet.col_info[col] && worksheet.col_info[col].format
          col_xf = worksheet.col_info[col].format
          attributes << ['s', col_xf.get_xf_index]
        end
        attributes
      end

      def display_url_string?
        true
      end
    end

		class NumberCellData < CellData # :nodoc:
      attr_reader :token

      def initialize(num, xf)
        @token = num
        @xf = xf
      end

      def data
        @token
      end

      def write_cell(worksheet, row, row_name, col)
        worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, row_name, col)) do
          worksheet.write_cell_value(token)
        end
      end
    end

		class StringCellData < CellData # :nodoc:
      attr_reader :token, :raw_string

      def initialize(index, xf, raw_string)
        @token = index
        @xf = xf
        @raw_string = raw_string
      end

      def data
        { sst_id: token }
      end

      TYPE_STR_ATTRS = %w[t s].freeze
      def write_cell(worksheet, row, row_name, col)
        attributes = cell_attributes(worksheet, row, row_name, col)
        attributes << TYPE_STR_ATTRS
        worksheet.writer.tag_elements('c', attributes) do
          worksheet.write_cell_value(token)
        end
      end

      def display_url_string?
        false
      end
    end

		class RichStringCellData < StringCellData # :nodoc:
    end

		class DateTimeCellData < NumberCellData # :nodoc:
    end

		class FormulaCellData < CellData # :nodoc:
      attr_reader :token, :result, :range, :link_type, :url

      def initialize(formula, xf, result)
        @token = formula
        @xf = xf
        @result = result
      end

      def data
        @result || 0
      end

      def write_cell(worksheet, row, row_name, col)
        truefalse = { 'TRUE' => 1, 'FALSE' => 0 }
        error_code = ['#DIV/0!', '#N/A', '#NAME?', '#NULL!', '#NUM!', '#REF!', '#VALUE!']

        attributes = cell_attributes(worksheet, row, row_name, col)
        if @result && !(@result.to_s =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)
          if truefalse[@result]
            attributes << %w[t b]
            @result = truefalse[@result]
          elsif error_code.include?(@result)
            attributes << %w[t e]
          else
            attributes << %w[t str]
          end
        end
        worksheet.writer.tag_elements('c', attributes) do
          worksheet.write_cell_formula(token)
          worksheet.write_cell_value(result || 0)
        end
      end
    end

		class FormulaArrayCellData < CellData # :nodoc:
      attr_reader :token, :result, :range, :link_type, :url

      def initialize(formula, xf, range, result)
        @token = formula
        @xf = xf
        @range = range
        @result = result
      end

      def data
        @result || 0
      end

      def write_cell(worksheet, row, row_name, col)
        worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, row_name, col)) do
          worksheet.write_cell_array_formula(token, range)
          worksheet.write_cell_value(result)
        end
      end
    end

		class DynamicFormulaArrayCellData < CellData # :nodoc:
      attr_reader :token, :result, :range, :link_type, :url

      def initialize(formula, xf, range, result)
        @token = formula
        @xf = xf
        @range = range
        @result = result
      end

      def data
        @result || 0
      end

      def write_cell(worksheet, row, row_name, col)
        # Add metadata linkage for dynamic array formulas.
        attributes = cell_attributes(worksheet, row, row_name, col)
        attributes << %w[cm 1]

        worksheet.writer.tag_elements('c', attributes) do
          worksheet.write_cell_array_formula(token, range)
          worksheet.write_cell_value(result)
        end
      end
    end

		class BooleanCellData < CellData # :nodoc:
      attr_reader :token

      def initialize(val, xf)
        @token = val
        @xf = xf
      end

      def data
        @token
      end

      def write_cell(worksheet, row, row_name, col)
        attributes = cell_attributes(worksheet, row, row_name, col)

        attributes << %w[t b]
        worksheet.writer.tag_elements('c', attributes) do
          worksheet.write_cell_value(token)
        end
      end
    end

		class BlankCellData < CellData # :nodoc:
      def initialize(xf)
        @xf = xf
      end

      def data
        ''
      end

      def write_cell(worksheet, row, row_name, col)
        worksheet.writer.empty_tag('c', cell_attributes(worksheet, row, row_name, col))
      end
    end
		
		class DataValidation   # :nodoc:
      include Writexlsx_kot::Utility

      attr_reader :value, :source, :minimum, :maximum, :validate, :criteria
      attr_reader :error_type, :cells, :other_cells
      attr_reader :ignore_blank, :dropdown, :show_input, :show_error
      attr_reader :error_title, :error_message, :input_title, :input_message

      def initialize(*args)
        # Check for a cell reference in A1 notation and substitute row and column.
        if (row_col_array = row_col_notation(args.first))
          case row_col_array.size
          when 2
            row1, col1 = row_col_array
            row2, col2, options = args[1..-1]
          when 4
            row1, col1, row2, col2 = row_col_array
            options = args[1]
          end
        else
          row1, col1, row2, col2, options = args
        end

        if row2.respond_to?(:keys)
          options_to_instance_variable(row2.dup)
          row2 = row1
          col2 = col1
        elsif options.respond_to?(:keys)
          options_to_instance_variable(options.dup)
        else
          raise WriteXLSXInsufficientArgumentError
        end
        raise WriteXLSXInsufficientArgumentError if [row1, col1, row2, col2].include?(nil)

        check_for_valid_input_params

        check_dimensions(row1, col1)
        check_dimensions(row2, col2)
        @cells = [[row1, col1, row2, col2]]

        @value = @source  if @source
        @value = @minimum if @minimum

        @validate = valid_validation_type[@validate.downcase]

        # No action is required for validate type 'any'
        # unless there are input messages.
        if @validate == 'none' && !@input_message && !@input_title
          @validate_none = true
          return
        end

        # The any, list and custom validations don't have a criteria
        # so we use a default of 'between'
        if %w[none list custom].include?(@validate)
          @criteria  = 'between'
          @maximum   = nil
        end

        check_criteria_required
        check_valid_citeria_types
        @criteria = valid_criteria_type[@criteria.downcase]

        check_maximum_value_when_criteria_is_between_or_notbetween
        @error_type = has_key?(:error_type) ? error_type_hash[@error_type.downcase] : 0

        convert_date_time_value_if_required
        # Check that the input title doesn't exceed the maximum length.
        raise "Length of input title '#{@input_title}' exceeds Excel's limit of 32" if @input_title && @input_title.length > 32
        # Check that the input message doesn't exceed the maximum length.
        raise "Length of input message '#{@input_message}' exceeds Excel's limit of 255" if @input_message && @input_message.length > 255

        set_some_defaults

        # A (for now) undocumented parameter to pass additional cell ranges.
        @other_cells.each { |cells| @cells << cells } if has_key?(:other_cells)
      end

      def options_to_instance_variable(params)
        params.each do |k, v|
          instance_variable_set("@#{k}", v)
        end
      end

      def keys
        instance_variables.collect { |v| v.to_s.sub(/@/, '').to_sym }
      end

      def validate_none?
        @validate_none
      end

      #
      # Write the <dataValidation> element.
      #
      def write_data_validation(writer) # :nodoc:
        @writer = writer
        if @validate == 'none'
          @writer.empty_tag('dataValidation', attributes)
        else
          @writer.tag_elements('dataValidation', attributes) do
            # Write the formula1 element.
            write_formula_1(@value)
            # Write the formula2 element.
            write_formula_2(@maximum) if @maximum
          end
        end
      end

      private

      #
      # Write the <formula1> element.
      #
      def write_formula_1(formula) # :nodoc:
        # Convert a list array ref into a comma separated string.
        formula   = %("#{formula.join(",")}") if formula.is_a?(Array)

        formula = formula.sub(/^=/, '') if formula.respond_to?(:sub)

        @writer.data_element('formula1', formula)
      end

      #
      # Write the <formula2> element.
      #
      def write_formula_2(formula) # :nodoc:
        formula = formula.sub(/^=/, '') if formula.respond_to?(:sub)

        @writer.data_element('formula2', formula)
      end

      def attributes
        sqref      = ''
        attributes = []

        # Set the cell range(s) for the data validation.
        @cells.each do |cells|
          # Add a space between multiple cell ranges.
          sqref += ' ' if sqref != ''

          row_first, col_first, row_last, col_last = cells

          # Swap last row/col for first row/col as necessary
          row_first, row_last = row_last, row_first if row_first > row_last
          col_first, col_last = col_last, col_first if col_first > col_last

          sqref += xl_range(row_first, row_last, col_first, col_last)
        end

        if @validate != 'none'
          attributes << ['type', @validate]
          attributes << ['operator', @criteria] if @criteria != 'between'
        end

        if @error_type
          attributes << %w[errorStyle warning] if @error_type == 1
          attributes << %w[errorStyle information] if @error_type == 2
        end
        attributes << ['allowBlank',       1] if @ignore_blank != 0
        attributes << ['showDropDown',     1] if @dropdown     == 0
        attributes << ['showInputMessage', 1] if @show_input   != 0
        attributes << ['showErrorMessage', 1] if @show_error   != 0

        attributes << ['errorTitle',  @error_title]   if @error_title
        attributes << ['error',       @error_message] if @error_message
        attributes << ['promptTitle', @input_title]   if @input_title
        attributes << ['prompt',      @input_message] if @input_message
        attributes << ['sqref',       sqref]
      end

      def has_key?(key)
        keys.index(key)
      end

      def set_some_defaults
        @ignore_blank ||= 1
        @dropdown     ||= 1
        @show_input   ||= 1
        @show_error   ||= 1
      end

      def check_for_valid_input_params
        check_parameter(self, valid_validation_parameter, 'data_validation')

        raise WriteXLSXOptionParameterError, "Parameter :validate is required in data_validation()" unless has_key?(:validate)

        unless valid_validation_type.has_key?(@validate.downcase)
          raise WriteXLSXOptionParameterError,
                "Unknown validation type '#{@validate}' for parameter :validate in data_validation()"
        end
        if @error_type && !error_type_hash.has_key?(@error_type.downcase)
          raise WriteXLSXOptionParameterError,
                "Unknown criteria type '#param[:error_type}' for parameter :error_type in data_validation()"
        end
      end

      def check_criteria_required
        raise WriteXLSXOptionParameterError, "Parameter :criteria is required in data_validation()" unless has_key?(:criteria)
      end

      def check_maximum_value_when_criteria_is_between_or_notbetween
        if @criteria == 'between' || @criteria == 'notBetween'
          unless has_key?(:maximum)
            raise WriteXLSXOptionParameterError,
                  "Parameter :maximum is required in data_validation() when using :between or :not between criteria"
          end
        else
          @maximum = nil
        end
      end

      def check_valid_citeria_types
        unless valid_criteria_type.has_key?(@criteria.downcase)
          raise WriteXLSXOptionParameterError,
                "Unknown criteria type '#{@criteria}' for parameter :criteria in data_validation()"
        end
      end

      def convert_date_time_value_if_required
        if @validate == 'date' || @validate == 'time'
          date_time = convert_date_time(@value)
          @value = date_time if date_time

          if @maximum
            date_time = convert_date_time(@maximum)
            @maximum = date_time if date_time
          end
        end
      end

      def error_type_hash
        { 'stop' => 0, 'warning' => 1, 'information' => 2 }
      end

      def valid_validation_type # :nodoc:
        {
          'any'          => 'none',
          'any value'    => 'none',
          'whole number' => 'whole',
          'whole'        => 'whole',
          'integer'      => 'whole',
          'decimal'      => 'decimal',
          'list'         => 'list',
          'date'         => 'date',
          'time'         => 'time',
          'text length'  => 'textLength',
          'length'       => 'textLength',
          'custom'       => 'custom'
        }
      end

      # List of valid input parameters.
      def valid_validation_parameter
        %i[
          validate
          criteria
          value
          source
          minimum
          maximum
          ignore_blank
          dropdown
          show_input
          input_title
          input_message
          show_error
          error_title
          error_message
          error_type
          other_cells
        ]
      end

      # List of valid criteria types.
      def valid_criteria_type  # :nodoc:
        {
          'between'                  => 'between',
          'not between'              => 'notBetween',
          'equal to'                 => 'equal',
          '='                        => 'equal',
          '=='                       => 'equal',
          'not equal to'             => 'notEqual',
          '!='                       => 'notEqual',
          '<>'                       => 'notEqual',
          'greater than'             => 'greaterThan',
          '>'                        => 'greaterThan',
          'less than'                => 'lessThan',
          '<'                        => 'lessThan',
          'greater than or equal to' => 'greaterThanOrEqual',
          '>='                       => 'greaterThanOrEqual',
          'less than or equal to'    => 'lessThanOrEqual',
          '<='                       => 'lessThanOrEqual'
        }
      end

      def date_1904?
        @date_1904
      end
    end
		
		class Hyperlink   # :nodoc:
      include Writexlsx_kot::Utility

      attr_reader :str, :tip

      MAXIMUM_URLS_SIZE = 2079

      def self.factory(url, str = nil, tip = nil, max_url_length = MAXIMUM_URLS_SIZE)
        if url =~ /^internal:(.+)/
          InternalHyperlink.new($~[1], str, tip, max_url_length)
        elsif url =~ /^external:(.+)/
          ExternalHyperlink.new($~[1], str, tip, max_url_length)
        else
          new(url, str, tip, max_url_length)
        end
      end

      def initialize(url, str, tip, max_url_length)
        # The displayed string defaults to the url string.
        str ||= url.dup

        # Strip the mailto header.
        normalized_str = str.sub(/^mailto:/, '')

        # Split url into the link and optional anchor/location.
        url, @url_str = url.split("#", 2)

        # Escape URL unless it looks already escaped.
        url = escape_url(url)

        # Excel limits the escaped URL and location/anchor to 255 characters.
        raise "Ignoring URL '#{url}' where link or anchor > #{max_url_length} characters since it exceeds Excel's limit for URLS. See LIMITATIONS section of the Excel::Writer::XLSX documentation." if url.bytesize > max_url_length || (!@url_str.nil? && @url_str.bytesize > max_url_length)

        @url       = url
        @str       = normalized_str
        @tip       = tip
      end

      def attributes(row, col, id)
        ref = xl_rowcol_to_cell(row, col)

        attr = [['ref', ref]]
        attr << r_id_attributes(id)

        attr << ['location', @url_str] if @url_str
        attr << ['display',  @display] if @display
        attr << ['tooltip',  @tip]     if @tip
        attr
      end

      def external_hyper_link
        ['/hyperlink', @url, 'External']
      end

      def display_on
        @display = @url_str
      end
    end

		class InternalHyperlink < Hyperlink
      undef external_hyper_link

      def initialize(url, str, tip, max_url_length)
        @url = url
        # The displayed string defaults to the url string.
        str ||= @url.dup

        # Strip the mailto header.
        @str = str.sub(/^mailto:/, '')

        # Copy string for use in hyperlink elements.
        @url_str = @str.dup

        # Excel limits escaped URL to #{max_url_length} characters.
        raise "URL '#{@url}' > #{max_url_length} characters, it exceeds Excel's limit for URLS." if @url.bytesize > max_url_length

        @tip = tip
      end

      def attributes(row, col, _dummy = nil)
        attr = [
          ['ref', xl_rowcol_to_cell(row, col)],
          ['location', @url]
        ]

        attr << ['tooltip', @tip] if @tip
        attr << ['display', @str]
      end
    end

		class ExternalHyperlink < Hyperlink
      def initialize(url, str, tip, max_url_length)
        # The displayed string defaults to the url string.
        str ||= url.dup

        # For external links change the directory separator from Unix to Dos.
        url = url.gsub(%r{/}, '\\')
        str = str.gsub(%r{/}, '\\')

        # Strip the mailto header.
        str = str.sub(/^mailto:/, '')

        # Split url into the link and optional anchor/location.
        url, url_str = url.split("#", 2)

        # Escape URL unless it looks already escaped.
        url = escape_url(url)

        # Add the file:/// URI to the url if non-local.
        if url =~ /:/ ||        # Windows style "C:/" link.
           url =~ /^\\\\/        # Network share.
          url = "file:///#{url}"
        end

        # Convert a ./dir/file.xlsx link to dir/file.xlsx.
        url = url.sub(/^.\\/, '')
        @url_str   = url_str

        # Excel limits the escaped URL and location/anchor to max_url_length characters.
        raise "Ignoring URL '#{url}' where link or anchor > #{max_url_length} characters since it exceeds Excel's limit for URLS. See LIMITATIONS section of the Excel::Writer::XLSX documentation." if url.bytesize > max_url_length || (!@url_str.nil? && @url_str.bytesize > max_url_length)

        @url       = url
        @str       = str
        @tip       = tip
      end
    end
		
		class PageSetup # :nodoc:
      include Writexlsx_kot::Utility

      attr_accessor :margin_left, :margin_right, :margin_top, :margin_bottom  # :nodoc:
      attr_accessor :margin_header, :margin_footer                            # :nodoc:
      attr_accessor :repeat_rows, :repeat_cols, :print_area                   # :nodoc:
      attr_accessor :hbreaks, :vbreaks, :scale                                # :nodoc:
      attr_accessor :fit_page, :fit_width, :fit_height, :page_setup_changed   # :nodoc:
      attr_writer :across                                                   # :nodoc:
      attr_accessor :orientation, :print_options_changed, :black_white  # :nodoc:
      attr_accessor :header, :footer, :header_footer_changed, :header_footer_aligns, :header_footer_scales
      attr_writer :page_start
      attr_writer :horizontal_dpi, :vertical_dpi

      def initialize # :nodoc:
        @margin_left = 0.7
        @margin_right = 0.7
        @margin_top = 0.75
        @margin_bottom = 0.75
        @margin_header = 0.3
        @margin_footer = 0.3
        @repeat_rows   = ''
        @repeat_cols   = ''
        @print_area    = ''
        @hbreaks = []
        @vbreaks = []
        @scale = 100
        @fit_page = false
        @fit_width  = nil
        @fit_height = nil
        @page_setup_changed = false
        @across = false
        @orientation = true
        @header_footer_aligns = true
        @header_footer_scales = true
      end

      def paper=(paper_size)
        if paper_size
          @paper_size = paper_size
          @page_setup_changed = true
        end
      end

      def center_horizontally
        @print_options_changed = true
        @hcenter               = true
      end

      def center_vertically
        @print_options_changed = true
        @vcenter               = true
      end

      def print_row_col_headers(headers)
        if headers
          @print_headers         = true
          @print_options_changed = true
        else
          @print_headers         = false
        end
      end

      def hide_gridlines(option)
        if option == 0 || !option
          @print_gridlines = true
          @print_options_changed = true
        else
          @print_gridlines  = false
        end
      end

      #
      # Write the <pageSetup> element.
      #
      # The following is an example taken from Excel.
      #
      # <pageSetup
      #     paperSize="9"
      #     scale="110"
      #     fitToWidth="2"
      #     fitToHeight="2"
      #     pageOrder="overThenDown"
      #     orientation="portrait"
      #     useFirstPageNumber="1"
      #     blackAndWhite="1"
      #     draft="1"
      #     horizontalDpi="200"
      #     verticalDpi="200"
      #     r:id="rId1"
      # />
      #
      def write_page_setup(writer) # :nodoc:
        return unless @page_setup_changed

        attributes = []
        attributes << ['paperSize',       @paper_size]    if @paper_size
        attributes << ['scale',           @scale]         if @scale != 100
        attributes << ['fitToWidth',      @fit_width]     if @fit_page && @fit_width != 1
        attributes << ['fitToHeight',     @fit_height]    if @fit_page && @fit_height != 1
        attributes << %w[pageOrder overThenDown] if @across
        attributes << ['firstPageNumber', @page_start]    if @page_start && @page_start > 1
        attributes << ['orientation',
                       if @orientation
                         'portrait'
                       else
                         'landscape'
                       end]
        attributes << ['blackAndWhite', 1]      if @black_white
        attributes << ['useFirstPageNumber', 1] if ptrue?(@page_start)

        # Set the DPI. Mainly only for testing.
        attributes << ['horizontalDpi',  @horizontal_dpi] if @horizontal_dpi
        attributes << ['verticalDpi',    @vertical_dpi]   if @vertical_dpi

        writer.empty_tag('pageSetup', attributes)
      end

      #
      # Write the <pageMargins> element.
      #
      def write_page_margins(writer) # :nodoc:
        writer.empty_tag('pageMargins', margin_attributes)
      end

      #
      # Write the <printOptions> element.
      #
      def write_print_options(writer) # :nodoc:
        return unless @print_options_changed

        attributes = []
        attributes << ['horizontalCentered', 1] if @hcenter
        attributes << ['verticalCentered',   1] if @vcenter
        attributes << ['headings',           1] if @print_headers
        attributes << ['gridLines',          1] if @print_gridlines
        writer.empty_tag('printOptions', attributes)
      end

      #
      # Write the <headerFooter> element.
      #
      def write_header_footer(writer, excel2003_style) # :nodoc:
        tag = 'headerFooter'
        attributes = []
        attributes << ['scaleWithDoc', 0]     unless ptrue?(@header_footer_scales)
        attributes << ['alignWithMargins', 0] unless ptrue?(@header_footer_aligns)

        if @header_footer_changed
          writer.tag_elements(tag, attributes) do
            write_odd_header(writer) if @header && @header != ''
            write_odd_footer(writer) if @footer && @footer != ''
          end
        elsif excel2003_style
          writer.empty_tag(tag, attributes)
        end
      end

      private

      #
      # Write the <oddHeader> element.
      #
      def write_odd_header(writer) # :nodoc:
        writer.data_element('oddHeader', @header)
      end

      #
      # Write the <oddFooter> element.
      #
      def write_odd_footer(writer) # :nodoc:
        writer.data_element('oddFooter', @footer)
      end

      def margin_attributes    # :nodoc:
        [
          ['left',   @margin_left],
          ['right',  @margin_right],
          ['top',    @margin_top],
          ['bottom', @margin_bottom],
          ['header', @margin_header],
          ['footer', @margin_footer]
        ]
      end
    end
  
  end
	class Chartsheet < Worksheet
    include Writexlsx_kot::Utility

    attr_writer :chart

    def initialize(workbook, index, name)
      super
      @drawings          = Drawings.new
      @is_chartsheet     = true
      @chart             = nil
      @charts            = [1]
      @zoom_scale_normal = 0
      @page_setup.orientation = false
    end

    #
    # Assemble and write the XML file.
    #
    def assemble_xml_file # :nodoc:
      return unless @writer

      write_xml_declaration do
        # Write the root chartsheet element.
        write_chartsheet do
          # Write the worksheet properties.
          write_sheet_pr
          # Write the sheet view properties.
          write_sheet_views
          # Write the sheetProtection element.
          write_sheet_protection
          # Write the printOptions element.
          write_print_options
          # Write the worksheet page_margins.
          write_page_margins
          # Write the worksheet page setup.
          write_page_setup
          # Write the headerFooter element.
          write_header_footer
          # Write the drawing element.
          write_drawings
          # Write the legaacyDrawingHF element.
          write_legacy_drawing_hf
          # Close the worksheet tag.
        end
      end
    end

    def protect(password = '', user_options = nil, options = {})
      # Objects are default on for chartsheets.
      if user_options
        options[:objects] = if user_options.has_key?(:objects)
                              if ptrue?(user_options[:objects])
                                0
                              else
                                1
                              end
                            else
                              0
                            end

        options[:content] = if user_options.has_key?(:content)
                              user_options[:content]
                            else
                              1
                            end
      else
        options[:objects] = 0
        options[:content] = 1
      end

      # Is objects and content are off then the chartsheet isn't locked.
      # except if it has a password.
      return if password == '' && ptrue?(options[:objects]) && !ptrue?(options[:content])

      @chart.protection = 1

      # Turn off worksheet defaults.
      options[:sheet]     = 0
      options[:scenarios] = 1

      super(password, options)
    end

    ###############################################################################
    #
    # Encapsulated Chart methods.
    #
    ###############################################################################

    def add_series(*args)
      @chart.add_series(*args)
    end

    def set_x_axis(*args)
      @chart.set_x_axis(*args)
    end

    def set_y_axis(*args)
      @chart.set_y_axis(*args)
    end

    def set_x2_axis(*args)
      @chart.set_x2_axis(*args)
    end

    def set_y2_axis(*args)
      @chart.set_y2_axis(*args)
    end

    def set_title(*args)
      @chart.set_title(*args)
    end

    def set_legend(*args)
      @chart.set_legend(*args)
    end

    def set_plotarea(*args)
      @chart.set_plotarea(*args)
    end

    def set_chartarea(*args)
      @chart.set_chartarea(*args)
    end

    def set_style(*args)
      @chart.set_style(*args)
    end

    def show_blanks_as(*args)
      @chart.show_blanks_as(*args)
    end

    def show_na_as_empty_cell
      @chart.show_na_as_empty_cell(*args)
    end

    def show_hidden_data(*args)
      @chart.show_hidden_data(*args)
    end

    def set_size(*args)
      @chart.set_size(*args)
    end

    def set_table(*args)
      @chart.set_table(*args)
    end

    def set_up_down_bars(*args)
      @chart.set_up_down_bars(*args)
    end

    def set_drop_lines(*args)
      @chart.set_drop_lines(*args)
    end

    def set_high_low_lines(*args)
      @chart.set_high_low_lines(*args)
    end

    #
    # Set up chart/drawings.
    #
    def prepare_chart(_index, chart_id, drawing_id) # :nodoc:
      @chart.id = chart_id - 1

      drawings  = Drawings.new
      @drawings = drawings
      @drawings.orientation = @page_setup.orientation

      @external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]

      @drawing_links << ['/chart', "../charts/chart#{chart_id}.xml"]
    end

    def external_links
      [
        @external_drawing_links,
        @external_vml_links
      ]
    end

    private

    #
    # Write the <chartsheet> element. This is the root element of Chartsheet.
    #
    def write_chartsheet(&block) # :nodoc:
      schema                 = 'http://schemas.openxmlformats.org/'
      xmlns                  = schema + 'spreadsheetml/2006/main'
      xmlns_r                = schema + 'officeDocument/2006/relationships'

      attributes = [
        ['xmlns',   xmlns],
        ['xmlns:r', xmlns_r]
      ]

      @writer.tag_elements('chartsheet', attributes, &block)
    end

    #
    # Write the <sheetPr> element for Sheet level properties.
    #
    def write_sheet_pr # :nodoc:
      attributes = []

      attributes << ['filterMode', 1] if ptrue?(@filter_on)

      if ptrue?(@fit_page) || ptrue?(@tab_color)
        @writer.tag_elements('sheetPr', attributes) do
          write_tab_color
          write_page_set_up_pr
        end
      else
        @writer.empty_tag('sheetPr', attributes)
      end
    end
  end
	class Formats
    include Writexlsx_kot::Utility

    attr_reader :formats, :xf_format_indices, :dxf_format_indices

    def initialize
      @formats = []
      @xf_format_indices = {}
      @dxf_format_indices = {}
    end

    def xf_index_by_key(key)
      @xf_format_indices[key]
    end

    def set_xf_index_by_key(key)
      @xf_format_indices[key] ||= 1 + @xf_format_indices.size
    end

    def dxf_index_by_key(key)
      @dxf_format_indices[key]
    end

    def set_dxf_index_by_key(key)
      @dxf_format_indices[key] ||= @dxf_format_indices.size
    end
  end
	class Shape
    include Writexlsx_kot::Utility

    attr_reader :edit_as, :drawing
    attr_reader :tx_box, :fill, :line, :format
    attr_reader :align, :valign, :anchor, :adjustments
    attr_accessor :name, :connect, :type, :id, :start, :end, :rotation
    attr_accessor :flip_h, :flip_v, :palette, :text, :stencil
    attr_accessor :row_start, :row_end, :column_start, :column_end
    attr_accessor :x1, :x2, :y1, :y2, :x_abs, :y_abs, :start_index, :end_index
    attr_accessor :x_offset, :y_offset, :width, :height, :scale_x, :scale_y
    attr_accessor :width_emu, :height_emu, :element, :line_weight, :line_type
    attr_accessor :start_side, :end_side

    def initialize(properties = {})
      @writer = Package::XMLWriterSimple.new
      @name   = nil
      @type   = 'rect'

      # Is a Connector shape. 1/0 Value is a hash lookup from type.
      @connect = 0

      # Is a Drawing. Always 0, since a single shape never fills an entire sheet.
      @drawing = 0

      # OneCell or Absolute: options to move and/or size with cells.
      @edit_as = nil

      # Auto-incremented, unless supplied by user.
      @id = 0

      # Shape text (usually centered on shape geometry).
      @text = 0

      # Shape stencil mode.  A copy (child) is created when inserted.
      # The link to parent is broken.
      @stencil = 1

      # Index to _shapes array when inserted.
      @element = -1

      # Shape ID of starting connection, if any.
      @start = nil

      # Shape vertex, starts at 0, numbered clockwise from 12 o'clock.
      @start_index = nil

      @end       = nil
      @end_index = nil

      # Number and size of adjustments for shapes (usually connectors).
      @adjustments = []

      # Start and end sides. t)op, b)ottom, l)eft, or r)ight.
      @start_side = ''
      @end_side   = ''

      # Flip shape Horizontally. eg. arrow left to arrow right.
      @flip_h = 0

      # Flip shape Vertically. eg. up arrow to down arrow.
      @flip_v = 0

      # shape rotation (in degrees 0-360).
      @rotation = 0

      # An alternate way to create a text box, because Excel allows it.
      # It is just a rectangle with text.
      @tx_box = false

      # Shape outline colour, or 0 for noFill (default black).
      @line = '000000'

      # Line type: dash, sysDot, dashDot, lgDash, lgDashDot, lgDashDotDot.
      @line_type = ''

      # Line weight (integer).
      @line_weight = 1

      # Shape fill colour, or 0 for noFill (default noFill).
      @fill = 0

      # Formatting for shape text, if any.
      @format = {}

      # copy of colour palette table from Workbook.pm.
      @palette = []

      # Vertical alignment: t, ctr, b.
      @valign = 'ctr'

      # Alignment: l, ctr, r, just
      @align = 'ctr'

      @x_offset = 0
      @y_offset = 0

      # Scale factors, which also may be set when the shape is inserted.
      @scale_x = 1
      @scale_y = 1

      # Default size, which can be modified and/or scaled.
      @width  = 50
      @height = 50

      # Initial assignment. May be modified when prepared.
      @column_start = 0
      @row_start    = 0
      @x1           = 0
      @y1           = 0
      @column_end   = 0
      @row_end      = 0
      @x2           = 0
      @y2           = 0
      @x_abs        = 0
      @y_abs        = 0

      set_properties(properties)
    end

    def set_properties(properties)
      # Override default properties with passed arguments
      properties.each do |key, value|
        # Strip leading "-" from Tk style properties e.g. -color => 'red'.
        instance_variable_set("@#{key}", value)
      end
    end

    #
    # Set the shape adjustments array (as a reference).
    #
    def adjustments=(args)
      @adjustments = *args
    end

    #
    # Calculate the vertices that define the position of a shape object within
    # the worksheet in EMUs.  Save the vertices with the object.
    #
    # The vertices are expressed as English Metric Units (EMUs). There are 12,700
    # EMUs per point. Therefore, 12,700 * 3 /4 = 9,525 EMUs per pixel.
    #
    def calc_position_emus(worksheet)
      c_start, r_start,
      xx1, yy1, c_end, r_end,
      xx2, yy2, x_abslt, y_abslt =
        worksheet.position_object_pixels(
          @column_start,
          @row_start,
          @x_offset,
          @y_offset,
          @width  * @scale_x,
          @height * @scale_y
        )

      # Now that x2/y2 have been calculated with a potentially negative
      # width/height we use the absolute value and convert to EMUs.
      @width_emu  = (@width  * 9_525).abs.to_i
      @height_emu = (@height * 9_525).abs.to_i

      @column_start = c_start.to_i
      @row_start    = r_start.to_i
      @column_end   = c_end.to_i
      @row_end      = r_end.to_i

      # Convert the pixel values to EMUs. See above.
      @x1    = (xx1 * 9_525).to_i
      @y1    = (yy1 * 9_525).to_i
      @x2    = (xx2 * 9_525).to_i
      @y2    = (yy2 * 9_525).to_i
      @x_abs = (x_abslt * 9_525).to_i
      @y_abs = (y_abslt * 9_525).to_i
    end

    def set_position(row_start, column_start, x_offset, y_offset, x_scale, y_scale, anchor)
      @row_start    = row_start
      @column_start = column_start
      @x_offset     = x_offset || 0
      @y_offset     = y_offset || 0
      @anchor       = anchor   || 1

      # Override shape scale if supplied as an argument. Otherwise, use the
      # existing shape scale factors.
      @scale_x = x_scale if x_scale
      @scale_y = y_scale if y_scale
    end

    #
    # Re-size connector shapes if they are connected to other shapes.
    #
    def auto_locate_connectors(shapes, shape_hash)
      # Valid connector shapes.
      connector_shapes = {
        straightConnector: 1,
        Connector:         1,
        bentConnector:     1,
        curvedConnector:   1,
        line:              1
      }

      shape_base = @type.chop.to_sym # Remove the number of segments from end of type.
      @connect = connector_shapes[shape_base] ? 1 : 0
      return if @connect == 0

      # Both ends have to be connected to size it.
      return if @start == 0 && @end == 0

      # Both ends need to provide info about where to connect.
      return if @start_side == 0 && @end_side == 0

      sid = @start
      eid = @end

      slink_id = shape_hash[sid] || 0
      sls      = shapes.fetch(slink_id, Shape.new)
      elink_id = shape_hash[eid] || 0
      els      = shapes.fetch(elink_id, Shape.new)

      # Assume shape connections are to the middle of an object, and
      # not a corner (for now).
      connect_type = @start_side + @end_side
      smidx        = sls.x_offset + (sls.width / 2)
      emidx        = els.x_offset + (els.width / 2)
      smidy        = sls.y_offset + (sls.height / 2)
      emidy        = els.y_offset + (els.height / 2)

      if connect_type == 'bt'
        sy = sls.y_offset + sls.height
        ey = els.y_offset

        @width = (emidx - smidx).to_i.abs
        @x_offset = [smidx, emidx].min.to_i
        @height =
          (els.y_offset - (sls.y_offset + sls.height)).to_i.abs
        @y_offset =
          [sls.y_offset + sls.height, els.y_offset].min.to_i
        @flip_h = smidx < emidx ? 1 : 0
        @rotation = 90

        if sy > ey
          @flip_v = 1

          # Create 3 adjustments for an end shape vertically above a
          # start @ Adjustments count from the upper left object.
          @adjustments = [-10, 50, 110] if @adjustments.empty?
          @type = 'bentConnector5'
        end
      elsif connect_type == 'rl'
        @width =
          (els.x_offset - (sls.x_offset + sls.width)).to_i.abs
        @height = (emidy - smidy).to_i.abs
        @x_offset =
          [sls.x_offset + sls.width, els.x_offset].min
        @y_offset = [smidy, emidy].min

        @flip_h = 1 if smidx < emidx && smidy > emidy
        @flip_h = 1 if smidx > emidx && smidy < emidy

        if smidx > emidx
          # Create 3 adjustments for an end shape to the left of a
          # start @
          @adjustments = [-10, 50, 110] if @adjustments.empty?
          @type = 'bentConnector5'
        end
      end
    end

    #
    # Check shape attributes to ensure they are valid.
    #
    def validate(index)
      raise "Shape #{index} (#{@type}) alignment (#{@align}) not in ['l', 'ctr', 'r', 'just']\n" unless %w[l ctr r just].include?(@align)

      raise "Shape #{index} (#{@type}) vertical alignment (#{@valign}) not in ['t', 'ctr', 'v']\n" unless %w[t ctr b].include?(@valign)
    end

    def dimensions
      [
        @column_start, @row_start,
        @x1,           @y1,
        @column_end,   @row_end,
        @x2,           @y2,
        @x_abs,        @y_abs,
        @width_emu,    @height_emu
      ]
    end
  end
	class Table
    include Writexlsx_kot::Utility

    attr_reader :horizontal, :vertical, :outline, :show_keys, :font

    def initialize(params = {})
      @horizontal = true
      @vertical = true
      @outline = true
      @show_keys = false
      @horizontal = params[:horizontal] if params.has_key?(:horizontal)
      @vertical   = params[:vertical]   if params.has_key?(:vertical)
      @outline    = params[:outline]    if params.has_key?(:outline)
      @show_keys  = params[:show_keys]  if params.has_key?(:show_keys)
      @font       = convert_font_args(params[:font])
    end

    attr_writer :palette

    def write_d_table(writer)
      @writer = writer
      @writer.tag_elements('c:dTable') do
        @writer.empty_tag('c:showHorzBorder', attributes) if ptrue?(horizontal)
        @writer.empty_tag('c:showVertBorder', attributes) if ptrue?(vertical)
        @writer.empty_tag('c:showOutline',    attributes) if ptrue?(outline)
        @writer.empty_tag('c:showKeys',       attributes) if ptrue?(show_keys)
        # Write the table font.
        write_tx_pr(font)                                 if ptrue?(font)
      end
    end

    private

    def attributes
      [['val', 1]]
    end
  end
	class ChartArea
    include Writexlsx_kot::Utility
    include Writexlsx_kot::Gradient

    attr_reader :line, :fill, :pattern, :gradient, :layout

    def initialize(params = {})
      @layout = layout_properties(params[:layout])

      # Allow 'border' as a synonym for 'line'.
      border = params_to_border(params)

      # Set the line properties for the chartarea.
      @line = line_properties(border || params[:line])

      # Set the pattern properties for the series.
      @pattern = pattern_properties(params[:pattern])

      # Set the gradient fill properties for the series.
      @gradient = gradient_properties(params[:gradient])

      # Map deprecated Spreadsheet::WriteExcel fill colour.
      fill = params[:color] ? { color: params[:color] } : params[:fill]
      @fill = fill_properties(fill)

      # Pattern fill overrides solid fill.
      @fill = nil if ptrue?(@pattern)

      # Gradient fill overrides solid and pattern fills.
      if ptrue?(@gradient)
        @pattern = nil
        @fill    = nil
      end
    end

    private

    def params_to_border(params)
      line_weight  = params[:line_weight]

      # Map deprecated Spreadsheet::WriteExcel line_weight.
      border = params[:border]
      border = { width: swe_line_weight(line_weight) } if line_weight

      # Map deprecated Spreadsheet::WriteExcel line_pattern.
      if params[:line_pattern]
        pattern = swe_line_pattern(params[:line_pattern])
        if pattern == 'none'
          border = { none: 1 }
        else
          border[:dash_type] = pattern
        end
      end

      # Map deprecated Spreadsheet::WriteExcel line colour.
      border[:color] = params[:line_color] if params[:line_color]
      border
    end

    #
    # Get the Spreadsheet::WriteExcel line pattern for backward compatibility.
    #
    def swe_line_pattern(val)
      swe_line_pattern_hash[numeric_or_downcase(val)] || 'solid'
    end

    def swe_line_pattern_hash
      {
        0              => 'solid',
        1              => 'dash',
        2              => 'dot',
        3              => 'dash_dot',
        4              => 'long_dash_dot_dot',
        5              => 'none',
        6              => 'solid',
        7              => 'solid',
        8              => 'solid',
        'solid'        => 'solid',
        'dash'         => 'dash',
        'dot'          => 'dot',
        'dash-dot'     => 'dash_dot',
        'dash-dot-dot' => 'long_dash_dot_dot',
        'none'         => 'none',
        'dark-gray'    => 'solid',
        'medium-gray'  => 'solid',
        'light-gray'   => 'solid'
      }
    end

    #
    # Get the Spreadsheet::WriteExcel line weight for backward compatibility.
    #
    def swe_line_weight(val)
      swe_line_weight_hash[numeric_or_downcase(val)] || 1
    end

    def swe_line_weight_hash
      {
        1          => 0.25,
        2          => 1,
        3          => 2,
        4          => 3,
        'hairline' => 0.25,
        'narrow'   => 1,
        'medium'   => 2,
        'wide'     => 3
      }
    end

    def numeric_or_downcase(val)
      val.respond_to?(:coerce) ? val : val.downcase
    end
  end
	class Chart
        class Caption
      include Writexlsx_kot::Utility

      attr_accessor :name, :formula, :data_id, :name_font
      attr_reader :layout, :overlay, :none

      def initialize(chart)
        @chart = chart
      end

      def merge_with_hash(params) # :nodoc:
        @name, @formula = @chart.process_names(params[:name], params[:name_formula])
        @data_id        = @chart.data_id(@formula, params[:data])
        @name_font      = convert_font_args(params[:name_font])
        @layout   = @chart.layout_properties(params[:layout], 1)

        # Set the title overlay option.
        @overlay  = params[:overlay]

        # Set the no automatic title option.
        @none = params[:none]
      end
    end
		class Column < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @subtype = subtype || 'clustered'
        @horiz_val_axis = 0

        # Override and reset the default axis values.
        @y_axis.defaults[:num_format] = '0%' if @subtype == 'percent_stacked'

        set_y_axis

        # Set the available data label positions for this chart type.
        @label_position_default = 'outside_end'
        @label_positions = {
          'center'      => 'ctr',
          'inside_base' => 'inBase',
          'inside_end'  => 'inEnd',
          'outside_end' => 'outEnd'
        }
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:barChart element.
        write_bar_chart(params)
      end

      #
      # Write the <c:barDir> element.
      #
      def write_bar_dir
        @writer.empty_tag('c:barDir', [%w[val col]])
      end

      #
      # Write the <c:errDir> element. Overridden from Chart class since it is not
      # used in Bar charts.
      #
      def write_err_dir(direction)
        # do nothing
      end
    end
		class Pie < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @vary_data_color = 1
        @rotation        = 0

        # Set the available data label positions for this chart type.
        @label_position_default = 'best_fit'
        @label_positions = {
          'center'      => 'ctr',
          'inside_base' => 'inBase',
          'inside_end'  => 'inEnd',
          'outside_end' => 'outEnd',
          'best_fit'    => 'bestFit'
        }
      end

      #
      # Set the Pie/Doughnut chart rotation: the angle of the first slice.
      #
      def set_rotation(rotation)
        return unless rotation

        if rotation >= 0 && rotation <= 360
          @rotation = rotation
        else
          raise "Chart rotation $rotation outside range: 0 <= rotation <= 360"
        end
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type
        # Write the c:areaChart element.
        write_pie_chart
      end

      #
      # Write the <c:pieChart> element. Over-ridden method to remove axis_id code
      # since pie charts don't require val and vat axes.
      #
      def write_pie_chart
        @writer.tag_elements('c:pieChart') do
          # Write the c:varyColors element.
          write_vary_colors
          # Write the series elements.
          @series.each { |s| write_series(s) }
          # Write the c:firstSliceAng element.
          write_first_slice_ang
        end
      end

      #
      # Over-ridden method to remove the cat_axis() and val_axis() code since
      # Pie/Doughnut charts don't require those axes.
      #
      # Write the <c:plotArea> element.
      #
      def write_plot_area
        second_chart = @combined

        @writer.tag_elements('c:plotArea') do
          # Write the c:layout element.
          write_layout(@plotarea.layout, 'plot')
          # Write the subclass chart type element.
          write_chart_type
          # Configure a combined chart if present.
          if second_chart
            # Secondary axis has unique id otherwise use same as primary.
            second_chart.id = if second_chart.is_secondary?
                                1000 + @id
                              else
                                @id
                              end

            # Share the same filehandle for writing
            second_chart.writer = @writer

            # Share series index with primary chart.
            second_chart.series_index = @series_index

            # Write the subclass chart type elements for combined chart.
            second_chart.write_chart_type
          end
          # Write the c:spPr eleent for the plotarea formatting.
          write_sp_pr(@plotarea)
        end
      end

      #
      # Over-ridden method to add <c:txPr> to legend.
      #
      # Write the <c:legend> element.
      #
      def write_legend
        allowed = %w[right left top bottom]
        delete_series = @legend.delete_series || []

        if @legend.position =~ /^overlay_/
          position = @legend.position.sub(/^overlay_/, '')
          overlay = true
        else
          position = @legend.position
          overlay = false
        end

        return if position == 'none'
        return unless allowed.include?(position)

        @writer.tag_elements('c:legend') do
          # Write the c:legendPos element.
          write_legend_pos(position[0])
          # Remove series labels from the legend.
          # Write the c:legendEntry element.
          delete_series.each { |index| write_legend_entry(index) }
          # Write the c:layout element.
          write_layout(@legend.layout, 'legend')
          # Write the c:overlay element.
          write_overlay if overlay
          # Write the c:spPr element.
          write_sp_pr(@legend)
          # Write the c:txPr element. Over-ridden.
          write_tx_pr_legend(0, @legend.font)
        end
      end

      #
      # Write the <c:txPr> element for legends.
      #
      def write_tx_pr_legend(horiz, font)
        rotation = nil
        rotation = font[:_rotation] if ptrue?(font) && font[:_rotation]

        @writer.tag_elements('c:txPr') do
          # Write the a:bodyPr element.
          write_a_body_pr(rotation, horiz)
          # Write the a:lstStyle element.
          write_a_lst_style
          # Write the a:p element.
          write_a_p_legend(font)
        end
      end

      #
      # Write the <a:p> element for legends.
      #
      def write_a_p_legend(font)
        @writer.tag_elements('a:p') do
          # Write the a:pPr element.
          write_a_p_pr_legend(font)
          # Write the a:endParaRPr element.
          write_a_end_para_rpr
        end
      end

      #
      # Write the <a:pPr> element for legends.
      #
      def write_a_p_pr_legend(font)
        @writer.tag_elements('a:pPr', [['rtl', 0]]) do
          # Write the a:defRPr element.
          write_a_def_rpr(font)
        end
      end

      #
      # Write the <c:varyColors> element.
      #
      def write_vary_colors
        @writer.empty_tag('c:varyColors', [['val', 1]])
      end

      #
      # Write the <c:firstSliceAng> element.
      #
      def write_first_slice_ang
        @writer.empty_tag('c:firstSliceAng', [['val', @rotation]])
      end

      #
      # Write the <c:showLeaderLines> element. This is for Pie/Doughnut charts.
      # Other chart types only supported leader lines after Excel 2015 via an
      # extension element.
      #
      def write_show_leader_lines
        val  = 1

        attributes = [['val', val]]

        @writer.empty_tag('c:showLeaderLines', attributes)
      end
    end
		class Doughnut < Pie
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @vary_data_color = 1
        @hole_size       = 50
        @rotation        = 0
      end

      #
      # set_hole_size
      #
      # Set the Doughnut chart hole size.
      #
      def set_hole_size(size)
        return unless size

        if size >= 10 && size <= 90
          @hole_size = size
        else
          raise "Hole size $size outside Excel range: 10 <= size <= 90"
        end
      end

      private

      #
      # write_chart_type
      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type
        # Write the c:doughnutChart element.
        write_doughnut_chart
      end

      #
      # write_doughnut_chart
      #
      # Write the <c:doughnutChart> element. Over-ridden method to remove axis_id code
      # since Doughnut charts don't require val and cat axes.
      #
      def write_doughnut_chart
        @writer.tag_elements('c:doughnutChart') do
          # Write the c:varyColors element.
          write_vary_colors

          # Write the series elements.
          @series.each { |s| write_ser(s) }

          # Write the c:firstSliceAng element.
          write_first_slice_ang

          # Write the c:holeSize element.
          write_hole_size
        end
      end

      #
      # write_hole_size
      #
      # Write the <c:holeSize> element.
      #
      def write_hole_size
        @writer.empty_tag('c:holeSize', [['val', @hole_size]])
      end
    end
		class Legend
      attr_accessor :line, :fill, :pattern, :gradient
      attr_accessor :position, :delete_series, :layout, :font

      def initialize
        @position = 'right'
      end
    end
		class Line < self
      include Writexlsx_kot::Utility
      include Writexlsx_kot::WriteDPtPoint

      def initialize(subtype)
        super(subtype)
        @subtype ||= 'standard'
        @default_marker = Marker.new(type: 'none')
        @smooth_allowed = 1

        # Override and reset the default axis values.
        @y_axis.defaults[:num_format] = '0%' if @subtype == 'percent_stacked'

        set_y_axis

        # Set the available data label positions for this chart type.
        @label_position_default = 'right'
        @label_positions = {
          'center' => 'ctr',
          'right'  => 'r',
          'left'   => 'l',
          'above'  => 't',
          'below'  => 'b',
          # For backward compatibility.
          'top'    => 't',
          'bottom' => 'b'
        }
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:barChart element.
        write_line_chart(params)
      end

      #
      # Write the <c:lineChart> element.
      #
      def write_line_chart(params)
        series = axes_series(params)
        return if series.empty?

        subtype = if @subtype == 'percent_stacked'
                    'percentStacked'
                  else
                    @subtype
                  end

        @writer.tag_elements('c:lineChart') do
          # Write the c:grouping element.
          write_grouping(subtype)
          # Write the series elements.
          series.each { |s| write_series(s) }

          # Write the c:dropLines element.
          write_drop_lines

          # Write the c:hiLowLines element.
          write_hi_low_lines

          # Write the c:upDownBars element.
          write_up_down_bars

          # Write the c:marker element.
          write_marker_value

          # Write the c:axId elements
          write_axis_ids(params)
        end
      end
    end
		class Radar < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @subtype = subtype || 'marker'
        @default_marker = Marker.new(type: 'none') if @subtype == 'marker'

        # Override and reset the default axis values.
        @x_axis.defaults[:major_gridlines] = { visible: 1 }
        set_x_axis

        # Hardcode major_tick_mark for now untill there is an accessor.
        @y_axis.major_tick_mark = 'cross'

        # Set the available data label positions for this chart type.
        @label_position_default = 'center'
        @label_positions = { 'center' => 'ctr' }
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:radarChart element.
        write_radar_chart(params)
      end

      #
      # Write the <c:radarChart> element.
      #
      def write_radar_chart(params)
        series = if ptrue?(params[:primary_axes])
                   get_primary_axes_series
                 else
                   get_secondary_axes_series
                 end

        return if series.empty?

        @writer.tag_elements('c:radarChart') do
          # Write the c:radarStyle element.
          write_radar_style

          # Write the series elements.
          series.each { |s| write_series(s) }

          # Write the c:axId elements
          write_axis_ids(params)
        end
      end

      #
      # Write the <c:radarStyle> element.
      #
      def write_radar_style
        val = 'marker'
        val = 'filled' if @subtype == 'filled'

        attributes = [['val', val]]

        @writer.empty_tag('c:radarStyle', attributes)
      end
    end
		class Scatter < self
      include Writexlsx_kot::Utility
      include Writexlsx_kot::WriteDPtPoint

      def initialize(subtype)
        super(subtype)
        @subtype           = subtype || 'marker_only'
        @cross_between     = 'midCat'
        @horiz_val_axis    = 0
        @val_axis_position = 'b'
        @smooth_allowed    = 1

        # Set the available data label positions for this chart type.
        @label_position_default = 'right'
        @label_positions = {
          'center' => 'ctr',
          'right'  => 'r',
          'left'   => 'l',
          'above'  => 't',
          'below'  => 'b',
          # For backward compatibility.
          'top'    => 't',
          'bottom' => 'b'
        }
      end

      #
      # Override parent method to add a warning.
      #
      def combine(_chart)
        raise 'Combined chart not currently supported with scatter chart as the primary chart'
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:areaChart element.
        write_scatter_chart(params)
      end

      #
      # Write the <c:scatterChart> element.
      #
      def write_scatter_chart(params)
        series = if params[:primary_axes] == 1
                   get_primary_axes_series
                 else
                   get_secondary_axes_series
                 end
        return if series.empty?

        style = 'lineMarker'
        # Set the user defined chart subtype
        style = 'smoothMarker' if %w[smooth_with_markers smooth].include?(@subtype)

        # Add default formatting to the series data.
        modify_series_formatting

        @writer.tag_elements('c:scatterChart') do
          # Write the c:scatterStyle element.
          write_scatter_style(style)
          # Write the series elements.
          series.each { |s| write_series(s) }
          # Write the c:marker element.
          write_marker_value
          # Write the c:axId elements
          write_axis_ids(params)
        end
      end

      #
      # Over-ridden to write c:xVal/c:yVal instead of c:cat/c:val elements.
      #
      # Write the <c:ser> element.
      #
      def write_ser(series)
        @writer.tag_elements('c:ser') do
          write_ser_base(series)
          # Write the c:xVal element.
          write_x_val(series)
          # Write the c:yVal element.
          write_y_val(series)
          # Write the c:smooth element.
          if @subtype =~ /smooth/ && !series.smooth
            write_c_smooth(1)
          else
            write_c_smooth(series.smooth)
          end
        end
        @series_index += 1
      end

      #
      # Over-ridden to have 2 valAx elements for scatter charts instead of
      # catAx/valAx.
      #
      # Write the <c:plotArea> element.
      #
      def write_plot_area
        @writer.tag_elements('c:plotArea') do
          # Write the c:layout element.
          write_layout(@plotarea.layout, 'plot')

          # Write the subclass chart type elements for primary and secondary axes.
          write_chart_type(primary_axes: 1)
          write_chart_type(primary_axes: 0)

          # Write c:catAx and c:valAx elements for series using primary axes.
          write_cat_val_axis(@x_axis, @y_axis, @axis_ids, 'b')

          tmp = @horiz_val_axis
          @horiz_val_axis = 1
          write_val_axis(@x_axis, @y_axis, @axis_ids, 'l')
          @horiz_val_axis = tmp

          # Write c:valAx and c:catAx elements for series using secondary axes.
          write_cat_val_axis(@x2_axis, @y2_axis, @axis2_ids, 'b')

          @horiz_val_axis = 1
          write_val_axis(@x2_axis, @y2_axis, @axis2_ids, 'l')

          # Write the c:spPr element for the plotarea formatting.
          write_sp_pr(@plotarea)
        end
      end

      #
      # Write the <c:xVal> element.
      #
      def write_x_val(series)
        formula = series.categories
        data_id = series.cat_data_id
        data    = @formula_data[data_id]

        @writer.tag_elements('c:xVal') do
          # Check the type of cached data.
          type = get_data_type(data)

          # TODO. Can a scatter plot have non-numeric data.

          if type == 'str'
            # Write the c:numRef element.
            write_str_ref(formula, data, type)
          else
            write_num_ref(formula, data, type)
          end
        end
      end

      #
      # Write the <c:yVal> element.
      #
      def write_y_val(series)
        formula = series.values
        data_id = series.val_data_id
        data    = @formula_data[data_id]

        @writer.tag_elements('c:yVal') do
          # Unlike Cat axes data should only be numeric

          # Write the c:numRef element.
          write_num_ref(formula, data, 'num')
        end
      end

      #
      # Write the <c:scatterStyle> element.
      #
      def write_scatter_style(val)
        @writer.empty_tag('c:scatterStyle', [['val', val]])
      end

      #
      # Add default formatting to the series data unless it has already been
      # specified by the user.
      #
      def modify_series_formatting
        # The default scatter style "markers only" requires a line type
        if @subtype == 'marker_only'
          # Go through each series and define default values.
          @series.each do |series|
            # Set a line type unless there is already a user defined type.
            series.line = line_properties(width: 2.25, none: 1, _defined: 1) unless series.line_defined?
          end
        end

        # Turn markers off for subtypes that don't have them
        unless @subtype =~ /marker/
          # Go through each series and define default values.
          @series.each do |series|
            # Set a marker type unless there is already a user defined type.
            series.marker = Marker.new(type: 'none', _defined: 1) unless ptrue?(series.marker)
          end
        end
      end

      #
      # Write the <c:valAx> element.
      # This is for the second valAx in scatter plots.
      #
      # Usually the X axis.
      #
      def write_cat_val_axis(x_axis, y_axis, axis_ids, position) # :nodoc:
        return unless axis_ids && !axis_ids.empty?

        write_val_axis_base(
          y_axis, x_axis,
          axis_ids[1],
          axis_ids[0],
          x_axis.position || position || @val_axis_position
        )
      end
    end
		class Chartline
      include Writexlsx_kot::Utility
      include Writexlsx_kot::Gradient

      attr_reader :line, :fill, :type, :gradient, :pattern

      def initialize(params)
        @line      = params[:line]
        @fill      = params[:fill]
        @pattern   = params[:pattern]
        @gradient  = params[:gradient]
        # Set the line properties for the marker..
        @line = line_properties(@line)
        # Allow 'border' as a synonym for 'line'.
        @line = line_properties(params[:border]) if params[:border]
        # Set the fill properties for the marker.
        @fill = fill_properties(@fill)
        # Set the pattern properties for the series.
        @pattern = pattern_properties(@pattern)
        # Set the gradient fill properties for the series.
        @gradient = gradient_properties(@gradient)
        # Pattern fill overrides solid fill.
        @fill = nil if ptrue?(@gradient)
        # Gradient fill overrides solid and pattern fills.
        if ptrue?(@gradient)
          @pattern = nil
          @fill    = nil
        end
      end

      def line_defined?
        line && ptrue?(line[:_defined])
      end

      def fill_defined?
        fill && ptrue?(fill[:_defined])
      end
    end
		class Point < Chartline
    end
		class Gridline < Chartline
      attr_reader :visible

      def initialize(params)
        super(params)
        @visible = params[:visible]
      end
    end
		class Trendline < Chartline
      attr_reader :name, :forward, :backward, :order, :period
      attr_reader :intercept, :display_equation, :display_r_squared
      attr_reader :label

      def initialize(params)
        super(params)

        @label = trendline_label_properties(params[:label])

        @name              = params[:name]
        @forward           = params[:forward]
        @backward          = params[:backward]
        @order             = params[:order]
        @period            = params[:period]
        @intercept         = params[:intercept]
        @display_equation  = params[:display_equation]
        @display_r_squared = params[:display_r_squared]
        @type              = value_or_raise(types, params[:type], 'trendline type')
      end

      private

      #
      # Convert user defined trendline label properties to the structure required
      # internally.
      #
      def trendline_label_properties(_label)
        return unless _label || _label.is_a?(Hash)

        # Copy the user supplied properties.
        label = {}

        # Set the font properties for the label.
        label[:font] = convert_font_args(_label[:font]) if ptrue?(_label[:font])

        # Set the line properties for the label.
        line = line_properties(_label[:line])

        # Allow 'border' as a synonym for 'line'.
        line = line_properties(_label[:border]) if ptrue?(_label[:border])

        # Set the fill properties for the label.
        fill = fill_properties(_label[:fill])

        # Set the pattern properties for the label.
        pattern = pattern_properties(_label[:pattern])

        # Set the gradient fill properties for the label.
        gradient = gradient_properties(_label[:gradient])

        # Pattern fill overrides solid fill.
        fill = nil if ptrue?(pattern)

        # Gradient fill overrides solid and pattern fills.
        if ptrue?(gradient)
          pattern = nil
          fill    = nil
        end

        label[:line]     = line
        label[:fill]     = fill
        label[:pattern]  = pattern
        label[:gradient] = gradient

        label
      end

      def types
        {
          exponential:    'exp',
          linear:         'linear',
          log:            'log',
          moving_average: 'movingAvg',
          polynomial:     'poly',
          power:          'power'
        }
      end
    end
		class Marker < Chartline
      attr_reader :size

      def initialize(params)
        super(params)

        @type = value_or_raise(types, params[:type], 'maker type') if params[:type]

        @size      = params[:size]
        @automatic = false
        @automatic = true if @type == 'automatic'
      end

      def automatic?
        @automatic
      end

      private

      def types
        {
          automatic:  'automatic',
          none:       'none',
          square:     'square',
          diamond:    'diamond',
          triangle:   'triangle',
          x:          'x',
          star:       'star',
          dot:        'dot',
          short_dash: 'dot',
          dash:       'dash',
          long_dash:  'dash',
          circle:     'circle',
          plus:       'plus',
          picture:    'picture'
        }
      end
    end
		class Errorbars
      include Writexlsx_kot::Utility

      attr_reader :type, :direction, :endcap, :value, :line, :fill
      attr_reader :plus_values, :minus_values, :plus_data, :minus_data

      def initialize(params)
        @type = types[params[:type].to_sym] || 'fixedVal'
        @value = params[:value] || 1    # value for error types that require it.
        @endcap = params[:end_style] || 1 # end-cap style.

        # Set the error bar direction.
        @direction = error_bar_direction(params[:direction])

        # Set any custom values
        @plus_values  = params[:plus_values]  || [1]
        @minus_values = params[:minus_values] || [1]
        @plus_data    = params[:plus_data]    || []
        @minus_data   = params[:minus_data]   || []

        # Set the line properties for the error bars.
        @line = line_properties(params[:line])
        @fill = params[:fill]
      end

      private

      def types
        {
          fixed:              'fixedVal',
          percentage:         'percentage',
          standard_deviation: 'stdDev',
          standard_error:     'stdErr',
          custom:             'cust'
        }
      end

      def error_bar_direction(direction)
        case direction
        when 'minus'
          'minus'
        when 'plus'
          'plus'
        else
          'both'
        end
      end
    end
		class Series
      include Writexlsx_kot::Utility
      include Writexlsx_kot::Gradient

      attr_reader :values, :categories, :name, :name_formula, :name_id
      attr_reader :cat_data_id, :val_data_id, :fill, :pattern, :gradient
      attr_reader :trendline, :smooth, :labels, :invert_if_negative
      attr_reader :inverted_color
      attr_reader :x2_axis, :y2_axis, :error_bars, :points
      attr_accessor :line, :marker

      def initialize(chart, params = {})
        @chart      = chart
        @values     = aref_to_formula(params[:values])
        @categories = aref_to_formula(params[:categories])
        @name, @name_formula =
          chart.process_names(params[:name], params[:name_formula])

        set_data_ids(params)

        @line       = line_properties(params[:border] || params[:line])
        @fill       = fill_properties(params[:fill])
        @pattern    = pattern_properties(params[:pattern])
        @gradient   = gradient_properties(params[:gradient])
        # Pattern fill overrides solid fill.
        @fill       = nil if ptrue?(@pattern)
        # Gradient fill overrides solid and patter fills.
        if ptrue?(@gradient)
          @pattern = nil
          @fill    = nil
        end

        # Set the marker properties for the series.
        @marker     = Marker.new(params[:marker]) if params[:marker]
        # Set the trendline properties for the series.
        @trendline  = Trendline.new(params[:trendline]) if params[:trendline]
        @error_bars = errorbars(params[:x_error_bars], params[:y_error_bars])
        if params[:points]
          @points = params[:points].collect { |p| p ? Point.new(p) : p }
        end

        @label_positions = chart.label_positions
        @label_position_default = chart.label_position_default
        @labels = labels_properties(params[:data_labels])
        @inverted_color = params[:invert_if_negative_color]

        %i[
          smooth invert_if_negative x2_axis y2_axis
        ].each { |key| instance_variable_set("@#{key}", params[key]) }
      end

      def ==(other)
        methods = %w[
          categories values name name_formula name_id
          cat_data_id val_data_id
          line fill gradient marker trendline
          smooth labels inverted_color
          x2_axis y2_axis error_bars points
        ]
        methods.each do |method|
          return false unless instance_variable_get("@#{method}") == other.instance_variable_get("@#{method}")
        end
        true
      end

      def line_defined?
        line && ptrue?(line[:_defined])
      end

      private

      #
      # Convert and aref of row col values to a range formula.
      #
      def aref_to_formula(data) # :nodoc:
        # If it isn't an array ref it is probably a formula already.
        return data unless data.is_a?(Array)

        xl_range_formula(*data)
      end

      def set_data_ids(params)
        @cat_data_id = @chart.data_id(@categories, params[:categories_data])
        @val_data_id = @chart.data_id(@values, params[:values_data])
        @name_id = @chart.data_id(@name_formula, params[:name_data])
      end

      def errorbars(x, y)
        {
          _x_error_bars: x ? Errorbars.new(x) : nil,
          _y_error_bars: y ? Errorbars.new(y) : nil
        }
      end

      #
      # Convert user defined labels properties to the structure required internally.
      #
      def labels_properties(labels) # :nodoc:
        return nil unless labels

        # Map user defined label positions to Excel positions.
        position = labels[:position]
        if ptrue?(position)
          if @label_positions[position]
            labels[:position] = if position == @label_position_default
                                  nil
                                else
                                  @label_positions[position]
                                end
          else
            raise "Unsupported label position '#{position}' for this chart type"
          end
        end

        # Map the user defined label separator to the Excel separator.
        separators = {
          ","  => ", ",
          ";"  => "; ",
          "."  => ". ",
          "\n" => "\n",
          " "  => " "
        }
        separator = labels[:separator]
        unless separator.nil? || separator.empty?
          raise "unsuppoted label separator #{separator}" unless separators[separator]

          labels[:separator] = separators[separator]
        end

        # Set the line properties for the data labels.
        line = line_properties(labels[:line])

        # Allow 'border' as a synonym for 'line'.
        line = line_properties(labels[:border]) if labels[:border]

        # Set the fill properties for the labels.
        fill = fill_properties(labels[:fill])

        # Set the pattern properties for the labels.
        pattern = pattern_properties(labels[:pattern])

        # Set the gradient fill properties for the labels.
        gradient = gradient_properties(labels[:gradient])

        # Pattern fill overrides solid fill.
        fill = nil if pattern

        # Gradient fill overrides solid and pattern fills.
        if gradient
          pattern = nil
          fill    = nil
        end

        labels[:line]     = line
        labels[:fill]     = fill
        labels[:pattern]  = pattern
        labels[:gradient] = gradient

        labels[:font] = convert_font_args(labels[:font]) if labels[:font]

        if labels[:custom]
          # Duplicate, and modify, the custom label properties.
          custom = []

          labels[:custom].each do |label|
            unless label
              custom << nil
              next
            end

            property = label.dup

            # Convert formula.
            property[:formula] = property[:value] if property[:value] && property[:value].to_s =~ /^=[^!]+!\$/

            if property[:formula]
              property[:formula] = property[:formula].sub(/^=/, '')

              data_id = @chart.data_id(property[:formula], property[:data])
              property[:data_id] = data_id
            end

            property[:font] = convert_font_args(property[:font]) if property[:font]

            # Allow 'border' as a synonym for 'line'.
            line = line_properties(property[:border] || property[:line])

            # Set the fill properties for the labels.
            fill = fill_properties(property[:fill])

            # Set the pattern properties for the labels.
            pattern = pattern_properties(property[:pattern])

            # Set the gradient fill properties for the labels.
            gradient = gradient_properties(property[:gradient])

            # Pattern fill overrides solid fill.
            fill = nil if pattern

            # Gradient fill overrides solid and pattern fills.
            if gradient
              pattern = nil
              fill    = nil
            end

            property[:line]     = line
            property[:fill]     = fill
            property[:pattern]  = pattern
            property[:gradient] = gradient

            custom << property
          end
          labels[:custom] = custom
        end

        labels
      end
    end
		class Stock < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @show_crosses  = false
        @hi_low_lines  = Chartline.new({})
        @date_category = true

        # Override and reset the default axis values.
        @x_axis.defaults[:num_format] = 'dd/mm/yyyy'
        @x2_axis.defaults[:num_format] = 'dd/mm/yyyy'
        set_x_axis
        set_x2_axis

        # Set the available data label positions for this chart type.
        @label_position_default = 'right'
        @label_positions = {
          'center' => 'ctr',
          'right'  => 'r',
          'left'   => 'l',
          'above'  => 't',
          'below'  => 'b',
          # For backward compatibility.
          'top'    => 't',
          'bottom' => 'b'
        }
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:areaChart element.
        write_stock_chart(params)
      end

      #
      # Write the <c:stockChart> element.
      # Overridden to add hi_low_lines(). TODO. Refactor up into the SUPER class
      #
      def write_stock_chart(params)
        series = if params[:primary_axes] == 1
                   get_primary_axes_series
                 else
                   get_secondary_axes_series
                 end
        return if series.empty?

        # Add default formatting to the series data.
        modify_series_formatting

        @writer.tag_elements('c:stockChart') do
          # Write the series elements.
          series.each { |s| write_series(s) }

          # Write the c:dtopLines element.
          write_drop_lines

          # Write the c:hiLowLines element.
          write_hi_low_lines if ptrue?(params[:primary_axes])

          # Write the c:upDownBars element.
          write_up_down_bars

          # Write the c:marker element.
          write_marker_value

          # Write the c:axId elements
          write_axis_ids(params)
        end
      end

      #
      # Add default formatting to the series data.
      #
      def modify_series_formatting
        array = []
        @series.each_with_index do |series, index|
          if index % 4 != 3
            unless series.line_defined?
              series.line = {
                width:    2.25,
                none:     1,
                _defined: 1
              }
            end

            unless ptrue?(series.marker)
              series.marker = if index % 4 == 2
                                Marker.new(type: 'dot', size: 3)
                              else
                                Marker.new(type: 'none')
                              end
            end
          end
          array << series
        end
        @series = array
      end
    end
 
	include Writexlsx_kot::Utility
    include Writexlsx_kot::Gradient

    attr_accessor :id, :name                                         # :nodoc:
    attr_writer :index, :palette, :protection                        # :nodoc:
    attr_reader :embedded, :formula_ids, :formula_data               # :nodoc:
    attr_reader :x_scale, :y_scale, :x_offset, :y_offset             # :nodoc:
    attr_reader :width, :height                                      # :nodoc:
    attr_reader :label_positions, :label_position_default, :combined # :nodoc:
    attr_writer :date_category, :already_inserted                    # :nodoc:
    attr_writer :series_index                                        # :nodoc:
    attr_writer :writer                                              # :nodoc:
    attr_reader :x2_axis, :y2_axis, :axis2_ids                       # :nodoc:

    #
    # Factory method for returning chart objects based on their class type.
    #
    def self.factory(current_subclass, subtype = nil) # :nodoc:
      case current_subclass.downcase.capitalize
      when 'Area'
        require 'write_xlsx/chart/area'
        Chart::Area.new(subtype)
      when 'Bar'
        require 'write_xlsx/chart/bar'
        Chart::Bar.new(subtype)
      when 'Column'
        require 'write_xlsx/chart/column'
        Chart::Column.new(subtype)
      when 'Doughnut'
        require 'write_xlsx/chart/doughnut'
        Chart::Doughnut.new(subtype)
      when 'Line'
        require 'write_xlsx/chart/line'
        Chart::Line.new(subtype)
      when 'Pie'
        require 'write_xlsx/chart/pie'
        Chart::Pie.new(subtype)
      when 'Radar'
        require 'write_xlsx/chart/radar'
        Chart::Radar.new(subtype)
      when 'Scatter'
        require 'write_xlsx/chart/scatter'
        Chart::Scatter.new(subtype)
      when 'Stock'
        require 'write_xlsx/chart/stock'
        Chart::Stock.new(subtype)
      end
    end

    def initialize(subtype)   # :nodoc:
      @writer = Package::XMLWriterSimple.new

      @subtype           = subtype
      @sheet_type        = 0x0200
      @series            = []
      @embedded          = 0
      @id                = -1
      @series_index      = 0
      @style_id          = 2
      @formula_ids       = {}
      @formula_data      = []
      @protection        = 0
      @chartarea         = ChartArea.new
      @plotarea          = ChartArea.new
      @title             = Caption.new(self)
      @name              = ''
      @table             = nil
      set_default_properties
      @combined          = nil
      @is_secondary      = false
    end

    def set_xml_writer(filename)   # :nodoc:
      @writer.set_xml_writer(filename)
    end

    #
    # Assemble and write the XML file.
    #
    def assemble_xml_file   # :nodoc:
      write_xml_declaration do
        # Write the c:chartSpace element.
        write_chart_space do
          # Write the c:lang element.
          write_lang
          # Write the c:style element.
          write_style
          # Write the c:protection element.
          write_protection
          # Write the c:chart element.
          write_chart
          # Write the c:spPr element for the chartarea formatting.
          write_sp_pr(@chartarea)
          # Write the c:printSettings element.
          write_print_settings if @embedded && @embedded != 0
        end
      end
    end

    #
    # Add a series and it's properties to a chart.
    #
    def add_series(params)
      # Check that the required input has been specified.
      raise "Must specify ':values' in add_series" unless params.has_key?(:values)

      raise "Must specify ':categories' in add_series for this chart type" if @requires_category != 0 && !params.has_key?(:categories)

      raise "The maximum number of series that can be added to an Excel Chart is 255." if @series.size == 255

      @series << Series.new(self, params)

      # Set the secondary axis properties.
      x2_axis = params[:x2_axis]
      y2_axis = params[:y2_axis]

      # Store secondary status for combined charts.
      @is_secondary = true if ptrue?(x2_axis) || ptrue?(y2_axis)

      # Set the gap and overlap for Bar/Column charts.
      if params[:gap]
        if ptrue?(y2_axis)
          @series_gap_2 = params[:gap]
        else
          @series_gap_1 = params[:gap]
        end
      end

      # Set the overlap for Bar/Column charts.
      if params[:overlap]
        if ptrue?(y2_axis)
          @series_overlap_2 = params[:overlap]
        else
          @series_overlap_1 = params[:overlap]
        end
      end
    end

    #
    # Set the properties of the x-axis.
    #
    def set_x_axis(params = {})
      @date_category = true if ptrue?(params[:date_axis])
      @x_axis.merge_with_hash(params)
    end

    #
    # Set the properties of the Y-axis.
    #
    # The set_y_axis() method is used to set properties of the Y axis.
    # The properties that can be set are the same as for set_x_axis,
    #
    def set_y_axis(params = {})
      @date_category = true if ptrue?(params[:date_axis])
      @y_axis.merge_with_hash(params)
    end

    #
    # Set the properties of the secondary X-axis.
    #
    def set_x2_axis(params = {})
      @date_category = true if ptrue?(params[:date_axis])
      @x2_axis.merge_with_hash(params)
    end

    #
    # Set the properties of the secondary Y-axis.
    #
    def set_y2_axis(params = {})
      @date_category = true if ptrue?(params[:date_axis])
      @y2_axis.merge_with_hash(params)
    end

    #
    # Set the properties of the chart title.
    #
    def set_title(params)
      @title.merge_with_hash(params)
    end

    #
    # Set the properties of the chart legend.
    #
    def set_legend(params)
      # Convert the user default properties to internal properties.
      legend_properties(params)
    end

    #
    # Set the properties of the chart plotarea.
    #
    def set_plotarea(params)
      # Convert the user defined properties to internal properties.
      @plotarea = ChartArea.new(params)
    end

    #
    # Set the properties of the chart chartarea.
    #
    def set_chartarea(params)
      # Convert the user defined properties to internal properties.
      @chartarea = ChartArea.new(params)
    end

    #
    # Set on of the 42 built-in Excel chart styles. The default style is 2.
    #
    def set_style(style_id = 2)
      style_id = 2 if style_id < 1 || style_id > 48
      @style_id = style_id
    end

    #
    # Set the option for displaying blank data in a chart. The default is 'gap'.
    #
    def show_blanks_as(option)
      return unless option

      raise "Unknown show_blanks_as() option '#{option}'\n" unless %i[gap zero span].include?(option.to_sym)

      @show_blanks = option
    end

    #
    # Display data in hidden rows or columns on the chart.
    #
    def show_hidden_data
      @show_hidden_data = true
    end

    #
    # Set dimensions for scale for the chart.
    #
    def set_size(params = {})
      @width    = params[:width]    if params[:width]
      @height   = params[:height]   if params[:height]
      @x_scale  = params[:x_scale]  if params[:x_scale]
      @y_scale  = params[:y_scale]  if params[:y_scale]
      @x_offset = params[:x_offset] if params[:x_offset]
      @y_offset = params[:y_offset] if params[:y_offset]
    end

    # Backward compatibility with poorly chosen method name.
    alias size set_size

    #
    # The set_table method adds a data table below the horizontal axis with the
    # data used to plot the chart.
    #
    def set_table(params = {})
      @table = Table.new(params)
      @table.palette = @palette
    end

    #
    # Set properties for the chart up-down bars.
    #
    def set_up_down_bars(params = {})
      # Map border to line.
      %i[up down].each do |up_down|
        if params[up_down]
          params[up_down][:line] = params[up_down][:border] if params[up_down][:border]
        else
          params[up_down] = {}
        end
      end

      # Set the up and down bar properties.
      @up_down_bars = {
        _up:   Chartline.new(params[:up]),
        _down: Chartline.new(params[:down])
      }
    end

    #
    # Set properties for the chart drop lines.
    #
    def set_drop_lines(params = {})
      @drop_lines = Chartline.new(params)
    end

    #
    # Set properties for the chart high-low lines.
    #
    def set_high_low_lines(params = {})
      @hi_low_lines = Chartline.new(params)
    end

    #
    # Add another chart to create a combined chart.
    #
    def combine(chart)
      @combined = chart
    end

    #
    # Setup the default configuration data for an embedded chart.
    #
    def set_embedded_config_data
      @embedded = 1
    end

    #
    # Write the <c:barChart> element.
    #
    def write_bar_chart(params)   # :nodoc:
      series = if ptrue?(params[:primary_axes])
                 get_primary_axes_series
               else
                 get_secondary_axes_series
               end
      return if series.empty?

      subtype = @subtype
      subtype = 'percentStacked' if subtype == 'percent_stacked'

      # Set a default overlap for stacked charts.
      @series_overlap_1 = 100 if @subtype =~ (/stacked/) && !@series_overlap_1

      @writer.tag_elements('c:barChart') do
        # Write the c:barDir element.
        write_bar_dir
        # Write the c:grouping element.
        write_grouping(subtype)
        # Write the c:ser elements.
        series.each { |s| write_ser(s) }

        # write the c:marker element.
        write_marker_value

        if ptrue?(params[:primary_axes])
          # Write the c:gapWidth element.
          write_gap_width(@series_gap_1)
          # Write the c:overlap element.
          write_overlap(@series_overlap_1)
        else
          # Write the c:gapWidth element.
          write_gap_width(@series_gap_2)
          # Write the c:overlap element.
          write_overlap(@series_overlap_2)
        end

        # write the c:overlap element.
        write_overlap(@series_overlap)

        # Write the c:axId elements
        write_axis_ids(params)
      end
    end

    #
    # Switch name and name_formula parameters if required.
    #
    def process_names(name = nil, name_formula = nil) # :nodoc:
      # Name looks like a formula, use it to set name_formula.
      if name.respond_to?(:to_ary)
        cell = xl_rowcol_to_cell(name[1], name[2], 1, 1)
        name_formula = "#{quote_sheetname(name[0])}!#{cell}"
        name = ''
      elsif name && name =~ /^=[^!]+!\$/
        name_formula = name
        name         = ''
      end

      [name, name_formula]
    end

    #
    # Assign an id to a each unique series formula or title/axis formula. Repeated
    # formulas such as for categories get the same id. If the series or title
    # has user specified data associated with it then that is also stored. This
    # data is used to populate cached Excel data when creating a chart.
    # If there is no user defined data then it will be populated by the parent
    # workbook in Workbook::_add_chart_data
    #
    def data_id(full_formula, data) # :nodoc:
      return unless full_formula

      # Strip the leading '=' from the formula.
      formula = full_formula.sub(/^=/, '')

      # Store the data id in a hash keyed by the formula and store the data
      # in a separate array with the same id.
      if @formula_ids.has_key?(formula)
        # Formula already seen. Return existing id.
        id = @formula_ids[formula]
        # Store user defined data if it isn't already there.
        @formula_data[id] ||= data
      else
        # Haven't seen this formula before.
        id = @formula_ids[formula] = @formula_data.size
        @formula_data << data
      end

      id
    end

    def already_inserted?
      @already_inserted
    end

    def is_secondary?
      @is_secondary
    end

    #
    # Set the option for displaying #N/A as an empty cell in a chart.
    #
    def show_na_as_empty_cell
      @show_na_as_empty = true
    end

    private

    def axis_setup
      @axis_ids          = []
      @axis2_ids         = []
      @cat_has_num_fmt   = false
      @requires_category = 0
      @cat_axis_position = 'b'
      @val_axis_position = 'l'
      @horiz_cat_axis    = 0
      @horiz_val_axis    = 1
      @x_axis            = Axis.new(self)
      @y_axis            = Axis.new(self)
      @x2_axis           = Axis.new(self)
      @y2_axis           = Axis.new(self)
    end

    def display_setup
      @orientation       = 0x0
      @width             = 480
      @height            = 288
      @x_scale           = 1
      @y_scale           = 1
      @x_offset          = 0
      @y_offset          = 0
      @legend            = Legend.new
      @smooth_allowed    = 0
      @cross_between     = 'between'
      @date_category     = false
      @show_blanks       = 'gap'
      @show_na_as_empty  = false
      @show_hidden_data  = false
      @show_crosses      = true
    end

    #
    # retun primary/secondary series by :primary_axes flag
    #
    def axes_series(params)
      if params[:primary_axes] == 0
        secondary_axes_series
      else
        primary_axes_series
      end
    end

    #
    # Find the overall type of the data associated with a series.
    #
    # TODO. Need to handle date type.
    #
    def get_data_type(data) # :nodoc:
      # Check for no data in the series.
      return 'none' unless data
      return 'none' if data.empty?
      return 'multi_str' if data.first.is_a?(Array)

      # If the token isn't a number assume it is a string.
      data.each do |token|
        next unless token
        return 'str' unless token.is_a?(Numeric)
      end

      # The series data was all numeric.
      'num'
    end

    #
    # Returns series which use the primary axes.
    #
    def get_primary_axes_series
      @series.reject { |s| s.y2_axis }
    end
    alias primary_axes_series get_primary_axes_series

    #
    # Returns series which use the secondary axes.
    #
    def get_secondary_axes_series
      @series.select { |s| s.y2_axis }
    end
    alias secondary_axes_series get_secondary_axes_series

    #
    # Add a unique ids for primary or secondary axis.
    #
    def add_axis_ids(params) # :nodoc:
      if ptrue?(params[:primary_axes])
        @axis_ids  += ids
      else
        @axis2_ids += ids
      end
    end

    def ids
      chart_id   = 5001 + @id
      axis_count = 1 + @axis2_ids.size + @axis_ids.size

      id1 = sprintf('%04d%04d', chart_id, axis_count)
      id2 = sprintf('%04d%04d', chart_id, axis_count + 1)

      [id1, id2]
    end

    #
    # Setup the default properties for a chart.
    #
    def set_default_properties # :nodoc:
      display_setup
      axis_setup
      set_axis_defaults

      set_x_axis
      set_y_axis

      set_x2_axis
      set_y2_axis
    end

    def set_axis_defaults
      @x_axis.defaults  = x_axis_defaults
      @y_axis.defaults  = y_axis_defaults
      @x2_axis.defaults = x2_axis_defaults
      @y2_axis.defaults = y2_axis_defaults
    end

    def x_axis_defaults
      {
        num_format:      'General',
        major_gridlines: { visible: 0 }
      }
    end

    def y_axis_defaults
      {
        num_format:      'General',
        major_gridlines: { visible: 1 }
      }
    end

    def x2_axis_defaults
      {
        num_format:     'General',
        label_position: 'none',
        crossing:       'max',
        visible:        0
      }
    end

    def y2_axis_defaults
      {
        num_format:      'General',
        major_gridlines: { visible: 0 },
        position:        'right',
        visible:         1
      }
    end

    #
    # Write the <c:chartSpace> element.
    #
    def write_chart_space(&block) # :nodoc:
      @writer.tag_elements('c:chartSpace', chart_space_attributes, &block)
    end

    # for <c:chartSpace> element.
    def chart_space_attributes # :nodoc:
      schema  = 'http://schemas.openxmlformats.org/'
      [
        ['xmlns:c', "#{schema}drawingml/2006/chart"],
        ['xmlns:a', "#{schema}drawingml/2006/main"],
        ['xmlns:r', "#{schema}officeDocument/2006/relationships"]
      ]
    end

    #
    # Write the <c:lang> element.
    #
    def write_lang # :nodoc:
      @writer.empty_tag('c:lang', [%w[val en-US]])
    end

    #
    # Write the <c:style> element.
    #
    def write_style # :nodoc:
      return if @style_id == 2

      @writer.empty_tag('c:style', [['val', @style_id]])
    end

    #
    # Write the <c:chart> element.
    #
    def write_chart # :nodoc:
      @writer.tag_elements('c:chart') do
        # Write the chart title elements.
        if @title.none
          # Turn off the title.
          write_auto_title_deleted
        elsif @title.formula
          write_title_formula(@title, nil, nil, @title.layout, @title.overlay)
        elsif @title.name
          write_title_rich(@title, nil, @title.name_font, @title.layout, @title.overlay)
        end

        # Write the c:plotArea element.
        write_plot_area
        # Write the c:legend element.
        write_legend
        # Write the c:plotVisOnly element.
        write_plot_vis_only

        # Write the c:dispBlanksAs element.
        write_disp_blanks_as

        # Write the c:extLst element.
        write_ext_lst_display_na if @show_na_as_empty
      end
    end

    #
    # Write the <c:dispBlanksAs> element.
    #
    def write_disp_blanks_as
      return if @show_blanks == 'gap'

      @writer.empty_tag('c:dispBlanksAs', [['val', @show_blanks]])
    end

    #
    # Write the <c:plotArea> element.
    #
    def write_plot_area   # :nodoc:
      second_chart = @combined
      @writer.tag_elements('c:plotArea') do
        # Write the c:layout element.
        write_layout(@plotarea.layout, 'plot')
        # Write the subclass chart type elements for primary and secondary axes.
        write_chart_type(primary_axes: 1)
        write_chart_type(primary_axes: 0)

        # Configure a combined chart if present.
        if second_chart

          # Secondary axis has unique id otherwise use same as primary.
          second_chart.id = if second_chart.is_secondary?
                              1000 + @id
                            else
                              @id
                            end

          # Share the same writer for writing.
          second_chart.writer = @writer

          # Share series index with primary chart.
          second_chart.series_index = @series_index

          # Write the subclass chart type elements for combined chart.
          second_chart.write_chart_type(primary_axes: 1)
          second_chart.write_chart_type(primary_axes: 0)
        end

        # Write the category and value elements for the primary axes.
        params = {
          x_axis:   @x_axis,
          y_axis:   @y_axis,
          axis_ids: @axis_ids
        }

        if @date_category
          write_date_axis(params)
        else
          write_cat_axis(params)
        end

        write_val_axis(@x_axis, @y_axis, @axis_ids)

        # Write the category and value elements for the secondary axes.
        params = {
          x_axis:   @x2_axis,
          y_axis:   @y2_axis,
          axis_ids: @axis2_ids
        }

        write_val_axis(@x2_axis, @y2_axis, @axis2_ids)

        # Write the secondary axis for the secondary chart.
        if second_chart && second_chart.is_secondary?

          params = {
            x_axis:   second_chart.x2_axis,
            y_axis:   second_chart.y2_axis,
            axis_ids: second_chart.axis2_ids
          }

          second_chart.write_val_axis(
            second_chart.x2_axis,
            second_chart.y2_axis,
            second_chart.axis2_ids
          )
        end

        if @date_category
          write_date_axis(params)
        else
          write_cat_axis(params)
        end

        # Write the c:dTable element.
        write_d_table

        # Write the c:spPr element for the plotarea formatting.
        write_sp_pr(@plotarea)
      end
    end

    #
    # Write the <c:layout> element.
    #
    def write_layout(layout = nil, type = nil) # :nodoc:
      tag = 'c:layout'

      if layout
        @writer.tag_elements(tag)  { write_manual_layout(layout, type) }
      else
        @writer.empty_tag(tag)
      end
    end

    #
    # Write the <c:manualLayout> element.
    #
    def write_manual_layout(layout, type)
      @writer.tag_elements('c:manualLayout') do
        # Plotarea has a layoutTarget element.
        @writer.empty_tag('c:layoutTarget', [%w[val inner]]) if type == 'plot'

        # Set the x, y positions.
        @writer.empty_tag('c:xMode', [%w[val edge]])
        @writer.empty_tag('c:yMode', [%w[val edge]])
        @writer.empty_tag('c:x',     [['val', layout[:x]]])
        @writer.empty_tag('c:y',     [['val', layout[:y]]])

        # For plotarea and legend set the width and height.
        if type != 'text'
          @writer.empty_tag('c:w', [['val', layout[:width]]])
          @writer.empty_tag('c:h', [['val', layout[:height]]])
        end
      end
    end

    #
    # Write the chart type element. This method should be overridden by the
    # subclasses.
    #
    def write_chart_type; end

    #
    # Write the <c:grouping> element.
    #
    def write_grouping(val) # :nodoc:
      @writer.empty_tag('c:grouping', [['val', val]])
    end

    #
    # Write the series elements.
    #
    def write_series(series) # :nodoc:
      write_ser(series)
    end

    #
    # Write the <c:ser> element.
    #
    def write_ser(series) # :nodoc:
      @writer.tag_elements('c:ser') do
        write_ser_base(series) do
          write_c_invert_if_negative(series.invert_if_negative)
        end
        # Write the c:cat element.
        write_cat(series)
        # Write the c:val element.
        write_val(series)
        # Write the c:smooth element.
        write_c_smooth(series.smooth) if ptrue?(@smooth_allowed)
        # Write the c:extLst element.
        write_ext_lst_inverted_fill(series.inverted_color) if series.inverted_color
      end
      @series_index += 1
    end

    def write_ext_lst_inverted_fill(color)
      uri = '{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}'
      xmlns_c_14 =
        'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'

      attributes_1 = [
        ['uri', uri],
        ['xmlns:c14', xmlns_c_14]
      ]

      attributes_2 = [
        ['xmlns:c14', xmlns_c_14]
      ]

      @writer.tag_elements('c:extLst') do
        @writer.tag_elements('c:ext', attributes_1) do
          @writer.tag_elements('c14:invertSolidFillFmt') do
            @writer.tag_elements('c14:spPr', attributes_2) do
              write_a_solid_fill(color: color)
            end
          end
        end
      end
    end

    #
    # Write the <c:extLst> element for the display N/A as empty cell option.
    #
    def write_ext_lst_display_na
      uri        = '{56B9EC1D-385E-4148-901F-78D8002777C0}'
      xmlns_c_16 = 'http://schemas.microsoft.com/office/drawing/2017/03/chart'

      attributes1 = [
        ['uri', uri],
        ['xmlns:c16r3', xmlns_c_16]
      ]

      attributes2 = [
        ['val', 1]
      ]

      @writer.tag_elements('c:extLst') do
        @writer.tag_elements('c:ext', attributes1) do
          @writer.tag_elements('c16r3:dataDisplayOptions16') do
            @writer.empty_tag('c16r3:dispNaAsBlank', attributes2)
          end
        end
      end
    end

    def write_ser_base(series)
      # Write the c:idx element.
      write_idx(@series_index)
      # Write the c:order element.
      write_order(@series_index)
      # Write the series name.
      write_series_name(series)
      # Write the c:spPr element.
      write_sp_pr(series)
      # Write the c:marker element.
      write_marker(series.marker)

      yield if block_given?

      # Write the c:dPt element.
      write_d_pt(series.points)
      # Write the c:dLbls element.
      write_d_lbls(series.labels)
      # Write the c:trendline element.
      write_trendline(series.trendline)
      # Write the c:errBars element.
      write_error_bars(series.error_bars)
    end

    #
    # Write the <c:idx> element.
    #
    def write_idx(val) # :nodoc:
      @writer.empty_tag('c:idx', [['val', val]])
    end

    #
    # Write the <c:order> element.
    #
    def write_order(val) # :nodoc:
      @writer.empty_tag('c:order', [['val', val]])
    end

    #
    # Write the series name.
    #
    def write_series_name(series) # :nodoc:
      if series.name_formula
        write_tx_formula(series.name_formula, series.name_id)
      elsif series.name
        write_tx_value(series.name)
      end
    end

    #
    # Write the <c:cat> element.
    #
    def write_cat(series) # :nodoc:
      formula = series.categories
      data_id = series.cat_data_id

      data = @formula_data[data_id] if data_id

      # Ignore <c:cat> elements for charts without category values.
      return unless formula

      @writer.tag_elements('c:cat') do
        # Check the type of cached data.
        type = get_data_type(data)
        if type == 'str'
          @cat_has_num_fmt = false
          # Write the c:strRef element.
          write_str_ref(formula, data, type)
        elsif type == 'multi_str'
          @cat_has_num_fmt = false
          # Write the c:multiLvLStrRef element.
          write_multi_lvl_str_ref(formula, data)
        else
          @cat_has_num_fmt = true
          # Write the c:numRef element.
          write_num_ref(formula, data, type)
        end
      end
    end

    #
    # Write the <c:val> element.
    #
    def write_val(series) # :nodoc:
      write_val_base(series.values, series.val_data_id, 'c:val')
    end

    def write_val_base(formula, data_id, tag) # :nodoc:
      data = @formula_data[data_id]

      @writer.tag_elements(tag) do
        # Unlike Cat axes data should only be numeric.

        # Write the c:numRef element.
        write_num_ref(formula, data, 'num')
      end
    end

    #
    # Write the <c:numRef> or <c:strRef> element.
    #
    def write_num_or_str_ref(tag, formula, data, type) # :nodoc:
      @writer.tag_elements(tag) do
        # Write the c:f element.
        write_series_formula(formula)
        if type == 'num'
          # Write the c:numCache element.
          write_num_cache(data)
        elsif type == 'str'
          # Write the c:strCache element.
          write_str_cache(data)
        end
      end
    end

    #
    # Write the <c:numRef> element.
    #
    def write_num_ref(formula, data, type) # :nodoc:
      write_num_or_str_ref('c:numRef', formula, data, type)
    end

    #
    # Write the <c:strRef> element.
    #
    def write_str_ref(formula, data, type) # :nodoc:
      write_num_or_str_ref('c:strRef', formula, data, type)
    end

    #
    # Write the <c:multiLvLStrRef> element.
    #
    def write_multi_lvl_str_ref(formula, data)
      return if data.empty?

      @writer.tag_elements('c:multiLvlStrRef') do
        # Write the c:f element.
        write_series_formula(formula)

        @writer.tag_elements('c:multiLvlStrCache') do
          # Write the c:ptCount element.
          write_pt_count(data.last.size)

          # Write the data arrays in reverse order.
          data.reverse.each do |arr|
            @writer.tag_elements('c:lvl') do
              # Write the c:pt element.
              arr.each_with_index { |a, i| write_pt(i, a) }
            end
          end
        end
      end
    end

    #
    # Write the <c:numLit> element for literal number list elements.
    #
    def write_num_lit(data)
      write_num_base('c:numLit', data)
    end

    #
    # Write the <c:f> element.
    #
    def write_series_formula(formula) # :nodoc:
      # Strip the leading '=' from the formula.
      formula = formula.sub(/^=/, '')

      @writer.data_element('c:f', formula)
    end

    #
    # Write the <c:axId> elements for the primary or secondary axes.
    #
    def write_axis_ids(params)
      # Generate the axis ids.
      add_axis_ids(params)

      if params[:primary_axes] == 0
        # Write the axis ids for the secondary axes.
        write_axis_id(@axis2_ids[0])
        write_axis_id(@axis2_ids[1])
      else
        # Write the axis ids for the primary axes.
        write_axis_id(@axis_ids[0])
        write_axis_id(@axis_ids[1])
      end
    end

    #
    # Write the <c:axId> element.
    #
    def write_axis_id(val) # :nodoc:
      @writer.empty_tag('c:axId', [['val', val]])
    end

    #
    # Write the <c:catAx> element. Usually the X axis.
    #
    def write_cat_axis(params) # :nodoc:
      x_axis   = params[:x_axis]
      y_axis   = params[:y_axis]
      axis_ids = params[:axis_ids]

      # if there are no axis_ids then we don't need to write this element
      return unless axis_ids
      return if axis_ids.empty?

      position  = @cat_axis_position
      is_y_axis = @horiz_cat_axis

      # Overwrite the default axis position with a user supplied value.
      position = x_axis.position || position

      @writer.tag_elements('c:catAx') do
        write_axis_id(axis_ids[0])
        # Write the c:scaling element.
        write_scaling(x_axis.reverse)

        write_delete(1) unless ptrue?(x_axis.visible)

        # Write the c:axPos element.
        write_axis_pos(position, y_axis.reverse)

        # Write the c:majorGridlines element.
        write_major_gridlines(x_axis.major_gridlines)

        # Write the c:minorGridlines element.
        write_minor_gridlines(x_axis.minor_gridlines)

        # Write the axis title elements.
        if x_axis.formula
          write_title_formula(x_axis, is_y_axis, @x_axis, x_axis.layout)
        elsif x_axis.name
          write_title_rich(x_axis, is_y_axis, x_axis.name_font, x_axis.layout)
        end

        # Write the c:numFmt element.
        write_cat_number_format(x_axis)

        # Write the c:majorTickMark element.
        write_major_tick_mark(x_axis.major_tick_mark)

        # Write the c:minorTickMark element.
        write_minor_tick_mark(x_axis.minor_tick_mark)

        # Write the c:tickLblPos element.
        write_tick_label_pos(x_axis.label_position)

        # Write the c:spPr element for the axis line.
        write_sp_pr(x_axis)

        # Write the axis font elements.
        write_axis_font(x_axis.num_font)

        # Write the c:crossAx element.
        write_cross_axis(axis_ids[1])

        write_crossing(y_axis.crossing) if @show_crosses || ptrue?(x_axis.visible)
        # Write the c:auto element.
        write_auto(1) unless x_axis.text_axis
        # Write the c:labelAlign element.
        write_label_align(x_axis.label_align)
        # Write the c:labelOffset element.
        write_label_offset(100)
        # Write the c:tickLblSkip element.
        write_tick_lbl_skip(x_axis.interval_unit)
        # Write the c:tickMarkSkip element.
        write_tick_mark_skip(x_axis.interval_tick)
      end
    end

    #
    # Write the <c:valAx> element. Usually the Y axis.
    #
    def write_val_axis(x_axis, y_axis, axis_ids, position = nil)
      return unless axis_ids && !axis_ids.empty?

      write_val_axis_base(
        x_axis, y_axis,
        axis_ids[0],
        axis_ids[1],
        y_axis.position || position || @val_axis_position
      )
    end
    public :write_val_axis

    def write_val_axis_base(x_axis, y_axis, axis_ids_0, axis_ids_1, position)  # :nodoc:
      @writer.tag_elements('c:valAx') do
        write_axis_id(axis_ids_1)

        # Write the c:scaling element.
        write_scaling_with_param(y_axis)

        write_delete(1) unless ptrue?(y_axis.visible)

        # Write the c:axPos element.
        write_axis_pos(position, x_axis.reverse)

        # Write the c:majorGridlines element.
        write_major_gridlines(y_axis.major_gridlines)

        # Write the c:minorGridlines element.
        write_minor_gridlines(y_axis.minor_gridlines)

        # Write the axis title elements.
        if y_axis.formula
          write_title_formula(y_axis, @horiz_val_axis, nil, y_axis.layout)
        elsif y_axis.name
          write_title_rich(y_axis, @horiz_val_axis, y_axis.name_font, y_axis.layout)
        end

        # Write the c:numberFormat element.
        write_number_format(y_axis)

        # Write the c:majorTickMark element.
        write_major_tick_mark(y_axis.major_tick_mark)

        # Write the c:minorTickMark element.
        write_minor_tick_mark(y_axis.minor_tick_mark)

        # Write the c:tickLblPos element.
        write_tick_label_pos(y_axis.label_position)

        # Write the c:spPr element for the axis line.
        write_sp_pr(y_axis)

        # Write the axis font elements.
        write_axis_font(y_axis.num_font)

        # Write the c:crossAx element.
        write_cross_axis(axis_ids_0)

        write_crossing(x_axis.crossing)

        # Write the c:crossBetween element.
        write_cross_between(x_axis.position_axis)

        # Write the c:majorUnit element.
        write_c_major_unit(y_axis.major_unit)

        # Write the c:minorUnit element.
        write_c_minor_unit(y_axis.minor_unit)

        # Write the c:dispUnits element.
        write_disp_units(y_axis.display_units, y_axis.display_units_visible)
      end
    end

    #
    # Write the <c:dateAx> element. Usually the X axis.
    #
    def write_date_axis(params)  # :nodoc:
      x_axis    = params[:x_axis]
      y_axis    = params[:y_axis]
      axis_ids  = params[:axis_ids]

      return unless axis_ids && !axis_ids.empty?

      position  = @cat_axis_position

      # Overwrite the default axis position with a user supplied value.
      position = x_axis.position || position

      @writer.tag_elements('c:dateAx') do
        write_axis_id(axis_ids[0])
        # Write the c:scaling element.
        write_scaling_with_param(x_axis)

        write_delete(1) unless ptrue?(x_axis.visible)

        # Write the c:axPos element.
        write_axis_pos(position, y_axis.reverse)

        # Write the c:majorGridlines element.
        write_major_gridlines(x_axis.major_gridlines)

        # Write the c:minorGridlines element.
        write_minor_gridlines(x_axis.minor_gridlines)

        # Write the axis title elements.
        if x_axis.formula
          write_title_formula(x_axis, nil, nil, x_axis.layout)
        elsif x_axis.name
          write_title_rich(x_axis, nil, x_axis.name_font, x_axis.layout)
        end
        # Write the c:numFmt element.
        write_number_format(x_axis)
        # Write the c:majorTickMark element.
        write_major_tick_mark(x_axis.major_tick_mark)

        # Write the c:tickLblPos element.
        write_tick_label_pos(x_axis.label_position)
        # Write the c:spPr element for the axis line.
        write_sp_pr(x_axis)
        # Write the font elements.
        write_axis_font(x_axis.num_font)
        # Write the c:crossAx element.
        write_cross_axis(axis_ids[1])

        write_crossing(y_axis.crossing) if @show_crosses || ptrue?(x_axis.visible)

        # Write the c:auto element.
        write_auto(1)
        # Write the c:labelOffset element.
        write_label_offset(100)
        # Write the c:tickLblSkip element.
        write_tick_lbl_skip(x_axis.interval_unit)
        # Write the c:tickMarkSkip element.
        write_tick_mark_skip(x_axis.interval_tick)
        # Write the c:majorUnit element.
        write_c_major_unit(x_axis.major_unit)
        # Write the c:majorTimeUnit element.
        write_c_major_time_unit(x_axis.major_unit_type) if x_axis.major_unit
        # Write the c:minorUnit element.
        write_c_minor_unit(x_axis.minor_unit)
        # Write the c:minorTimeUnit element.
        write_c_minor_time_unit(x_axis.minor_unit_type) if x_axis.minor_unit
      end
    end

    def write_crossing(crossing)
      # Note, the category crossing comes from the value axis.
      if [nil, 'max', 'min'].include?(crossing)
        # Write the c:crosses element.
        write_crosses(crossing)
      else
        # Write the c:crossesAt element.
        write_c_crosses_at(crossing)
      end
    end

    def write_scaling_with_param(param)
      write_scaling(
        param.reverse,
        param.min,
        param.max,
        param.log_base
      )
    end

    #
    # Write the <c:scaling> element.
    #
    def write_scaling(reverse, min = nil, max = nil, log_base = nil) # :nodoc:
      @writer.tag_elements('c:scaling') do
        # Write the c:logBase element.
        write_c_log_base(log_base)
        # Write the c:orientation element.
        write_orientation(reverse)
        # Write the c:max element.
        write_c_max(max)
        # Write the c:min element.
        write_c_min(min)
      end
    end

    #
    # Write the <c:logBase> element.
    #
    def write_c_log_base(val) # :nodoc:
      return unless ptrue?(val)

      @writer.empty_tag('c:logBase', [['val', val]])
    end

    #
    # Write the <c:orientation> element.
    #
    def write_orientation(reverse = nil) # :nodoc:
      val = ptrue?(reverse) ? 'maxMin' : 'minMax'

      @writer.empty_tag('c:orientation', [['val', val]])
    end

    #
    # Write the <c:max> element.
    #
    def write_c_max(max = nil) # :nodoc:
      @writer.empty_tag('c:max', [['val', max]]) if max
    end

    #
    # Write the <c:min> element.
    #
    def write_c_min(min = nil) # :nodoc:
      @writer.empty_tag('c:min', [['val', min]]) if min
    end

    #
    # Write the <c:axPos> element.
    #
    def write_axis_pos(val, reverse = false) # :nodoc:
      if reverse
        val = 'r' if val == 'l'
        val = 't' if val == 'b'
      end

      @writer.empty_tag('c:axPos', [['val', val]])
    end

    #
    # Write the <c:numberFormat> element. Note: It is assumed that if a user
    # defined number format is supplied (i.e., non-default) then the sourceLinked
    # attribute is 0. The user can override this if required.
    #

    def write_number_format(axis) # :nodoc:
      axis.write_number_format(@writer)
    end

    #
    # Write the <c:numFmt> element. Special case handler for category axes which
    # don't always have a number format.
    #
    def write_cat_number_format(axis)
      axis.write_cat_number_format(@writer, @cat_has_num_fmt)
    end

    #
    # Write the <c:numberFormat> element for data labels.
    #
    def write_data_label_number_format(format_code)
      source_linked = 0

      attributes = [
        ['formatCode',   format_code],
        ['sourceLinked', source_linked]
      ]

      @writer.empty_tag('c:numFmt', attributes)
    end

    #
    # Write the <c:majorTickMark> element.
    #
    def write_major_tick_mark(val)
      return unless ptrue?(val)

      @writer.empty_tag('c:majorTickMark', [['val', val]])
    end

    #
    # Write the <c:minorTickMark> element.
    #
    def write_minor_tick_mark(val)
      return unless ptrue?(val)

      @writer.empty_tag('c:minorTickMark', [['val', val]])
    end

    #
    # Write the <c:tickLblPos> element.
    #
    def write_tick_label_pos(val) # :nodoc:
      val ||= 'nextTo'
      val = 'nextTo' if val == 'next_to'

      @writer.empty_tag('c:tickLblPos', [['val', val]])
    end

    #
    # Write the <c:crossAx> element.
    #
    def write_cross_axis(val = 'autoZero') # :nodoc:
      @writer.empty_tag('c:crossAx', [['val', val]])
    end

    #
    # Write the <c:crosses> element.
    #
    def write_crosses(val) # :nodoc:
      val ||= 'autoZero'

      @writer.empty_tag('c:crosses', [['val', val]])
    end

    #
    # Write the <c:crossesAt> element.
    #
    def write_c_crosses_at(val) # :nodoc:
      @writer.empty_tag('c:crossesAt', [['val', val]])
    end

    #
    # Write the <c:auto> element.
    #
    def write_auto(val) # :nodoc:
      @writer.empty_tag('c:auto', [['val', val]])
    end

    #
    # Write the <c:labelAlign> element.
    #
    def write_label_align(val) # :nodoc:
      val ||= 'ctr'
      if val == 'right'
        val = 'r'
      elsif val == 'left'
        val = 'l'
      end
      @writer.empty_tag('c:lblAlgn', [['val', val]])
    end

    #
    # Write the <c:labelOffset> element.
    #
    def write_label_offset(val) # :nodoc:
      @writer.empty_tag('c:lblOffset', [['val', val]])
    end

    #
    # Write the <c:tickLblSkip> element.
    #
    def write_tick_lbl_skip(val) # :nodoc:
      return unless val

      @writer.empty_tag('c:tickLblSkip', [['val', val]])
    end

    #
    # Write the <c:tickMarkSkip> element.
    #
    def write_tick_mark_skip(val)  # :nodoc:
      return unless val

      @writer.empty_tag('c:tickMarkSkip', [['val', val]])
    end

    #
    # Write the <c:majorGridlines> element.
    #
    def write_major_gridlines(gridlines) # :nodoc:
      write_gridlines_base('c:majorGridlines', gridlines)
    end

    #
    # Write the <c:minorGridlines> element.
    #
    def write_minor_gridlines(gridlines)  # :nodoc:
      write_gridlines_base('c:minorGridlines', gridlines)
    end

    def write_gridlines_base(tag, gridlines)  # :nodoc:
      return unless gridlines
      return if gridlines.respond_to?(:[]) and !ptrue?(gridlines[:_visible])

      if gridlines.line_defined?
        @writer.tag_elements(tag) { write_sp_pr(gridlines) }
      else
        @writer.empty_tag(tag)
      end
    end

    #
    # Write the <c:crossBetween> element.
    #
    def write_cross_between(val = nil) # :nodoc:
      val ||= @cross_between

      @writer.empty_tag('c:crossBetween', [['val', val]])
    end

    #
    # Write the <c:majorUnit> element.
    #
    def write_c_major_unit(val = nil) # :nodoc:
      return unless val

      @writer.empty_tag('c:majorUnit', [['val', val]])
    end

    #
    # Write the <c:minorUnit> element.
    #
    def write_c_minor_unit(val = nil) # :nodoc:
      return unless val

      @writer.empty_tag('c:minorUnit', [['val', val]])
    end

    #
    # Write the <c:majorTimeUnit> element.
    #
    def write_c_major_time_unit(val) # :nodoc:
      val ||= 'days'

      @writer.empty_tag('c:majorTimeUnit', [['val', val]])
    end

    #
    # Write the <c:minorTimeUnit> element.
    #
    def write_c_minor_time_unit(val) # :nodoc:
      val ||= 'days'

      @writer.empty_tag('c:minorTimeUnit', [['val', val]])
    end

    #
    # Write the <c:legend> element.
    #
    def write_legend # :nodoc:
      position = @legend.position.sub(/^overlay_/, '')
      return if position == 'none' || !position_allowed.has_key?(position)

      @delete_series = @legend.delete_series if @legend.delete_series.is_a?(Array)
      @writer.tag_elements('c:legend') do
        # Write the c:legendPos element.
        write_legend_pos(position_allowed[position])
        # Remove series labels from the legend.
        # Write the c:legendEntry element.
        @delete_series.each { |i| write_legend_entry(i) } if @delete_series
        # Write the c:layout element.
        write_layout(@legend.layout, 'legend')
        # Write the c:overlay element.
        write_overlay if @legend.position =~ /^overlay_/
        # Write the c:spPr element.
        write_sp_pr(@legend)
        # Write the c:txPr element.
        write_tx_pr(@legend.font) if ptrue?(@legend.font)
      end
    end

    def position_allowed
      {
        'right'     => 'r',
        'left'      => 'l',
        'top'       => 't',
        'bottom'    => 'b',
        'top_right' => 'tr'
      }
    end

    #
    # Write the <c:legendPos> element.
    #
    def write_legend_pos(val) # :nodoc:
      @writer.empty_tag('c:legendPos', [['val', val]])
    end

    #
    # Write the <c:legendEntry> element.
    #
    def write_legend_entry(index) # :nodoc:
      @writer.tag_elements('c:legendEntry') do
        # Write the c:idx element.
        write_idx(index)
        # Write the c:delete element.
        write_delete(1)
      end
    end

    #
    # Write the <c:overlay> element.
    #
    def write_overlay # :nodoc:
      @writer.empty_tag('c:overlay', [['val', 1]])
    end

    #
    # Write the <c:plotVisOnly> element.
    #
    def write_plot_vis_only # :nodoc:
      val  = 1

      # Ignore this element if we are plotting hidden data.
      return if @show_hidden_data

      @writer.empty_tag('c:plotVisOnly', [['val', val]])
    end

    #
    # Write the <c:printSettings> element.
    #
    def write_print_settings # :nodoc:
      @writer.tag_elements('c:printSettings') do
        # Write the c:headerFooter element.
        write_header_footer
        # Write the c:pageMargins element.
        write_page_margins
        # Write the c:pageSetup element.
        write_page_setup
      end
    end

    #
    # Write the <c:headerFooter> element.
    #
    def write_header_footer # :nodoc:
      @writer.empty_tag('c:headerFooter')
    end

    #
    # Write the <c:pageMargins> element.
    #
    def write_page_margins # :nodoc:
      b      = 0.75
      l      = 0.7
      r      = 0.7
      t      = 0.75
      header = 0.3
      footer = 0.3

      attributes = [
        ['b',      b],
        ['l',      l],
        ['r',      r],
        ['t',      t],
        ['header', header],
        ['footer', footer]
      ]

      @writer.empty_tag('c:pageMargins', attributes)
    end

    #
    # Write the <c:pageSetup> element.
    #
    def write_page_setup # :nodoc:
      @writer.empty_tag('c:pageSetup')
    end

    #
    # Write the <c:autoTitleDeleted> element.
    #
    def write_auto_title_deleted
      attributes = [['val', 1]]

      @writer.empty_tag('c:autoTitleDeleted', attributes)
    end

    #
    # Write the <c:title> element for a rich string.
    #
    def write_title_rich(title, is_y_axis, font, layout, overlay = nil) # :nodoc:
      @writer.tag_elements('c:title') do
        # Write the c:tx element.
        write_tx_rich(title, is_y_axis, font)
        # Write the c:layout element.
        write_layout(layout, 'text')
        # Write the c:overlay element.
        write_overlay if overlay
      end
    end

    #
    # Write the <c:title> element for a rich string.
    #
    def write_title_formula(title, is_y_axis = nil, axis = nil, layout = nil, overlay = nil) # :nodoc:
      @writer.tag_elements('c:title') do
        # Write the c:tx element.
        write_tx_formula(title.formula, axis ? axis.data_id : title.data_id)
        # Write the c:layout element.
        write_layout(layout, 'text')
        # Write the c:overlay element.
        write_overlay if overlay
        # Write the c:txPr element.
        write_tx_pr(axis ? axis.name_font : title.name_font, is_y_axis)
      end
    end

    #
    # Write the <c:tx> element.
    #
    def write_tx_rich(title, is_y_axis, font) # :nodoc:
      @writer.tag_elements('c:tx') do
        write_rich(title, font, is_y_axis)
      end
    end

    #
    # Write the <c:tx> element with a simple value such as for series names.
    #
    def write_tx_value(title) # :nodoc:
      @writer.tag_elements('c:tx') { write_v(title) }
    end

    #
    # Write the <c:tx> element.
    #
    def write_tx_formula(title, data_id) # :nodoc:
      data = @formula_data[data_id] if data_id

      @writer.tag_elements('c:tx') { write_str_ref(title, data, 'str') }
    end

    #
    # Write the <c:rich> element.
    #
    def write_rich(title, font, is_y_axis, ignore_rich_pr = false) # :nodoc:
      rotation = nil

      rotation = font[:_rotation] if font && font[:_rotation]
      @writer.tag_elements('c:rich') do
        # Write the a:bodyPr element.
        write_a_body_pr(rotation, is_y_axis)
        # Write the a:lstStyle element.
        write_a_lst_style
        # Write the a:p element.
        write_a_p_rich(title, font, ignore_rich_pr)
      end
    end

    #
    # Write the <a:p> element for rich string titles.
    #
    def write_a_p_rich(title, font, ignore_rich_pr) # :nodoc:
      @writer.tag_elements('a:p') do
        # Write the a:pPr element.
        write_a_p_pr_rich(font) unless ignore_rich_pr
        # Write the a:r element.
        write_a_r(title, font)
      end
    end

    #
    # Write the <a:pPr> element for rich string titles.
    #
    def write_a_p_pr_rich(font) # :nodoc:
      @writer.tag_elements('a:pPr') { write_a_def_rpr(font) }
    end

    #
    # Write the <a:r> element.
    #
    def write_a_r(title, font) # :nodoc:
      @writer.tag_elements('a:r') do
        # Write the a:rPr element.
        write_a_r_pr(font)
        # Write the a:t element.
        write_a_t(title.respond_to?(:name) ? title.name : title)
      end
    end

    #
    # Write the <a:rPr> element.
    #
    def write_a_r_pr(font) # :nodoc:
      attributes = [%w[lang en-US]]
      attr_font = get_font_style_attributes(font)
      attributes += attr_font unless attr_font.empty?

      write_def_rpr_r_pr_common(font, attributes, 'a:rPr')
    end

    #
    # Write the <a:t> element.
    #
    def write_a_t(title) # :nodoc:
      @writer.data_element('a:t', title)
    end

    #
    # Write the <c:marker> element.
    #
    def write_marker(marker = nil) # :nodoc:
      marker ||= @default_marker

      return unless ptrue?(marker)
      return if ptrue?(marker.automatic?)

      @writer.tag_elements('c:marker') do
        # Write the c:symbol element.
        write_symbol(marker.type)
        # Write the c:size element.
        size = marker.size
        write_marker_size(size) if ptrue?(size)
        # Write the c:spPr element.
        write_sp_pr(marker)
      end
    end

    #
    # Write the <c:marker> element without a sub-element.
    #
    def write_marker_value # :nodoc:
      return unless @default_marker

      @writer.empty_tag('c:marker', [['val', 1]])
    end

    #
    # Write the <c:size> element.
    #
    def write_marker_size(val) # :nodoc:
      @writer.empty_tag('c:size', [['val', val]])
    end

    #
    # Write the <c:symbol> element.
    #
    def write_symbol(val) # :nodoc:
      @writer.empty_tag('c:symbol', [['val', val]])
    end

    def has_fill_formatting(element)
      line     = series_property(element, :line)
      fill     = series_property(element, :fill)
      pattern  = series_property(element, :pattern)
      gradient = series_property(element, :gradient)

      (line && ptrue?(line[:_defined])) ||
        (fill && ptrue?(fill[:_defined])) || pattern || gradient
    end

    #
    # Write the <c:spPr> element.
    #
    def write_sp_pr(series) # :nodoc:
      return unless has_fill_formatting(series)

      line     = series_property(series, :line)
      fill     = series_property(series, :fill)
      pattern  = series_property(series, :pattern)
      gradient = series_property(series, :gradient)

      @writer.tag_elements('c:spPr') do
        # Write the fill elements for solid charts such as pie/doughnut and bar.
        if fill && fill[:_defined] != 0
          if ptrue?(fill[:none])
            # Write the a:noFill element.
            write_a_no_fill
          else
            # Write the a:solidFill element.
            write_a_solid_fill(fill)
          end
        end
        write_a_patt_fill(pattern) if ptrue?(pattern)
        if ptrue?(gradient)
          # Write the a:gradFill element.
          write_a_grad_fill(gradient)
        end
        # Write the a:ln element.
        write_a_ln(line) if line && ptrue?(line[:_defined])
      end
    end

    def series_property(object, property)
      if object.respond_to?(property)
        object.send(property)
      elsif object.respond_to?(:[])
        object[property]
      end
    end

    #
    # Write the <a:ln> element.
    #
    def write_a_ln(line) # :nodoc:
      attributes = []

      # Add the line width as an attribute.
      if line[:width]
        width = line[:width]
        # Round width to nearest 0.25, like Excel.
        width = ((width + 0.125) * 4).to_i / 4.0

        # Convert to internal units.
        width = (0.5 + (12700 * width)).to_i

        attributes << ['w', width]
      end

      if ptrue?(line[:none]) || ptrue?(line[:color]) || line[:dash_type]
        @writer.tag_elements('a:ln', attributes) do
          # Write the line fill.
          if ptrue?(line[:none])
            # Write the a:noFill element.
            write_a_no_fill
          elsif ptrue?(line[:color])
            # Write the a:solidFill element.
            write_a_solid_fill(line)
          end

          # Write the line/dash type.
          if line[:dash_type]
            # Write the a:prstDash element.
            write_a_prst_dash(line[:dash_type])
          end
        end
      else
        @writer.empty_tag('a:ln', attributes)
      end
    end

    #
    # Write the <a:noFill> element.
    #
    def write_a_no_fill # :nodoc:
      @writer.empty_tag('a:noFill')
    end

    #
    # Write the <a:alpha> element.
    #
    def write_a_alpha(val)
      val = (100 - val.to_i) * 1000

      @writer.empty_tag('a:alpha', [['val', val]])
    end

    #
    # Write the <a:prstDash> element.
    #
    def write_a_prst_dash(val) # :nodoc:
      @writer.empty_tag('a:prstDash', [['val', val]])
    end

    #
    # Write the <c:trendline> element.
    #
    def write_trendline(trendline) # :nodoc:
      return unless trendline

      @writer.tag_elements('c:trendline') do
        # Write the c:name element.
        write_name(trendline.name)
        # Write the c:spPr element.
        write_sp_pr(trendline)
        # Write the c:trendlineType element.
        write_trendline_type(trendline.type)
        # Write the c:order element for polynomial trendlines.
        write_trendline_order(trendline.order) if trendline.type == 'poly'
        # Write the c:period element for moving average trendlines.
        write_period(trendline.period) if trendline.type == 'movingAvg'
        # Write the c:forward element.
        write_forward(trendline.forward)
        # Write the c:backward element.
        write_backward(trendline.backward)
        if trendline.intercept
          # Write the c:intercept element.
          write_intercept(trendline.intercept)
        end
        if trendline.display_r_squared
          # Write the c:dispRSqr element.
          write_disp_rsqr
        end
        if trendline.display_equation
          # Write the c:dispEq element.
          write_disp_eq
          # Write the c:trendlineLbl element.
          write_trendline_lbl(trendline)
        end
      end
    end

    #
    # Write the <c:trendlineType> element.
    #
    def write_trendline_type(val) # :nodoc:
      @writer.empty_tag('c:trendlineType', [['val', val]])
    end

    #
    # Write the <c:name> element.
    #
    def write_name(data) # :nodoc:
      return unless data

      @writer.data_element('c:name', data)
    end

    #
    # Write the <c:order> element.
    #
    def write_trendline_order(val = 2) # :nodoc:
      @writer.empty_tag('c:order', [['val', val]])
    end

    #
    # Write the <c:period> element.
    #
    def write_period(val = 2) # :nodoc:
      @writer.empty_tag('c:period', [['val', val]])
    end

    #
    # Write the <c:forward> element.
    #
    def write_forward(val) # :nodoc:
      return unless val

      @writer.empty_tag('c:forward', [['val', val]])
    end

    #
    # Write the <c:backward> element.
    #
    def write_backward(val) # :nodoc:
      return unless val

      @writer.empty_tag('c:backward', [['val', val]])
    end

    #
    # Write the <c:intercept> element.
    #
    def write_intercept(val)
      @writer.empty_tag('c:intercept', [['val', val]])
    end

    #
    # Write the <c:dispEq> element.
    #
    def write_disp_eq
      @writer.empty_tag('c:dispEq', [['val', 1]])
    end

    #
    # Write the <c:dispRSqr> element.
    #
    def write_disp_rsqr
      @writer.empty_tag('c:dispRSqr', [['val', 1]])
    end

    #
    # Write the <c:trendlineLbl> element.
    #
    def write_trendline_lbl(trendline)
      @writer.tag_elements('c:trendlineLbl') do
        # Write the c:layout element.
        write_layout
        # Write the c:numFmt element.
        write_trendline_num_fmt
        # Write the c:spPr element for the label formatting.
        write_sp_pr(trendline.label)
        # Write the data label font elements.
        if trendline.label && ptrue?(trendline.label[:font])
          write_axis_font(trendline.label[:font])
        end
      end
    end

    #
    # Write the <c:numFmt> element.
    #
    def write_trendline_num_fmt
      format_code   = 'General'
      source_linked = 0

      attributes = [
        ['formatCode',   format_code],
        ['sourceLinked', source_linked]
      ]

      @writer.empty_tag('c:numFmt', attributes)
    end

    #
    # Write the <c:hiLowLines> element.
    #
    def write_hi_low_lines # :nodoc:
      write_lines_base(@hi_low_lines, 'c:hiLowLines')
    end

    #
    # Write the <c:dropLines> elent.
    #
    def write_drop_lines
      write_lines_base(@drop_lines, 'c:dropLines')
    end

    def write_lines_base(lines, tag)
      return unless lines

      if lines.line_defined?
        @writer.tag_elements(tag) { write_sp_pr(lines) }
      else
        @writer.empty_tag(tag)
      end
    end

    #
    # Write the <c:overlap> element.
    #
    def write_overlap(val = nil) # :nodoc:
      return unless val

      @writer.empty_tag('c:overlap', [['val', val]])
    end

    #
    # Write the <c:numCache> element.
    #
    def write_num_cache(data) # :nodoc:
      write_num_base('c:numCache', data)
    end

    def write_num_base(tag, data)
      @writer.tag_elements(tag) do
        # Write the c:formatCode element.
        write_format_code('General')

        # Write the c:ptCount element.
        count = if data
                  data.size
                else
                  0
                end
        write_pt_count(count)

        data.each_with_index do |token, i|
          # Write non-numeric data as 0.
          if token &&
             !(token.to_s =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)
            token = 0
          end

          # Write the c:pt element.
          write_pt(i, token)
        end
      end
    end

    #
    # Write the <c:strCache> element.
    #
    def write_str_cache(data) # :nodoc:
      @writer.tag_elements('c:strCache') do
        write_pt_count(data.size)
        write_pts(data)
      end
    end

    def write_pts(data)
      data.each_index { |i| write_pt(i, data[i]) }
    end

    #
    # Write the <c:formatCode> element.
    #
    def write_format_code(data) # :nodoc:
      @writer.data_element('c:formatCode', data)
    end

    #
    # Write the <c:ptCount> element.
    #
    def write_pt_count(val) # :nodoc:
      @writer.empty_tag('c:ptCount', [['val', val]])
    end

    #
    # Write the <c:pt> element.
    #
    def write_pt(idx, value) # :nodoc:
      return unless value

      attributes = [['idx', idx]]

      @writer.tag_elements('c:pt', attributes) { write_v(value) }
    end

    #
    # Write the <c:v> element.
    #
    def write_v(data) # :nodoc:
      @writer.data_element('c:v', data)
    end

    #
    # Write the <c:protection> element.
    #
    def write_protection # :nodoc:
      return if @protection == 0

      @writer.empty_tag('c:protection')
    end

    #
    # Write the <c:dPt> elements.
    #
    def write_d_pt(points = nil)
      return unless ptrue?(points)

      index = -1
      points.each do |point|
        index += 1
        next unless ptrue?(point)

        write_d_pt_point(index, point)
      end
    end

    #
    # Write an individual <c:dPt> element.
    #
    def write_d_pt_point(index, point)
      @writer.tag_elements('c:dPt') do
        # Write the c:idx element.
        write_idx(index)
        # Write the c:spPr element.
        write_sp_pr(point)
      end
    end

    #
    # Write the <c:dLbls> element.
    #
    def write_d_lbls(labels) # :nodoc:
      return unless labels

      @writer.tag_elements('c:dLbls') do
        # Write the custom c:dLbl elements.
        write_custom_labels(labels, labels[:custom]) if labels[:custom]
        # Write the c:numFmt element.
        write_data_label_number_format(labels[:num_format]) if labels[:num_format]
        # Write the c:spPr element.
        write_sp_pr(labels)
        # Write the data label font elements.
        write_axis_font(labels[:font]) if labels[:font]
        # Write the c:dLblPos element.
        write_d_lbl_pos(labels[:position]) if ptrue?(labels[:position])
        # Write the c:showLegendKey element.
        write_show_legend_key if labels[:legend_key]
        # Write the c:showVal element.
        write_show_val if labels[:value]
        # Write the c:showCatName element.
        write_show_cat_name if labels[:category]
        # Write the c:showSerName element.
        write_show_ser_name if labels[:series_name]
        # Write the c:showPercent element.
        write_show_percent if labels[:percentage]
        # Write the c:separator element.
        write_separator(labels[:separator]) if labels[:separator]
        # Write the c:showLeaderLines element.
        write_show_leader_lines if labels[:leader_lines]
      end
    end

    #
    # Write the <c:dLbl> element.
    #
    def write_custom_labels(parent, labels)
      index  = 0

      labels.each do |label|
        index += 1
        next unless ptrue?(label)

        @writer.tag_elements('c:dLbl') do
          # Write the c:idx element.
          write_idx(index - 1)

          if label[:delete]
            write_delete(1)
          elsif label[:formula]
            write_custom_label_formula(label)

            write_d_lbl_pos(parent[:position]) if parent[:position]
            write_show_val      if parent[:value]
            write_show_cat_name if parent[:category]
            write_show_ser_name if parent[:series_name]
          elsif label[:value]
            write_custom_label_str(label)

            write_d_lbl_pos(parent[:position]) if parent[:position]
            write_show_val      if parent[:value]
            write_show_cat_name if parent[:category]
            write_show_ser_name if parent[:series_name]
          else
            write_custom_label_format_only(label)
          end
        end
      end
    end

    #
    # Write parts of the <c:dLbl> element for strings.
    #
    def write_custom_label_str(label)
      value          = label[:value]
      font           = label[:font]
      is_y_axis      = 0
      has_formatting = has_fill_formatting(label)

      # Write the c:layout element.
      write_layout

      @writer.tag_elements('c:tx') do
        # Write the c:rich element.
        write_rich(value, font, is_y_axis, !has_formatting)
      end

      # Write the c:cpPr element.
      write_sp_pr(label)
    end

    #
    # Write parts of the <c:dLbl> element for formulas.
    #
    def write_custom_label_formula(label)
      formula = label[:formula]
      data_id = label[:data_id]

      data = @formula_data[data_id] if data_id

      # Write the c:layout element.
      write_layout

      @writer.tag_elements('c:tx') do
        # Write the c:strRef element.
        write_str_ref(formula, data, 'str')
      end

      # Write the data label formatting, if any.
      write_custom_label_format_only(label)
    end

    #
    # Write parts of the <c:dLbl> element for labels where only the formatting has
    # changed.
    #
    def write_custom_label_format_only(label)
      font           = label[:font]
      has_formatting = has_fill_formatting(label)

      if has_formatting
        # Write the c:spPr element.
        write_sp_pr(label)
        write_tx_pr(font)
      elsif font
        @writer.empty_tag('c:spPr')
        write_tx_pr(font)
      end
    end

    #
    # Write the <c:showLegendKey> element.
    #
    def write_show_legend_key
      @writer.empty_tag('c:showLegendKey', [['val', 1]])
    end

    #
    # Write the <c:showVal> element.
    #
    def write_show_val # :nodoc:
      @writer.empty_tag('c:showVal', [['val', 1]])
    end

    #
    # Write the <c:showCatName> element.
    #
    def write_show_cat_name # :nodoc:
      @writer.empty_tag('c:showCatName', [['val', 1]])
    end

    #
    # Write the <c:showSerName> element.
    #
    def write_show_ser_name # :nodoc:
      @writer.empty_tag('c:showSerName', [['val', 1]])
    end

    #
    # Write the <c:showPercent> element.
    #
    def write_show_percent
      @writer.empty_tag('c:showPercent', [['val', 1]])
    end

    #
    # Write the <c:separator> element.
    #
    def write_separator(data)
      @writer.data_element('c:separator', data)
    end

    # Write the <c:showLeaderLines> element. This is different for Pie/Doughnut
    # charts. Other chart types only supported leader lines after Excel 2015 via
    # an extension element.
    def write_show_leader_lines
      uri        = '{CE6537A1-D6FC-4f65-9D91-7224C49458BB}'
      xmlns_c_15 = 'http://schemas.microsoft.com/office/drawing/2012/chart'

      attributes1 = [
        ['uri', uri],
        ['xmlns:c15', xmlns_c_15]
      ]

      attributes2 = [['val',  1]]

      @writer.tag_elements('c:extLst') do
        @writer.tag_elements('c:ext', attributes1) do
          @writer.empty_tag('c15:showLeaderLines', attributes2)
        end
      end
    end

    #
    # Write the <c:dLblPos> element.
    #
    def write_d_lbl_pos(val)
      @writer.empty_tag('c:dLblPos', [['val', val]])
    end

    #
    # Write the <c:delete> element.
    #
    def write_delete(val) # :nodoc:
      @writer.empty_tag('c:delete', [['val', val]])
    end

    #
    # Write the <c:invertIfNegative> element.
    #
    def write_c_invert_if_negative(invert = nil) # :nodoc:
      return unless ptrue?(invert)

      @writer.empty_tag('c:invertIfNegative', [['val', 1]])
    end

    #
    # Write the axis font elements.
    #
    def write_axis_font(font) # :nodoc:
      return unless font

      @writer.tag_elements('c:txPr') do
        write_a_body_pr(font[:_rotation])
        write_a_lst_style
        @writer.tag_elements('a:p') do
          write_a_p_pr_rich(font)
          write_a_end_para_rpr
        end
      end
    end

    #
    # Write the <a:latin> element.
    #
    def write_a_latin(args)  # :nodoc:
      @writer.empty_tag('a:latin', args)
    end

    #
    # Write the <c:dTable> element.
    #
    def write_d_table
      @table.write_d_table(@writer) if @table
    end

    #
    # Write the X and Y error bars.
    #
    def write_error_bars(error_bars)
      return unless ptrue?(error_bars)

      write_err_bars('x', error_bars[:_x_error_bars]) if error_bars[:_x_error_bars]
      write_err_bars('y', error_bars[:_y_error_bars]) if error_bars[:_y_error_bars]
    end

    #
    # Write the <c:errBars> element.
    #
    def write_err_bars(direction, error_bars)
      return unless ptrue?(error_bars)

      @writer.tag_elements('c:errBars') do
        # Write the c:errDir element.
        write_err_dir(direction)

        # Write the c:errBarType element.
        write_err_bar_type(error_bars.direction)

        # Write the c:errValType element.
        write_err_val_type(error_bars.type)

        unless ptrue?(error_bars.endcap)
          # Write the c:noEndCap element.
          write_no_end_cap
        end

        case error_bars.type
        when 'stdErr'
          # Don't need to write a c:errValType tag.
        when 'cust'
          # Write the custom error tags.
          write_custom_error(error_bars)
        else
          # Write the c:val element.
          write_error_val(error_bars.value)
        end

        # Write the c:spPr element.
        write_sp_pr(error_bars)
      end
    end

    #
    # Write the <c:errDir> element.
    #
    def write_err_dir(val)
      @writer.empty_tag('c:errDir', [['val', val]])
    end

    #
    # Write the <c:errBarType> element.
    #
    def write_err_bar_type(val)
      @writer.empty_tag('c:errBarType', [['val', val]])
    end

    #
    # Write the <c:errValType> element.
    #
    def write_err_val_type(val)
      @writer.empty_tag('c:errValType', [['val', val]])
    end

    #
    # Write the <c:noEndCap> element.
    #
    def write_no_end_cap
      @writer.empty_tag('c:noEndCap', [['val', 1]])
    end

    #
    # Write the <c:val> element.
    #
    def write_error_val(val)
      @writer.empty_tag('c:val', [['val', val]])
    end

    #
    # Write the custom error bars type.
    #
    def write_custom_error(error_bars)
      if ptrue?(error_bars.plus_values)
        write_custom_error_base('c:plus',  error_bars.plus_values,  error_bars.plus_data)
        write_custom_error_base('c:minus', error_bars.minus_values, error_bars.minus_data)
      end
    end

    def write_custom_error_base(tag, values, data)
      @writer.tag_elements(tag) do
        write_num_ref_or_lit(values, data)
      end
    end

    def write_num_ref_or_lit(values, data)
      if values.to_s =~ /^=/                # '=Sheet1!$A$1:$A$5'
        write_num_ref(values, data, 'num')
      else                                  # [1, 2, 3]
        write_num_lit(values)
      end
    end

    #
    # Write the <c:upDownBars> element.
    #
    def write_up_down_bars
      return unless ptrue?(@up_down_bars)

      @writer.tag_elements('c:upDownBars') do
        # Write the c:gapWidth element.
        write_gap_width(150)

        # Write the c:upBars element.
        write_up_bars(@up_down_bars[:_up])

        # Write the c:downBars element.
        write_down_bars(@up_down_bars[:_down])
      end
    end

    #
    # Write the <c:gapWidth> element.
    #
    def write_gap_width(val = nil)
      return unless val

      @writer.empty_tag('c:gapWidth', [['val', val]])
    end

    #
    # Write the <c:upBars> element.
    #
    def write_up_bars(format)
      write_bars_base('c:upBars', format)
    end

    #
    # Write the <c:upBars> element.
    #
    def write_down_bars(format)
      write_bars_base('c:downBars', format)
    end

    #
    # Write the <c:smooth> element.
    #
    def write_c_smooth(smooth)
      return unless ptrue?(smooth)

      attributes = [['val', 1]]

      @writer.empty_tag('c:smooth', attributes)
    end

    #
    # Write the <c:dispUnits> element.
    #
    def write_disp_units(units, display)
      return unless ptrue?(units)

      attributes = [['val', units]]

      @writer.tag_elements('c:dispUnits') do
        @writer.empty_tag('c:builtInUnit', attributes)
        if ptrue?(display)
          @writer.tag_elements('c:dispUnitsLbl') do
            @writer.empty_tag('c:layout')
          end
        end
      end
    end

    #
    # Write the <a:gradFill> element.
    #
    def write_a_grad_fill(gradient)
      attributes = [
        %w[flip none],
        ['rotWithShape', 1]
      ]
      attributes = [] if gradient[:type] == 'linear'

      @writer.tag_elements('a:gradFill', attributes) do
        # Write the a:gsLst element.
        write_a_gs_lst(gradient)

        if gradient[:type] == 'linear'
          # Write the a:lin element.
          write_a_lin(gradient[:angle])
        else
          # Write the a:path element.
          write_a_path(gradient[:type])

          # Write the a:tileRect element.
          write_a_tile_rect(gradient[:type])
        end
      end
    end

    #
    # Write the <a:gsLst> element.
    #
    def write_a_gs_lst(gradient)
      positions = gradient[:positions]
      colors    = gradient[:colors]

      @writer.tag_elements('a:gsLst') do
        (0..colors.size - 1).each do |i|
          pos = (positions[i] * 1000).to_i

          attributes = [['pos', pos]]
          @writer.tag_elements('a:gs', attributes) do
            color = color(colors[i])

            # Write the a:srgbClr element.
            # TODO: Wait for a feature request to support transparency.
            write_a_srgb_clr(color)
          end
        end
      end
    end

    #
    # Write the <a:lin> element.
    #
    def write_a_lin(angle)
      scaled = 0

      angle = (60000 * angle).to_i

      attributes = [
        ['ang',    angle],
        ['scaled', scaled]
      ]

      @writer.empty_tag('a:lin', attributes)
    end

    #
    # Write the <a:path> element.
    #
    def write_a_path(type)
      attributes = [['path', type]]

      @writer.tag_elements('a:path', attributes) do
        # Write the a:fillToRect element.
        write_a_fill_to_rect(type)
      end
    end

    #
    # Write the <a:fillToRect> element.
    #
    def write_a_fill_to_rect(type)
      attributes = if type == 'shape'
                     [
                       ['l', 50000],
                       ['t', 50000],
                       ['r', 50000],
                       ['b', 50000]
                     ]
                   else
                     [
                       ['l', 100000],
                       ['t', 100000]
                     ]
                   end

      @writer.empty_tag('a:fillToRect', attributes)
    end

    #
    # Write the <a:tileRect> element.
    #
    def write_a_tile_rect(type)
      attributes = if type == 'shape'
                     []
                   else
                     [
                       ['r', -100000],
                       ['b', -100000]
                     ]
                   end

      @writer.empty_tag('a:tileRect', attributes)
    end

    #
    # Write the <a:pattFill> element.
    #
    def write_a_patt_fill(pattern)
      attributes = [['prst', pattern[:pattern]]]

      @writer.tag_elements('a:pattFill', attributes) do
        write_a_fg_clr(pattern[:fg_color])
        write_a_bg_clr(pattern[:bg_color])
      end
    end

    def write_a_fg_clr(color)
      @writer.tag_elements('a:fgClr') { write_a_srgb_clr(color(color)) }
    end

    def write_a_bg_clr(color)
      @writer.tag_elements('a:bgClr') { write_a_srgb_clr(color(color)) }
    end

    def write_bars_base(tag, format)
      if format.line_defined? || format.fill_defined?
        @writer.tag_elements(tag) { write_sp_pr(format) }
      else
        @writer.empty_tag(tag)
      end
    end
      class Area < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @subtype = subtype || 'standard'
        @cross_between = 'midCat'
        @show_crosses  = false

        # Override and reset the default axis values.
        @y_axis.defaults[:num_format] = '0%' if @subtype == 'percent_stacked'

        set_y_axis

        # Set the available data label positions for this chart type.
        @label_position_default = 'center'
        @label_positions = { 'center' => 'ctr' }
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        # Write the c:areaChart element.
        write_area_chart(params)
      end

      #
      # Write the <c:areaChart> element.
      #
      def write_area_chart(params)
        series = axes_series(params)
        return if series.empty?

        subtype = if @subtype == 'percent_stacked'
                    'percentStacked'
                  else
                    @subtype
                  end
        @writer.tag_elements('c:areaChart') do
          # Write the c:grouping element.
          write_grouping(subtype)
          # Write the series elements.
          series.each { |s| write_series(s) }

          # Write the c:dropLines element.
          write_drop_lines

          # Write the c:marker element.
          write_marker_value

          # Write the c:axId elements
          write_axis_ids(params)
        end
      end
    end
	class Axis < Caption
      include Writexlsx_kot::Utility

      attr_accessor :defaults
      attr_accessor :min, :max, :num_format, :position
      attr_accessor :major_tick_mark, :minor_tick_mark
      attr_reader :minor_unit, :major_unit, :minor_unit_type, :major_unit_type
      attr_reader :display_units_visible, :display_units
      attr_reader :log_base, :crossing, :position_axis, :label_position, :visible
      attr_reader :num_format_linked, :num_font, :layout, :interval_unit
      attr_reader :interval_tick, :major_gridlines, :minor_gridlines, :reverse
      attr_reader :line, :fill, :text_axis, :label_align

      #
      # Convert user defined axis values into axis instance.
      #
      def merge_with_hash(params) # :nodoc:
        super
        args      = (defaults || {}).merge(params)

        %i[
          reverse min max minor_unit major_unit minor_unit_type
          major_unit_type log_base crossing position_axis
          label_position num_format num_format_linked interval_unit
          interval_tick line fill label_align
        ].each { |val| instance_variable_set("@#{val}", args[val]) }
        set_major_minor_gridlines(args)

        @visible           = args[:visible] || 1
        set_display_units(args)
        set_display_units_visible(args)
        set_position(args)
        set_position_axis
        set_font_properties(args)
        set_axis_name_layout(args)
        set_axis_line(args)
        set_axis_fill(args)
        if ptrue?(args[:text_axis])
          @chart.date_category = false
          @text_axis = true
        end

        # Set the tick marker types.
        @major_tick_mark = get_tick_type(params[:major_tick_mark])
        @minor_tick_mark = get_tick_type(params[:minor_tick_mark])
      end

      #
      # Write the <c:numberFormat> element. Note: It is assumed that if a user
      # defined number format is supplied (i.e., non-default) then the sourceLinked
      # attribute is 0. The user can override this if required.
      #

      def write_number_format(writer) # :nodoc:
        writer.empty_tag('c:numFmt', num_fmt_attributes)
      end

      #
      # Write the <c:numFmt> element. Special case handler for category axes which
      # don't always have a number format.
      #
      def write_cat_number_format(writer, cat_has_num_fmt)
        return unless user_defined_num_fmt_set? || cat_has_num_fmt

        writer.empty_tag('c:numFmt', num_fmt_attributes)
      end

      private

      def user_defined_num_fmt_set?
        @defaults && @num_format != @defaults[:num_format]
      end

      def source_linked
        value = 1
        value = 0 if user_defined_num_fmt_set?
        value = 1 if @num_format_linked

        value
      end

      def num_fmt_attributes
        [
          ['formatCode',   @num_format],
          ['sourceLinked', source_linked]
        ]
      end

      def set_major_minor_gridlines(args)
        # Map major/minor_gridlines properties.
        %i[major_gridlines minor_gridlines].each do |lines|
          if args[lines] && ptrue?(args[lines][:visible])
            instance_variable_set("@#{lines}", Gridline.new(args[lines]))
          else
            instance_variable_set("@#{lines}", nil)
          end
        end
      end

      #
      #
      # Convert user defined display units to internal units.
      #
      def get_display_units(display_units)
        return unless ptrue?(display_units)

        types = {
          'hundreds'          => 'hundreds',
          'thousands'         => 'thousands',
          'ten_thousands'     => 'tenThousands',
          'hundred_thousands' => 'hundredThousands',
          'millions'          => 'millions',
          'ten_millions'      => 'tenMillions',
          'hundred_millions'  => 'hundredMillions',
          'billions'          => 'billions',
          'trillions'         => 'trillions'
        }

        types[display_units] || warn("Unknown display_units type '$display_units'\n")
      end

      #
      # Convert user tick types to internal units.
      #
      def get_tick_type(tick_type)
        return unless ptrue?(tick_type)

        types = {
          'outside' => 'out',
          'inside'  => 'in',
          'none'    => 'none',
          'cross'   => 'cross'
        }

        types[tick_type] || raise("Unknown tick_type type '#{tick_type}'\n")
      end

      def set_display_units(args)
        @display_units = get_display_units(args[:display_units])
      end

      def set_display_units_visible(args)
        @display_units_visible = args[:display_units_visible] || 1
      end

      def set_position(args)
        # Only use the first letter of bottom, top, left or right.
        @position = args[:position]
        @position = @position.downcase[0, 1] if @position
      end

      def set_position_axis
        # Set the position for a category axis on or between the tick marks.
        if @position_axis
          if @position_axis == 'on_tick'
            @position_axis = 'midCat'
          elsif @position_axis == 'between'
          # Doesn't neet to be modified.
          else
            # Otherwise use the default value.
            @position_axis = nil
          end
        end
      end

      def set_font_properties(args)
        @num_font  = convert_font_args(args[:num_font])
        @name_font = convert_font_args(args[:name_font])
      end

      def set_axis_name_layout(args)
        @layout    = @chart.layout_properties(args[:name_layout], 1)
      end

      def set_axis_line(args)
        @line = @chart.line_properties(args[:line])
      end

      def set_axis_fill(args)
        @fill = @chart.fill_properties(args[:fill])
      end
    end
	class Bar < self
      include Writexlsx_kot::Utility

      def initialize(subtype)
        super(subtype)
        @subtype = subtype || 'clustered'
        @cat_axis_position = 'l'
        @val_axis_position = 'b'
        @horiz_val_axis    = 0
        @horiz_cat_axis    = 1
        @show_crosses      = false
        # Override and reset the default axis values.
        axis_defaults_set
        set_x_axis
        set_y_axis

        # Set the available data label positions for this chart type.
        @label_position_default = 'outside_end'
        @label_positions = {
          'center'      => 'ctr',
          'inside_base' => 'inBase',
          'inside_end'  => 'inEnd',
          'outside_end' => 'outEnd'
        }
      end

      #
      # Override parent method to add an extra check that is required for Bar
      # charts to ensure that their combined chart is on a secondary axis.
      #
      def combine(chart)
        raise 'Charts combined with Bar charts must be on a secondary axis' unless chart.is_secondary?

        super
      end

      #
      # Override the virtual superclass method with a chart specific method.
      #
      def write_chart_type(params)
        if params[:primary_axes] != 0
          # Reverse X and Y axes for Bar charts.
          @y_axis, @x_axis = @x_axis, @y_axis
          @y2_axis.position = 't' if @y2_axis.position == 'r'
        end

        # Write the c:barChart element.
        write_bar_chart(params)
      end

      #
      # Write the <c:barDir> element.
      #
      def write_bar_dir
        @writer.empty_tag('c:barDir', [%w[val bar]])
      end

      #
      # Write the <c:errDir> element. Overridden from Chart class since it is not
      # used in Bar charts.
      #
      def write_err_dir(direction)
        # do nothing
      end

      private

      def axis_defaults_set
        if @x_axis.defaults
          @x_axis.defaults[:major_gridlines] = { visible: 1 }
        else
          @x_axis.defaults = { major_gridlines: { visible: 1 } }
        end
        if @y_axis.defaults
          @y_axis.defaults[:major_gridlines] = { visible: 0 }
        else
          @y_axis.defaults = { major_gridlines: { visible: 0 } }
        end
        @x_axis.defaults[:num_format] = '0%' if @subtype == 'percent_stacked'
      end
    end

  end
	
end

class ColName
  include Singleton

  def initialize
    @col_str_table = []
    @row_str_table = []
  end

  def col_str(col)
    @col_str_table[col] ||= col_str_build(col)
  end

  def row_str(row)
    @row_str_table[row] ||= row.to_s
  end

  private

  def col_str_build(col)
    # Change from 0-indexed to 1 indexed.
    col = col.to_i + 1
    col_str = ''

    while col > 0
      # Set remainder from 1 .. 26
      remainder = col % 26
      remainder = 26 if remainder == 0

      # Convert the remainder to a character. C-ishly.
      col_letter = ("A".ord + remainder - 1).chr

      # Accumulate the column letters, right to left.
      col_str = col_letter + col_str

      # Get the next order of magnitude.
      col = (col - 1) / 26
    end

    col_str
  end
end

class WriteXLSX_kot < Writexlsx_kot::Workbook
  $KCODE = 'u' if RUBY_VERSION < '1.9'
end

class WriteXLSXInsufficientArgumentError < StandardError
end

class WriteXLSXDimensionError < StandardError
end

class WriteXLSXOptionParameterError < StandardError
end

class String # :nodoc:
  def first_line
    each_line { |line| return line }
  end
  alias lines to_a unless "".respond_to?(:lines)
  unless "".respond_to?(:each_char)
    def each_char(&block) # :nodoc:
      # copied from jcode
      if block_given?
        scan(/./m, &block)
      else
        scan(/./m)
      end
    end
  end

  unless "".respond_to?(:bytesize)
    def bytesize # :nodoc:
      length
    end
  end

  unless "".respond_to?(:ord)
    def ord
      self[0]
    end
  end

  unless "".respond_to?(:ascii_only?)
    def ascii_only?
      !!(self =~ %r{[^!"#$%&'()*+,\-./:;<=>?@0-9A-Za-z_\[\\\]\{\}^` ~\0\n]})
    end
  end
end

unless File.respond_to?(:binread)
  def File.binread(file) # :nodoc:
    File.open(file, "rb:utf-8:utf-8") { |f| f.read }
  end
end

if RUBY_VERSION < "1.9"

  def ruby_18 # :nodoc:
    yield
  end

  def ruby_19 # :nodoc:
    false
  end

else

  def ruby_18 # :nodoc:
    false
  end

  def ruby_19 # :nodoc:
    yield
  end
end