# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import contextlib
import json
import os
import pathlib
import socket
import platform
import sys
import struct

sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..'))
from util import build_utils

# Use a unix abstract domain socket:
# https://man7.org/linux/man-pages/man7/unix.7.html#:~:text=abstract:
SOCKET_ADDRESS = '\0chromium_build_server_socket'
BUILD_SERVER_ENV_VARIABLE = 'INVOKED_BY_BUILD_SERVER'

ADD_TASK = 'add_task'
QUERY_BUILD = 'query_build'
POLL_HEARTBEAT = 'poll_heartbeat'
REGISTER_BUILDER = 'register_builder'
CANCEL_BUILD = 'cancel_build'
STOP_SERVER = 'stop_server'

SERVER_SCRIPT = (
    pathlib.Path(build_utils.DIR_SOURCE_ROOT)
    / 'build'
    / 'android'
    / 'fast_local_dev_server.py'
)


def AssertEnvironmentVariables():
    assert os.environ.get('AUTONINJA_BUILD_ID')
    assert os.environ.get('AUTONINJA_STDOUT_NAME')


def MaybeRunCommand(name, argv, stamp_file, use_build_server=False):
    """Returns True if the command was successfully sent to the build server."""
    if not use_build_server or platform.system() == 'Darwin':
        # Build server does not support Mac.
        return False

    # When the build server runs a command, it sets this environment variable.
    # This prevents infinite recursion where the script sends a request to the
    # build server, then the build server runs the script, and then the script
    # sends another request to the build server.
    if BUILD_SERVER_ENV_VARIABLE in os.environ:
        return False

    build_id = os.environ.get('AUTONINJA_BUILD_ID')
    if not build_id:
        raise Exception(
            'AUTONINJA_BUILD_ID is not set. android_static_analysis = build_server '
            'requires autoninja integration.'
        )
    stdout_name = os.environ.get('AUTONINJA_STDOUT_NAME')
    # If we get a bad tty (happens when autoninja is not run from the terminal
    # directly but as part of another script), ignore the build server and build
    # normally since the build server will not know where to output to otherwise.
    if not stdout_name or not os.path.exists(stdout_name):
        return False

    with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock:
        try:
            sock.connect(SOCKET_ADDRESS)
        except socket.error as e:
            # [Errno 111] Connection refused. Either the server has not been started
            #             or the server is not currently accepting new connections.
            if e.errno == 111:
                raise RuntimeError(
                    '\n\nBuild server is not running and '
                    'android_static_analysis="build_server" is set. Look at '
                    'buildserver.log.0 in your output directory to see why the server '
                    'crashed.\n\n'
                ) from None
            raise e

        SendMessage(
            sock,
            {
                'name': name,
                'message_type': ADD_TASK,
                'cmd': [sys.executable] + argv,
                'cwd': os.getcwd(),
                'build_id': build_id,
                'stamp_file': stamp_file,
            },
        )

    # Siso needs the stamp file to be created in order for the build step to
    # complete. If the task fails when the build server runs it, the build server
    # will delete the stamp file so that it will be run again next build.
    build_utils.Touch(stamp_file)
    return True


def MaybeTouch(stamp_file):
    """Touch |stamp_file| if we are not running under the build_server."""
    # If we are running under the build server, the stamp file has already been
    # touched when the task was created. If we touch it again, siso will consider
    # the target dirty.
    if BUILD_SERVER_ENV_VARIABLE in os.environ:
        return
    build_utils.Touch(stamp_file)


def SendMessage(sock: socket.socket, message: dict):
    data = json.dumps(message).encode('utf-8')
    size_prefix = struct.pack('!i', len(data))
    sock.sendall(size_prefix + data)


def ReceiveMessage(sock: socket.socket) -> dict:
    size_prefix = b''
    remaining = 4  # sizeof(int)
    while remaining > 0:
        data = sock.recv(remaining)
        if not data:
            return None
        remaining -= len(data)
        size_prefix += data
    (remaining,) = struct.unpack('!i', size_prefix)
    received = []
    while remaining > 0:
        data = sock.recv(remaining)
        if not data:
            break
        received.append(data)
        remaining -= len(data)
    if received:
        return json.loads(b''.join(received))
    return None
