<?php
/**
 * PHP Command Line Tools
 *
 * This source file is subject to the MIT license that is bundled
 * with this package in the file LICENSE.
 *
 * @author    James Logsdon <dwarf@girsbrain.org>
 * @copyright 2010 James Logsdom (http://girsbrain.org)
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
 */

namespace cli\table;

use cli\Colors;
use cli\Shell;

/**
 * The ASCII renderer renders tables with ASCII borders.
 */
class Ascii extends Renderer {
	protected $_characters = array(
		'corner'  => '+',
		'line'    => '-',
		'border'  => '|',
		'padding' => ' ',
	);
	protected $_border = null;
	protected $_constraintWidth = null;
	protected $_pre_colorized = false;

	/**
	 * Set the widths of each column in the table.
	 *
	 * @param array  $widths    The widths of the columns.
	 * @param bool   $fallback  Whether to use these values as fallback only.
	 */
	public function setWidths(array $widths, $fallback = false) {
		if ($fallback) {
			foreach ( $this->_widths as $index => $value ) {
			    $widths[$index] = $value;
			}
		}
		$this->_widths = $widths;

		if ( is_null( $this->_constraintWidth ) ) {
			$this->_constraintWidth = (int) Shell::columns();
		}
		$col_count = count( $widths );
		$col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0;
		$table_borders_count = strlen( $this->_characters['border'] ) * 2;
		$col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2;
		$max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count;

		if ( $widths && $max_width && array_sum( $widths ) > $max_width ) {

			$avg = floor( $max_width / count( $widths ) );
			$resize_widths = array();
			$extra_width = 0;
			foreach( $widths as $width ) {
				if ( $width > $avg ) {
					$resize_widths[] = $width;
				} else {
					$extra_width = $extra_width + ( $avg - $width );
				}
			}

			if ( ! empty( $resize_widths ) && $extra_width ) {
				$avg_extra_width = floor( $extra_width / count( $resize_widths ) );
				foreach( $widths as &$width ) {
					if ( in_array( $width, $resize_widths ) ) {
						$width = $avg + $avg_extra_width;
						array_shift( $resize_widths );
						// Last item gets the cake
						if ( empty( $resize_widths ) ) {
							$width = 0; // Zero it so not in sum.
							$width = $max_width - array_sum( $widths );
						}
					}
				}
			}

		}

		$this->_widths = $widths;
	}

	/**
	 * Set the constraint width for the table
	 *
	 * @param int $constraintWidth
	 */
	public function setConstraintWidth( $constraintWidth ) {
		$this->_constraintWidth = $constraintWidth;
	}

	/**
	 * Set the characters used for rendering the Ascii table.
	 *
	 * The keys `corner`, `line` and `border` are used in rendering.
	 *
	 * @param $characters  array  Characters used in rendering.
	 */
	public function setCharacters(array $characters) {
		$this->_characters = array_merge($this->_characters, $characters);
	}

	/**
	 * Render a border for the top and bottom and separating the headers from the
	 * table rows.
	 *
	 * @return string  The table border.
	 */
	public function border() {
		if (!isset($this->_border)) {
			$this->_border = $this->_characters['corner'];
			foreach ($this->_widths as $width) {
				$this->_border .= str_repeat($this->_characters['line'], $width + 2);
				$this->_border .= $this->_characters['corner'];
			}
		}

		return $this->_border;
	}

	/**
	 * Renders a row for output.
	 *
	 * @param array  $row  The table row.
	 * @return string  The formatted table row.
	 */
	public function row( array $row ) {

		$extra_row_count = 0;

		if ( count( $row ) > 0 ) {
			$extra_rows = array_fill( 0, count( $row ), array() );

			foreach ( $row as $col => $value ) {
				$value              = $value ?: '';
				$col_width          = $this->_widths[ $col ];
				$encoding           = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false;
				$original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding );
				if ( $col_width && ( $original_val_width > $col_width || strpos( $value, "\n" ) !== false ) ) {
					$split_lines = preg_split( '/\r\n|\n/', $value );

					$wrapped_lines = [];
					foreach ( $split_lines as $line ) {
						do {
							$wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding );
							$val_width     = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding );
							if ( $val_width ) {
								$wrapped_lines[] = $wrapped_value;
								$line            = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
							}
						} while ( $line );
					}

					$row[ $col ] = array_shift( $wrapped_lines );
					foreach ( $wrapped_lines as $wrapped_line ) {
						$extra_rows[ $col ][] = $wrapped_line;
						++$extra_row_count;
					}
				}
			}
		}

		$row = array_map(array($this, 'padColumn'), $row, array_keys($row));
		array_unshift($row, ''); // First border
		array_push($row, ''); // Last border

		$ret = join($this->_characters['border'], $row);
		if ( $extra_row_count ) {
			foreach( $extra_rows as $col => $col_values ) {
				while( count( $col_values ) < $extra_row_count ) {
					$col_values[] = '';
				}
			}

			do {
				$row_values = array();
				$has_more = false;
				foreach( $extra_rows as $col => &$col_values ) {
					$row_values[ $col ] = ! empty( $col_values ) ? array_shift( $col_values ) : '';
					if ( count( $col_values ) ) {
						$has_more = true;
					}
				}

				$row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values));
				array_unshift($row_values, ''); // First border
				array_push($row_values, ''); // Last border

				$ret .= PHP_EOL . join($this->_characters['border'], $row_values);
			} while( $has_more );
		}
		return $ret;
	}

	private function padColumn($content, $column) {
		$content = str_replace( "\t", '    ', (string) $content );
		return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding'];
	}

	/**
	 * Set whether items are pre-colorized.
	 *
	 * @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized.
	 */
	public function setPreColorized( $pre_colorized ) {
		$this->_pre_colorized = $pre_colorized;
	}

	/**
	 * Is a column pre-colorized?
	 *
	 * @param int $column Column index to check.
	 * @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false.
	 */
	public function isPreColorized( $column ) {
		if ( is_bool( $this->_pre_colorized ) ) {
			return $this->_pre_colorized;
		}
		if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) {
			return $this->_pre_colorized[ $column ];
		}
		return false;
	}
}
