<?php

// +----------------------------------------------------------------------+
// | Copyright 2013  Madpixels  (email : visualizer@madpixels.net)        |
// +----------------------------------------------------------------------+
// | This program is free software; you can redistribute it and/or modify |
// | it under the terms of the GNU General Public License, version 2, as  |
// | published by the Free Software Foundation.                           |
// |                                                                      |
// | This program is distributed in the hope that it will be useful,      |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of       |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        |
// | GNU General Public License for more details.                         |
// |                                                                      |
// | You should have received a copy of the GNU General Public License    |
// | along with this program; if not, write to the Free Software          |
// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,               |
// | MA 02110-1301 USA                                                    |
// +----------------------------------------------------------------------+
// | Author: Eugene Manuilov <eugene@manuilov.org>                        |
// +----------------------------------------------------------------------+
/**
 * Source manager for JSON URLs.
 *
 * @category Visualizer
 * @package Source
 *
 * @since 1.0.0
 */
class Visualizer_Source_Json extends Visualizer_Source {

	/**
	 * The url to the data.
	 *
	 * @since 1.0.0
	 *
	 * @access protected
	 * @var string
	 */
	protected $_url;

	/**
	 * The root to the data.
	 *
	 * @since 1.0.0
	 *
	 * @access protected
	 * @var string
	 */
	protected $_root;

	/**
	 * The paging element.
	 *
	 * @since 1.0.0
	 *
	 * @access protected
	 * @var string
	 */
	protected $_paging;

	/**
	 * The headers.
	 *
	 * @since ?
	 *
	 * @access protected
	 * @var array
	 */
	protected $_headers;

	/**
	 * The other headers.
	 *
	 * @since 3.4.11
	 *
	 * @access protected
	 * @var array
	 */
	protected $_additional_headers;

	/**
	 * The array that contains the definition of the data.
	 *
	 * @since 1.0.0
	 *
	 * @access protected
	 * @var array
	 */
	protected $_args;

	const TAG_SEPARATOR = '>';
	const TAG_SEPARATOR_VIEW = ' &#x27A4; ';

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @access public
	 * @param array $params The array that contains the definition of the data.
	 */
	public function __construct( $params = null ) {
		$this->_args = $params;
		if ( isset( $this->_args['url'] ) ) {
			$this->_url = trim( $this->_args['url'] );
		}
		if ( isset( $this->_args['root'] ) ) {
			$this->_root = trim( $this->_args['root'] );
		}
		if ( isset( $this->_args['paging'] ) ) {
			$this->_paging = trim( $this->_args['paging'] );
		}
		if ( isset( $this->_args['auth'] ) && ! empty( $this->_args['auth'] ) ) {
			$this->_headers = array( 'auth' => $this->_args['auth'] );
		} elseif ( isset( $this->_args['username'] ) && isset( $this->_args['password'] ) ) {
			$this->_headers = array( 'username' => $this->_args['username'], 'password' => $this->_args['password'] );
		}
		if ( isset( $this->_args['method'] ) ) {
			$this->_headers['method'] = $this->_args['method'];
		}
		if ( isset( $this->_args['additional_headers'] ) ) {
			$this->_additional_headers = $this->_args['additional_headers'];
		}
		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Constructor called for params = %s', print_r( $params, true ) ), 'debug', __FILE__, __LINE__ );
	}

	/**
	 * Get the root elements for JSON-endpoint.
	 *
	 * @since ?
	 *
	 * @access public
	 */
	public function fetchRoots() {
		$roots  = apply_filters( 'visualizer_json_get_root_elements', false, $this->_url );
		if ( false !== $roots ) {
			return $roots;
		}
		$roots = $this->getRootElements( 'root', '', array(), $this->getJSON() );
		if ( is_null( $roots ) ) {
			$this->_error = esc_html__( 'This does not appear to be a valid JSON feed. Please try again.', 'visualizer' );
			return false;
		}
		return $roots;
	}

	/**
	 * Get the name of the elements that are likely to contain the paginated URLs.
	 *
	 * @since ?
	 *
	 * @access public
	 */
	public function getPaginationElements() {
		$pages = array();
		$root   = explode( self::TAG_SEPARATOR, $this->_root );
		// the pagination element is usually one level above the root element we are going to use
		// e.g. if the data is in root > results, the pagination element (let's call it "next") would be root > next and not root > results > next.
		array_pop( $root );

		// base of the next element.
		$base = implode( self::TAG_SEPARATOR, $root );
		// get rid of the first element as that is the faux root element indicator
		array_shift( $root );

		$array  = $this->getJSON();
		if ( is_null( $array ) ) {
			return $pages;
		}

		$leaf   = $array;
		if ( ! empty( $root ) ) {
			foreach ( $root as $tag ) {
				if ( array_key_exists( $tag, $leaf ) ) {
					$leaf = $array[ $tag ];
				} else {
					// if the tag does not exist, we assume it is present in the 0th element of the current array.
					$leaf = $leaf[0][ $tag ];
				}
			}
		}

		$paging = array();
		foreach ( $leaf as $key => $value ) {
			// the paging element's value will most probably contain the url of the feed.
			if ( ! is_string( $value ) ) {
				continue;
			}

			// strip the url off the request parameters e.g. format=json as sometimes the pagination urls may not contain them.
			$url = wp_parse_url( $value );
			if ( 0 === stripos( $value, $this->_url ) || false !== stripos( $this->_url, $url['path'] ) ) {
				$paging[] = $key;
			}
		}

		foreach ( array_filter( array_unique( $paging ) ) as $page ) {
			$pages[] = $base . self::TAG_SEPARATOR . $page;
		}
		return $pages;
	}

	/**
	 * Parse the JSON-endpoint from the chosen root as the base.
	 *
	 * @since ?
	 *
	 * @access public
	 */
	public function fetch() {
		$url    = $this->_url;
		$data   = array();
		$page   = 1;

		while ( ! is_null( $url ) && $page++ < apply_filters( 'visualizer_json_fetch_pages', 5, $this->_url ) ) {
			$array  = $this->getJSON( $url );
			if ( is_null( $array ) ) {
				break;
			}

			$root   = explode( self::TAG_SEPARATOR, $this->_root );
			if ( count( $root ) > 1 ) {
				// get rid of the first element as that is the faux root element indicator
				array_shift( $root );
			}
			$leaf   = $array;
			foreach ( $root as $tag ) {
				if ( array_key_exists( $tag, $leaf ) ) {
					$leaf = $leaf[ $tag ];
				} else {
					// if the tag does not exist, we assume it is present in every element of the current array.
					$temp = array();
					for ( $depth = 0; $depth < count( $leaf ); $depth++ ) {
						if ( ! isset( $leaf[ $depth ][ $tag ] ) ) {
							do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Element %s not present at depth %d in %d elements. Ignoring.', $tag, $depth, count( $leaf ) ), 'debug', __FILE__, __LINE__ );
							continue;
						}
						$temp[] = $leaf[ $depth ][ $tag ];
					}
					$leaf = $temp;
				}
			}

			// JSONs that do not have a root element may fall into this condition e.g. /wp-json/wp/v2/posts
			if ( empty( $leaf ) ) {
				$leaf = $array;
			}

			// now that we have got the final array we need to operate on, we will use this as the collection of series.
			// lets check if the series is a flat-series e.g. https://api.exchangeratesapi.io/latest
			// in this, all values of `$leaf` would be a string, not an array.
			$values = array_values( $leaf );
			if ( ! is_array( $values[0] ) ) {
				do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Looks like a flat-series = %s', print_r( $leaf, true ) ), 'debug', __FILE__, __LINE__ );
				$inner_data = array();
				foreach ( $leaf as $datum => $value ) {
					$inner_data[ $datum ] = $value;
				}
				$data[] = $inner_data;
			} else {
				$row_num = 0;
				// we will filter out all elements of this array that have array as a value.
				foreach ( $leaf as $datum ) {
					$inner_data = array();
					foreach ( $datum as $key => $value ) {
						if ( is_array( $value ) ) {
							continue;
						}
						$inner_data[ $key ] = $value;
					}
					// if we want to exclude entire rows on the basis of some data/key or row index.
					if ( apply_filters( 'visualizer_json_include_row', true, $inner_data, $this->_root, $this->_url, ++$row_num ) ) {
						$data[] = $inner_data;
					}
				}
			}

			$data = $this->collectCommonElements( $data );

			$url    = $this->getNextPage( $array );
		}

		$this->_data = $data;

		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Parsed data endpoint %s with root %s is %s', $this->_url, $this->_root, print_r( $data, true ) ), 'debug', __FILE__, __LINE__ );

		return true;
	}

	/**
	 * Determines which keys are common to all the data and returns an array with only those elements.
	 * This is to ensure that the data set displayed on the table is consistent.
	 * Inconsistent data causes the table to not display.
	 *
	 * @since ?
	 *
	 * @access private
	 */
	private function collectCommonElements( $data ) {
		$keys       = array();
		foreach ( $data as $datum ) {
			if ( empty( $keys ) ) {
				$keys   = array_keys( $datum );
				continue;
			}
			$keys = array_intersect( $keys, array_keys( $datum ) );
		}

		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Extracting data only for the common keys = %s', print_r( $keys, true ) ), 'debug', __FILE__, __LINE__ );

		$rows = array();
		foreach ( $data as $datum ) {
			$row = array();
			foreach ( $datum as $key => $value ) {
				if ( in_array( $key, $keys, true ) ) {
					$row[ $key ] = $value;
				}
			}
			$rows[] = $row;
		}

		return $rows;

	}

	/**
	 * Get the next page URL.
	 *
	 * @since ?
	 *
	 * @access private
	 */
	private function getNextPage( $array ) {
		if ( empty( $this->_paging ) ) {
			return null;
		}

		$root   = explode( self::TAG_SEPARATOR, $this->_paging );
		// get rid of the first element as that is the faux root element indicator
		array_shift( $root );
		$leaf   = $array;
		foreach ( $root as $tag ) {
			if ( array_key_exists( $tag, $leaf ) ) {
				$leaf = $array[ $tag ];
			} else {
				// if the tag does not exist, we assume it is present in the 0th element of the current array.
				// TODO: we may want to change this to a filter later.
				$leaf = $leaf[0][ $tag ];
			}
		}

		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Got next page as %s for paging element %s', $leaf, $this->_paging ), 'debug', __FILE__, __LINE__ );

		return $leaf;
	}

	/**
	 * Get the root elements for JSON-endpoint.
	 *
	 * @since ?
	 *
	 * @access private
	 */
	private function getRootElements( $parent, $now, $root, $array ) {
		if ( is_null( $array ) ) {
			return null;
		}

		$root[] = $parent;
		foreach ( $array as $key => $value ) {
			if ( is_array( $value ) && ! empty( $value ) ) {
				if ( ! is_numeric( $key ) ) {
					$now = sprintf( '%s%s%s', $parent, self::TAG_SEPARATOR, $key );
					$root[] = $now;
				} else {
					$now = $parent;
				}
				$root = $this->getRootElements( $now, $key, $root, $value );
			}
		}
		$roots = array_unique( $root );
		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Roots found for %s = %s', $this->_url, print_r( $roots, true ) ), 'debug', __FILE__, __LINE__ );
		return $roots;
	}

	/**
	 * Get the JSON for the JSON-endpoint.
	 *
	 * @since ?
	 *
	 * @access private
	 */
	private function getJSON( $url = null ) {
		if ( is_null( $url ) ) {
			$url = $this->_url;
		}

		$response = $this->connect( $url );
		if ( is_wp_error( $response ) || ! in_array( intval( $response['response']['code'] ), array( 200, 201 ), true ) ) {
			do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Error while fetching JSON endpoint %s = %s', $url, print_r( $response, true ) ), 'error', __FILE__, __LINE__ );
			return null;
		}

		// this filter can be used to rearrange columns e.g. in candlestick charts
		// or to add additional data to the root.
		$bom = pack( 'H*', 'EFBBBF' );
		$response_body = wp_remote_retrieve_body( $response );
		$response_body = preg_replace( "/^$bom/", '', $response_body );
		$array  = apply_filters( 'visualizer_json_massage_data', json_decode( $response_body, true ), $url );

		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'JSON array for the endpoint is %s = ', print_r( $array, true ) ), 'debug', __FILE__, __LINE__ );

		return $array;
	}

	/**
	 * Sets the authentication/authorization headers and connects to the endpoint.
	 *
	 * @since ?
	 *
	 * @access private
	 */
	private function connect( $url ) {
		$args = array( 'method' => 'GET' );
		if ( ! empty( $this->_headers ) ) {
			$args = array( 'method' => strtoupper( $this->_headers['method'] ) );
			if ( array_key_exists( 'auth', $this->_headers ) ) {
				if ( ! empty( $this->_headers['auth'] ) ) {
					$header_auth = explode( ':', $this->_headers['auth'] );
					$header_key = explode( ' ', reset( $header_auth ) );
					$args['headers'] = array( 'Authorization' => reset( $header_key ) . ' ' . base64_encode( end( $header_key ) . ':' . end( $header_auth ) ) );
				}
			} elseif ( array_key_exists( 'username', $this->_headers ) && array_key_exists( 'password', $this->_headers ) && ! empty( $this->_headers['username'] ) && ! empty( $this->_headers['password'] ) ) {
				$args['headers'] = array( 'Authorization' => 'Basic ' . base64_encode( $this->_headers['username'] . ':' . $this->_headers['password'] ) );
			}
		}

		$args = apply_filters( 'visualizer_json_args', $args, $url );
		if ( ! empty( $this->_additional_headers ) ) {
			$this->_additional_headers = explode( ',', $this->_additional_headers );
			$this->_additional_headers = array_filter( $this->_additional_headers );
			if ( ! empty( $this->_additional_headers ) ) {
				$this->_additional_headers = array_map(
					function( $headers ) {
						$headers = explode( ':', $headers );
						$headers = array_map( 'trim', $headers );
						return $headers;
					},
					$this->_additional_headers
				);
				foreach ( $this->_additional_headers as $additional_headers ) {
					$header_key = reset( $additional_headers );
					$header_value = end( $additional_headers );
					$args['headers'][ $header_key ] = $header_value;
				}
			}
		}

		// Check if this is a WooCommerce endpoint request and add verification token.
		if ( $this->is_woocommerce_request( $url ) ) {
			// Generate a unique token for this specific request.
			$token = wp_generate_password( 32, false );
			set_transient( 'visualizer_wc_token_' . $token, time(), 60 );
			if ( ! isset( $args['headers'] ) ) {
				$args['headers'] = array();
			}
			$args['headers']['X-Visualizer-Token'] = $token;
		}

		do_action( 'themeisle_log_event', Visualizer_Plugin::NAME, sprintf( 'Connecting to %s with args = %s ', $url, print_r( $args, true ) ), 'debug', __FILE__, __LINE__ );
		return wp_remote_request( $url, $args );
	}

	/**
	 * Refresh the data for the provided series.
	 *
	 * @since ?
	 *
	 * @access public
	 */
	public function refresh( $series ) {
		$this->_series = $series;
		$this->fetch();

		$headers    = wp_list_pluck( $this->_series, 'label' );
		$data = array();
		foreach ( $this->_data as $line ) {
			$data_row = array();
			foreach ( $line as $header => $value ) {
				// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
				if ( in_array( $header, $headers ) ) {
					$data_row[] = $value;
				}
			}
			$data[] = $this->_normalizeData( $data_row );
		}
		$this->_data = $data;
		return true;
	}

	/**
	 * Check if the URL is a WooCommerce endpoint request.
	 *
	 * @access private
	 * @param string $url The URL to check.
	 * @return bool True if it's a WooCommerce request, false otherwise.
	 */
	private function is_woocommerce_request( $url ) {
		if ( empty( $url ) ) {
			return false;
		}

		$parsed_url = function_exists( 'wp_parse_url' ) ? wp_parse_url( $url ) : parse_url( $url );
		if ( empty( $parsed_url ) || empty( $parsed_url['host'] ) || empty( $parsed_url['path'] ) ) {
			return false;
		}

		$site_url   = function_exists( 'home_url' ) ? home_url() : ( function_exists( 'site_url' ) ? site_url() : '' );
		$site_parts = $site_url ? ( function_exists( 'wp_parse_url' ) ? wp_parse_url( $site_url ) : parse_url( $site_url ) ) : array();
		if ( empty( $site_parts['host'] ) ) {
			return false;
		}

		$target_host = strtolower( $parsed_url['host'] );
		$site_host   = strtolower( $site_parts['host'] );
		if ( $target_host !== $site_host ) {
			return false;
		}

		$path        = '/' . ltrim( $parsed_url['path'], '/' );
		$wc_patterns = array(
			'/wp-json/wc/',
			'/wp-json/wc-analytics/',
			'/wc-analytics/',
		);

		foreach ( $wc_patterns as $pattern ) {
			if ( strpos( $path, $pattern ) !== false ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns source name.
	 *
	 * @since 1.0.0
	 *
	 * @access public
	 * @return string The name of source.
	 */
	public function getSourceName() {
		return __CLASS__;
	}

}
