module Scruffy::Layers
  # ==Scruffy::Layers::Base
  #
  # Author:: Brasten Sager
  # Extended By:: A.J. Ostman
  # Created:: August 5th, 2006
  # Last Modified:: August 27, 2006
  #
  # Scruffy::Layers::Base contains the basic functionality needed by the various types of graphs.  The Base
  # class is responsible holding layer information such as the title and data points.
  #
  # When the graph is rendered, the graph renderer calls Base#render.  Base#render sets up
  # some standard information, and calculates the x,y coordinates of each data point.  The draw() method,
  # which should have been overridden by the current instance, is then called.  The actual rendering of
  # the graph takes place there.
  #
  # ====Create New Graph Types
  #
  # Assuming the information generated by Scruffy::Layers::Base is sufficient, you can create a new graph type
  # simply by overriding the draw() method.  See Base#draw for arguments.
  #
  class Base
    # The following attributes are user-definable at any time.
    # title, points, relevant_data, preferred_color, options
    attr_accessor :title
    attr_accessor :points
    attr_accessor :relevant_data
    attr_accessor :preferred_color
    attr_accessor :options          # On-the-fly values for easy customization / acts as attributes.
    
    # The following attributes are set during the layer's render process,
    # and act more as a record of what just happened for later processes.
    # height, width, min_value, max_value, color, opacity, complexity
    attr_reader :height, :width
    attr_reader :min_value, :max_value
    attr_reader :color
    attr_reader :opacity
    attr_reader :complexity

    # Returns a new Base object.
    #
    # Any options other that those specified below are stored in the @options variable for
    # possible later use.  This would be a good place to store options needed for a custom
    # graph.
    #
    # Options:
    # title:: Name/title of data group
    # points:: Array of data points
    # preferred_color:: Color used to render this graph, overrides theme color.
    # relevant_data:: Rarely used - indicates the data on this graph should not 
    #                 included in any graph data aggregations, such as averaging data points.
    def initialize(options = {})
      @title              = options.delete(:title) || ''
      @preferred_color    = options.delete(:preferred_color)
      @relevant_data      = options.delete(:relevant_data) || true
      @points             = options.delete(:points) || []
      @points.extend Scruffy::Helpers::PointContainer unless @points.kind_of? Scruffy::Helpers::PointContainer
      
      @options            = options
    end
  
    # Builds SVG code for this graph using the provided Builder object.
    # This method actually generates data needed by this graph, then passes the
    # rendering responsibilities to Base#draw.
    #
    # svg:: a Builder object used to create SVG code.
    def render(svg, options = {})
      setup_variables(options)
      coords = generate_coordinates(options)
    
      draw(svg, coords, options)
    end
  
    # The method called by Base#draw to render the graph.
    # 
    # svg:: a Builder object to use for creating SVG code.
    # coords:: An array of coordinates relating to the graph's data points.  ie: [[100, 120], [200, 140], [300, 40]]
    # options:: Optional arguments.
    def draw(svg, coords, options={})
      raise RenderError, "You must override the Base#draw method."
    end
  
    # Returns a hash with information to be used by the legend.
    #
    # Alternatively, returns nil if you don't want this layer to be in the legend,
    # or an array of hashes if this layer should have multiple legend entries (stacked?)
    #
    # By default, #legend_data returns nil automatically if relevant_data is set to false
    # or the @color attribute is nil.  @color is set when the layer is rendered, so legends
    # must be rendered AFTER layers.
    def legend_data
      if relevant_data? && @color
        {:title => title, 
         :color => @color,
         :priority => :normal}
      else
        nil
      end
    end
  
    # Returns the value of relevant_data
    def relevant_data?
      @relevant_data
    end

    # The highest data point on this layer, or nil if relevant_data == false
    def top_value
      @relevant_data ? points.maximum_value : nil
    end
  
    # The lowest data point on this layer, or nil if relevant_data == false
    def bottom_value
       @relevant_data ? points.minimum_value : nil
    end

    # The sum of all values
    def sum_values
      points.sum
    end

    protected
      # Sets up several variables that almost every graph layer will need to render
      # itself.
      def setup_variables(options = {})
        @color = (preferred_color || options.delete(:color))
        @width, @height = options.delete(:size)
        @min_value, @max_value = options[:min_value], options[:max_value]
        @opacity = options[:opacity] || 1.0
        @complexity = options[:complexity]
      end

      # Optimistic generation of coordinates for layer to use.  These coordinates are
      # just a best guess, and can be overridden or thrown away (for example, this is overridden
      # in pie charting and bar charts).
      def generate_coordinates(options = {})
        options[:point_distance] = width / (points.size - 1).to_f

        points.inject_with_index([]) do |memo, point, idx|
          x_coord = options[:point_distance] * idx

          if point
            relative_percent = ((point == min_value) ? 0 : ((point - min_value) / (max_value - min_value).to_f))
            y_coord = (height - (height * relative_percent))
  
            memo << [x_coord, y_coord]
          end
          
          memo
        end
      end
    
      # Converts a percentage into a pixel value, relative to the height.
      #
      # Example:
      #   relative(5)   # On a 100px high layer, this returns 5.  200px high layer, this returns 10, etc.
      def relative(pct)
        # Default to Relative Height
        relative_height(pct)
      end

      def relative_width(pct)
        if pct # Added to handle nils
          @width * (pct / 100.to_f)
        end
      end
      
      def relative_height(pct)
        if pct # Added to handle nils
          @height * (pct / 100.to_f)
        end
      end      

      # Some SVG elements take a long string of multiple coordinates.  This is here
      # to make that a little easier.
      def stringify_coords(coords) # :nodoc:
        coords.map { |c| c.join(',') }
      end
  end

end # scruffy::layers