# frozen_string_literal: true

require "bundler/setup"
require "socket"
require "json"
require "fileutils"
require "open3"

require "prettier_print"
require "syntax_tree"
require "syntax_tree/haml"
require "syntax_tree/rbs"

# First, require all of the plugins that the user specified.
ARGV.shift[/^--plugins=(.*)$/, 1]
  .split(",")
  .each { |plugin| require "syntax_tree/#{plugin}" }

# Make sure we trap these signals to be sure we get the quit command coming from
# the parent node process
quit = false
trap(:INT) { quit = true }
trap(:TERM) { quit = true }

if Signal.list.key?("QUIT") && RUBY_ENGINE != "jruby"
  trap(:QUIT) { quit = true }
end

# The information variable stores the actual connection information, which will
# either be an IP address and port or a path to a unix socket file.
information = ""

# The candidates array is a list of potential programs that could be used to
# connect to our server. We'll run through them after the server starts to find
# the best one to use.
candidates = []

if Gem.win_platform?
  # If we're on windows, we're going to start up a TCP server. The 0 here means
  # to bind to some available port.
  server = TCPServer.new("127.0.0.1", 0)
  address = server.local_address

  # Ensure that we close the server when this process exits.
  at_exit { server.close }

  information = "#{address.ip_address} #{address.ip_port}"
  candidates = %w[nc telnet]
else
  # If we're not on windows, then we're going to assume we can use unix socket
  # files (since they're faster than a TCP server).
  filepath = "/tmp/prettier-ruby-parser-#{Process.pid}.sock"
  server = UNIXServer.new(filepath)

  # Ensure that we close the server and delete the socket file when this
  # process exits.
  at_exit do
    server.close
    File.unlink(filepath)
  end

  information = server.local_address.unix_path
  candidates = ["nc -w 3 -U", "ncat -w 3 -U"]
end

# This is the actual listening thread that will be acting as our server. We have
# to start it in another thread to begin with so that we can run through our
# candidate connection programs. Eventually we'll just join into this thread
# though and it will act as a daemon.
listener =
  Thread.new do
    loop do
      break if quit

      # Start up a new thread that will handle each successive connection.
      Thread.new(server.accept_nonblock) do |socket|
        parser, width, source =
          socket.read.force_encoding("UTF-8").split("|", 3)

        source.each_line do |line|
          case line
          when /^\s*#.+?coding/
            # If we've found an encoding comment, then we're going to take that
            # into account and reclassify the encoding for the source.
            encoding = Ripper.new(line).tap(&:parse).encoding
            source = source.force_encoding(encoding)
            break
          when /^\s*#/
            # continue
          else
            break
          end
        end

        maxwidth = width.to_i
        response =
          case parser
          when "ping"
            "pong"
          when "ruby"
            formatter = SyntaxTree::Formatter.new(source, [], maxwidth)
            SyntaxTree.parse(source).format(formatter)
            formatter.flush
            formatter.output.join
          when "rbs"
            formatter = SyntaxTree::RBS::Formatter.new(source, [], maxwidth)
            SyntaxTree::RBS.parse(source).format(formatter)
            formatter.flush
            formatter.output.join
          when "haml"
            PrettierPrint.format(+"", maxwidth) do |q|
              SyntaxTree::Haml.parse(source).format(q)
            end
          end

        if response
          socket.write(JSON.fast_generate(response.force_encoding("UTF-8")))
        else
          socket.write("{ \"error\": true }")
        end
      rescue SyntaxTree::Parser::ParseError => error
        loc = { start: { line: error.lineno, column: error.column } }
        socket.write(JSON.fast_generate(error: error.message, loc: loc))
      rescue StandardError => error
        begin
          socket.write(JSON.fast_generate(error: error.message))
        rescue Errno::EPIPE
          # Do nothing, the pipe has been closed by the parent process so we don't
          # actually care about writing to it anymore.
        end
      ensure
        socket.close
      end
    rescue IO::WaitReadable, Errno::EINTR
      # Wait for select(2) to give us a connection that has content for 1
      # second. Otherwise timeout and continue on (so that we hit our
      # "break if quit" pretty often).
      IO.select([server], nil, nil, 1)

      retry unless quit
    end
  end

# Map each candidate connection method to a thread that will check if it works.
candidates.map! do |candidate|
  Thread.new do
    Thread.current.report_on_exception = false

    # We do not care about stderr here, so throw it away
    stdout, _stderr, status =
      Open3.capture3("#{candidate} #{information}", stdin_data: "ping")

    candidate if JSON.parse(stdout) == "pong" && status.exitstatus == 0
  rescue StandardError
    # We don't actually care if this fails, because we'll just skip that
    # connection option.
  end
end

# Find the first one prefix that successfully returned the correct value.
prefix =
  candidates.detect do |candidate|
    value = candidate.value
    break value if value
  end

# Default to running the netcat.js script that we ship with the plugin. It's a
# good fallback as it will always work, but it is slower than the other options.
prefix ||= "node #{File.expand_path("netcat.js", __dir__)}"

# Write out our connection information to the file given as the first argument
# to this script.
File.write(ARGV[0], "#{prefix} #{information}")

listener.join
