module Finitio
  class TypeFactory

    DSL_METHODS = [
      :attribute,
      :heading,
      :constraint,
      :constraints,
      :contract,
      :contracts,
      :any,
      :builtin,
      :adt,
      :subtype,
      :union,
      :seq,
      :set,
      :struct,
      :tuple,
      :multi_tuple,
      :relation,
      :multi_relation,
      :type,
      :proxy
    ]

    ################################################################## Factory

    def type(type, name = nil, metadata = nil, &bl)
      return subtype(type(type, name, metadata), bl) if bl
      case type
      when Type
        alias_type(type, name, metadata)
      when Module
        BuiltinType.new(type, name || type.name.to_s, metadata)
      when Hash
        tuple(type, name, metadata)
      when Array
        case type.size
        when 0
          fail!("Array of arity > 0 expected, got `#{type}`")
        when 1
          seq(type.first, name, metadata)
        else
          struct(type, name, metadata)
        end
      when Set
        fail!("Set of arity 1 expected, got `#{type}`") unless type.size==1
        sub = type(type.first)
        if sub.is_a?(TupleType)
          relation(sub.heading, name, metadata)
        else
          set(sub, name, metadata)
        end
      when Range
        clazz = [type.begin, type.end].map(&:class).uniq
        fail!("Unsupported range `#{type}`") unless clazz.size==1
        subtype(clazz.first, type, name, metadata)
      when Regexp
        subtype(String, type, name, metadata)
      else
        fail!("Unable to factor a Finitio::Type from `#{type}`")
      end
    end

    ########################################################### Type Arguments

    def ruby_type(type)
      unless type.is_a?(Module)
        fail!("Ruby module expected, got `#{type}`")
      end

      type
    end

    def name(name)
      unless name.nil? or (name.is_a?(String) and name.strip.size > 1)
        fail!("Wrong type name `#{name}`")
      end

      name.nil? ? nil : name.strip
    end

    def metadata(metadata)
      unless metadata.nil? or metadata.is_a?(Hash)
        fail!("Wrong metadata `#{metadata}`")
      end

      metadata
    end

    def constraint(constraint, name = nil)
      case constraint
      when Constraint then constraint
      else Constraint.new(constraint, name)
      end
    end

    def constraints(constraints = nil, &bl)
      constrs = []
      constrs << Constraint.new(bl) if bl
      case constraints
      when Hash
        constraints.each_pair do |name, cstr|
          constrs << constraint(cstr, name)
        end
      when Array
        constraints.each do |c|
          constrs << constraint(c)
        end
      else
        constrs << Constraint.new(constraints)
      end
      constrs
    end

    def attribute(name, type, required = true, metadata = nil)
      Attribute.new(name, type(type), required, metadata)
    end

    def attributes(attributes)
      case attributes
      when Hash
        attributes.each_pair.map do |name, type|
          attribute(name, type)
        end
      else
        fail!("Hash expected, got `#{attributes}`")
      end
    end

    def heading(heading)
      case heading
      when Heading
        heading
      when TupleType, RelationType
        heading.heading
      when Hash
        Heading.new(attributes(heading))
      else
        fail!("Heading expected, got `#{heading}`")
      end
    end

    def contract(infotype, dresser, undresser, name = nil, metadata = nil)
      infotype = type(infotype)
      Contract.new(infotype, dresser, undresser, name, metadata)
    end

    def contracts(contracts)
      case contracts
      when Array
        unless contracts.all?{|c| c.is_a?(Contract) }
          fail!("[Contract] expected, got `#{contracts}`")
        end
        contracts
      when Hash
        contracts.map do |k,v|
          contract(*v.push(k.to_sym))
        end
      end
    end

    ########################################################## Type generators

    def any(name = nil, metadata = nil)
      name = name(name)
      meta = metadata(metadata)

      AnyType.new(name, meta)
    end

    def builtin(ruby_type, name = nil, metadata = nil)
      ruby_type = ruby_type(ruby_type)
      name      = name(name)
      meta      = metadata(metadata)

      BuiltinType.new(ruby_type, name, meta)
    end

    def adt(ruby_type, contracts, name = nil, metadata = nil)
      ruby_type = ruby_type(ruby_type) if ruby_type
      contracts = contracts(contracts)
      name      = name(name)
      meta      = metadata(metadata)

      AdType.new(ruby_type, contracts, name, meta)
    end

    ### Sub and union

    def subtype(super_type, constraints = nil, name = nil, metadata = nil, &bl)
      super_type  = type(super_type)
      constraints = constraints(constraints, &bl)
      name        = name(name)
      meta        = metadata(metadata)

      SubType.new(super_type, constraints, name, metadata)
    end

    def union(*args)
      candidates, name, meta = [], nil, nil
      args.each do |arg|
        case arg
        when Array  then candidates = arg.map{|t| type(t) }
        when String then name = name(arg)
        else
          candidates << type(arg)
        end
      end

      UnionType.new(candidates, name, meta)
    end

    ### Collections

    def seq(elm_type, name = nil, metadata = nil)
      elm_type = type(elm_type)
      name     = name(name)
      meta     = metadata(metadata)

      SeqType.new(elm_type, name, meta)
    end

    def set(elm_type, name = nil, metadata = nil)
      elm_type = type(elm_type)
      name     = name(name)
      meta     = metadata(metadata)

      SetType.new(elm_type, name, meta)
    end

    def struct(component_types, name = nil, metadata = nil)
      component_types = component_types.map{|t| type(t) }
      name            = name(name)
      meta            = metadata(metadata)

      StructType.new(component_types, name, meta)
    end

    ### Tuples and relations

    def tuple(heading, name = nil, metadata = nil)
      heading = heading(heading)
      name    = name(name)
      meta    = metadata(metadata)

      TupleType.new(heading, name, meta)
    end

    def multi_tuple(heading, name = nil, metadata = nil)
      heading = heading(heading)
      name    = name(name)
      meta    = metadata(metadata)

      MultiTupleType.new(heading, name, meta)
    end

    def relation(heading, name = nil, metadata = nil)
      heading = heading(heading)
      name    = name(name)
      meta    = metadata(metadata)

      RelationType.new(heading, name, meta)
    end

    def multi_relation(heading, name = nil, metadata = nil)
      heading = heading(heading)
      name    = name(name)
      meta    = metadata(metadata)

      MultiRelationType.new(heading, name, meta)
    end

    def proxy(target_name)
      ProxyType.new(target_name)
    end

  private

    def alias_type(type, name, metadata)
      raise "Type expected `#{type}`" unless type.is_a?(Type)
      if (name && type.named?) or (metadata && type.metadata?)
        AliasType.new(type, name, metadata)
      else
        type.name = name         if name
        type.metadata = metadata if metadata
        type
      end
    end

    def fail!(message)
      raise ArgumentError, message, caller
    end

  end # class TypeBuilder
end # module Finitio
