# frozen_string_literal: true
require 'json'
require 'stringio'

module ThemeCheck
  module LanguageServer
    class DoneStreaming < StandardError; end

    class IncompatibleStream < StandardError; end

    class Server
      attr_reader :handler
      attr_reader :should_raise_errors

      def initialize(
        in_stream: STDIN,
        out_stream: STDOUT,
        err_stream: STDERR,
        should_raise_errors: false
      )
        validate!([in_stream, out_stream, err_stream])

        @handler = Handler.new(self)
        @in = in_stream
        @out = out_stream
        @err = err_stream

        # Because programming is fun,
        #
        # Ruby on Windows turns \n into \r\n. Which means that \r\n
        # gets turned into \r\r\n. Which means that the protocol
        # breaks on windows unless we turn STDOUT into binary mode.
        #
        # Hours wasted: 9.
        @out.binmode

        @out.sync = true # do not buffer
        @err.sync = true # do not buffer

        @should_raise_errors = should_raise_errors
      end

      def listen
        loop do
          process_request

        # support ctrl+c and stuff
        rescue SignalException, DoneStreaming
          cleanup
          return 0

        rescue Exception => e # rubocop:disable Lint/RescueException
          raise e if should_raise_errors
          log(e)
          log(e.backtrace)
          return 1
        end
      end

      def send_response(response)
        response_body = JSON.dump(response)
        log(JSON.pretty_generate(response)) if $DEBUG

        @out.write("Content-Length: #{response_body.bytesize}\r\n")
        @out.write("\r\n")
        @out.write(response_body)
        @out.flush
      end

      def log(message)
        @err.puts(message)
        @err.flush
      end

      private

      def supported_io_classes
        [IO, StringIO]
      end

      def validate!(streams = [])
        streams.each do |stream|
          unless supported_io_classes.find { |klass| stream.is_a?(klass) }
            raise IncompatibleStream, incompatible_stream_message
          end
        end
      end

      def incompatible_stream_message
        'if provided, in_stream, out_stream, and err_stream must be a kind of '\
        "one of the following: #{supported_io_classes.join(', ')}"
      end

      def process_request
        request_body = read_new_content
        request_json = JSON.parse(request_body)
        log(JSON.pretty_generate(request_json)) if $DEBUG

        id = request_json['id']
        method_name = request_json['method']
        params = request_json['params']
        method_name = "on_#{to_snake_case(method_name)}"

        if @handler.respond_to?(method_name)
          @handler.send(method_name, id, params)
        end
      end

      def to_snake_case(method_name)
        StringHelpers.underscore(method_name.gsub(/[^\w]/, '_'))
      end

      def initial_line
        # Scanning for lines that fit the protocol.
        while true
          initial_line = @in.gets
          # gets returning nil means the stream was closed.
          raise DoneStreaming if initial_line.nil?

          if initial_line.match(/Content-Length: (\d+)/)
            break
          end
        end
        initial_line
      end

      def read_new_content
        length = initial_line.match(/Content-Length: (\d+)/)[1].to_i
        content = ''
        while content.length < length + 2
          begin
            # Why + 2? Because \r\n
            content += @in.read(length + 2)
          rescue => e
            log(e)
            log(e.backtrace)
            # We have almost certainly been disconnected from the server
            cleanup
            raise DoneStreaming
          end
        end

        content
      end

      def cleanup
        @err.close
        @out.close
      rescue
        # I did my best
      end
    end
  end
end