require_relative '../constants'
require_relative '../conf'
require 'ostruct'

# Intended to be used a a mix-in,
# For example use see Box as an example
module Squib::Args::ArgLoader

  # wrapper for compatibility
  def extract!(args, deck)
    @deck = deck
    load!(args,
          expand_by: deck.size,
          layout: deck.layout,
          dpi: deck.dpi,
          cell_px: deck.cell_px)
  end

  # Main class invoked by the client (i.e. dsl/ methods)
  def load!(args, expand_by: 1, layout: {}, dpi: 300, cell_px: 37.5)
    @dpi = dpi
    @cell_px = cell_px
    args[:layout] = prep_layout_args(args[:layout], expand_by: expand_by)
    expand_and_set_and_defaultify(args: args, by: expand_by, layout: layout)
    validate
    convert_units dpi: dpi, cell_px: cell_px
    self
  end

  def expand_and_set_and_defaultify(args: {}, by: 1, layout: {})
    attributes = self.class.parameters.keys
    attributes.each do |p|
      args[p] = defaultify(p, args, layout)
      val = if expandable_singleton?(p, args[p])
              [args[p]] * by
            else
              args[p] # not an expanding parameter
            end
      instance_variable_set "@#{p}", val
    end
    self.class.class_eval { attr_reader *(attributes) }
  end

  # Must be:
  #  (a) an expanding parameter, and
  #  (b) a singleton already (i.e. doesn't respond to :each)
  def expandable_singleton?(p, arg)
    self.class.expanding_parameters.include?(p) && !arg.respond_to?(:each)
  end

  # Incorporate defaults and layouts
  #  (1) Use whatever is specified if it is
  #  (2) Go over all layout specifications (if any) and look them up
  #     - Use layout when it's specified for that card
  #     - Use "default" if no layout was specified, or the layout itself did not specify
  #           Defaut can be overriden for a given dsl method (@dsl_method_defaults)
  #           (e.g stroke width is 0.0 for text, non-zero everywhere else)
  #
  def defaultify(p, args, layout)
    return args[p] if args.key? p # arg was specified, no defaults used
    defaults = self.class.parameters.merge(@dsl_method_defaults || {})
    args[:layout].map do |layout_arg|
      return defaults[p] if layout_arg.nil?  # no layout specified, use default
      unless layout.key? layout_arg.to_s     # specified a layout, but it doesn't exist in layout. Oops!
        Squib.logger.warn("Layout \"#{layout_arg.to_s}\" does not exist in layout file - using default instead")
        return defaults[p]
      end
      if layout[layout_arg.to_s].key?(p.to_s)
        layout[layout_arg.to_s][p.to_s]     # param specified in layout
      else
        defaults[p]                         # layout specified, but not this param
      end
    end
  end

  # Do singleton expansion on the layout argument as well
  # Treated differently since layout is not always specified
  def prep_layout_args(layout_args, expand_by: 1)
    unless layout_args.respond_to?(:each)
      layout_args = [layout_args] * expand_by
    end
    layout_args || []
  end

  # For each parameter/attribute foo we try to invoke a validate_foo
  def validate
    self.class.parameters.each do |param, default|
      method = "validate_#{param}"
      if self.respond_to? method
        attribute = "@#{param}"
        val = instance_variable_get(attribute)
        if val.respond_to? :each
          new_val = val.map.with_index{ |v, i| send(method, v, i) }
          instance_variable_set(attribute, new_val)
        else
          instance_variable_set(attribute, send(method, val))
        end
      end
    end
  end

  # Access an individual arg for a given card
  # @return an OpenStruct that looks just like the mixed-in class
  # @api private
  def [](i)
    card_arg = OpenStruct.new
    self.class.expanding_parameters.each do |p|
      p_val = instance_variable_get("@#{p}")
      card_arg[p] = p_val[i]
    end
    card_arg
  end

  # Convert units
  def convert_units(dpi: 300, cell_px: 37.5)
    self.class.params_with_units.each do |p|
      p_str = "@#{p}"
      p_val = instance_variable_get(p_str)
      if p_val.respond_to? :each
        arr = p_val.map { |x| Squib::Args::UnitConversion.parse(x, dpi, cell_px) }
        instance_variable_set p_str, arr
      else
        instance_variable_set p_str, Squib::Args::UnitConversion.parse(p_val, dpi, cell_px)
      end
    end
    self
  end

  # Return the deck's configuration
  # This keeps the @deck local to this mixin instead of forcing args classes
  # to "know" that @deck works.
  #
  # It also makes unit testing easier. Sue me.
  def deck_conf
    @deck.conf
  end

end
