<?php

namespace Aws\Api\Serializer;

use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Service;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Api\TimestampShape;
use XMLWriter;

/**
 * @internal Formats the XML body of a REST-XML services.
 */
class XmlBody {
	/** @var Service */
	private $api;

	/**
	 * @param Service $api API being used to create the XML body.
	 */
	public function __construct( Service $api ) {
		$this->api = $api;
	}

	/**
	 * Builds the XML body based on an array of arguments.
	 *
	 * @param Shape $shape Operation being constructed
	 * @param array $args Associative array of arguments
	 *
	 * @return string
	 */
	public function build( Shape $shape, array $args ) {
		$xml = new XMLWriter();
		$xml->openMemory();
		$xml->startDocument( '1.0', 'UTF-8' );
		$this->format( $shape, $shape['locationName'] ?: $shape['name'], $args, $xml );
		$xml->endDocument();

		return $xml->outputMemory();
	}

	private function format( Shape $shape, $name, $value, XMLWriter $xml ) {
		// Any method mentioned here has a custom serialization handler.
		static $methods = [
			'add_structure' => true,
			'add_list'      => true,
			'add_blob'      => true,
			'add_timestamp' => true,
			'add_boolean'   => true,
			'add_map'       => true,
			'add_string'    => true
		];

		$type = 'add_' . $shape['type'];
		if ( isset( $methods[ $type ] ) ) {
			$this->{$type}( $shape, $name, $value, $xml );
		} else {
			$this->defaultShape( $shape, $name, $value, $xml );
		}
	}

	private function defaultShape( Shape $shape, $name, $value, XMLWriter $xml ) {
		$this->startElement( $shape, $name, $xml );
		$xml->text( $value );
		$xml->endElement();
	}

	private function startElement( Shape $shape, $name, XMLWriter $xml ) {
		$xml->startElement( $name );

		if ( $ns = $shape['xmlNamespace'] ) {
			$xml->writeAttribute(
				isset( $ns['prefix'] ) ? "xmlns:{$ns['prefix']}" : 'xmlns',
				$shape['xmlNamespace']['uri']
			);
		}
	}

	private function add_structure(
		StructureShape $shape,
		$name,
		array $value,
		XMLWriter $xml
	) {
		$this->startElement( $shape, $name, $xml );

		foreach ( $this->getStructureMembers( $shape, $value ) as $k => $definition ) {
			$this->format(
				$definition['member'],
				$definition['member']['locationName'] ?: $k,
				$definition['value'],
				$xml
			);
		}

		$xml->endElement();
	}

	private function getStructureMembers( StructureShape $shape, array $value ) {
		$members = [];

		foreach ( $value as $k => $v ) {
			if ( $v !== null && $shape->hasMember( $k ) ) {
				$definition = [
					'member' => $shape->getMember( $k ),
					'value'  => $v,
				];

				if ( $definition['member']['xmlAttribute'] ) {
					// array_unshift_associative
					$members = [ $k => $definition ] + $members;
				} else {
					$members[ $k ] = $definition;
				}
			}
		}

		return $members;
	}

	private function add_list(
		ListShape $shape,
		$name,
		array $value,
		XMLWriter $xml
	) {
		$items = $shape->getMember();

		if ( $shape['flattened'] ) {
			$elementName = $name;
		} else {
			$this->startElement( $shape, $name, $xml );
			$elementName = $items['locationName'] ?: 'member';
		}

		foreach ( $value as $v ) {
			$this->format( $items, $elementName, $v, $xml );
		}

		if ( ! $shape['flattened'] ) {
			$xml->endElement();
		}
	}

	private function add_map(
		MapShape $shape,
		$name,
		array $value,
		XMLWriter $xml
	) {
		$xmlEntry = $shape['flattened'] ? $shape['locationName'] : 'entry';
		$xmlKey   = $shape->getKey()['locationName'] ?: 'key';
		$xmlValue = $shape->getValue()['locationName'] ?: 'value';

		$this->startElement( $shape, $name, $xml );

		foreach ( $value as $key => $v ) {
			$this->startElement( $shape, $xmlEntry, $xml );
			$this->format( $shape->getKey(), $xmlKey, $key, $xml );
			$this->format( $shape->getValue(), $xmlValue, $v, $xml );
			$xml->endElement();
		}

		$xml->endElement();
	}

	private function add_blob( Shape $shape, $name, $value, XMLWriter $xml ) {
		$this->startElement( $shape, $name, $xml );
		$xml->writeRaw( base64_encode( $value ) );
		$xml->endElement();
	}

	private function add_timestamp(
		TimestampShape $shape,
		$name,
		$value,
		XMLWriter $xml
	) {
		$this->startElement( $shape, $name, $xml );
		$timestampFormat = ! empty( $shape['timestampFormat'] )
			? $shape['timestampFormat']
			: 'iso8601';
		$xml->writeRaw( TimestampShape::format( $value, $timestampFormat ) );
		$xml->endElement();
	}

	private function add_boolean(
		Shape $shape,
		$name,
		$value,
		XMLWriter $xml
	) {
		$this->startElement( $shape, $name, $xml );
		$xml->writeRaw( $value ? 'true' : 'false' );
		$xml->endElement();
	}

	private function add_string(
		Shape $shape,
		$name,
		$value,
		XMLWriter $xml
	) {
		if ( $shape['xmlAttribute'] ) {
			$xml->writeAttribute( $shape['locationName'] ?: $name, $value );
		} else {
			$this->defaultShape( $shape, $name, $value, $xml );
		}
	}
}
