<?php
/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 *
 * 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.
 */

namespace Google\Cloud\Debugger;

use Google\Cloud\Debugger\Connection\ConnectionInterface;
use Google\Cloud\Core\Exception\ServiceException;

/**
 * This class represents a debuggee - a service that can handle breakpoints.
 *
 * Example:
 * ```
 * use Google\Cloud\Debugger\DebuggerClient;
 *
 * $client = new DebuggerClient();
 * $debuggee = $client->debuggee('debuggeeid');
 * ```
 *
 * @see https://cloud.google.com/debugger/api/reference/rest/v2/Debuggee Debuggee API Documentation
 */
class Debuggee
{
    /**
     * @var ConnectionInterface $connection Represents a connection to Debugger
     */
    private $connection;

    /**
     * @var string The unique identifier for this Debuggee
     */
    private $id;

    /**
     * @var string Project the debuggee is associated with. Use the project
     *      number when registering a Google Cloud Platform project.
     */
    private $project;

    /**
     * @var string Debuggee uniquifier within the project. Any string that
     *      identifies the application within the project can be used. Including
     *      environment and version or build IDs is recommended.
     */
    private $uniquifier;

    /**
     * @var string Human readable description of the debuggee. Including a
     *      human-readable project name, environment name and version
     *      information is recommended.
     */
    private $description;

    /**
     * @var bool If set to true, indicates that the debuggee is considered as
     *      inactive by the Controller service.
     */
    private $isInactive = false;

    /**
     * @var string Version ID of the agent release. The version ID is structured
     *      as following: domain/type/vmajor.minor (for example
     *      google.com/gcp-java/v1.1).
     */
    private $agentVersion;

    /**
     * @var ExtendedSourceContext[] References to the locations and revisions of
     *      the source code used in the deployed application. Contexts
     *      describing a remote repo related to the source code have a category
     *      label of remote_repo. Source snapshot source contexts have a
     *      category of snapshot.
     */
    private $extSourceContexts = [];

    /**
     * @var array A set of custom debuggee properties, populated by the agent,
     *      to be displayed to the user.
     */
    private $labels = [];

    /**
     * @var StatusMessage|array|null $status Human readable message to be
     *            displayed to the user about this debuggee. Absence of this
     *            field indicates no status. The message can be either
     *            informational or an error status.
     */
    private $status;

    /**
     * Instantiate a new Debuggee.
     *
     * @param ConnectionInterface $connection
     * @param array $info [optional] {
     *      Configuration options.
     *
     *      @type string $id Unique identifier for the debuggee generated by the
     *            controller service.
     *      @type string $project Project the debuggee is associated with. Use
     *            the project number when registering a Google Cloud Platform
     *            project.
     *      @type string $uniquifier Debuggee uniquifier within the project. Any
     *            string that identifies the application within the project can
     *            be used. Including environment and version or build IDs is
     *            recommended.
     *      @type string $description Human readable description of the
     *            debuggee. Including a human-readable project name, environment
     *            name and version information is recommended.
     *      @type string $isInactive If set to true, indicates that the debuggee
     *            is considered as inactive by the Controller service.
     *      @type string $agentVersion Version ID of the agent release. The
     *            version ID is structured as following:
     *            domain/type/vmajor.minor (for example
     *            google.com/gcp-java/v1.1).
     *      @type string $status Human readable message to be displayed to the
     *            user about this debuggee. Absence of this field indicates no
     *            status. The message can be either informational or an error
     *            status.
     *      @type ExtendedSourceContext[] $extSourceContexts References to the
     *            locations and revisions of the source code used in the
     *            deployed application.
     *      @type array $labels  A set of custom debuggee properties, populated
     *            by the agent, to be displayed to the user.
     * }
     */
    public function __construct(ConnectionInterface $connection, array $info = [])
    {
        $this->connection = $connection;

        $info += [
            'id' => null,
            'isInactive' => false,
            'agentVersion' => DebuggerClient::getDefaultAgentVersion(),
            'status' => null,
            'extSourceContexts' => [],
            'uniquifier' => null,
            'description' => null,
            'labels' => [],
            'project' => null
        ];

        $this->id = $info['id'];
        $this->isInactive = $info['isInactive'];
        $this->agentVersion = $info['agentVersion'];
        $this->status = $info['status'];
        $this->extSourceContexts = $info['extSourceContexts'];
        $this->uniquifier = $info['uniquifier'];
        $this->description = $info['description'];
        $this->labels = $info['labels'];
        $this->project = $info['project'];
    }

    /**
     * Return the debuggee identifier.
     *
     * Example:
     * ```
     * echo $debuggee->id();
     * ```
     *
     * @return string
     */
    public function id()
    {
        return $this->id;
    }

    /**
     * Register this debuggee with the Stackdriver backend.
     *
     * Example:
     * ```
     * $debuggee->register();
     * echo $debuggee->id();
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/controller.debuggees/register Debuggee register API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param array $options
     * @return bool
     */
    public function register(array $options = [])
    {
        $resp = $this->connection->registerDebuggee(['debuggee' => $this->info()] + $options);
        if (array_key_exists('debuggee', $resp)) {
            $this->id = $resp['debuggee']['id'];
            return true;
        }
        return false;
    }

    /**
     * Fetch the list of breakpoints this debugee should try to handle.
     *
     * Example:
     * ```
     * $breakpoints = $debuggee->breakpoints();
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/controller.debuggees.breakpoints/list Breakpoints list API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param array $options [optional] {
     *      Configuration options.
     *
     *      @type string $waitToken A wait token that, if specified, blocks the
     *            method call until the list of active breakpoints has changed,
     *            or a server selected timeout has expired. The value should be
     *            set from the last returned response.
     * }
     * @return Breakpoint[]
     */
    public function breakpoints(array $options = [])
    {
        $ret = $this->breakpointsWithWaitToken($options);
        return $ret['breakpoints'];
    }

    /**
     * Fetch the list of breakpoints this debugee should try to handle and a
     * wait token for the next request. The return value is an associative array
     * with keys of breakpoints and nextWaitToken.
     *
     * Example:
     * ```
     * $resp = $debuggee->breakpointsWithWaitToken();
     * $nextWaitToken = $resp['nextWaitToken'];
     * $breakpoints = $resp['breakpoints'];
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/controller.debuggees.breakpoints/list Breakpoints list API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param array $options [optional] {
     *      Configuration options.
     *
     *      @type string $waitToken A wait token that, if specified, blocks the
     *            method call until the list of active breakpoints has changed,
     *            or a server selected timeout has expired. The value should be
     *            set from the last returned response.
     * }
     * @return array
     */
    public function breakpointsWithWaitToken(array $options = [])
    {
        $ret = $this->connection->listBreakpoints(['debuggeeId' => $this->id] + $options);

        if (array_key_exists('breakpoints', $ret)) {
            $ret['breakpoints'] = array_map(function ($breakpointData) {
                return new Breakpoint($breakpointData);
            }, $ret['breakpoints']);
        } else {
            $ret['breakpoints'] = [];
        }
        return $ret;
    }

    /**
     * Update the provided, modified breakpoint.
     *
     * Example:
     * ```
     * $debuggee->updateBreakpoint($breakpoint);
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/controller.debuggees.breakpoints/update Breakpoint update API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param Breakpoint $breakpoint The modified breakpoint.
     * @param array $options [optional] Configuration options. See
     *        {@see Google\Cloud\Core\RequestWrapper::__construct()} for
     *        configuration options which apply to all network requests.
     * @return void
     * @throws ServiceException
     */
    public function updateBreakpoint(Breakpoint $breakpoint, array $options = [])
    {
        $this->connection->updateBreakpoint([
            'debuggeeId' => $this->id,
            'id' => $breakpoint->id(),
            'breakpoint' => $breakpoint->info()
        ] + $options);
    }

    /**
     * Set a breakpoint for this debuggee.
     *
     * Example:
     * ```
     * $breakpoint = $debuggee->setBreakpoint('DebuggerClient.php', 10);
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/debugger.debuggees.breakpoints/set Breakpoint set API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param string $path Path to the source file.
     * @param int $line Line within the source file.
     * @param array $options [optional] Array of Breakpoint constructor arguments. See
     *        {@see Google\Cloud\Debugger\Breakpoint::__construct()} for
     *        configuration details. See
     *        {@see Google\Cloud\Core\RequestWrapper::__construct()} for
     *        configuration options which apply to all network requests.
     */
    public function setBreakpoint($path, $line, array $options = [])
    {
        $resp = $this->connection->setBreakpoint([
            'debuggeeId' => $this->id,
            'location' => [
                'path' => $path,
                'line' => $line
            ]
        ] + $options);
        return new Breakpoint($resp['breakpoint']);
    }

    /**
     * Update multiple breakpoints.
     *
     * Example:
     * ```
     * $debuggee->updateBreakpointBatch([$breakpoint1, $breakpoint2]);
     * ```
     *
     * @codingStandardsIgnoreStart
     * @see https://cloud.google.com/debugger/api/reference/rest/v2/controller.debuggees.breakpoints/update Breakpoint update API documentation.
     * @codingStandardsIgnoreEnd
     *
     * @param Breakpoint[] $breakpoints The modified breakpoints.
     * @param array $options [optional] Configuration options. See
     *        {@see Google\Cloud\Core\RequestWrapper::__construct()} for
     *        configuration options which apply to all network requests.
     * @return void
     * @throws ServiceException
     */
    public function updateBreakpointBatch(array $breakpoints, array $options = [])
    {
        foreach ($breakpoints as $breakpoint) {
            $this->updateBreakpoint($breakpoint, $options);
        }
    }

    /**
     * Return a serializable version of this object
     *
     * @access private
     * @return array
     */
    public function info()
    {
        $info = [
            'project' => $this->project,
            'uniquifier' => $this->uniquifier,
            'description' => $this->description,
            'isInactive' => $this->isInactive,
            'agentVersion' => $this->agentVersion,
            'sourceContexts' => array_map(function ($esc) {
                if (empty($esc)) {
                    return [];
                }
                return is_array($esc) ? $esc['context'] : $esc->context()->info();
            }, $this->extSourceContexts),
            'extSourceContexts' => array_map(function ($esc) {
                return is_array($esc) ? $esc : $esc->info();
            }, $this->extSourceContexts)
        ];

        // Do not include the ID unless it is set.
        if ($this->id) {
            $info['id'] = $this->id;
        }

        if ($this->status instanceof StatusMessage) {
            $info['status'] = $this->status->info();
        } elseif ($this->status) {
            $info['status'] = $this->status;
        }

        if (!empty($this->labels)) {
            $info['labels'] = $this->labels;
        }

        return $info;
    }
}
