module ActiveZuora
  class Generator

    attr_reader :document, :classes

    def initialize(document, options={})
      # document is a parsed wsdl document.
      @document = document
      @classes = []
      @class_nesting = options[:inside] || ActiveZuora
      @class_nesting.const_set("CollectionProxy", CollectionProxy) unless @class_nesting.constants.include?(:CollectionProxy)
    end

    def generate_classes

      # Defines the classes based on the wsdl document.
      # Assumes the following namespaces in the wsdl.
      # xmlns="http://schemas.xmlsoap.org/wsdl/"
      # xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
      # xmlns:xs="http://www.w3.org/2001/XMLSchema"
      # xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
      # xmlns:zns="http://api.zuora.com/"
      # xmlns:ons="http://object.api.zuora.com/"
      # xmlns:fns="http://fault.api.zuora.com/"
      @document.xpath('.//xs:schema[@targetNamespace]').each do |schema|
        namespace = schema.attribute("targetNamespace").value

        schema.xpath('.//xs:complexType[@name]').each do |complex_type|
          class_name = complex_type.attribute("name").value
          # Skip the zObject base class, we define our own.
          next if class_name == "zObject"

          class_name[0] = class_name[0].upcase
          zuora_class = Class.new
          @class_nesting.const_set(class_name, zuora_class)
          @classes << zuora_class

          # Include the Base module for adding fields.
          zuora_class.send :include, Base
          zuora_class.namespace = namespace

          # If it's a zObject, include that module as well.
          if complex_type.xpath(".//xs:extension[@base='ons:zObject']").any?
            zuora_class.send :include, ZObject
          end

          # Define the fields
          complex_type.xpath('.//xs:element[@name][@type]').each do |element|
            # attributes: name, type, nillable, minoccurs, maxoccurs
            zuora_name = element.attribute("name").value
            field_name = zuora_name.underscore
            field_type = element.attribute("type").value
            is_array = element_is_an_array?(element)

            case field_type
            when "string", "xs:string", "zns:ID", "xs:base64Binary"
              zuora_class.field field_name, :string,
                :zuora_name => zuora_name, :array => is_array
            when "boolean", "xs:boolean"
              zuora_class.field field_name, :boolean,
                :zuora_name => zuora_name, :array => is_array
            when "int", "short", "long", "xs:int"
              zuora_class.field field_name, :integer,
                :zuora_name => zuora_name, :array => is_array
            when "decimal"
              zuora_class.field field_name, :decimal,
                :zuora_name => zuora_name, :array => is_array
            when "date"
              zuora_class.field field_name, :date,
                :zuora_name => zuora_name, :array => is_array
            when "dateTime"
              zuora_class.field field_name, :datetime,
                :zuora_name => zuora_name, :array => is_array
            when /\A(zns:|ons:)/
              zuora_class.field field_name, :object,
                :zuora_name => zuora_name, :array => is_array,
                :class_name => zuora_class.nested_class_name(field_type.split(':').last)
            else
              puts "Unknown field type: #{field_type}"
            end
          end # each element

        end # each complexType
      end # each schema

      add_obvious_associations
      add_extra_customizations
    end

    private

    def add_obvious_associations
      # When a zuora class has a field called InvoiceId, it's a safe
      # assuption that it references the an Invoice.
      # Build those associations automatically.
      @classes.each do |zuora_class|
        zuora_class.fields.each do |field|
          # If it looks like an Id field and the name
          # matches a generated ZObject class
          if match = field.zuora_name.match(/\A(.+?)Id\Z/)
            if zobject_class = zobject_class_with_name(match[1])
              # Add a belongs to relationship.
              zuora_class.belongs_to zobject_class.zuora_object_name.underscore
              # If the current class is also a ZObject, add a has_many
              # to the referenced class.
              if zuora_class < ZObject
                zobject_class.has_many zuora_class.zuora_object_name.underscore.pluralize
              end
            end
          end
        end
      end
    end

    def add_extra_customizations
      # We can't know everything from the wsdl, like which fields are
      # usable in queries.  This function does some final customizations
      # based on the existing Zuora documentation.
      # Also, it's possible classes customized here weren't defined
      # in your wsdl, so it will only customize them if they are defined.

      nesting = @class_nesting

      customize 'Account' do
        belongs_to :bill_to, :class_name => nested_class_name('Contact') if field? :bill_to
        if field? :parent_id
          belongs_to :parent, :class_name => nested_class_name('Account')
          has_many :children, :class_name => nested_class_name('Account'), :foreign_key => :parent_id, :inverse_of => :parent
        end
        belongs_to :sold_to, :class_name => nested_class_name('Contact') if field? :sold_to
        validates :currency, :presence => true if field? :currency
        validates :name, :presence => true if field? :name
        validates :status, :presence => true if field? :status
      end

      customize 'Amendment' do
        exclude_from_queries :rate_plan_data,
          :destination_account_id, :destination_invoice_owner_id
      end

      customize 'AmendRequest' do
        include Amend
      end

      customize 'Import' do
        exclude_from_queries :file_content
      end

      customize 'Invoice' do
        include Generate
        include LazyAttr
        # The body field can only be accessed for a single invoice at a time, so
        # exclude it here to not break collections of invoices. The contents of
        # the body field will be lazy loaded in when needed.
        exclude_from_queries :regenerate_invoice_pdf, :body, :bill_run_id
        lazy_load :body
      end

      customize 'InvoiceItem' do
        exclude_from_queries :product_rate_plan_charge_id
      end

      customize 'BillingPreviewRequest' do
        include BillingPreview
      end

      customize 'InvoiceItemAdjustment' do
        exclude_from_queries :customer_name, :customer_number
      end

      customize 'Payment' do
        exclude_from_queries :applied_invoice_amount,
          :gateway_option_data, :invoice_id, :invoice_number, :invoice_payment_data
      end

      customize 'PaymentMethod' do
        exclude_from_queries :ach_account_number, :bank_transfer_account_number,
          :credit_card_number, :credit_card_security_code, :gateway_option_data,
          :second_token_id, :skip_validation, :token_id
      end

      customize 'ProductRatePlan' do
        include LazyAttr
        exclude_from_queries :active_currencies
        lazy_load :active_currencies
      end

      customize 'ProductRatePlanCharge' do
        exclude_from_queries :product_rate_plan_charge_tier_data, :revenue_recognition_rule_name, :deferred_revenue_account, :recognized_revenue_account
      end

      customize 'Usage' do
        exclude_from_queries :ancestor_account_id, :invoice_id, :invoice_number
      end

      customize 'RatePlanCharge' do
        include LazyAttr
        exclude_from_queries :overage_price, :included_units,
          :discount_amount, :discount_percentage, :rollover_balance, :price
        lazy_load :price
      end

      customize 'Refund' do
        exclude_from_queries :gateway_option_data, :payment_id
      end

      customize 'Subscription' do
        exclude_from_queries :ancestor_account_id
        belongs_to :creator_account, :class_name => nested_class_name('Account') if field? :creator_account_id
        belongs_to :creator_invoice_owner, :class_name => nested_class_name('Account') if field? :creator_invoice_owner_id
        belongs_to :invoice_owner, :class_name => nested_class_name('Account') if field? :invoice_owner_id
        belongs_to :original, :class_name => name if field? :original_id
        belongs_to :original_subscription, :class_name => name if field? :original_subscription_id
        belongs_to :previous_subscription, :class_name => name if field? :previous_subscription_id
      end

      customize 'SubscribeRequest' do
        include Subscribe
      end
    end

    def customize(zuora_class_name, &block)
      if @class_nesting.const_defined?(zuora_class_name)
        @class_nesting.const_get(zuora_class_name).instance_eval(&block)
      end
    end


    def element_is_an_array?(element)
      attribute_is_more_than_one?(element.attribute("minOccurs")) ||
        attribute_is_more_than_one?(element.attribute("maxOccurs"))
    end

    def attribute_is_more_than_one?(attribute)
      attribute && ( attribute.value == "unbounded" || attribute.value.to_i > 1 )
    end

    def zobject_class_with_name(name)
      @classes.find { |zuora_class| zuora_class.zuora_object_name == name && zuora_class < ZObject }
    end

  end
end
