<?php
/**
 * @category   Zend
 * @package    Zend_Cloud_Infrastructure
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */

require_once 'Zend/Service/Amazon/Ec2/Instance.php';
require_once 'Zend/Service/Amazon/Ec2/Image.php';
require_once 'Zend/Service/Amazon/Ec2/Availabilityzones.php';
require_once 'Zend/Service/Amazon/Ec2/CloudWatch.php';
require_once 'Zend/Cloud/Infrastructure/Instance.php';
require_once 'Zend/Cloud/Infrastructure/InstanceList.php';
require_once 'Zend/Cloud/Infrastructure/Image.php';
require_once 'Zend/Cloud/Infrastructure/ImageList.php';
require_once 'Zend/Cloud/Infrastructure/Adapter/AbstractAdapter.php';

/**
 * Amazon EC2 adapter for infrastructure service
 *
 * @package    Zend_Cloud_Infrastructure
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Cloud_Infrastructure_Adapter_Ec2 extends Zend_Cloud_Infrastructure_Adapter_AbstractAdapter
{
    /**
     * @var bool
     */
    protected $error;

    /**
     * AWS constants
     */
    public const AWS_ACCESS_KEY     = 'aws_accesskey';
    public const AWS_SECRET_KEY     = 'aws_secretkey';
    public const AWS_REGION         = 'aws_region';
    public const AWS_SECURITY_GROUP = 'securityGroup';

    /**
     * Ec2 Instance
     *
     * @var Ec2Instance
     */
    protected $ec2;

    /**
     * Ec2 Image
     *
     * @var Ec2Image
     */
    protected $ec2Image;

    /**
     * Ec2 Zone
     *
     * @var Ec2Zone
     */
    protected $ec2Zone;

    /**
     * Ec2 Monitor
     *
     * @var Ec2Monitor
     */
    protected $ec2Monitor;

    /**
     * AWS Access Key
     *
     * @var string
     */
    protected $accessKey;

    /**
     * AWS Access Secret
     *
     * @var string
     */
    protected $accessSecret;

    /**
     * Region zone
     *
     * @var string
     */
    protected $region;

    /**
     * Map array between EC2 and Infrastructure status
     *
     * @var array
     */
    protected $mapStatus = [
        'running'       => Zend_Cloud_Infrastructure_Instance::STATUS_RUNNING,
        'terminated'    => Zend_Cloud_Infrastructure_Instance::STATUS_TERMINATED,
        'pending'       => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'shutting-down' => Zend_Cloud_Infrastructure_Instance::STATUS_SHUTTING_DOWN,
        'stopping'      => Zend_Cloud_Infrastructure_Instance::STATUS_PENDING,
        'stopped'       => Zend_Cloud_Infrastructure_Instance::STATUS_STOPPED,
        'rebooting'     => Zend_Cloud_Infrastructure_Instance::STATUS_REBOOTING,
    ];

    /**
     * Map monitor metrics between Infrastructure and EC2
     *
     * @var array
     */
    protected $mapMetrics= [
        Zend_Cloud_Infrastructure_Instance::MONITOR_CPU         => 'CPUUtilization',
        Zend_Cloud_Infrastructure_Instance::MONITOR_DISK_READ   => 'DiskReadBytes',
        Zend_Cloud_Infrastructure_Instance::MONITOR_DISK_WRITE  => 'DiskWriteBytes',
        Zend_Cloud_Infrastructure_Instance::MONITOR_NETWORK_IN  => 'NetworkIn',
        Zend_Cloud_Infrastructure_Instance::MONITOR_NETWORK_OUT => 'NetworkOut',
    ];

    /**
     * Constructor
     *
     * @param  array|Zend_Config $options
     * @return void
     */
    public function __construct($options = [])
    {
        if (is_object($options)) {
            if (method_exists($options, 'toArray')) {
                $options= $options->toArray();
            } elseif ($options instanceof Traversable) {
                $options = iterator_to_array($options);
            }
        }

        if (empty($options) || !is_array($options)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Invalid options provided');
        }

        if (!isset($options[self::AWS_ACCESS_KEY])
            || !isset($options[self::AWS_SECRET_KEY])
        ) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('AWS keys not specified!');
        }

        $this->accessKey    = $options[self::AWS_ACCESS_KEY];
        $this->accessSecret = $options[self::AWS_SECRET_KEY];
        $this->region       = '';

        if (isset($options[self::AWS_REGION])) {
            $this->region= $options[self::AWS_REGION];
        }

        try {
            $this->ec2 = new Zend_Service_Amazon_Ec2_Instance($options[self::AWS_ACCESS_KEY], $options[self::AWS_SECRET_KEY], $this->region);
        } catch (Exception  $e) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('Error on create: ' . $e->getMessage(), $e->getCode(), $e);
        }

        if (isset($options[self::HTTP_ADAPTER])) {
            $this->ec2->getHttpClient()->setAdapter($options[self::HTTP_ADAPTER]);
        }
    }

    /**
     * Convert the attributes of EC2 into attributes of Infrastructure
     *
     * @param  array $attr
     * @return array
     */
    private function convertAttributes($attr)
    {
        $result = [];
        if (!empty($attr) && is_array($attr)) {
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_ID]         = $attr['instanceId'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STATUS]     = $this->mapStatus[$attr['instanceState']['name']];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_IMAGEID]    = $attr['imageId'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_ZONE]       = $attr['availabilityZone'];
            $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_LAUNCHTIME] = $attr['launchTime'];

            switch ($attr['instanceType']) {
                case Zend_Service_Amazon_Ec2_Instance::MICRO:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '1 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '613MB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '0GB';
                    break;
                case Zend_Service_Amazon_Ec2_Instance::SMALL:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '1 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '1.7GB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '160GB';
                    break;
                case Zend_Service_Amazon_Ec2_Instance::LARGE:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '2 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '7.5GB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '850GB';
                    break;
                case Zend_Service_Amazon_Ec2_Instance::XLARGE:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '4 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '15GB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '1690GB';
                    break;
                case Zend_Service_Amazon_Ec2_Instance::HCPU_MEDIUM:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '2 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '1.7GB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '350GB';
                    break;
                case Zend_Service_Amazon_Ec2_Instance::HCPU_XLARGE:
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_CPU]     = '8 virtual core';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_RAM]     = '7GB';
                    $result[Zend_Cloud_Infrastructure_Instance::INSTANCE_STORAGE] = '1690GB';
                    break;
            }
        }
        return $result;
    }

    /**
     * Return a list of the available instancies
     *
     * @return Zend_Cloud_Infrastructure_InstanceList
     */
    public function listInstances()
    {
        $this->adapterResult = $this->ec2->describe();

        $result = [];
        foreach ($this->adapterResult['instances'] as $instance) {
            $result[]= $this->convertAttributes($instance);
        }
        return new Zend_Cloud_Infrastructure_InstanceList($this, $result);
    }

    /**
     * Return the status of an instance
     *
     * @param  string $id
     * @return string|boolean
     */
    public function statusInstance($id)
    {
        $this->adapterResult = $this->ec2->describe($id);
        if (empty($this->adapterResult['instances'])) {
            return false;
        }
        $result = $this->adapterResult['instances'][0];
        return $this->mapStatus[$result['instanceState']['name']];
    }

    /**
     * Return the public DNS name of the instance
     *
     * @param  string $id
     * @return string|boolean
     */
    public function publicDnsInstance($id)
    {
        $this->adapterResult = $this->ec2->describe($id);
        if (empty($this->adapterResult['instances'])) {
            return false;
        }
        $result = $this->adapterResult['instances'][0];
        return $result['dnsName'];
    }

    /**
     * Reboot an instance
     *
     * @param string $id
     * @return boolean
     */
    public function rebootInstance($id)
    {
        $this->adapterResult= $this->ec2->reboot($id);
        return $this->adapterResult;
    }

    /**
     * Create a new instance
     *
     * @param string $name
     * @param array $options
     * @return false|Zend_Cloud_Infrastructure_Instance
     */
    public function createInstance($name, $options)
    {
        // @todo instance's name management?
        $this->adapterResult = $this->ec2->run($options);
        if (empty($this->adapterResult['instances'])) {
            return false;
        }
        $this->error= false;
        return new Zend_Cloud_Infrastructure_Instance($this, $this->convertAttributes($this->adapterResult['instances'][0]));
    }

    /**
     * Stop an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function stopInstance($id)
    {
        require_once 'Zend/Cloud/Infrastructure/Exception.php';
        throw new Zend_Cloud_Infrastructure_Exception('The stopInstance method is not implemented in the adapter');
    }

    /**
     * Start an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function startInstance($id)
    {
        require_once 'Zend/Cloud/Infrastructure/Exception.php';
        throw new Zend_Cloud_Infrastructure_Exception('The startInstance method is not implemented in the adapter');
    }

    /**
     * Destroy an instance
     *
     * @param  string $id
     * @return boolean
     */
    public function destroyInstance($id)
    {
        $this->adapterResult = $this->ec2->terminate($id);
        return (!empty($this->adapterResult));
    }

    /**
     * Return a list of all the available instance images
     *
     * @return Zend_Cloud_Infrastructure_ImageList
     */
    public function imagesInstance()
    {
        if (!isset($this->ec2Image)) {
            $this->ec2Image = new Zend_Service_Amazon_Ec2_Image($this->accessKey, $this->accessSecret, $this->region);
        }

        $this->adapterResult = $this->ec2Image->describe();

        $images = [];

        foreach ($this->adapterResult as $result) {
            switch (strtolower($result['platform'])) {
                case 'windows' :
                    $platform = Zend_Cloud_Infrastructure_Image::IMAGE_WINDOWS;
                    break;
                default:
                    $platform = Zend_Cloud_Infrastructure_Image::IMAGE_LINUX;
                    break;
            }

            $images[]= [
                Zend_Cloud_Infrastructure_Image::IMAGE_ID           => $result['imageId'],
                Zend_Cloud_Infrastructure_Image::IMAGE_NAME         => '',
                Zend_Cloud_Infrastructure_Image::IMAGE_DESCRIPTION  => $result['imageLocation'],
                Zend_Cloud_Infrastructure_Image::IMAGE_OWNERID      => $result['imageOwnerId'],
                Zend_Cloud_Infrastructure_Image::IMAGE_ARCHITECTURE => $result['architecture'],
                Zend_Cloud_Infrastructure_Image::IMAGE_PLATFORM     => $platform,
            ];
        }
        return new Zend_Cloud_Infrastructure_ImageList($images,$this->ec2Image);
    }

    /**
     * Return all the available zones
     *
     * @return array
     */
    public function zonesInstance()
    {
        if (!isset($this->ec2Zone)) {
            $this->ec2Zone = new Zend_Service_Amazon_Ec2_AvailabilityZones($this->accessKey,$this->accessSecret,$this->region);
        }
        $this->adapterResult = $this->ec2Zone->describe();

        $zones = [];
        foreach ($this->adapterResult as $zone) {
            if (strtolower($zone['zoneState']) === 'available') {
                $zones[] = [
                    Zend_Cloud_Infrastructure_Instance::INSTANCE_ZONE => $zone['zoneName'],
                ];
            }
        }
        return $zones;
    }

    /**
     * Return the system information about the $metric of an instance
     *
     * @param  string $id
     * @param  string $metric
     * @param  null|array $options
     * @return array
     */
    public function monitorInstance($id, $metric, $options = null)
    {
        if (empty($id) || empty($metric)) {
            return false;
        }

        if (!in_array($metric,$this->validMetrics)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception(sprintf(
                'The metric "%s" is not valid',
                $metric
            ));
        }

        if (!empty($options) && !is_array($options)) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception('The options must be an array');
        }

        if (!empty($options)
            && (empty($options[Zend_Cloud_Infrastructure_Instance::MONITOR_START_TIME])
                || empty($options[Zend_Cloud_Infrastructure_Instance::MONITOR_END_TIME]))
        ) {
            require_once 'Zend/Cloud/Infrastructure/Exception.php';
            throw new Zend_Cloud_Infrastructure_Exception(sprintf(
                'The options array must contain: "%s" and "%s"',
                $options[Zend_Cloud_Infrastructure_Instance::MONITOR_START_TIME],
                $options[Zend_Cloud_Infrastructure_Instance::MONITOR_END_TIME]
            ));
        }

        if (!isset($this->ec2Monitor)) {
            $this->ec2Monitor = new Zend_Service_Amazon_Ec2_CloudWatch($this->accessKey, $this->accessSecret, $this->region);
        }

        $param = [
            'MeasureName' => $this->mapMetrics[$metric],
            'Statistics'  => ['Average'],
            'Dimensions'  => ['InstanceId' => $id],
        ];

        if (!empty($options)) {
            $param['StartTime'] = $options[Zend_Cloud_Infrastructure_Instance::MONITOR_START_TIME];
            $param['EndTime']   = $options[Zend_Cloud_Infrastructure_Instance::MONITOR_END_TIME];
        }

        $this->adapterResult = $this->ec2Monitor->getMetricStatistics($param);

        $monitor             = [];
        $num                 = 0;
        $average             = 0;

        if (!empty($this->adapterResult['datapoints'])) {
            foreach ($this->adapterResult['datapoints'] as $result) {
                $monitor['series'][] = [
                    'timestamp' => $result['Timestamp'],
                    'value'     => $result['Average'],
                ];
                $average += $result['Average'];
                $num++;
            }
        }

        if ($num > 0) {
            $monitor['average'] = $average / $num;
        }

        return $monitor;
    }

    /**
     * Get the adapter
     *
     * @return Zend_Service_Amazon_Ec2_Instance
     */
    public function getAdapter()
    {
        return $this->ec2;
    }

    /**
     * Get last HTTP request
     *
     * @return string
     */
    public function getLastHttpRequest()
    {
        return $this->ec2->getHttpClient()->getLastRequest();
    }

    /**
     * Get the last HTTP response
     *
     * @return Zend_Http_Response
     */
    public function getLastHttpResponse()
    {
        return $this->ec2->getHttpClient()->getLastResponse();
    }
}
