<?php
/**
 * @version     1.4.6
 * @package     sellacious
 *
 * @copyright   Copyright (C) 2012-2017 Bhartiy Web Technologies. All rights reserved.
 * @license     SPL Sellacious Private License; see http://www.sellacious.com/spl.html
 * @author      Izhar Aazmi <info@bhartiy.com> - http://www.bhartiy.com
 */
// no direct access.
defined('_JEXEC') or die;

use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;

/**
 * Sellacious model.
 */
class SellaciousModelProduct extends SellaciousModelAdmin
{
	/**
	 * Method to test whether a record can be deleted.
	 *
	 * @param   object  $record  A record object.
	 *
	 * @return  bool  True if allowed to delete the record. Defaults to the permission for the component.
	 *
	 * @since   12.2
	 */
	protected function canDelete($record)
	{
		if ($this->helper->access->check('product.delete'))
		{
			return true;
		}

		$me       = JFactory::getUser();
		$owned_by = ArrayHelper::getValue((array) $record, 'owned_by');

		return $this->helper->access->check('product.delete.own') && $owned_by == $me->get('id');
	}

	/**
	 * Method to test whether a record can be deleted.
	 *
	 * @param   object  $record  A record object.
	 *
	 * @return  boolean  True if allowed to change the state of the record. Defaults to the permission for the component.
	 *
	 * @since   1.0.0
	 */
	protected function canEditState($record)
	{
		return $this->helper->access->check('product.edit.state');
	}

	/**
	 * Return a product item
	 *
	 * @param   int  $pk  The id of the primary key.
	 *
	 * @return  stdClass
	 *
	 * @since   1.0.0
	 */
	public function getItem($pk = null)
	{
		static $cache;

		$pk = (!empty($pk)) ? $pk : (int) $this->getState('product.id');

		if (empty($cache[$pk]))
		{
			$item = new stdClass;

			$item->id         = $pk;
			$item->basic      = $this->helper->product->getItem($pk);
			$item->categories = $this->helper->product->getCategories($pk);

			$cache[$pk] = $item;
		}

		return $cache[$pk];
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @param   string  $context  The context identifier.
	 * @param   mixed   &$data    The data to be processed. It gets altered directly.
	 * @param   string  $group    The name of the plugin group to import (defaults to "content").
	 *
	 * @return  void
	 *
	 * @since   1.0.0
	 */
	protected function preprocessData($context, &$data, $group = 'content')
	{
		if ($wasArray = is_array($data))
		{
			$data = ArrayHelper::toObject($data);
		}

		$me  = JFactory::getUser();
		$app = JFactory::getApplication();

		// Enforce type if configured so
		$product_type  = $this->helper->config->get('allowed_product_type', 'both');
		$allow_package = $this->helper->config->get('allowed_product_package', 1);

		if ($data->basic->type == 'package' && $allow_package)
		{
			// That's fine if package is allowed and that is selected
		}
		elseif (($product_type == 'physical' || $product_type == 'electronic'))
		{
			// If only a one specific type is allowed enforce it
			$data->basic->type = $product_type;
		}
		elseif (!($data->basic->type == 'physical' || $data->basic->type == 'electronic'))
		{
			// If not a package, and not from of the allowed ones then clear selected
			$data->basic->type = null;
		}

		// Get the attributes pertinent to the specific product type attribution
		if (($data->basic->type == 'physical' && empty($data->physical))
			|| ($data->basic->type == 'electronic' && empty($data->electronic))
			|| ($data->basic->type == 'package' && empty($data->package))
		)
		{
			$data->{$data->basic->type} = $this->helper->product->getAttributesByType($data->id, $data->basic->type);
		}

		// Get the product specifications
		if (!empty($data->id) && empty($data->specifications))
		{
			$data->specifications = $this->helper->field->getValue('products', $data->id);
		}

		// No default selection for Admin, but a must for sellers
		$multi_seller   = $this->helper->config->get('multi_seller', 0);
		$default_seller = $this->helper->config->get('default_seller', 0);

		if ($this->helper->access->checkAny(array('seller', 'pricing', 'shipping'), 'product.edit.'))
		{
			if (!$multi_seller)
			{
				$data->seller_uid = $default_seller;
			}
			elseif (!isset($data->seller_uid))
			{
				$data->seller_uid = $app->getUserState('com_sellacious.edit.product.seller_uid', null);
			}
		}
		elseif ($this->helper->access->checkAny(array('seller.own', 'pricing.own', 'shipping.own'), 'product.edit.'))
		{
			$data->seller_uid = $multi_seller || $default_seller == $me->id ? $me->id : 0;
		}
		else
		{
			$data->seller_uid = 0;
		}

		$app->setUserState('com_sellacious.edit.product.seller_uid', $data->seller_uid);

		if ($data->seller_uid)
		{
			// Seller was just selected or changed?
			if (empty($data->seller))
			{
				// Reset price if seller switched and load the seller specific attributes
				$data->prices = null;
				$data->seller = $this->helper->product->getSellerAttributesByType($data->id, $data->seller_uid, $data->basic->type);
			}

			// Fallback prices and
			if (empty($data->prices))
			{
				$fallback_price = $this->getPrices($data->id, $data->seller_uid, true);
				$product_prices = $this->getPrices($data->id, $data->seller_uid, false);

				$data->prices           = new stdClass;
				$data->prices->fallback = reset($fallback_price);
				$data->prices->product  = $product_prices;

				if ($this->helper->config->get('multi_variant', 0))
				{
					$filter = array(
						'list.select' => 's.*',
						'list.join'   => array(
							array('left', '#__sellacious_variant_sellers AS s ON s.variant_id = a.id'),
						),
						'list.where'  => array(
							'a.product_id = ' . $data->id,
							's.seller_uid = ' . $data->seller_uid,
						),
					);

					$data->prices->variants = $this->helper->variant->loadObjectList($filter);
				}
			}
		}

		if ($wasArray)
		{
			// Temporary workaround to reset data type to original
			$data = ArrayHelper::fromObject($data);
		}

		parent::preprocessData($context, $data, $group);
	}

	/**
	 * Method to allow derived classes to preprocess the form.
	 *
	 * @param   JForm   $form   A JForm object.
	 * @param   mixed   $data   The data expected for the form.
	 * @param   string  $group  The name of the plugin group to import (defaults to "content").
	 *
	 * @return  void
	 *
	 * @see     JFormField
	 * @throws  Exception if there is an error in the form event.
	 * @since   1.0.0
	 */
	protected function preprocessForm(JForm $form, $data, $group = 'sellacious')
	{
		$me       = JFactory::getUser();
		$registry = new Registry($data);
		$isNew    = $registry->get('id', 0) == 0;

		if ($isNew)
		{
			$form->setFieldAttribute('id', 'type', 'hidden');
		}

		$product_type  = $registry->get('basic.type', 'physical');
		$type_allowed  = $this->helper->config->get('allowed_product_type', 'both');
		$allow_package = $this->helper->config->get('allowed_product_package', 1);

		// If only a specific type is allowed, disallow change BUT if package is allowed then we still show list
		if (!$allow_package && ($type_allowed == 'physical' || $type_allowed == 'electronic'))
		{
			$form->setFieldAttribute('type', 'type', 'hidden', 'basic');
		}

		// If allowed then extend with edit form
		$allowCreate = $this->helper->access->check('product.create');
		$owned_by    = $registry->get('basic.owned_by');
		$isOwner     = $owned_by > 0 && $owned_by == $me->get('id');

		$basicEdit = $isNew ? $allowCreate : $this->helper->access->check('product.edit.basic')
			|| ($isOwner && $this->helper->access->check('product.edit.basic.own'));

		$variantEdit = $this->helper->access->check('variant.create')
			|| $this->helper->access->check('variant.edit')
			|| $this->helper->access->check('variant.edit.own');

		$multiVariant = $this->helper->config->get('multi_variant', 0);

		if ($product_type)
		{
			// Disable type change at all after save.
			if (!$isNew)
			{
				$form->setFieldAttribute('type', 'type', 'hidden', 'basic');
				$form->setFieldAttribute('type', 'readonly', 'true', 'basic');
			}

			$product_id = $registry->get('id');
			$seller_uid = $registry->get('seller_uid');
			$categories = $this->helper->category->getParents($registry->get('categories'), true);

			if ($basicEdit)
			{
				$form->loadFile('product/basic');

				$form->setFieldAttribute('attachments', 'record_id', $product_id, 'basic');
				$form->setFieldAttribute('images', 'record_id', $product_id, 'basic');

				if ($product_type == 'physical')
				{
					$form->loadFile('product/physical');

					$form->setFieldAttribute('categories', 'group', 'product/physical');
				}
				elseif ($product_type == 'electronic')
				{
					$form->loadFile('product/electronic');

					$form->setFieldAttribute('categories', 'group', 'product/electronic');
				}
				elseif ($product_type == 'package')
				{
					$form->loadFile('product/package');

					// Allow both type of categories for now
					$form->setFieldAttribute('categories', 'group', 'product/physical;product/electronic');
					$form->setFieldAttribute('products', 'product_id', $registry->get('id'), 'package');
				}

				// Core (+ variant @20150903@) fields defined in all the categories in hierarchy defines the specifications
				$field_ids = $this->getFields($categories, array('core', 'variant'));
				$specs     = $this->getFieldsXML($field_ids, 'specifications', 'specifications');

				$form->load($specs);
			}
			else
			{
				$form->setFieldAttribute('id', 'type', 'hidden');
			}

			// Todo: Variant owned by, and variant x-ref edits e.g. - primary variant
			if (!$isNew && $variantEdit && $multiVariant)
			{
				$form->loadFile('product/variant');

				if ($product_type == 'electronic' && $seller_uid)
				{
					$types = $this->helper->config->get('eproduct_file_type', array());
					$types = empty($types) ? array('image', 'document', 'archive', 'audio', 'video') : (array) $types;

					$form->setFieldAttribute('eproduct', 'filetype', implode(',', $types), 'variant');
					$form->setFieldAttribute('eproduct', 'product_id', $product_id, 'variant');
					$form->setFieldAttribute('eproduct', 'seller_uid', $seller_uid, 'variant');
				}
				else
				{
					$form->removeField('eproduct', 'variant');
				}

				$field_ids = $this->getFields($categories, array('variant'));
				$specs     = $this->getFieldsXML($field_ids, 'variants', 'variant_fields');

				$form->load($specs);
			}

			$form->setFieldAttribute('seller_uid', 'product_id', $product_id);

			$multiSeller = $this->helper->config->get('multi_seller', 0);

			// If Seller cannot be switched, hide the list
			if (!$this->helper->access->checkAny(array('seller', 'pricing', 'shipping'), 'product.edit.') || !$multiSeller)
			{
				$form->setFieldAttribute('seller_uid', 'type', 'hidden');
				$form->setFieldAttribute('seller_uid', 'readonly', 'true');
			}

			if ($seller_uid)
			{
				$sellerEdit = $this->helper->access->check('product.edit.seller') ||
					($this->helper->access->check('product.edit.seller.own') && $seller_uid == $me->id);

				if ($sellerEdit)
				{
					$form->loadFile('product/seller');

					$form->setFieldAttribute('attachments', 'record_id', $registry->get('seller.id'), 'seller');

					if ($product_type == 'physical')
					{
						$form->loadFile('product/seller/physical');

						$allow_return   = $this->helper->config->get('purchase_return', 0);
						$allow_exchange = $this->helper->config->get('purchase_exchange', 0);

						if ($allow_return == 2 || $allow_exchange == 2)
						{
							$form->loadFile('product/returnexchange');

							if ($allow_return != 2)
							{
								$form->removeField('return_days', 'seller');
								$form->removeField('return_tnc', 'seller');
							}

							if ($allow_exchange != 2)
							{
								$form->removeField('exchange_days', 'seller');
								$form->removeField('exchange_tnc', 'seller');
							}
						}
					}
					elseif ($product_type == 'electronic')
					{
						$form->loadFile('product/seller/electronic');

						$types = $this->helper->config->get('eproduct_file_type', array());
						$types = empty($types) ? array('image', 'document', 'archive', 'audio', 'video') : (array) $types;

						$form->setFieldAttribute('eproduct', 'filetype', implode(',', $types), 'seller');
						$form->setFieldAttribute('eproduct', 'product_id', $product_id, 'seller');
						$form->setFieldAttribute('eproduct', 'variant_id', 0, 'seller');
						$form->setFieldAttribute('eproduct', 'seller_uid', $seller_uid, 'seller');
					}
					elseif ($product_type == 'package')
					{
						$form->loadFile('product/seller/package');

						$allow_return   = $this->helper->config->get('purchase_return', 0);
						$allow_exchange = $this->helper->config->get('purchase_exchange', 0);

						if ($allow_return == 2 || $allow_exchange == 2)
						{
							$form->loadFile('product/returnexchange');

							if ($allow_return != 2)
							{
								$form->removeField('return_days', 'seller');
								$form->removeField('return_tnc', 'seller');
							}

							if ($allow_exchange != 2)
							{
								$form->removeField('exchange_days', 'seller');
								$form->removeField('exchange_tnc', 'seller');
							}
						}
					}
				}

				$priceEdit = $this->helper->access->check('product.edit.pricing') ||
					($this->helper->access->check('product.edit.pricing.own') && $seller_uid == $me->id);

				if ($priceEdit)
				{
					$form->loadFile('product/prices');

					$allow = $this->helper->config->get('allowed_price_display', array(0));

					if (!in_array(0, $allow))
					{
						$form->removeField('fallback', 'prices');
						$form->removeField('product', 'prices');
						$form->removeField('variants', 'prices');
					}

					if (!$multiVariant)
					{
						$form->removeField('variants', 'prices');
					}

					$pricing_model = $this->helper->config->get('pricing_model');

					if ($pricing_model == 'basic')
					{
						$form->removeField('product', 'prices');
					}
					elseif ($pricing_model == 'flat')
					{
						$form->removeField('product', 'prices');
						$form->setFieldAttribute('fallback', 'mode', 'flat', 'prices');
					}

					if (!in_array(1, $allow))
					{
						$form->removeField('phone', 'seller');
					}

					if (!in_array(2, $allow))
					{
						$form->removeField('email', 'seller');
					}

					if (!in_array(3, $allow))
					{
						$form->removeField('query_form', 'seller');
					}
				}

				$shippingEdit = $this->helper->access->check('product.edit.shipping') ||
					($this->helper->access->check('product.edit.shipping.own') && $seller_uid == $me->id);

				if ($shippingEdit && $product_type == 'physical')
				{
					$form->loadFile('product/shipping');

					if ($this->helper->config->get('shipped_by') != 'seller')
					{
						$form->removeField('flat_shipping', 'seller');
						$form->removeField('shipping_flat_fee', 'seller');
					}
				}
			}

			if ($this->helper->access->check('product.edit.seo')
				|| (($isNew || $isOwner) && $this->helper->access->check('product.edit.seo.own'))
			)
			{
				$form->loadFile('product/seo');
			}

			if ($this->helper->access->check('product.edit.related')
				|| (($isNew || $isOwner) && $this->helper->access->check('product.edit.related.own'))
			)
			{
				$form->loadFile('product/related');
				$form->setFieldAttribute('groups', 'product_id', $product_id, 'related');
			}
		}
		elseif (!$allowCreate)
		{
			$form->removeField('seller_uid');
			$form->removeField('type', 'basic');
			$form->setFieldAttribute('id', 'type', 'hidden');
		}
		else
		{
			$form->removeField('seller_uid');
		}

		parent::preprocessForm($form, $data, $group);
	}

	/**
	 * Ajax query suggestion list
	 *
	 * @param   string  $context  What item type is searched - product-sku, product or manufacturer
	 * @param   string  $key      Searched key
	 * @param   int[]   $ids      The preselected Ids
	 *
	 * @return  stdClass[]
	 *
	 * @since   1.0.0
	 */
	public function suggest($context, $key, $ids = null)
	{
		$items   = array();
		$filters = array('list.select' => 'a.id, a.title', 'state' => 1);

		if (isset($ids))
		{
			$filters['id'] = $ids;
		}

		if ($key)
		{
			$keyword = $this->_db->q('%' . $this->_db->escape($key) . '%', false);

			$filters['list.where'] = 'a.title LIKE ' . $keyword;
		}

		if ($context == 'product-sku')
		{
			if ($key)
			{
				$keyword = $this->_db->q('%' . $this->_db->escape($key) . '%', false);

				$filters['list.where'] = '(' . 'a.title LIKE ' . $keyword . ' OR a.local_sku LIKE ' . $keyword . ')';
			}

			$items = $this->helper->product->loadObjectList($filters);
		}
		elseif ($context == 'product')
		{
			$items = $this->helper->product->loadObjectList($filters);
		}
		elseif ($context == 'manufacturer')
		{
			$items = $this->helper->manufacturer->loadObjectList($filters);
		}

		return $items;
	}

	/**
	 * Method to search all products/variants from the given filters and search query
	 *
	 * @param   array  $filters  The filters to limit the search result
	 * @param   int    $offset   Search result list offset
	 * @param   int    $limit    Max number of result items to return
	 * @param   bool   &$more    Whether more results are available
	 *
	 * @return  stdClass[]
	 *
	 * @throws  Exception
	 *
	 * @since   1.4.1
	 */
	public function search($filters, $offset = 0, $limit = 0, &$more = false)
	{
		$db    = $this->getDbo();
		$query = $db->getQuery(true);

		$query->select('a.id, a.variant_id, a.title, a.variant_title, a.local_sku, a.variant_sku')
			->from('#__sellacious_products_cache a');

		$seller_uid = ArrayHelper::getValue($filters, 'seller_uid', 0, 'int');
		$keyword    = ArrayHelper::getValue($filters, 'keyword', '', 'string');
		$type       = ArrayHelper::getValue($filters, 'type', '', 'string');

		if ($seller_uid)
		{
			$query->join('inner', '#__sellacious_product_sellers ps ON ps.product_id = a.id')
				->where('ps.seller_uid = ' . (int) $seller_uid);
		}

		if ($keyword)
		{
			$match = $db->q('%' . $db->escape($keyword, true) . '%', false);
			$wh    = array(
				"CONCAT(a.title, ' ', a.variant_title) LIKE " . $match,
				"CONCAT(a.local_sku, ' ', a.variant_sku) LIKE " . $match,
			);
			$query->where('(' . implode(' OR ', $wh). ')');
		}

		try
		{
			$items = array();
			$rows  = $db->setQuery($query, $offset, $limit ? $limit + 1 : 0)->loadObjectList();
			$count = count($rows);
			$more  = $count > $limit;

			foreach ($rows as $i => $item)
			{
				$item->code = $this->helper->product->getCode($item->id, $item->variant_id, $seller_uid);
				$items[]    = $item;

				if ($limit && $limit <= $i + 1)
				{
					break;
				}
			}
		}
		catch (Exception $e)
		{
			throw $e;
		}

		return $items;
	}

	/**
	 * Method to search all products/variants from the given filters and search query
	 *
	 * @param   array  $filters  The filters to limit the search result
	 * @param   int    $offset   Search result list offset
	 * @param   int    $limit    Max number of result items to return
	 * @param   bool   &$more    Whether more results are available
	 *
	 * @return  stdClass[]
	 *
	 * @throws  Exception
	 *
	 * @since   1.4.1
	 */
	public function searchSeller($filters, $offset = 0, $limit = 0, &$more = false)
	{
		$db    = $this->getDbo();
		$query = $db->getQuery(true);

		$p_code     = ArrayHelper::getValue($filters, 'product', '', 'string');
		$keyword    = ArrayHelper::getValue($filters, 'keyword', '', 'string');
		$product_id = 0;

		// Skip if product selected is not valid
		if ($p_code && !$this->helper->product->parseCode($p_code, $product_id, $variant_id))
		{
			return array();
		}

		$query->select('s.user_id, s.title company, s.code, u.name')
			->from('#__sellacious_sellers s')
			->join('inner', '#__sellacious_product_sellers ps ON s.user_id = ps.seller_uid')
			->join('inner', '#__users u ON s.user_id = u.id')
			->where('u.block = 0')
			->group('u.id');

		if ($product_id)
		{
			$query->where('ps.product_id = ' . (int) $product_id);
		}

		if ($keyword)
		{
			$match = $db->q('%' . $db->escape($keyword, true) . '%', false);
			$query->where('(s.title LIKE ' . $match . ' OR u.name LIKE ' . $match . ')');
		}

		try
		{
			$items = array();
			$rows  = $db->setQuery($query, $offset, $limit ? $limit + 1 : 0)->loadObjectList();
			$count = count($items);
			$more  = $count > $limit;

			foreach ($rows as $i => $item)
			{
				$item->title = sprintf('%s (%s)', $item->company ?: $item->name, $item->code);

				$items[] = $item;

				if ($limit && $limit <= $i + 1)
				{
					break;
				}
			}
		}
		catch (Exception $e)
		{
			throw $e;
		}

		return $items;
	}

	/**
	 * Method to get xml form element for the set of custom fields in a category
	 * multiple forms returned from this functions can be merged directly using JForm::load
	 *
	 * @param   int[]   $field_ids  Ids of the fields that should be added to the form
	 * @param   string  $fieldset   Name for the fields group generated
	 * @param   string  $group      Name for the fieldset generated
	 *
	 * @return  SimpleXMLElement  XML for the form thus generated
	 *
	 * @since   1.0.0
	 */
	protected function getFieldsXML($field_ids, $fieldset, $group = null)
	{
		$label = JText::_('COM_SELLACIOUS_PRODUCT_FIELDSET_' . strtoupper($fieldset));
		$form  = $this->helper->field->createFormXml($field_ids, $fieldset, $group, $label);

		return $form;
	}

	/**
	 * Method to validate the form data.
	 *
	 * @param   JForm   $form   The form to validate against.
	 * @param   array   $data   The data to validate.
	 * @param   string  $group  The name of the field group to validate.
	 *
	 * @return  mixed  Array of filtered data if valid, false otherwise.
	 *
	 * @see     JFormRule
	 * @see     JFilterInput
	 * @since   12.2
	 */
	public function validate($form, $data, $group = null)
	{
		// Variants are entered separately;
		$tForm = clone $form;
		$tForm->removeGroup('variant');
		$tForm->removeGroup('variant_fields');

		return parent::validate($tForm, $data, $group);
	}

	/**
	 * Method to save the form data
	 *
	 * @param   array  $data  The form data
	 *
	 * @return  bool
	 *
	 * @since   1.0.0
	 */
	public function save($data)
	{
		$me    = JFactory::getUser();
		$table = $this->getTable();
		$pk    = !empty($data['id']) ? $data['id'] : (int) $this->getState('product.id');
		$isNew = ($pk == 0);

		JPluginHelper::importPlugin('sellacious');

		try
		{
			$registry = new Registry($data);

			// Save the basic info first
			$registry->set('basic.id', $pk);

			if ($isNew)
			{
				$product_id = 0;
				$registry->set('basic.state', 1);

				if (!$this->helper->access->check('product.edit.basic'))
				{
					// Assign ownership if a new product and the creator cannot add/modify shop (global) owned products
					$registry->set('basic.owned_by', $me->id);
				}
			}
			else
			{
				$table->load($pk);
				$product_id = $table->get('id');

				$registry->set('basic.owned_by', $table->get('owned_by'));
			}

			$owned_by  = $registry->get('basic.owned_by');
			$isOwner   = $owned_by > 0 && $owned_by == $me->get('id');
			$basicEdit = $isNew ? $this->helper->access->check('product.create') : $this->helper->access->check('product.edit.basic')
				|| ($isOwner && $this->helper->access->check('product.edit.basic.own'));

			// Add extended product attributes based on type
			$type          = $registry->get('basic.type');
			$type_allowed  = $this->helper->config->get('allowed_product_type', 'both');
			$allow_package = $this->helper->config->get('allowed_product_package', 1);

			$valid_type  = ($type == 'package' && $allow_package)
						|| ($type == 'physical' && ($type_allowed == 'both' || $type_allowed == 'physical'))
						|| ($type == 'electronic' && ($type_allowed == 'both' || $type_allowed == 'electronic'));

			if (!$valid_type)
			{
				throw new Exception(JText::_('COM_SELLACIOUS_PRODUCT_SAVE_INVALID_TYPE'));
			}

			if ($basicEdit)
			{
				if ($basic = $registry->extract('basic'))
				{
					if ($bFeatures = $basic->get('features'))
					{
						$bFeatures = array_values(array_filter((array) $bFeatures, 'trim'));

						$basic->set('features', $bFeatures);
					}

					$table->save($basic->toArray());
				}

				// Get updated record id (for new inserts)
				$product_id = $table->get('id');

				// Update state beforehand
				$this->setState('product.id', $product_id);

				// Assign categories to product
				$this->helper->product->setCategories($product_id, (array) $registry->get('categories'));

				if ($attributes = $registry->extract($type))
				{
					$this->helper->product->setAttributesByType($attributes->toArray(), $product_id, $type);
				}

				// Add extended specifications
				if ($specs = $registry->extract('specifications'))
				{
					$this->helper->product->setSpecifications($product_id, $specs->toArray(), true);
				}
			}

			// Different db table and different approach for variant fields. They are added via ajax already.
			if ($this->helper->access->check('product.edit.related'))
			{
				// Add related product groups
				$this->helper->relatedProduct->setProduct($product_id, explode(',', $registry->get('related.groups')));
			}

			// Add seller specific attributes
			if ($seller_uid = $registry->get('seller_uid'))
			{
				/*
				 * If the listing is free in the settings, auto-renew the listing for next 1 year
				 * We've set up a link in product list view to facilitate this. User needs to click manually now.
				 */
				if ($this->helper->config->get('free_listing'))
				{
					$active = $this->helper->listing->getActive($product_id, $seller_uid, 0);

					// Require renewal in the last 15 days of year for now, this is to avoid renewal on every save action.
					if ($active->state == 0 || (strtotime($active->publish_down) - strtotime('now') <= 15 * 24 * 60 * 60))
					{
						$this->helper->listing->extend($product_id, $seller_uid, 0, 365, true);
					}
				}

				/*
				 * Shipping data is in sellers table itself, don't need to worry about other values as they'll be
				 * filtered out in the **validation** process automatically.
				 */
				$sellerEdit = $this->helper->access->checkAny(array('shipping', 'seller'), 'product.edit.')
					|| ($this->helper->access->checkAny(array('shipping.own', 'seller.own'), 'product.edit.') && $seller_uid == $me->id);

				if ($sellerEdit && ($attributes = $registry->extract('seller')))
				{
					$this->helper->product->setSellerAttributesByType($attributes->toArray(), $product_id, $seller_uid, $type);
				}

				if ($this->helper->access->check('product.edit.pricing')
					|| ($this->helper->access->check('product.edit.pricing.own') && $seller_uid == $me->id))
				{
					// Prices (Overrides and Fallback)
					$fallback = $registry->get('prices.fallback');
					$prices   = $registry->get('prices.product');
					$prices   = is_object($prices) ? ArrayHelper::fromObject($prices) : ($prices ? $prices : array());

					if ($fallback)
					{
						$fallback = ArrayHelper::fromObject($fallback);

						$fallback['is_fallback'] = 1;

						$prices[] = $fallback;
					}

					if (count($prices))
					{
						$prices = $this->setPrices($product_id, $seller_uid, $prices);
					}

					$var_seller_specs = (array) $registry->get('prices.variants');

					foreach ($var_seller_specs as $var_seller_spec)
					{
						if ($variant_id = $var_seller_spec->variant_id)
						{
							$this->helper->variant->setSellerAttributes($variant_id, $seller_uid, (array) $var_seller_spec);
						}
					}
				}
			}
		}
		catch (Exception $e)
		{
			$this->setError($e->getMessage());

			return false;
		}

		return true;
	}

	/**
	 * Method to save the basic/fallback price and stock for the given product-seller
	 *
	 * @param   array  $item  The form data
	 *
	 * @return  bool
	 * @throws  Exception
	 *
	 * @since   1.0.0
	 */
	public function savePriceAndStock($item)
	{
		$me         = JFactory::getUser();
		$product_id = ArrayHelper::getValue($item, 'product_id');
		$seller_uid = ArrayHelper::getValue($item, 'seller_uid');
		$price      = ArrayHelper::getValue($item, 'price');
		$stock      = ArrayHelper::getValue($item, 'stock');

		if (!$this->helper->access->isSubscribed())
		{
			throw new Exception(JText::_('COM_SELLACIOUS_ACCESS_PREMIUM_NOT_ALLOWED'));
		}

		if ($this->helper->access->check('product.edit.pricing') ||
			($this->helper->access->check('product.edit.pricing.own') && $seller_uid == $me->id)
		)
		{
			$table = $this->getTable('ProductPrices');

			$price['product_id']  = $product_id;
			$price['seller_uid']  = $seller_uid;
			$price['margin_type'] = isset($price['margin_type']) ? $price['margin_type'] : 0;
			$price['state']       = 1;
			$price['is_fallback'] = 1;

			$table->save($price);
		}

		if ($this->helper->access->check('product.edit.seller') ||
			($this->helper->access->check('product.edit.seller.own') && $seller_uid == $me->id))
		{
			$table = $this->getTable('ProductSeller');
			$table->load(array('product_id' => $product_id, 'seller_uid' => $seller_uid));

			$table->set('seller_uid', $seller_uid);
			$table->set('product_id', $product_id);
			$table->set('stock', $stock);

			$table->check();
			$table->store();
		}

		return true;
	}

	/**
	 * Method to delete one or more records.
	 *
	 * @param   array &$pks An array of record primary keys.
	 *
	 * @return  boolean  True if successful, false if an error occurs.
	 *
	 * @since   12.2
	 */
	public function delete(&$pks)
	{
		array_walk($pks, 'intval');

		$deleted = parent::delete($pks);

		if ($deleted && count($pks))
		{
			// Direct references to products
			$tables = array(
				'#__sellacious_cart'                => 'product_id',
				'#__sellacious_eproduct_media'      => 'product_id',
				'#__sellacious_package_items'       => 'product_id',
				'#__sellacious_prices_cache'        => 'product_id',
				'#__sellacious_products_cache'      => 'id',
				'#__sellacious_product_categories'  => 'product_id',
				'#__sellacious_product_physical'    => 'product_id',
				'#__sellacious_product_prices'      => 'product_id',
				'#__sellacious_product_queries'     => 'product_id',
				'#__sellacious_product_sellers'     => 'product_id',
				'#__sellacious_ratings'             => 'product_id',
				'#__sellacious_relatedproducts'     => 'product_id',
				'#__sellacious_seller_listing'      => 'product_id',
				'#__sellacious_variants'            => 'product_id',
				'#__sellacious_wishlist'            => 'product_id',
			);

			$query = $this->_db->getQuery(true);
			$pid   = $this->_db->getQuery(true)->select('id')->from('#__sellacious_products');

			$queries = array();

			foreach ($tables as $table => $key)
			{
				$queries[] = (string) $query->clear()->delete($table)->where($this->_db->qn($key) . ' NOT IN (' . $pid . ')');
			}

			// Seller references for products
			$psx = $this->_db->getQuery(true)->select('id')->from('#__sellacious_product_sellers');

			// Variant references
			$variants = $this->_db->getQuery(true)->select('id')->from('#__sellacious_variants');

			$queries[] = (string) $query->clear()->delete('#__sellacious_physical_sellers')->where('psx_id NOT IN (' . $psx . ')');
			$queries[] = (string) $query->clear()->delete('#__sellacious_eproduct_sellers')->where('psx_id NOT IN (' . $psx . ')');
			$queries[] = (string) $query->clear()->delete('#__sellacious_package_sellers')->where('psx_id NOT IN (' . $psx . ')');
			$queries[] = (string) $query->clear()->delete('#__sellacious_variant_sellers')->where('variant_id NOT IN (' . $variants . ')');

			// Execute all queries
			foreach ($queries as $query)
			{
				try
				{
					$this->_db->setQuery($query)->execute();
				}
				catch (Exception $e)
				{
				}
			}
		}

		return $deleted;
	}

	/**
	 * Get list of fields in given categories including hierarchical parents
	 *
	 * @param   int[]  $categories  Ids of the product categories from which the above given field ids were picked up
	 * @param   array  $types       Field group(s) to load. Permitted values are 'core' and 'variant'
	 *
	 * @return  int[]
	 *
	 * @since   1.0.0
	 */
	protected function getFields($categories, $types)
	{
		static $cache = array();

		$key = md5(serialize(func_get_args()));

		if (empty($cache[$key]))
		{
			$fields = $this->helper->category->getFields($categories, $types, true);
			$fields = $this->helper->field->getListWithGroup($fields);

			$cache[$key] = $fields;
		}

		return $cache[$key];
	}

	/**
	 * Get a product item including all prices details from given keys
	 *
	 * @param   int  $product_id
	 * @param   int  $variant_id
	 * @param   int  $seller_uid
	 *
	 * @return  object
	 */
	public function getProduct($product_id, $variant_id, $seller_uid)
	{
		return (object) compact(get_defined_vars());
	}

	/**
	 * Get all price variations for a combination of a product and a seller
	 *
	 * @param   int   $product_id  Product Id in concern
	 * @param   int   $seller_uid  Selected seller
	 * @param   bool  $fallback    Load the fallback price for selected product & seller
	 *
	 * @return  stdClass[]
	 *
	 * @since   1.0.0
	 */
	public function getPrices($product_id, $seller_uid, $fallback = false)
	{
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);
		$table = $this->getTable('ProductPrices');

		$query->select('a.*')
			->from($db->qn($table->getTableName(), 'a'))
			->where('a.product_id = ' . $db->q($product_id))
			->where('a.seller_uid = ' . $db->q($seller_uid))
			->where('a.is_fallback = ' . $db->q($fallback ? 1 : 0))
			->select('GROUP_CONCAT(cat_id) AS cat_id')
			->join('left', $db->qn('#__sellacious_productprices_clientcategory_xref', 'ccx') . ' ON ccx.product_price_id = a.id')
			->group('a.id');

		$db->setQuery($query);

		$prices = $db->loadObjectList();

		if ($prices)
		{
			foreach ($prices as &$price)
			{
				$price->cat_id = json_decode('[' . $price->cat_id . ']');
			}
		}

		return (array) $prices;
	}

	/**
	 * Add price slots for a given product and a seller
	 *
	 * @param   int    $product_id
	 * @param   int    $seller_uid
	 * @param   mixed  $prices
	 *
	 * @return  array  The prices array with updated record ids
	 * @throws  Exception
	 *
	 * @since   1.0.0
	 */
	public function setPrices($product_id, $seller_uid, $prices)
	{
		$return = array();

		$pks   = array();
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);

		$cat_map_remove = array();

		foreach ($prices as $price)
		{
			// Save price info
			$price['product_id']  = $product_id;
			$price['seller_uid']  = $seller_uid;
			$price['margin_type'] = isset($price['margin_type']) ? $price['margin_type'] : 0;
			$price['state']       = 1;

			$table = $this->getTable('ProductPrices');

			$table->save($price);

			$pp_id = $table->get('id');
			$pks[] = $pp_id;

			// Save client category map
			$client_cats = empty($price['cat_id']) ? array() : (array) $price['cat_id'];

			// Set remaining client category mappings for removal
			if (count($client_cats))
			{
				foreach ($client_cats as $cat_id)
				{
					$xref = array('product_price_id' => $pp_id, 'cat_id' => $cat_id);

					$catTable = $this->getTable('ProductPricesClientCategoryXref');
					$catTable->load($xref);
					$catTable->save($xref);
				}

				$cond = sprintf('(product_price_id = %d AND cat_id NOT IN (%s))', $pp_id, implode(', ', $db->q($client_cats)));
			}
			else
			{
				$cond = 'product_price_id = ' . (int) $pp_id;
			}

			$cat_map_remove[] = $cond;

			$price['id'] = $pp_id;

			$return[] = $price;
		}

		// Remove remaining price records, they are marked deleted by the user.
		$query->clear()
			->delete('#__sellacious_product_prices')
			->where('product_id = ' . $db->q($product_id))
			->where('seller_uid = ' . $db->q($seller_uid));

		if (count($pks))
		{
			$query->where('id NOT IN (' . implode(',', $db->q($pks)) . ')');
		}

		$db->setQuery($query);
		$db->execute();

		// Remove expired client category mappings
		$cat_map_remove[] = 'product_price_id NOT IN (SELECT id FROM #__sellacious_product_prices)';

		$catTable = $this->getTable('ProductPricesClientCategoryXref');
		$query->clear()->delete($db->qn($catTable->getTableName()))->where($cat_map_remove, 'OR');

		$db->setQuery($query);
		$db->execute();

		return $return;
	}

	/**
	 * Set selling state for product - seller
	 *
	 * @param   int  $product_id
	 * @param   int  $seller_uid
	 * @param   int  $value
	 *
	 * @return  bool
	 * @throws  Exception
	 *
	 * @since   1.0.0
	 */
	public function setSelling($product_id, $seller_uid, $value)
	{
		if (!$this->helper->product->count(array('id' => $product_id)))
		{
			throw new Exception(JText::_('COM_SELLACIOUS_PRODUCT_NOT_FOUND'));
		}

		$table = $this->getTable('ProductSeller');
		$data  = compact('product_id', 'seller_uid');

		$table->load($data);
		$table->bind($data);
		$table->set('state', $value);

		// If the listing is expiring in a month and is free extend it automatically for a year
		$this->helper->listing->extend($product_id, $seller_uid, 0, 365, true);

		return $table->store();
	}

	/**
	 * Remove the selected e-product media record and its referenced media files. With (optionally) extra check for the product_id.
	 *
	 * @param   int  $id
	 *
	 * @return  bool
	 * @throws  Exception
	 *
	 * @since   1.0.0
	 */
	public function removeEProductMedia($id)
	{
		$app   = JFactory::getApplication();
		$table = $this->getTable('EProductMedia');

		// Since deletion is only allowed when editing a certain product for a seller so session match is optimum for security.
		$product_id = $app->getUserState('com_sellacious.edit.product.id', null);
		$seller_uid = $app->getUserState('com_sellacious.edit.product.seller_uid', null);

		$table->load($id);

		if (!$table->get('id'))
		{
			throw new Exception('Not found');
		}

		if ($table->get('product_id') != $product_id || $table->get('seller_uid') != $seller_uid)
		{
			throw new Exception('Not authorised');
		}

		$filter = array(
			'list.select' => 'a.id',
			'table_name'  => 'eproduct_media',
			'context'     => array('media', 'sample'),
			'record_id'   => $table->get('id'),
		);

		$files = $this->helper->media->loadColumn($filter);

		// Attempt to remove linked files
		$this->helper->media->remove($files);

		return $table->delete();
	}
}
