require_relative './fast_excel/binding'
require 'set'

# not used for now
#require_relative '../ext/fast_excel/text_width_ext'

module FastExcel

  class Formula
    attr_accessor :fml
    def initialize(fml)
      @fml = fml
    end
  end

  class URL
    attr_accessor :url
    def initialize(url)
      @url = url
    end
  end

  DEF_COL_WIDTH = 8.43

  def self.open(filename = nil, constant_memory: false, default_format: nil)
    tmp_file = false
    if filename
      if File.exist?(filename) && File.size(filename) > 0
        raise ArgumentError, "File '#{filename}' already exists. FastExcel can not open existing files, only create new files"
      end
    else
      require 'tmpdir'
      filename = "#{Dir.mktmpdir}/fast_excel.xlsx"
      tmp_file = true
    end

    filename = filename.to_s if defined?(Pathname) && filename.is_a?(Pathname)

    opt = Libxlsxwriter::WorkbookOptions.new
    opt[:constant_memory] = constant_memory ? 1 : 0
    workbook = Libxlsxwriter.workbook_new_opt(filename, opt)

    result = Libxlsxwriter::Workbook.new(workbook)

    if default_format
      raise "default_format argument must be a hash" unless default_format.is_a?(Hash)
      result.default_format.set(default_format)
    end

    result.tmp_file = tmp_file
    result.filename = filename
    result
  end

  # Creates internal Libxlsxwriter::Datetime from Datetime object
  def self.lxw_datetime(time)
    date = Libxlsxwriter::Datetime.new
    date[:year] = time.year
    date[:month] = time.month
    date[:day] = time.day
    date[:hour] = time.hour
    date[:min] = time.minute
    date[:sec] = time.second
    date
  end

  # Creates internal Libxlsxwriter::Datetime from Time object
  def self.lxw_time(time)
    date = Libxlsxwriter::Datetime.new
    date[:year] = time.year
    date[:month] = time.month
    date[:day] = time.day
    date[:hour] = time.hour
    date[:min] = time.min
    date[:sec] = time.sec
    date
  end

  # seconds in 1 day
  XLSX_DATE_DAY = 86400.0

  # days between 1970-jan-01 and 1900-jan-01
  XLSX_DATE_EPOCH_DIFF = 25569

  # Convert time to number of days, and change beginning point from 1st jan 1970 to 1st jan 1900
  # Offset argument should be number of seconds, if not specified then it will use Time.zone.utc_offset || 0
  #
  # https://support.microsoft.com/en-us/help/214330/differences-between-the-1900-and-the-1904-date-system-in-excel
  def self.date_num(time, offset = nil)
    unless offset
      # Try use value utc_offset
      if time.respond_to?(:utc_offset)
        offset = time.utc_offset
      # Else try use Rails' app timezone
      elsif Time.respond_to?(:zone) && !Time.zone.nil?
        offset = Time.zone.utc_offset
      else
        offset = 0 # rollback to UTC
      end
    end

    time.to_f / XLSX_DATE_DAY + XLSX_DATE_EPOCH_DIFF + offset / XLSX_DATE_DAY
  end

  def self.print_ffi_obj(value, do_print: true, offset: "", deep: false)
    result = "#{value.class}"

    value.members.each do |key|
      fval = value[key]
      field_val = if fval.is_a?(FFI::Pointer) && fval.null? || fval.nil?
        "nil"
      elsif fval.is_a?(FFI::StructLayout::CharArray)
        fval.to_str.inspect
      elsif fval.is_a?(String)
        fval.inspect
      elsif fval.is_a?(Symbol)
        fval.inspect
      elsif fval.is_a?(FFI::Struct) && deep
        print_ffi_obj(fval, do_print: false, offset: offset + "    ", deep: deep)
      else
        fval
      end

      result += "\n#{offset}* #{key}: #{field_val}"
    end

    if do_print
      puts result
    else
      return result
    end
  end


  ERROR_ENUM = Libxlsxwriter.enum_type(:error)
  COLOR_ENUM = Libxlsxwriter.enum_type(:defined_colors)
  EXTRA_COLORS = {
    alice_blue: 0xF0F8FF,
    antique_white: 0xFAEBD7,
    aqua: 0x00FFFF,
    aquamarine: 0x7FFFD4,
    azure: 0xF0FFFF,
    beige: 0xF5F5DC,
    bisque: 0xFFE4C4,
    black: 0x000000,
    blanched_almond: 0xFFEBCD,
    blue: 0x0000FF,
    blue_violet: 0x8A2BE2,
    brown: 0xA52A2A,
    burly_wood: 0xDEB887,
    cadet_blue: 0x5F9EA0,
    chartreuse: 0x7FFF00,
    chocolate: 0xD2691E,
    coral: 0xFF7F50,
    cornflower_blue: 0x6495ED,
    cornsilk: 0xFFF8DC,
    crimson: 0xDC143C,
    cyan: 0x00FFFF,
    dark_blue: 0x00008B,
    dark_cyan: 0x008B8B,
    dark_golden_rod: 0xB8860B,
    dark_gray: 0xA9A9A9,
    dark_grey: 0xA9A9A9,
    dark_green: 0x006400,
    dark_khaki: 0xBDB76B,
    dark_magenta: 0x8B008B,
    dark_olive_green: 0x556B2F,
    dark_orange: 0xFF8C00,
    dark_orchid: 0x9932CC,
    dark_red: 0x8B0000,
    dark_salmon: 0xE9967A,
    dark_sea_green: 0x8FBC8F,
    dark_slate_blue: 0x483D8B,
    dark_slate_gray: 0x2F4F4F,
    dark_slate_grey: 0x2F4F4F,
    dark_turquoise: 0x00CED1,
    dark_violet: 0x9400D3,
    deep_pink: 0xFF1493,
    deep_sky_blue: 0x00BFFF,
    dim_gray: 0x696969,
    dim_grey: 0x696969,
    dodger_blue: 0x1E90FF,
    fire_brick: 0xB22222,
    floral_white: 0xFFFAF0,
    forest_green: 0x228B22,
    fuchsia: 0xFF00FF,
    gainsboro: 0xDCDCDC,
    ghost_white: 0xF8F8FF,
    gold: 0xFFD700,
    golden_rod: 0xDAA520,
    gray: 0x808080,
    grey: 0x808080,
    green: 0x008000,
    green_yellow: 0xADFF2F,
    honey_dew: 0xF0FFF0,
    hot_pink: 0xFF69B4,
    indian_red: 0xCD5C5C,
    indigo: 0x4B0082,
    ivory: 0xFFFFF0,
    khaki: 0xF0E68C,
    lavender: 0xE6E6FA,
    lavender_blush: 0xFFF0F5,
    lawn_green: 0x7CFC00,
    lemon_chiffon: 0xFFFACD,
    light_blue: 0xADD8E6,
    light_coral: 0xF08080,
    light_cyan: 0xE0FFFF,
    light_golden_rod_yellow: 0xFAFAD2,
    light_gray: 0xD3D3D3,
    light_grey: 0xD3D3D3,
    light_green: 0x90EE90,
    light_pink: 0xFFB6C1,
    light_salmon: 0xFFA07A,
    light_sea_green: 0x20B2AA,
    light_sky_blue: 0x87CEFA,
    light_slate_gray: 0x778899,
    light_slate_grey: 0x778899,
    light_steel_blue: 0xB0C4DE,
    light_yellow: 0xFFFFE0,
    lime: 0x00FF00,
    lime_green: 0x32CD32,
    linen: 0xFAF0E6,
    magenta: 0xFF00FF,
    maroon: 0x800000,
    medium_aqua_marine: 0x66CDAA,
    medium_blue: 0x0000CD,
    medium_orchid: 0xBA55D3,
    medium_purple: 0x9370DB,
    medium_sea_green: 0x3CB371,
    medium_slate_blue: 0x7B68EE,
    medium_spring_green: 0x00FA9A,
    medium_turquoise: 0x48D1CC,
    medium_violet_red: 0xC71585,
    midnight_blue: 0x191970,
    mint_cream: 0xF5FFFA,
    misty_rose: 0xFFE4E1,
    moccasin: 0xFFE4B5,
    navajo_white: 0xFFDEAD,
    navy: 0x000080,
    old_lace: 0xFDF5E6,
    olive: 0x808000,
    olive_drab: 0x6B8E23,
    orange: 0xFFA500,
    orange_red: 0xFF4500,
    orchid: 0xDA70D6,
    pale_golden_rod: 0xEEE8AA,
    pale_green: 0x98FB98,
    pale_turquoise: 0xAFEEEE,
    pale_violet_red: 0xDB7093,
    papaya_whip: 0xFFEFD5,
    peach_puff: 0xFFDAB9,
    peru: 0xCD853F,
    pink: 0xFFC0CB,
    plum: 0xDDA0DD,
    powder_blue: 0xB0E0E6,
    purple: 0x800080,
    rebecca_purple: 0x663399,
    red: 0xFF0000,
    rosy_brown: 0xBC8F8F,
    royal_blue: 0x4169E1,
    saddle_brown: 0x8B4513,
    salmon: 0xFA8072,
    sandy_brown: 0xF4A460,
    sea_green: 0x2E8B57,
    sea_shell: 0xFFF5EE,
    sienna: 0xA0522D,
    silver: 0xC0C0C0,
    sky_blue: 0x87CEEB,
    slate_blue: 0x6A5ACD,
    slate_gray: 0x708090,
    slate_grey: 0x708090,
    snow: 0xFFFAFA,
    spring_green: 0x00FF7F,
    steel_blue: 0x4682B4,
    tan: 0xD2B48C,
    teal: 0x008080,
    thistle: 0xD8BFD8,
    tomato: 0xFF6347,
    turquoise: 0x40E0D0,
    violet: 0xEE82EE,
    wheat: 0xF5DEB3,
    white: 0xFFFFFF,
    white_smoke: 0xF5F5F5,
    yellow: 0xFFFF00,
    yellow_green: 0x9ACD32
  }.freeze

  # Convert hex string, color name or hex number to color hex number
  def self.color_to_hex(value)
    orig_value = value
    value = value.to_s if value.is_a?(Symbol)

    if value.is_a?(String)
      if EXTRA_COLORS[value.to_sym]
        return EXTRA_COLORS[value.to_sym]
      elsif COLOR_ENUM.find(value.to_sym)
        return COLOR_ENUM.find(value.to_sym)
      elsif COLOR_ENUM.find("color_#{value.to_sym}")
        return COLOR_ENUM.find("color_#{value.to_sym}")
      elsif value =~ /^#?(0x)?([\da-f]){6}$/i
        value = value.sub('#', '') if value.start_with?('#')
        return value.start_with?('0x') ? value.to_i(16) : "0x#{value}".to_i(16)
      else
        raise ArgumentError, "Unknown color value #{orig_value.inspect}, expected hex string or color name"
      end
    end

    return value if value.is_a?(Numeric)

    raise ArgumentError, "Can not use #{value.class} (#{value.inspect}) for color value, expected String or Hex Number"
  end

  module AttributeHelper
    def set(values)
      values.each do |key, value|
        if respond_to?("#{key}=")
          send("#{key}=", value)
        elsif respond_to?("set_#{key}=")
          send("set_#{key}=", value)
        else
          self[key] = value
        end
      end

      self
    end

    def fields_hash
      res = {}
      members.each do |key|
        #p [key, self[key]]
        res[key] = respond_to?(key) ? send(key) : self[key]
      end
      res
    end

    def pretty_print(pp)
      pp fields_hash
    end
  end

  module WorkbookExt
    include AttributeHelper
    attr_accessor :tmp_file, :is_open, :filename

    def initialize(struct)
      @is_open = true
      @sheet_names = Set.new
      @sheets = []
      super(struct)
    end

    def add_format(options = nil)
      new_format = super()
      new_format.set(options) if options
      new_format
    end

    def bold_cell_format
      bold = add_format
      bold.set_bold
      bold
    end

    alias_method :bold_format, :bold_cell_format

    # "#,##0.00"
    # "[$-409]m/d/yy h:mm AM/PM;@"
    def number_format(pattern)
      format = add_format
      format.set_num_format(pattern)
      format
    end

    def add_worksheet(sheetname = nil)
      if !sheetname.nil?
        error = validate_worksheet_name(sheetname)
        if error != :no_error
          error_code = ERROR_ENUM.find(error)
          error_str = error_code ? Libxlsxwriter.strerror(error_code) : ''
          raise ArgumentError, "Invalid worksheet name '#{sheetname}': (#{error_code} - #{error}) #{error_str}"
        end
      end

      sheet = super(sheetname)
      sheet.workbook = self
      @sheets << sheet
      @sheet_names << sheet[:name]
      sheet
    end

    def get_worksheet_by_name(name)
      sheet = super(name)
      sheet.workbook = self

      sheet
    end

    def close
      @is_open = false
      @sheets.each(&:close)
      super
    end

    def read_string
      close if @is_open
      File.open(filename, 'rb', &:read)
    ensure
      remove_tmp_folder
    end

    def remove_tmp_folder
      FileUtils.remove_entry(File.dirname(filename)) if tmp_file
    end

    def constant_memory?
      #FastExcel.print_ffi_obj(self[:options])
      @constant_memory ||= self[:options][:constant_memory] != 0
    end
  end

  module WorksheetExt
    attr_accessor :workbook

    include AttributeHelper

    def initialize(struct)
      @is_open = true
      @col_formats = {}
      @last_row_number = -1
      super(struct)
    end

    def write_row(row_number, values, formats = nil)
      values.each_with_index do |value, index|
        format = if formats
          formats.is_a?(Array) ? formats[index] : formats
        end

        write_value(row_number, index, value, format)
      end
    end

    def auto_width?
      defined?(@auto_width) && @auto_width
    end

    def auto_width=(v)
      @auto_width = v
      @column_widths = {}
    end

    def calculated_column_widths
      @column_widths || {}
    end

    def write_value(row_number, cell_number, value, format = nil)

      if workbook.constant_memory? && row_number < @last_row_number
        raise ArgumentError, "Can not write to saved row in constant_memory mode (attempted row: #{row_number}, last saved row: #{last_row_number})"
      end

      if value.is_a?(Numeric)
        write_number(row_number, cell_number, value, format)
      elsif defined?(Date) && value.is_a?(Date)
        write_datetime(row_number, cell_number, FastExcel.lxw_datetime(value.to_datetime), format)
      elsif value.is_a?(Time)
        write_number(row_number, cell_number, FastExcel.date_num(value), format)
      elsif defined?(DateTime) && value.is_a?(DateTime)
        write_number(row_number, cell_number, FastExcel.date_num(value), format)
      elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
        write_boolean(row_number, cell_number, value ? 1 : 0, format)
      elsif value.is_a?(FastExcel::Formula)
        write_formula(row_number, cell_number, value.fml, format)
      elsif value.is_a?(FastExcel::URL)
        write_url(row_number, cell_number, value.url, format)
        add_text_width(value.url, format, cell_number) if auto_width?
      else
        write_string(row_number, cell_number, value.to_s, format)
        add_text_width(value, format, cell_number) if auto_width?
      end

      @last_row_number = row_number > @last_row_number ? row_number : @last_row_number
    end

    def add_text_width(value, format, cell_number)
      font_size = 0
      if format
        font_size = format.font_size
      end

      if font_size == 0
        if @col_formats[cell_number] && @col_formats[cell_number].font_size
          font_size = @col_formats[cell_number].font_size
        end
      end

      if font_size == 0
        font_size = workbook.default_format.font_size
      end

      font_size = 13 if font_size == nil || font_size == 0

      scale = 0.08
      new_width = (scale * font_size * value.to_s.length )
      @column_widths[cell_number] = if new_width > (@column_widths[cell_number] || 0)
        new_width
      else
        @column_widths[cell_number]
      end
    end

    def append_row(values, formats = nil)
      @last_row_number += 1
      write_row(last_row_number, values, formats)
    end

    def <<(values)
      append_row(values)
    end

    def last_row_number
      @last_row_number
    end

    def set_column(start_col, end_col, width = nil, format = nil)
      super(start_col, end_col, width || DEF_COL_WIDTH, format)

      return unless format
      start_col.upto(end_col) do |i|
        @col_formats[i] = format
      end
    end

    def set_column_width(col, width)
      set_column(col, col, width, @col_formats[col])
    end

    def set_columns_width(start_col, end_col, width)
      start_col.upto(end_col) do |i|
        set_column_width(i, width)
      end
    end

    def enable_filters!(start_col: 0, end_col:)
      autofilter(0, start_col, @last_row_number, end_col)
    end

    def close
      if auto_width?
        @column_widths.transform_values!{ |width| width || DEF_COL_WIDTH }.each do |num, width|
          set_column_width(num, width + 0.2)
        end
      end
    end
  end

  module FormatExt
    include AttributeHelper

    [:font_size, :underline, :font_script, :rotation, :indent, :pattern, :border].each do |prop|
      define_method(prop) do
        self[prop]
      end
      define_method("#{prop}=") do |value|
        send("set_#{prop}", value)
      end
    end

    [:bold, :italic, :font_outline, :font_shadow, :hidden, :text_wrap, :font_strikeout, :shrink, :text_justlast].each do |prop|
      define_method(prop) do
        self[prop]
      end
      define_method("#{prop}=") do |value|
        value ? send("set_#{prop}") : self[prop] = false
      end
    end

    [:num_format, :font_name].each do |prop|
      define_method(prop) do
        self[prop].to_ptr.read_string
      end

      define_method("#{prop}=") do |value|
        send("set_#{prop}", value)
      end
    end

    ALIGN_ENUM = Libxlsxwriter.enum_type(:format_alignments)

    # Can be called as:
    #
    #  format.align = :align_center
    #  format.align = "align_center"
    #  format.align = :center
    #  format.align = :align_center
    #  format.align = {v: "center", h: "center"}
    #
    # Possible values:
    #
    #   :align_none, :align_left, :align_center, :align_right, :align_fill, :align_justify,
    #   :align_center_across, :align_distributed, :align_vertical_top, :align_vertical_bottom,
    #   :align_vertical_center, :align_vertical_justify, :align_vertical_distributed
    #
    def align=(value)
      value = value.to_sym if value.is_a?(String)

      if value.is_a?(Symbol)
        if ALIGN_ENUM.find(value)
          set_align(value)
        elsif ALIGN_ENUM.find(prefixed = "align_#{value}".to_sym)
          set_align(prefixed)
        else
          raise ArgumentError, "Can not set align = #{value.inspect}, possible values are: #{ALIGN_ENUM.symbols}"
        end
      elsif value.is_a?(Hash)
        if value[:horizontal]
          self.align = "align_#{value[:horizontal].to_s.sub(/^align_/, '')}".to_sym
        end
        if value[:h]
          self.align = "align_#{value[:h].to_s.sub(/^align_/, '')}".to_sym
        end
        if value[:vertical]
          self.align = "align_vertical_#{value[:vertical].to_s.sub(/^align_vertical_/, '')}".to_sym
        end
        if value[:v]
          self.align = "align_vertical_#{value[:v].to_s.sub(/^align_vertical_/, '')}".to_sym
        end
        possible = [:horizontal, :h, :vertical, :v]
        extras = value.keys - possible
        if extras.size > 0
          raise ArgumentError, "Not allowed keys for align: #{extras.inspect}, possible keys: #{possible.inspect}"
        end
      else
        raise ArgumentError, "value must be a symbol or a hash"
      end
    end

    def align
      {
        horizontal: ALIGN_ENUM.find(self[:text_h_align]),
        vertical:   ALIGN_ENUM.find(self[:text_v_align])
      }
    end

    [:font_color, :bg_color, :fg_color, :bottom_color, :diag_color, :left_color, :right_color, :top_color].each do |prop|
      define_method("#{prop}=") do |value|
        send("set_#{prop}", FastExcel.color_to_hex(value))
      end
      define_method(prop) do
        self[prop]
      end
    end

    [:bottom_color, :left_color, :right_color, :top_color].each do |prop|
      alias_method :"border_#{prop}=", :"#{prop}="
      alias_method :"border_#{prop}", :"#{prop}"
    end

    BORDER_ENUM = Libxlsxwriter.enum_type(:format_borders)

    [:bottom, :diag_border, :left, :right, :top].each do |prop|
      define_method("#{prop}=") do |value|

        send("set_#{prop}", border_value(value))
      end
      define_method(prop) do
        BORDER_ENUM.find(self[prop])
      end

      unless prop == :diag_border
        alias_method :"border_#{prop}=", :"#{prop}="
        alias_method :"border_#{prop}", :"#{prop}"
      end
    end

    def border_value(value)
      # if a number
      return value if value.is_a?(Numeric) && BORDER_ENUM.find(value)

      orig_value = value
      value = value.to_sym if value.is_a?(String)

      return BORDER_ENUM.find(value) if BORDER_ENUM.find(value)
      return BORDER_ENUM.find(:"border_#{value}") if BORDER_ENUM.find(:"border_#{value}")

      short_symbols = BORDER_ENUM.symbols.map {|s| s.to_s.sub(/^border_/, '').to_sym }
      raise ArgumentError, "Unknown value #{orig_value.inspect} for border. Possible values: #{short_symbols}"
    end

    def set_font_size(value)
      if value < 0
        raise ArgumentError, "font size should be >= 0 (use 0 for user default font size)"
      end
      super(value)
    end

    def font_family
      font_name
    end

    def font_family=(value)
      self.font_name = value
    end
  end
end

Libxlsxwriter::Workbook.instance_eval do
  include FastExcel::WorkbookExt
end

Libxlsxwriter::Format.instance_eval do
  include FastExcel::FormatExt
end

Libxlsxwriter::Worksheet.instance_eval do
  include FastExcel::WorksheetExt
end
