require 'thread'

module Hawkins
  module Commands
    class LiveServe < Jekyll::Command
      class << self
        COMMAND_OPTIONS = {
          "swf"      => ["--swf", "Use Flash for WebSockets support"],
          # TODO Should probably only accept fnmatch-esque strings and convert them to regexs
          "ignore"   => ["--ignore [REGEX]", "Files not to reload"],
          "min_delay" => ["--min-delay [SECONDS]", "Minimum reload delay"],
          "max_delay" => ["--max-delay [SECONDS]", "Maximum reload delay"],
          "reload_port" => ["--reload-port [PORT]", Integer, "Port for LiveReload to listen on"],
        }.merge(Jekyll::Commands::Serve.singleton_class::COMMAND_OPTIONS).freeze

        LIVERELOAD_PORT = 35729

        #

        def init_with_program(prog)
          prog.command(:liveserve) do |cmd|
            cmd.description "Serve your site locally with LiveReload"
            cmd.syntax "liveserve [options]"
            cmd.alias :liveserver
            cmd.alias :l

            add_build_options(cmd)
            COMMAND_OPTIONS.each do |key, val|
              cmd.option(key, *val)
            end

            cmd.action do |_, opts|
              # TODO need to figure out how to set defaults correctly
              opts["reload_port"] ||= LIVERELOAD_PORT

              opts["serving"] = true
              opts["watch"] = true unless opts.key?("watch")
              start(opts)
            end
          end
        end

        def start(opts)
          opts = configuration_from_options(opts)

          @running = Queue.new
          @reload_reactor = LiveReloadReactor.new(opts)
          @reload_reactor.start
          Jekyll::Commands::Build.process(opts)
          LiveServe.process(opts)
        end

        def process(opts)
          destination = opts["destination"]
          setup(destination)

          @server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }

          @server.mount("#{opts['baseurl']}/__livereload",
            WEBrick::HTTPServlet::FileHandler, LIVERELOAD_DIR)
          @server.mount(opts["baseurl"], ReloadServlet, destination, file_handler_opts)

          Jekyll.logger.info "Server address:", server_address(@server, opts)
          launch_browser(@server, opts) if opts["open_url"]
          boot_or_detach(@server, opts)
        end

        def running?
          !(@running.nil? || @running.empty?)
        end

        def shutdown
          @server.shutdown if running?
        end

        # Do a base pre-setup of WEBRick so that everything is in place
        # when we get ready to party, checking for an setting up an error page
        # and making sure our destination exists.

        private
        def setup(destination)
          require_relative "./servlet"

          FileUtils.mkdir_p(destination)
          if File.exist?(File.join(destination, "404.html"))
            WEBrick::HTTPResponse.class_eval do
              def create_error_page
                @header["Content-Type"] = "text/html; charset=UTF-8"
                @body = IO.read(File.join(@config[:DocumentRoot], "404.html"))
              end
            end
          end
        end

        #

        private
        def webrick_opts(opts)
          opts = {
            :JekyllOptions      => opts,
            :DoNotReverseLookup => true,
            :MimeTypes          => mime_types,
            :DocumentRoot       => opts["destination"],
            :StartCallback      => start_callback(opts["detach"]),
            :StopCallback       => stop_callback(opts["detach"]),
            :BindAddress        => opts["host"],
            :Port               => opts["port"],
            :DirectoryIndex     => %w(
              index.htm
              index.html
              index.rhtml
              index.cgi
              index.xml
            ),
          }

          enable_ssl(opts)
          enable_logging(opts)
          opts
        end

        # Recreate NondisclosureName under utf-8 circumstance

        private
        def file_handler_opts
          WEBrick::Config::FileHandler.merge(
            :FancyIndexing     => true,
            :NondisclosureName => [
              '.ht*', '~*'
            ]
          )
        end

        #

        private
        def server_address(server, opts)
          address = server.config[:BindAddress]
          baseurl = "#{opts['baseurl']}/" if opts["baseurl"]
          port = server.config[:Port]

          if opts['ssl_cert'] && opts['ssl_key']
            protocol = "https"
          else
            protocol = "http"
          end

          "#{protocol}://#{address}:#{port}#{baseurl}"
        end

        #

        private
        def launch_browser(server, opts)
          command =
            if Utils::Platforms.windows?
              "start"
            elsif Utils::Platforms.osx?
              "open"
            else
              "xdg-open"
            end
          system command, server_address(server, opts)
        end

        # Keep in our area with a thread or detach the server as requested
        # by the user.  This method determines what we do based on what you
        # ask us to do.

        private
        def boot_or_detach(server, opts)
          if opts["detach"]
            pid = Process.fork do
              server.start
            end

            Process.detach(pid)
            Jekyll.logger.info "Server detached with pid '#{pid}'.", \
              "Run `pkill -f jekyll' or `kill -9 #{pid}' to stop the server."
          else
            t = Thread.new { server.start }
            trap("INT") { server.shutdown }
            t.join
          end
        end

        # Make the stack verbose if the user requests it.

        private
        def enable_logging(opts)
          opts[:AccessLog] = []
          level = WEBrick::Log.const_get(opts[:JekyllOptions]["verbose"] ? :DEBUG : :WARN)
          opts[:Logger] = WEBrick::Log.new($stdout, level)
        end

        # Add SSL to the stack if the user triggers --enable-ssl and they
        # provide both types of certificates commonly needed.  Raise if they
        # forget to add one of the certificates.

        private
        def enable_ssl(opts)
          jekyll_opts = opts[:JekyllOptions]
          return if !jekyll_opts['ssl_cert'] && !jekyll_opts['ssl_key']
          if !jekyll_opts['ssl_cert'] || !jekyll_opts['ssl_key']
            raise "--ssl-cert or --ssl-key missing."
          end

          Jekyll.logger.info("LiveReload:", "Serving over SSL/TLS.  If you are using a "\
            "certificate signed by an unknown CA, you will need to add an exception for both "\
            "#{jekyll_opts['host']}:#{jekyll_opts['port']} and "\
            "#{jekyll_opts['host']}:#{jekyll_opts['reload_port']}")

          require "openssl"
          require "webrick/https"
          source_key = Jekyll.sanitized_path(jekyll_opts['source'], jekyll_opts['ssl_key'])
          source_certificate = Jekyll.sanitized_path(jekyll_opts['source'], jekyll_opts['ssl_cert'])
          opts[:SSLCertificate] = OpenSSL::X509::Certificate.new(File.read(source_certificate))
          opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(File.read(source_key))
          opts[:SSLEnable] = true
        end

        private
        def start_callback(detached)
          unless detached
            proc do
              @running << '.'
              Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
            end
          end
        end

        private
        def stop_callback(detached)
          unless detached
            proc do
              @reload_reactor.stop
              @running.clear
            end
          end
        end

        private
        def mime_types
          file = File.expand_path('../mime.types', File.dirname(__FILE__))
          WEBrick::HTTPUtils.load_mime_types(file)
        end
      end
    end
  end
end