#
# Author:: Daniel DeLeo (<dan@opscode.com>)
# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'rubygems'
require 'webrick'
require 'webrick/https'
require 'rack'
#require 'thin'
require 'singleton'
require 'open-uri'
require 'chef/config'

module TinyServer

  class Server < Rack::Server

    attr_writer :app

    def self.setup(options=nil, &block)
      tiny_app = new(options)
      app_code = Rack::Builder.new(&block).to_app
      tiny_app.app = app_code
      tiny_app
    end

    def shutdown
      server.shutdown
    end
  end

  class Manager

    # 5 == debug, 3 == warning
    LOGGER = WEBrick::Log.new(STDOUT, 3)
    DEFAULT_OPTIONS = {
      :server => 'webrick',
      :Port => 9000,
      :Host => 'localhost',
      :environment => :none,
      :Logger => LOGGER,
      :AccessLog => [] # Remove this option to enable the access log when debugging.
    }

    def initialize(options=nil)
      @options = options ? DEFAULT_OPTIONS.merge(options) : DEFAULT_OPTIONS
      @creator = caller.first
    end

    def start
      @server_thread = Thread.new do
        @server = Server.setup(@options) do
          run API.instance
        end
        @old_handler = trap(:INT, "EXIT")
        @server.start
      end
      block_until_started
      trap(:INT, @old_handler)
    end

    def url
      "http://localhost:#{@options[:Port]}"
    end

    def block_until_started
      200.times do
        if started? && !@server.nil?
          return true
        end
      end
      raise "ivar weirdness" if started? && @server.nil?
      raise "TinyServer failed to boot :/"
    end

    def started?
      open(url)
      true
    rescue OpenURI::HTTPError
      true
    rescue Errno::ECONNREFUSED, EOFError, Errno::ECONNRESET => e
      sleep 0.1
      true
      # If the host has ":::1 localhost" in its hosts file and if IPv6
      # is not enabled we can get NetworkUnreachable exception...
    rescue Errno::ENETUNREACH, Net::ReadTimeout, IO::EAGAINWaitReadable,
        Errno::EHOSTUNREACH => e
      sleep 0.1
      false
    end

    def stop
      # yes, this is terrible.
      @server.shutdown
      @server_thread.kill
      @server_thread.join
      @server_thread = nil
    end

  end

  class API
    include Singleton

    GET     = "GET"
    PUT     = "PUT"
    POST    = "POST"
    DELETE  = "DELETE"

    attr_reader :routes

    def initialize
      clear
    end

    def clear
      @routes = {GET => [], PUT => [], POST => [], DELETE => []}
    end

    def get(path, response_code, data=nil, headers=nil, &block)
      @routes[GET] << Route.new(path, Response.new(response_code, data, headers, &block))
    end

    def put(path, response_code, data=nil, headers=nil, &block)
      @routes[PUT] << Route.new(path, Response.new(response_code, data, headers, &block))
    end

    def post(path, response_code, data=nil, headers=nil, &block)
      @routes[POST] << Route.new(path, Response.new(response_code, data, headers, &block))
    end

    def delete(path, response_code, data=nil, headers=nil, &block)
      @routes[DELETE] << Route.new(path, Response.new(response_code, data, headers, &block))
    end

    def call(env)
      if response = response_for_request(env)
        response.call
      else
        debug_info = {:message => "no data matches the request for #{env['REQUEST_URI']}",
                      :available_routes => @routes, :request => env}
        # Uncomment me for glorious debugging
        # pp :not_found => debug_info
        [404, {'Content-Type' => 'application/json'}, [ Chef::JSONCompat.to_json(debug_info) ]]
      end
    end

    def response_for_request(env)
      if route = @routes[env["REQUEST_METHOD"]].find { |route| route.matches_request?(env["REQUEST_URI"]) }
        route.response
      end
    end
  end

  class Route
    attr_reader :response

    def initialize(path_spec, response)
      @path_spec, @response = path_spec, response
    end

    def matches_request?(uri)
      uri = URI.parse(uri).request_uri
      @path_spec === uri
    end

    def to_s
      "#{@path_spec} => (#{@response})"
    end

  end

  class Response
    HEADERS = {'Content-Type' => 'application/json'}

    def initialize(response_code=200, data=nil, headers=nil, &block)
      @response_code, @data = response_code, data
      @response_headers = headers ? HEADERS.merge(headers) : HEADERS
      @block = block_given? ? block : nil
    end

    def call
      data = @data || @block.call
      [@response_code, @response_headers, Array(data)]
    end

    def to_s
      "#{@response_code} => #{(@data|| @block)}"
    end

  end

end