<?php

namespace MyListing\Src;

use \MyListing\Src\Conditions;
use \MyListing\Src\User as User;
use \MyListing\Src\Schema as Schema;

class Listing {

	public static $instances = [];

	private
		$data,
		$categories,
		$special_keys = [],
		$special_keys_cache = [];

	public
		$schedule,
		$schema,
		$type = null,
		$author = null;

	/**
	 * Stores all listing field objects.
	 *
	 * @since 2.2
	 */
	private $fields;

	public static $aliases = [
		'title'       => 'job_title',
		'tagline'     => 'job_tagline',
		'location'    => 'job_location',
		'category'    => 'job_category',
		'tags'        => 'job_tags',
		'description' => 'job_description',
		'email'       => 'job_email',
		'logo'        => 'job_logo',
		'cover'       => 'job_cover',
		'gallery'     => 'job_gallery',
		'website'     => 'job_website',
		'phone'       => 'job_phone',
		'video_url'   => 'job_video_url',
		'date'        => 'job_date',
	];

	/**
	 * Get a new listing instance (Multiton pattern).
	 * When called the first time, listing will be fetched from database.
	 * Otherwise, it will return the previous instance.
	 *
	 * @since 1.6.0
	 * @param $listing int or \WP_Post
	 */
	public static function get( $listing ) {
		if ( is_numeric( $listing ) ) {
			$listing = get_post( $listing );
		}

		if ( ! $listing instanceof \WP_Post ) {
			return false;
		}

		if ( $listing->post_type !== 'job_listing' ) {
			return false;
		}

		if ( ! array_key_exists( $listing->ID, self::$instances ) ) {
			self::$instances[ $listing->ID ] = new self( $listing );
		}

		return self::$instances[ $listing->ID ];
	}

	/**
	 * Ignore cache and retrieve listing information from db.
	 *
	 * @since 2.1
	 */
	public static function force_get( $listing_id ) {
		clean_post_cache( $listing_id );
		unset( self::$instances[ $listing_id ] );
		return self::get( $listing_id );
	}

	public function __construct( \WP_Post $post ) {
		self::$instances[ $post->ID ] = $this;
		$this->data = $post;
		$this->schedule = new \MyListing\Src\Work_Hours( (array) get_post_meta( $this->data->ID, '_work_hours', true ) );
		$this->author = new User( $this->data->post_author );

		$this->set_type();
		$this->setup_special_keys();
		$this->schema = new Schema( $this );
	}

	public function get_id() {
		return $this->data->ID;
	}

	/**
	 * Get the listing title.
	 *
	 * @since 1.0
	 */
	public function get_name() {
		return $this->data->post_title;
	}

	/**
	 * Alias of `$this->get_name()`
	 *
	 * @since 2.4
	 */
	public function get_title() {
		return $this->get_name();
	}

	public function get_slug() {
		return $this->data->post_name;
	}

	public function get_status() {
		return $this->data->post_status;
	}

	/**
	 * Get the label for the current listing status.
	 *
	 * @since 2.1
	 */
	public function get_status_label() {
		$statuses = self::get_post_statuses();
		return isset( $statuses[ $this->get_status() ] )
			? $statuses[ $this->get_status() ]
			: _x( 'Inactive', 'post status', 'my-listing' );
	}

	public function get_logo( $size = 'thumbnail' ) {
		if ( $this->has_field( 'logo' ) ) {
			return c27()->get_resized_image( $this->get_field( 'logo' ), $size );
		}

		if ( $this->type && ( $default_logo = $this->type->get_default_logo( $size ) ) ) {
			return apply_filters( 'mylisting\listing\get_logo\default', $default_logo, $this );
		}

		return apply_filters( 'mylisting\listing\get_logo\default', '', $this );
	}

	public function get_cover_image( $size = 'large' ) {
		if ( $this->has_field( 'cover' ) ) {
			return c27()->get_resized_image( $this->get_field( 'cover' ), $size );
		}

		if ( $this->type && ( $default_cover = $this->type->get_default_cover( $size ) ) ) {
			return apply_filters( 'mylisting\listing\get_cover_image\default', $default_cover, $this );
		}

		return apply_filters( 'mylisting\listing\get_cover_image\default', '', $this );
	}

	public function get_data( $key = null ) {
		if ( $key ) {
			if ( isset( $this->data->$key ) ) {
				return $this->data->$key;
			}

			return null;
		}

		return $this->data;
	}

	public function get_link() {
		return get_permalink( $this->data );
	}

	public function get_schedule() {
		return $this->schedule;
	}

	/**
	 * Get the listing type this listing belongs to.
	 *
	 * @since  1.0
	 * @return Listing_Type|false
	 */
	private function set_type() {
		if ( $this->type ) {
			return $this->type;
		}

		// Get listing type based on listing type id (slug).
		$type_id = get_post_meta( $this->get_id(), '_case27_listing_type', true );
		if ( $type_id && ( $type = ( get_page_by_path( $type_id, OBJECT, 'case27_listing_type' ) ) ) ) {
			$this->type = \MyListing\Src\Listing_Type::get( $type );
			return $this->type;
		}

		// If not available, set to null.
		$this->type = null;
		return $this->type;
	}

	public function get_author() {
		return $this->author;
	}

	public function get_author_id() {
		return absint( $this->get_data('post_author') );
	}

	public function get_rating() {
		return \MyListing\Ext\Reviews\Reviews::get_listing_rating_optimized( $this->get_id() );
	}

	/**
	 * Retrieve listing priority.
	 *
	 * @since 1.7.0
	 */
	public function get_priority() {
		return absint( $this->get_data( '_featured' ) );
	}

	/**
	 * Get the amount of listing reviews (first level comments).
	 * If it's not stored in listing meta, then count them again and store the result.
	 *
	 * @since 1.6.3
	 */
	public function get_review_count() {
		if ( ( $count = $this->get_data( '_case27_review_count' ) ) !== null ) {
			return (int) $count;
		}

		return \MyListing\Ext\Reviews\Reviews::count_reviews( $this->get_id() );
	}

	/**
	 * Get the list of all fields as objects. We have to clone them from the
	 * listing type's `get_fields` method, to make sure each field references
	 * the correct listing in its `$listing` property.
	 *
	 * @since 2.2
	 */
	public function get_fields() {
		if ( ! $this->type ) {
			return [];
		}

		if ( ! empty( $this->fields ) ) {
			return $this->fields;
		}

		$fields = $this->type->get_fields();

		foreach ( $fields as $key => $field ) {
			$this->fields[ $key ] = clone $field;
			$this->fields[ $key ]->set_listing( $this );
		}

		return $this->fields;
	}

	/**
	 * Determine if the requested field has a value that should be displayed.
	 *
	 * @since 1.7.2
	 */
	public function has_field( $key ) {
		$field = $this->get_field( $key, true );

		// if the requested field is a special key or doesn't exist
		if ( ! is_subclass_of( $field, \MyListing\Src\Forms\Fields\Base_Field::class ) ) {
			return ( ! empty( $field ) || in_array( $field, [ 0, '0', 0.0 ], true ) );
		}

		// otherwise, this is a valid field
		$value = $field->get_value();

		// 0, '0', and 0.0 need special handling since they're valid, but PHP considers them falsy values.
		return ( ! empty( $value ) || in_array( $value, [ 0, '0', 0.0 ], true ) );
	}

	public function get_field( $key, $object = false, $check_conditions = true ) {
		// check if it's requesting a special key
		if ( ( $special_key = $this->get_special_key( $key ) ) && ! $object ) {
			return $special_key;
		}

		// check if the requested field key is an alias of another field
		if ( array_key_exists( $key, self::$aliases ) ) {
			return $this->get_field( self::$aliases[ $key ], $object );
		}

		// populate $fields
		$this->get_fields();

		// check if requested field exists
		if ( empty( $this->fields ) || ! isset( $this->fields[ $key ] ) ) {
			return false;
		}

		$field = $this->fields[ $key ];

		// check conditions
		if ( ! $field->passes_conditions() && $check_conditions === true ) {
			return false;
		}

		if ( $object === true ) {
			return $field;
		}

		return $field->get_value();
	}

	public function get_field_object( $key ) {
		return $this->get_field( $key, true );
	}

	public function get_social_networks() {
		if ( ! $this->has_field( 'links' ) ) {
			return [];
		}

		$networks = [];
		$allowed_networks = \MyListing\Src\Forms\Fields\Links_Field::allowed_networks();

		foreach ( (array) $this->get_field( 'links' ) as $link ) {
            if ( ! is_array( $link ) || empty( $link['network'] ) ) {
            	continue;
        	}

        	if ( empty( $link['url'] ) || ! isset( $allowed_networks[ $link['network'] ] ) ) {
        		continue;
        	}

        	$network = $allowed_networks[ $link['network'] ];
        	$network['link'] = $link['url'];

        	$networks[] = $network;
		}

		return array_filter( $networks );
	}

	/**
	 * Get the text to be used when listing is shared on social networks.
	 *
	 * @since  1.6.3
	 * @return string $description
	 */
	public function get_share_description() {
		$description = wp_kses( $this->get_field( 'description' ), [] );

		if ( $this->has_field( 'tagline' ) ) {
			$description = $this->get_field( 'tagline' );
		}

		return apply_filters( 'mylisting\listing\share\description', $description, $this );
	}

	/**
	 * Get the image to be used when listing is shared on social networks.
	 *
	 * @since  1.6.3
	 * @return string $image
	 */
	public function get_share_image() {
		$field = apply_filters( 'mylisting\single\og:image', 'logo' );
		$image = '';

		if ( $field == 'logo' ) {
			$image = $this->get_logo( 'large' );
		} elseif ( $field == 'cover' ) {
			$image = $this->get_cover_image( 'large' );
		} elseif ( $this->has_field( $field ) ) {
			$image = c27()->get_resized_image( $this->get_field( $field ), 'large' );
		}

		if ( $image && filter_var( $image, FILTER_VALIDATE_URL ) !== false ) {
			$image = esc_url( $image );
		}

		return apply_filters( 'mylisting\listing\share\image', $image, $this );
	}

	/**
	 * Get the WooCommerce Product ID assigned to this listing.
	 * Product type can be Listing Package or Listing Subscription.
	 *
	 * @since 2.1
	 * @return int|null $product_id
	 */
	public function get_product_id() {
		return get_post_meta( $this->get_id(), '_package_id', true );
	}

	/**
	 * Get the WooCommerce Product assigned to this listing.
	 *
	 * @since 2.1
	 * @return \WC_Product|false $product
	 */
	public function get_product() {
		$package_id = $this->get_product_id();
		if ( ! ( $package_id && function_exists( 'wc_get_product' ) ) ) {
			return false;
		}

		return wc_get_product( $package_id );
	}

	/**
	 * Get the payment package ID assigned to this listing.
	 *
	 * @since 2.1.6
	 * @return int|null $package_id
	 */
	public function get_package_id() {
		return get_post_meta( $this->get_id(), '_user_package_id', true );
	}

	/**
	 * Get the payment package assigned to this listing.
	 *
	 * @since 2.1.6
	 * @return \MyListing\Src\Package|false $package
	 */
	public function get_package() {
		return \MyListing\Src\Package::get( $this->get_package_id() );
	}

	/**
	 * Get the listing expiry date in Y-m-d format.
	 *
	 * @since 2.1.6
	 * @return \DateTime|false
	 */
	public function get_expiry_date() {
		$date = get_post_meta( $this->get_id(), '_job_expires', true );
		$timestamp = strtotime( $date );
		if ( $timestamp === false ) {
			return false;
		}

		$date = new \DateTime;
		$date->setTimestamp( $timestamp );
		return $date;
	}

	/**
	 * Compile strings that use field bracket syntax.
	 *
	 * @since 1.5.0
	 */
	public function compile_string( $string, $require_all_fields = true ) {
		return \MyListing\compile_string( $string, $require_all_fields, $this );
	}

	public function setup_special_keys() {
		$this->special_keys = apply_filters( 'mylisting/special-keys', [
			':id'              => $this->get_id(),
			':url'             => $this->get_link(),
			':lat'             => $this->get_data('geolocation_lat'),
			':lng'             => $this->get_data('geolocation_long'),
			':date'            => date_i18n( get_option( 'date_format' ), strtotime( $this->get_data('post_date') ) ),
			':rawdate'         => $this->get_data('post_date'),
			':last-modified'   => get_the_modified_date( '', $this->get_id() ),
			':authid'          => $this->get_data('post_author'),
			':authname'        => get_the_author_meta( 'display_name', $this->get_data('post_author') ),
			':authlogin'       => get_the_author_meta( 'user_login', $this->get_data('post_author') ),
			':currentuserid'   => get_current_user_id(),
			':currentusername' => get_the_author_meta( 'display_name', get_current_user_id() ),
			':currentuserlogin' => get_the_author_meta( 'user_login', get_current_user_id() ),
			':reviews-mode'    => $this->type ? $this->type->get_review_mode() : 10,
			':reviews-average' => function() {
				return $this->get_rating();
			},
			':reviews-count' => function() {
				return $this->get_review_count();
			},
			':reviews-stars' => function() {
				ob_start();
				mylisting_locate_template( 'partials/star-ratings.php', [
			        'rating' => $this->get_rating(),
			        'max-rating' => \MyListing\Ext\Reviews\Reviews::max_rating( $this->get_id() ),
			        'class' => '',
			    ] );
			    return ob_get_clean();
			},
		], $this );
	}

	/**
	 * Retrieve an item from the special keys list. Some values may be
	 * expensive to retrieve; these values can be stored as callbacks
	 * in the special_keys array, and they're only calculated when called,
	 * after which they're cached for subsequent calls.
	 *
	 * @since 2.4.5
	 */
	public function get_special_key( $key ) {
		if ( ! array_key_exists( $key, $this->special_keys ) ) {
			return false;
		}

		// handle callbacks
		if ( is_callable( $this->special_keys[ $key ] ) ) {
			if ( ! isset( $this->special_keys_cache[ $key ] ) ) {
				$this->special_keys_cache[ $key ] = $this->special_keys[ $key ]();
			}

			return $this->special_keys_cache[ $key ];
		}

		// handle regular values
		return $this->special_keys[ $key ];
	}

	public function editable_by_current_user() {
		return (
			current_user_can( 'edit_others_posts', $this->get_id() ) ||
			absint( $this->get_data( 'post_author' ) ) === absint( get_current_user_id() )
		);
	}

	public static function user_can_edit( $listing_id ) {
		if ( ! ( $listing = self::get( $listing_id ) ) ) {
			return false;
		}

		return $listing->editable_by_current_user();
	}

	/**
	 * Check if listing has been verified. Previous to v2.1.6, this served to
	 * identify claimed listings, but that's no longer necessary. The `_claimed`
	 * meta key is kept to maintain backwards compatibility for versions pre v2.1.6.
	 *
	 * @since 1.6
	 */
	public function is_verified() {
		return (bool) $this->get_data( '_claimed' );
	}

	/**
	 * Determine whether this listing can be claimed by other users.
	 *
	 * @since 2.1.6
	 */
	public function is_claimable() {
		// claims must be enabled
		if ( ! mylisting_get_setting( 'claims_enabled' ) ) {
			return false;
		}

		// author can't claim their own listing
		if ( absint( get_current_user_id() ) === absint( $this->get_author_id() ) ) {
			return false;
		}

		// if user does not have permission to add new listings, claiming should also be disabled
		if ( is_user_logged_in() && ! \MyListing\Src\User_Roles\user_can_add_listings() ) {
			return false;
		}

		// only published listings can be claimed
		if ( $this->get_status() !== 'publish' ) {
			return false;
		}

		// if a listing already has a package, it cannot be claimed, unless the
		// package has been specially configured by the admin to still allow claims.
		$package = $this->get_package();
		if ( $package ) {
			return $package->is_claimable();
		}

		// listing doesn't have a package, it should be claimable
		return true;
	}

	/**
	 * Get list of available listing post statuses.
	 *
	 * @since 2.1
	 */
	public static function get_post_statuses() {
		return [
			'draft'  => _x( 'Draft', 'post status', 'my-listing' ),
			'expired' => _x( 'Expired', 'post status', 'my-listing' ),
			'preview' => _x( 'Preview', 'post status', 'my-listing' ),
			'pending' => _x( 'Pending approval', 'post status', 'my-listing' ),
			'pending_payment' => _x( 'Pending payment', 'post status', 'my-listing' ),
			'publish' => _x( 'Active', 'post status', 'my-listing' ),
		];
	}

	/**
	 * Calculates the expiry date for the given listing.
	 *
	 * @since 2.1
	 */
	public static function calculate_expiry( $listing_id ) {
		// Get duration from the listing if set...
		$duration = get_post_meta( $listing_id, '_job_duration', true );

		// ...otherwise use the global option.
		if ( ! metadata_exists( 'post', $listing_id, '_job_duration' ) ) {
			$duration = absint( mylisting_get_setting( 'submission_default_duration' ) );
		}

		if ( $duration ) {
			return date( 'Y-m-d', strtotime( "+{$duration} days", current_time( 'timestamp' ) ) );
		}

		return '';
	}
}
