# encoding: utf-8

module Axiom
  class Optimizer
    module Algebra

      # Abstract base class representing Projection optimizations
      class Projection < Relation::Operation::Unary

      private

        # Wrap the operand's operand in a Projection
        #
        # @return [Projection]
        #
        # @api private
        def wrap_operand(operand = operand.operand)
          operand.project(header)
        end

        # Optimize when the operand is a Projection
        class ProjectionOperand < self

          # Test if the operand is a Projection
          #
          # @return [Boolean]
          #
          # @api private
          def optimizable?
            operand.kind_of?(operation.class)
          end

          # Flatten nested Projections into a single Projection
          #
          # @return [Projection]
          #
          # @api private
          def optimize
            wrap_operand
          end

        end # class ProjectionOperand

        # Optimize when the operand is an Extension
        class ExtensionOperand < self

          # Test if the operand is an Extension
          #
          # @return [Boolean]
          #
          # @api private
          def optimizable?
            operand.kind_of?(Axiom::Algebra::Extension) &&
            operand.extensions != new_extensions
          end

          # Extend the operand with the attributes not projected away
          #
          # This avoid performing an extension when the new attributes are
          # immediately removed.
          #
          # @return [Projection]
          #
          # @api private
          def optimize
            extend_operand.project(operation.header)
          end

        private

          # Extend the operand with only the new extensions
          #
          # @return [Extension]
          #
          # @api private
          def extend_operand
            unwrap_operand.extend(new_extensions)
          end

          # Unwrap the operand from the Extension
          #
          # @return [Relation]
          #
          # @api private
          def unwrap_operand
            operand.operand
          end

          # Extensions minus the removed attributes
          #
          # @return [Hash{Attribute => Function}]
          #
          # @api private
          def new_extensions
            extensions = operand.extensions
            attributes = extensions.keys - removed_attributes
            Hash[attributes.zip(extensions.values_at(*attributes))]
          end

          # Attributes removed by the projection
          #
          # @return [Header]
          #
          # @api private
          def removed_attributes
            operand.header - operation.header
          end

          memoize :new_extensions, :removed_attributes
        end

        # Optimize when the operand is a Union
        class UnionOperand < self

          # Test if the operand is a Union
          #
          # @return [Boolean]
          #
          # @api private
          def optimizable?
            operand.kind_of?(Axiom::Algebra::Union)
          end

          # Wrap each operand in the Union in a Projection
          #
          # @return [Set]
          #
          # @api private
          def optimize
            wrap_left.union(wrap_right)
          end

        private

          # Utility method to wrap the left operand in a Projection
          #
          # @return [Projection]
          #
          # @api private
          def wrap_left
            wrap_operand(operand.left)
          end

          # Utility method to wrap the right operand in a Projection
          #
          # @return [Projection]
          #
          # @api private
          def wrap_right
            wrap_operand(operand.right)
          end

        end # class UnionOperand

        # Optimize when the operand is an Sorted
        class SortedOperand < self
          include Relation::Operation::Unary::SortedOperand
        end # class SortedOperand

        # Optimize when the operand is Empty
        class EmptyOperand < self

          # Test if the operand is empty
          #
          # @return [Boolean]
          #
          # @api private
          def optimizable?
            operand.kind_of?(Axiom::Relation::Empty)
          end

          # Return a new Empty relation with the operation's headers
          #
          # @return [Empty]
          #
          # @api private
          def optimize
            Axiom::Relation::Empty.new(header)
          end

        end # class EmptyOperand

        # Optimize when operand is optimizable
        class UnoptimizedOperand < self
          include Function::Unary::UnoptimizedOperand

          # Return a Projection with an optimized operand
          #
          # @return [Projection]
          #
          # @api private
          def optimize
            wrap_operand(operand)
          end

        end # class UnoptimizedOperand

        Axiom::Algebra::Projection.optimizer = chain(
          UnchangedHeader,
          ProjectionOperand,
          ExtensionOperand,
          UnionOperand,
          SortedOperand,
          EmptyOperand,
          MaterializedOperand,
          UnoptimizedOperand
        )

      end # class Projection
    end # module Algebra
  end # class Optimizer
end # module Axiom
