text: # Copyright (c) 2014-present PlatformIO <contact@platformio.org>
  #
  # 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.
  
  import os
  from fnmatch import fnmatch
  
  import click
  from twisted.internet import protocol, reactor, task  # pylint: disable=import-error
  from twisted.spread import pb  # pylint: disable=import-error
  
  from platformio.commands.remote.client.base import RemoteClientBase
  
  
  class SMBridgeProtocol(protocol.Protocol):  # pylint: disable=no-init
      def connectionMade(self):
          self.factory.add_client(self)
  
      def connectionLost(self, reason):  # pylint: disable=unused-argument
          self.factory.remove_client(self)
  
      def dataReceived(self, data):
          self.factory.send_to_server(data)
  
  
  class SMBridgeFactory(protocol.ServerFactory):
      def __init__(self, cdm):
          self.cdm = cdm
          self._clients = []
  
      def buildProtocol(self, addr):  # pylint: disable=unused-argument
          p = SMBridgeProtocol()
          p.factory = self  # pylint: disable=attribute-defined-outside-init
          return p
  
      def add_client(self, client):
          self.cdm.log.debug("SMBridge: Client connected")
          self._clients.append(client)
          self.cdm.acread_data()
  
      def remove_client(self, client):
          self.cdm.log.debug("SMBridge: Client disconnected")
          self._clients.remove(client)
          if not self._clients:
              self.cdm.client_terminal_stopped()
  
      def has_clients(self):
          return len(self._clients)
  
      def send_to_clients(self, data):
          if not self._clients:
              return None
          for client in self._clients:
              client.transport.write(data)
          return len(data)
  
      def send_to_server(self, data):
          self.cdm.acwrite_data(data)
  
  
  class DeviceMonitorClient(  # pylint: disable=too-many-instance-attributes
      RemoteClientBase
  ):
  
      MAX_BUFFER_SIZE = 1024 * 1024
  
      def __init__(self, agents, **kwargs):
          RemoteClientBase.__init__(self)
          self.agents = agents
          self.cmd_options = kwargs
  
          self._bridge_factory = SMBridgeFactory(self)
          self._agent_id = None
          self._ac_id = None
          self._d_acread = None
          self._d_acwrite = None
          self._acwrite_buffer = ""
  
      def agent_pool_ready(self):
          d = task.deferLater(
              reactor, 1, self.agentpool.callRemote, "cmd", self.agents, "device.list"
          )
          d.addCallback(self._cb_device_list)
          d.addErrback(self.cb_global_error)
  
      def _cb_device_list(self, result):
          devices = []
          hwid_devindexes = []
          for (success, value) in result:
              if not success:
                  click.secho(value, fg="red", err=True)
                  continue
              (agent_name, ports) = value
              for item in ports:
                  if "VID:PID" in item["hwid"]:
                      hwid_devindexes.append(len(devices))
                  devices.append((agent_name, item))
  
          if len(result) == 1 and self.cmd_options["port"]:
              if set(["*", "?", "[", "]"]) & set(self.cmd_options["port"]):
                  for agent, item in devices:
                      if fnmatch(item["port"], self.cmd_options["port"]):
                          return self.start_remote_monitor(agent, item["port"])
              return self.start_remote_monitor(result[0][1][0], self.cmd_options["port"])
  
          device = None
          if len(hwid_devindexes) == 1:
              device = devices[hwid_devindexes[0]]
          else:
              click.echo("Available ports:")
              for i, device in enumerate(devices):
                  click.echo(
                      "{index}. {host}{port} \t{description}".format(
                          index=i + 1,
                          host=device[0] + ":" if len(result) > 1 else "",
                          port=device[1]["port"],
                          description=device[1]["description"]
                          if device[1]["description"] != "n/a"
                          else "",
                      )
                  )
              device_index = click.prompt(
                  "Please choose a port (number in the list above)",
                  type=click.Choice([str(i + 1) for i, _ in enumerate(devices)]),
              )
              device = devices[int(device_index) - 1]
  
          self.start_remote_monitor(device[0], device[1]["port"])
  
          return None
  
      def start_remote_monitor(self, agent, port):
          options = {"port": port}
          for key in ("baud", "parity", "rtscts", "xonxoff", "rts", "dtr"):
              options[key] = self.cmd_options[key]
  
          click.echo(
              "Starting Serial Monitor on {host}:{port}".format(
                  host=agent, port=options["port"]
              )
          )
          d = self.agentpool.callRemote("cmd", [agent], "device.monitor", options)
          d.addCallback(self.cb_async_result)
          d.addErrback(self.cb_global_error)
  
      def cb_async_result(self, result):
          if len(result) != 1:
              raise pb.Error("Invalid response from Remote Cloud")
          success, value = result[0]
          if not success:
              raise pb.Error(value)
  
          reconnected = self._agent_id is not None
          self._agent_id, self._ac_id = value
  
          if reconnected:
              self.acread_data(force=True)
              self.acwrite_data("", force=True)
              return
  
          # start bridge
          port = reactor.listenTCP(0, self._bridge_factory)
          address = port.getHost()
          self.log.debug("Serial Bridge is started on {address!r}", address=address)
          if "sock" in self.cmd_options:
              with open(os.path.join(self.cmd_options["sock"], "sock"), "w") as fp:
                  fp.write("socket://localhost:%d" % address.port)
  
      def client_terminal_stopped(self):
          try:
              d = self.agentpool.callRemote("acclose", self._agent_id, self._ac_id)
              d.addCallback(lambda r: self.disconnect())
              d.addErrback(self.cb_global_error)
          except (AttributeError, pb.DeadReferenceError):
              self.disconnect(exit_code=1)
  
      def acread_data(self, force=False):
          if force and self._d_acread:
              self._d_acread.cancel()
              self._d_acread = None
  
          if (
              self._d_acread and not self._d_acread.called
          ) or not self._bridge_factory.has_clients():
              return
  
          try:
              self._d_acread = self.agentpool.callRemote(
                  "acread", self._agent_id, self._ac_id
              )
              self._d_acread.addCallback(self.cb_acread_result)
              self._d_acread.addErrback(self.cb_global_error)
          except (AttributeError, pb.DeadReferenceError):
              self.disconnect(exit_code=1)
  
      def cb_acread_result(self, result):
          if result is None:
              self.disconnect(exit_code=1)
          else:
              self._bridge_factory.send_to_clients(result)
              self.acread_data()
  
      def acwrite_data(self, data, force=False):
          if force and self._d_acwrite:
              self._d_acwrite.cancel()
              self._d_acwrite = None
  
          self._acwrite_buffer += data
          if len(self._acwrite_buffer) > self.MAX_BUFFER_SIZE:
              self._acwrite_buffer = self._acwrite_buffer[-1 * self.MAX_BUFFER_SIZE :]
          if (self._d_acwrite and not self._d_acwrite.called) or not self._acwrite_buffer:
              return
  
          data = self._acwrite_buffer
          self._acwrite_buffer = ""
          try:
              d = self.agentpool.callRemote("acwrite", self._agent_id, self._ac_id, data)
              d.addCallback(self.cb_acwrite_result)
              d.addErrback(self.cb_global_error)
          except (AttributeError, pb.DeadReferenceError):
              self.disconnect(exit_code=1)
  
      def cb_acwrite_result(self, result):
          assert result > 0
          if self._acwrite_buffer:
              self.acwrite_data("")
  