require 'stringio'
require 'date'
require 'time'
require 'tempfile'
require 'thread'

module Seahorse
  module Client
    class ParamConverter

      @mutex = Mutex.new
      @converters = Hash.new { |h,k| h[k] = {} }

      # @param [Model::Shapes::Shape] shape
      def initialize(shape)
        @shape = shape
      end

      # @param [Hash] params
      # @return [Hash]
      def convert(params)
        structure(@shape, params)
      end

      private

      def structure(structure, values)
        values = c(structure, values)
        if values.is_a?(Hash)
          values.each do |k, v|
            unless v.nil?
              if structure.member?(k)
                values[k] = member(structure.member(k), v)
              end
            end
          end
        end
        values
      end

      def list(list, values)
        values = c(list, values)
        if values.is_a?(Array)
          values.map { |v| member(list.member, v) }
        else
          values
        end
      end

      def map(map, values)
        values = c(map, values)
        if values.is_a?(Hash)
          values.each.with_object({}) do |(key, value), hash|
            hash[member(map.key, key)] = member(map.value, value)
          end
        else
          values
        end
      end

      def member(shape, value)
        case shape
        when Model::Shapes::Structure then structure(shape, value)
        when Model::Shapes::List then list(shape, value)
        when Model::Shapes::Map then map(shape, value)
        else c(shape, value)
        end
      end

      def c(shape, value)
        self.class.c(shape.class, value)
      end

      class << self

        # @param [Model::Shapes::InputShape] shape
        # @param [Hash] params
        # @return [Hash]
        def convert(shape, params)
          new(shape).convert(params)
        end

        # Registers a new value converter.  Converters run in the context
        # of a shape and value class.
        #
        #     # add a converter that stringifies integers
        #     shape_class = Seahorse::Model::Shapes::StringShape
        #     ParamConverter.add(shape_class, Integer) { |i| i.to_s }
        #
        # @param [Class<Model::Shapes::Shape>] shape_class
        # @param [Class] value_class
        # @param [#call] converter (nil) An object that responds to `#call`
        #    accepting a single argument.  This function should perform
        #    the value conversion if possible, returning the result.
        #    If the conversion is not possible, the original value should
        #    be returned.
        # @return [void]
        def add(shape_class, value_class, converter = nil, &block)
          @converters[shape_class][value_class] = converter || block
        end

        # @api private
        def c(shape, value)
          if converter = converter_for(shape, value)
            converter.call(value)
          else
            value
          end
        end

        private

        def converter_for(shape_class, value)
          unless @converters[shape_class].key?(value.class)
            @mutex.synchronize {
              unless @converters[shape_class].key?(value.class)
                @converters[shape_class][value.class] = find(shape_class, value)
              end
            }
          end
          @converters[shape_class][value.class]
        end

        def find(shape_class, value)
          converter = nil
          each_base_class(shape_class) do |klass|
            @converters[klass].each do |value_class, block|
              if value_class === value
                converter = block
                break
              end
            end
            break if converter
          end
          converter
        end

        def each_base_class(shape_class, &block)
          shape_class.ancestors.each do |ancestor|
            yield(ancestor) if @converters.key?(ancestor)
          end
        end

      end

      add(Model::Shapes::Structure, Hash) { |h| h.dup }
      add(Model::Shapes::Structure, Struct) do |s|
        s.members.each.with_object({}) {|k,h| h[k] = s[k] }
      end

      add(Model::Shapes::Map, Hash) { |h| h.dup }
      add(Model::Shapes::Map, Struct) do |s|
        s.members.each.with_object({}) {|k,h| h[k] = s[k] }
      end

      add(Model::Shapes::List, Array) { |a| a.dup }
      add(Model::Shapes::List, Enumerable) { |value| value.to_a }

      add(Model::Shapes::String, String)
      add(Model::Shapes::String, Symbol) { |sym| sym.to_s }

      add(Model::Shapes::Integer, Integer)
      add(Model::Shapes::Integer, Float) { |f| f.to_i }
      add(Model::Shapes::Integer, String) do |str|
        begin
          Integer(str)
        rescue ArgumentError
          str
        end
      end

      add(Model::Shapes::Float, Float)
      add(Model::Shapes::Float, Integer) { |i| i.to_f }
      add(Model::Shapes::Float, String) do |str|
        begin
          Float(str)
        rescue ArgumentError
          str
        end
      end

      add(Model::Shapes::Timestamp, Time)
      add(Model::Shapes::Timestamp, Date) { |d| d.to_time }
      add(Model::Shapes::Timestamp, DateTime) { |dt| dt.to_time }
      add(Model::Shapes::Timestamp, Integer) { |i| Time.at(i) }
      add(Model::Shapes::Timestamp, String) do |str|
        begin
          Time.parse(str)
        rescue ArgumentError
          str
        end
      end

      add(Model::Shapes::Boolean, TrueClass)
      add(Model::Shapes::Boolean, FalseClass)
      add(Model::Shapes::Boolean, String) do |str|
        { 'true' => true, 'false' => false }[str]
      end

      add(Model::Shapes::Blob, IO)
      add(Model::Shapes::Blob, Tempfile)
      add(Model::Shapes::Blob, StringIO)
      add(Model::Shapes::Blob, String)

    end
  end
end