<?php

/**
 * LICENSE: 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.
 *
 * PHP version 5
 *
 * @category  Microsoft
 *
 * @author    Azure PHP SDK <azurephpsdk@microsoft.com>
 * @copyright 2012 Microsoft Corporation
 * @license   http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
 *
 * @link      https://github.com/windowsazure/azure-sdk-for-php
 */

namespace WindowsAzure\ServiceRuntime;

use WindowsAzure\Common\Internal\Resources;
use WindowsAzure\ServiceRuntime\Internal\CurrentState;
use WindowsAzure\ServiceRuntime\Internal\GoalState;
use WindowsAzure\ServiceRuntime\Internal\IRoleEnvironmentChange;
use WindowsAzure\ServiceRuntime\Internal\IRuntimeClient;
use WindowsAzure\ServiceRuntime\Internal\LocalResource;
use WindowsAzure\ServiceRuntime\Internal\Role;
use WindowsAzure\ServiceRuntime\Internal\RoleEnvironmentData;
use WindowsAzure\ServiceRuntime\Internal\RoleInstance;
use WindowsAzure\ServiceRuntime\Internal\RuntimeKernel;
use WindowsAzure\ServiceRuntime\Internal\RoleEnvironmentNotAvailableException;
use WindowsAzure\ServiceRuntime\Internal\ChannelNotAvailableException;
use WindowsAzure\ServiceRuntime\Internal\CurrentStatus;
use WindowsAzure\ServiceRuntime\Internal\AcquireCurrentState;
use WindowsAzure\ServiceRuntime\Internal\ReleaseCurrentState;
use WindowsAzure\ServiceRuntime\Internal\RoleInstanceStatus;
use WindowsAzure\ServiceRuntime\Internal\RoleEnvironmentConfigurationSettingChange;
use WindowsAzure\ServiceRuntime\Internal\RoleEnvironmentTopologyChange;

/**
 * Represents the Windows Azure environment in which an instance of a role is
 * running.
 *
 * @category  Microsoft
 *
 * @author    Azure PHP SDK <azurephpsdk@microsoft.com>
 * @copyright 2012 Microsoft Corporation
 * @license   http://www.apache.org/licenses/LICENSE-2.0  Apache License 2.0
 *
 * @version   Release: 0.5.0_2016-11
 *
 * @link      https://github.com/windowsazure/azure-sdk-for-php
 */
class RoleEnvironment
{
    /**
     * Specifies the environment variable that contains the path to the endpoint.
     *
     * @var string
     */
    const VERSION_ENDPOINT_ENVIRONMENT_NAME = 'WaRuntimeEndpoint';

    /**
     * Specifies the endpoint fixed path.
     *
     * @var string
     */
    const VERSION_ENDPOINT_FIXED_PATH = '\\\\.\\pipe\\WindowsAzureRuntime';

    /**
     * @var string
     */
    private static $_clientId;

    /**
     * @var \DateTime
     */
    private static $_maxDateTime;

    /**
     * @var IRuntimeClient
     */
    private static $_runtimeClient;

    /**
     * @var GoalState
     */
    private static $_currentGoalState;

    /**
     * @var RoleEnvironmentData
     */
    private static $_currentEnvironmentData;

    /**
     * @var callable[]
     */
    private static $_changingListeners;

    /**
     * @var callable[]
     */
    private static $_changedListeners;

    /**
     * @var callable[]
     */
    private static $_stoppingListeners;

    /**
     * @var CurrentState
     */
    private static $_lastState;

    /**
     * @var string
     */
    private static $_versionEndpoint;

    /**
     * @var mixed
     */
    private static $_tracking;

    /**
     * Initializes the role environment.
     *
     * @static
     */
    public static function init()
    {
        self::$_clientId = uniqid();

        // 2038-01-19 04:14:07
        self::$_maxDateTime = new \DateTime(
            date(Resources::TIMESTAMP_FORMAT, Resources::INT32_MAX)
        );

        self::$_tracking = true;
    }

    /**
     * Returns the client identifier.
     *
     * @static
     *
     * @return string
     */
    public static function getClientId()
    {
        return self::$_clientId;
    }

    /**
     * Initializes the runtime client.
     *
     * @param bool $keepOpen Boolean value indicating if the connection
     *                       should remain open
     *
     * @throws RoleEnvironmentNotAvailableException
     * @static
     */
    private static function _initialize($keepOpen = false)
    {
        try {
            if (is_null(self::$_runtimeClient)) {
                self::$_versionEndpoint = getenv(
                    self::VERSION_ENDPOINT_ENVIRONMENT_NAME
                );

                if (self::$_versionEndpoint == false) {
                    self::$_versionEndpoint = self::VERSION_ENDPOINT_FIXED_PATH;
                }

                $kernel = RuntimeKernel::getKernel();
                $kernel->getProtocol1RuntimeGoalStateClient()->setKeepOpen(
                    $keepOpen
                );

                self::$_runtimeClient = $kernel->getRuntimeVersionManager()
                    ->getRuntimeClient(self::$_versionEndpoint);

                self::$_currentGoalState = self::$_runtimeClient
                    ->getCurrentGoalState();

                self::$_currentEnvironmentData = self::$_runtimeClient
                    ->getRoleEnvironmentData();
            } else {
                self::$_currentGoalState = self::$_runtimeClient
                    ->getCurrentGoalState();

                self::$_currentEnvironmentData = self::$_runtimeClient
                    ->getRoleEnvironmentData();
            }
        } catch (ChannelNotAvailableException $ex) {
            throw new RoleEnvironmentNotAvailableException();
        }
    }

    /**
     * Tracks role environment changes raising events as necessary.
     *
     * This method is blocking and can/should be called in a separate fork.
     *
     * @static
     */
    public static function trackChanges()
    {
        self::_initialize(true);

        while (self::$_tracking) {
            $newGoalState = self::$_runtimeClient->getCurrentGoalState();

            switch ($newGoalState->getExpectedState()) {
            case CurrentStatus::STARTED:
                $newIncarnation = $newGoalState->getIncarnation();
                $currentIncarnation = self::$_currentGoalState->getIncarnation();

                if ($newIncarnation > $currentIncarnation) {
                    self::_processGoalStateChange($newGoalState);
                }

                break;
            case CurrentStatus::STOPPED:
                self::_raiseStoppingEvent();

                $stoppedState = new AcquireCurrentState(
                    self::$_clientId,
                    $newGoalState->getIncarnation(),
                    CurrentStatus::STOPPED,
                    self::$_maxDateTime
                );

                self::$_runtimeClient->setCurrentState($stoppedState);
                break;
            }

            if (is_int(self::$_tracking)) {
                --self::$_tracking;
            }
        }
    }

    /**
     * Processes a goal state change.
     *
     * @param GoalState $newGoalState The new goal state
     *
     * @static
     */
    private static function _processGoalStateChange($newGoalState)
    {
        $last = self::$_lastState;
        $changes = self::_calculateChanges();

        if (count($changes) == 0) {
            self::_acceptLatestIncarnation($newGoalState, $last);
        } else {
            self::_raiseChangingEvent($changes);

            self::_acceptLatestIncarnation($newGoalState, $last);

            self::$_currentEnvironmentData = self::$_runtimeClient
                ->getRoleEnvironmentData();

            self::_raiseChangedEvent($changes);
        }
    }

    /**
     * Accepts the latest incarnation.
     *
     * @param GoalState    $newGoalState The new goal state
     * @param CurrentState $last         The last state
     *
     * @static
     */
    private static function _acceptLatestIncarnation(GoalState $newGoalState, CurrentState $last)
    {
        if (!is_null($last) && $last instanceof AcquireCurrentState) {
            $acquireState = $last;

            $acceptState = new AcquireCurrentState(
                self::$_clientId,
                $newGoalState->getIncarnation(),
                $acquireState->getStatus(),
                $acquireState->getExpiration()
            );

            self::$_runtimeClient->setCurrentState($acceptState);
        }

        self::$_currentGoalState = $newGoalState;
    }

    /**
     * Raises a stopping event.
     *
     * @static
     */
    private static function _raiseStoppingEvent()
    {
        foreach (self::$_stoppingListeners as $callback) {
            call_user_func($callback);
        }
    }

    /**
     * Raises a changing event.
     *
     * @param array $changes The changes
     *
     * @static
     */
    private static function _raiseChangingEvent(array $changes)
    {
        foreach (self::$_changingListeners as $callback) {
            call_user_func($callback, $changes);
        }
    }

    /**
     * Raises a changed event.
     *
     * @param array $changes The changes
     *
     * @static
     */
    private static function _raiseChangedEvent(array $changes)
    {
        foreach (self::$_changedListeners as $callback) {
            call_user_func($callback, $changes);
        }
    }

    /**
     * Calculates changes.
     *
     * @static
     *
     * @return IRoleEnvironmentChange[]
     */
    private static function _calculateChanges()
    {
        $current = self::$_currentEnvironmentData;
        $newData = self::$_runtimeClient->getRoleEnvironmentData();

        $changes = self::_calculateConfigurationChanges($current, $newData);

        $currentRoles = $current->getRoles();
        $newRoles = $newData->getRoles();
        $changedRoleSet = [];

        foreach ($currentRoles as $roleName => $role) {
            if (array_key_exists($roleName, $newRoles)) {
                $currentRole = $currentRoles[$roleName];
                $newRole = $newRoles[$roleName];

                $currentRoleInstances = $currentRole->getInstances();
                $newRoleInstances = $newRole->getInstances();

                $changedRoleSet = array_merge(
                    $changedRoleSet,
                    self::_calculateNewRoleInstanceChanges(
                        $role,
                        $currentRoleInstances,
                        $newRoleInstances
                    )
                );
            } else {
                $changedRoleSet[] = $role;
            }
        }

        foreach ($newRoles as $roleName => $role) {
            if (array_key_exists($roleName, $currentRoles)) {
                $currentRole = $currentRoles[$roleName];
                $newRole = $newRoles[$roleName];

                $currentRoleInstances = $currentRole->getInstances();
                $newRoleInstances = $newRole->getInstances();

                $changedRoleSet = array_merge(
                    $changedRoleSet,
                    self::_calculateCurrentRoleInstanceChanges(
                        $role,
                        $currentRoleInstances,
                        $newRoleInstances
                    )
                );
            } else {
                $changedRoleSet[] = $role;
            }
        }

        foreach ($changedRoleSet as $role) {
            $changes[] = new RoleEnvironmentTopologyChange($role);
        }

        return $changes;
    }

    /**
     * Calculates the configuration changes.
     *
     * @param RoleEnvironmentData $currentRoleEnvironment The current role
     *                                                    environment data
     * @param RoleEnvironmentData $newRoleEnvironment     The new role
     *                                                    environment data
     *
     * @static
     *
     * @return RoleEnvironmentConfigurationSettingChange[]
     */
    private static function _calculateConfigurationChanges(
        RoleEnvironmentData $currentRoleEnvironment,
        RoleEnvironmentData $newRoleEnvironment
    ) {
        $changes = [];
        $currentConfig = $currentRoleEnvironment->getConfigurationSettings();
        $newConfig = $newRoleEnvironment->getConfigurationSettings();

        foreach ($currentConfig as $settingKey => $setting) {
            if (array_key_exists($settingKey, $newConfig)) {
                if ($newConfig[$settingKey] != $currentConfig[$settingKey]) {
                    $changes[] = new RoleEnvironmentConfigurationSettingChange(
                        $settingKey
                    );
                }
            } else {
                $changes[] = new RoleEnvironmentConfigurationSettingChange(
                    $settingKey
                );
            }
        }

        foreach ($newConfig as $settingKey => $setting) {
            if (!array_key_exists($settingKey, $currentConfig)) {
                $changes[] = new RoleEnvironmentConfigurationSettingChange(
                    $settingKey
                );
            }
        }

        return $changes;
    }

    /**
     * Calculates which instances / instance endpoints were added from the current
     * role to the new role.
     *
     * @param Role  $role                 The current role
     * @param array $currentRoleInstances The current role instances
     * @param array $newRoleInstances     The new role instances
     *
     * @static
     *
     * @return array
     */
    private static function _calculateNewRoleInstanceChanges(
        Role $role,
        array $currentRoleInstances,
        array $newRoleInstances
    ) {
        $changedRoleSet = [];

        foreach ($currentRoleInstances as $instanceKey => $currentInstance) {
            if (array_key_exists($instanceKey, $newRoleInstances)) {
                $newInstance = $newRoleInstances[$instanceKey];

                $currentUpdateDomain = $currentInstance->getUpdateDomain();
                $newUpdateDomain = $newInstance->getUpdateDomain();
                $currentFaultDomain = $currentInstance->getFaultDomain();
                $newFaultDomain = $newInstance->getFaultDomain();

                if ($currentUpdateDomain == $newUpdateDomain
                    && $currentFaultDomain == $newFaultDomain
                ) {
                    $currentInstanceEndpoints = $currentInstance
                        ->getInstanceEndpoints();
                    $newInstanceEndpoints = $newInstance->getInstanceEndpoints();

                    $changedRoleSet = array_merge(
                        $changedRoleSet,
                        self::_calculateNewRoleInstanceEndpointsChanges(
                            $role,
                            $currentInstanceEndpoints,
                            $newInstanceEndpoints
                        )
                    );
                } else {
                    $changedRoleSet[] = $role;
                }
            } else {
                $changedRoleSet[] = $role;
            }
        }

        return $changedRoleSet;
    }

    /**
     * Calculates which endpoints / endpoint were added from the current
     * role to the new role.
     *
     * @param Role  $role                     The current role
     * @param array $currentInstanceEndpoints The current instance endpoints
     * @param array $newInstanceEndpoints     The new instance endpoints
     *
     * @static
     *
     * @return array
     */
    private static function _calculateNewRoleInstanceEndpointsChanges(
        Role $role,
        array $currentInstanceEndpoints,
        array $newInstanceEndpoints
    ) {
        $changedRoleSet = [];

        foreach ($currentInstanceEndpoints as $endpointKey => $currentEndpoint) {
            if (array_key_exists($endpointKey, $newInstanceEndpoints)) {
                $newEndpoint = $newInstanceEndpoints[$endpointKey];

                $currentProtocol = $currentEndpoint->getProtocol();
                $newProtocol = $newEndpoint->getProtocol();
                $currentAddress = $currentEndpoint->getAddress();
                $newAddress = $newEndpoint->getAddress();
                $currentPort = $currentEndpoint->getPort();
                $newPort = $newEndpoint->getPort();
                if ($currentProtocol != $newProtocol
                    || $currentAddress != $newAddress
                    || $currentPort != $newPort
                ) {
                    $changedRoleSet[] = $role;
                }
            } else {
                $changedRoleSet[] = $role;
            }
        }

        return $changedRoleSet;
    }

    /**
     * Calculates which instances / instance endpoints were removed from the current
     * role to the new role.
     *
     * @param Role  $role                 The current role
     * @param array $currentRoleInstances The current role instances
     * @param array $newRoleInstances     The new role instances
     *
     * @static
     *
     * @return array
     */
    private static function _calculateCurrentRoleInstanceChanges(
        Role $role,
        array $currentRoleInstances,
        array $newRoleInstances
    ) {
        $changedRoleSet = [];

        foreach ($newRoleInstances as $instanceKey => $newInstance) {
            if (array_key_exists($instanceKey, $currentRoleInstances)) {
                $currentInstance = $currentRoleInstances[$instanceKey];

                $currentUpdateDomain = $currentInstance->getUpdateDomain();
                $newUpdateDomain = $newInstance->getUpdateDomain();
                $currentFaultDomain = $currentInstance->getFaultDomain();
                $newFaultDomain = $newInstance->getFaultDomain();

                if ($currentUpdateDomain == $newUpdateDomain
                    && $currentFaultDomain == $newFaultDomain
                ) {
                    $newInstanceEndpoints = $newInstance
                        ->getInstanceEndpoints();
                    $currentInstanceEndpoints = $currentInstance
                        ->getInstanceEndpoints();

                    $changedRoleSet = array_merge(
                        $changedRoleSet,
                        self::_calculateCurrentRoleInstanceEndpointsChanges(
                            $role,
                            $currentInstanceEndpoints,
                            $newInstanceEndpoints
                        )
                    );
                }
                // Intentionally not adding since if the values are different,
                // it should have already been added
            } else {
                $changedRoleSet[] = $role;
            }
        }

        return $changedRoleSet;
    }

    /**
     * Calculates which endpoints / endpoint were removed from the current
     * role to the new role.
     *
     * @param Role  $role                     The current changed role set
     * @param array $currentInstanceEndpoints The current instance endpoints
     * @param array $newInstanceEndpoints     The new instance endpoints
     *
     * @static
     *
     * @return array
     */
    private static function _calculateCurrentRoleInstanceEndpointsChanges(
        Role  $role,
        array $currentInstanceEndpoints,
        array $newInstanceEndpoints
    ) {
        $changedRoleSet = [];

        foreach ($newInstanceEndpoints as $endpointKey => $newEndpoint) {
            if (!array_key_exists(
                $endpointKey,
                $currentInstanceEndpoints
            )
            ) {
                $changedRoleSet[] = $role;
            }
        }

        return $changedRoleSet;
    }

    /**
     * Returns a RoleInstance object that represents the role instance
     * in which this code is currently executing.
     *
     * @static
     *
     * @return RoleInstance
     */
    public static function getCurrentRoleInstance()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->getCurrentInstance();
    }

    /**
     * Returns the deployment ID that uniquely identifies the deployment in
     * which this role instance is running.
     *
     * @static
     *
     * @return string
     */
    public static function getDeploymentId()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->getDeploymentId();
    }

    /**
     * Indicates whether the role instance is running in the Windows Azure
     * environment.
     *
     * @static
     *
     * @return bool
     */
    public static function isAvailable()
    {
        try {
            self::_initialize();
        } catch (RoleEnvironmentNotAvailableException $ex) {
            return false;
        } catch (ChannelNotAvailableException $ex) {
            return false;
        }

        return self::$_runtimeClient != null;
    }

    /**
     * Indicates whether the role instance is running in the development fabric.
     *
     * @static
     *
     * @return bool
     */
    public static function isEmulated()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->isEmulated();
    }

    /**
     * Returns the set of Role objects defined for your service.
     *
     * Roles are defined in the service definition file.
     *
     * @static
     *
     * @return Role[]
     */
    public static function getRoles()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->getRoles();
    }

    /**
     * Retrieves the settings in the service configuration file.
     *
     * A role's configuration settings are defined in the service definition
     * file. Values for configuration settings are set in the service
     * configuration file.
     *
     * @static
     *
     * @return array
     */
    public static function getConfigurationSettings()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->getConfigurationSettings();
    }

    /**
     * Retrieves the set of named local storage resources.
     *
     * @static
     *
     * @return LocalResource[]
     */
    public static function getLocalResources()
    {
        self::_initialize();

        return self::$_currentEnvironmentData->getLocalResources();
    }

    /**
     * Requests that the current role instance be stopped and restarted.
     * Before the role instance is recycled, the Windows Azure load balancer
     * takes the role instance out of rotation.
     *
     * This ensures that no new requests are routed to the instance while it
     * is restarting.
     *
     * @static
     */
    public static function requestRecycle()
    {
        self::_initialize();

        $recycleState = new AcquireCurrentState(
            self::$_clientId,
            self::$_currentGoalState->getIncarnation(),
            CurrentStatus::RECYCLE,
            self::$_maxDateTime
        );

        self::$_runtimeClient->setCurrentState($recycleState);
    }

    /**
     * Sets the status of the role instance.
     *
     * An instance may indicate that it is in one of two states: Ready or Busy.
     * If an instance's state is Ready, it is prepared to receive requests from
     * the load balancer. If the instance's state is Busy, it will not receive
     * requests from the load balancer.
     *
     * @param int       $status        he new role status
     * @param \DateTime $expirationUtc The expiration UTC time
     *
     * @static
     */
    public static function setStatus($status, \DateTime $expirationUtc)
    {
        self::_initialize();

        $currentStatus = CurrentStatus::STARTED;

        switch ($status) {
        case RoleInstanceStatus::BUSY:
            $currentStatus = CurrentStatus::BUSY;
            break;
        case RoleInstanceStatus::READY:
            $currentStatus = CurrentStatus::STARTED;
            break;
        }

        $newState = new AcquireCurrentState(
            self::$_clientId,
            self::$_currentGoalState->getIncarnation(),
            $currentStatus,
            $expirationUtc
        );

        self::$_lastState = $newState;

        self::$_runtimeClient->setCurrentState($newState);
    }

    /**
     * Clears the status of the role instance.
     *
     * An instance may indicate that it has completed communicating status by
     * calling this method.
     *
     * @static
     */
    public static function clearStatus()
    {
        self::_initialize();

        $newState = new ReleaseCurrentState(self::$_clientId);

        self::$_lastState = $newState;

        self::$_runtimeClient->setCurrentState($newState);
    }

    /**
     * Adds an event listener for the changed event, which occurs
     * after a configuration change has been applied to a role instance.
     *
     * To listen for events, one should call trackChanges.
     *
     * @param callable $listener The changed listener
     */
    public static function addRoleEnvironmentChangedListener($listener)
    {
        self::$_changedListeners[] = $listener;
    }

    /**
     * Removes an event listener for the Changed event.
     *
     * @param callable $listener The changed listener
     *
     * @static
     *
     * @return bool
     */
    public static function removeRoleEnvironmentChangedListener($listener)
    {
        foreach (self::$_changedListeners as $key => $changedListener) {
            if ($changedListener == $listener) {
                unset(self::$_changedListeners[$key]);

                return true;
            }
        }

        return false;
    }

    /**
     * Adds an event listener for the Changing event, which occurs
     * before a change to the service configuration is applied to the running
     * instances of the role.
     *
     * Service configuration changes are applied on-the-fly to running role
     * instances. Configuration changes include changes to the service
     * configuration changes and changes to the number of instances in the
     * service.
     *
     * This event occurs after the new configuration file has been submitted to
     * Windows Azure but before the changes have been applied to each running
     * role instance. This event can be cancelled for a given instance to
     * prevent the configuration change.
     *
     * Note that cancelling this event causes the instance to be automatically
     * recycled. When the instance is recycled, the configuration change is
     * applied when it restarts.
     *
     * To listen for events, one should call trackChanges.
     *
     * @param callable $listener The changing listener
     *
     * @static
     */
    public static function addRoleEnvironmentChangingListener($listener)
    {
        self::$_changingListeners[] = $listener;
    }

    /**
     * Removes an event listener for the Changing event.
     *
     * @param callable $listener The changing listener
     *
     * @static
     *
     * @return bool
     */
    public static function removeRoleEnvironmentChangingListener($listener)
    {
        foreach (self::$_changingListeners as $key => $changingListener) {
            if ($changingListener == $listener) {
                unset(self::$_changingListeners[$key]);

                return true;
            }
        }

        return false;
    }

    /**
     * Adds an event listener for the Stopping event, which occurs
     * when the role is stopping.
     * To listen for events, one should call trackChanges.
     *
     * @param callable $listener The stopping listener
     *
     * @static
     */
    public static function addRoleEnvironmentStoppingListener($listener)
    {
        self::$_stoppingListeners[] = $listener;
    }

    /**
     * Removes an event listener for the Stopping event.
     *
     * @param callable $listener The stopping listener
     *
     * @static
     *
     * @return bool
     */
    public static function removeRoleEnvironmentStoppingListener($listener)
    {
        foreach (self::$_stoppingListeners as $key => $stoppingListener) {
            if ($stoppingListener == $listener) {
                unset(self::$_stoppingListeners[$key]);

                return true;
            }
        }

        return false;
    }
}

// Initialize static fields
RoleEnvironment::init();
