/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of the Liferay Enterprise
 * Subscription License ("License"). You may not use this file except in
 * compliance with the License. You can obtain a copy of the License by
 * contacting Liferay, Inc. See the License for the specific language governing
 * permissions and limitations under the License, including but not limited to
 * distribution rights of the Software.
 *
 *
 *
 */

package com.liferay.layout.internal.util;

import com.liferay.asset.kernel.service.AssetCategoryLocalService;
import com.liferay.asset.kernel.service.AssetTagLocalService;
import com.liferay.asset.model.AssetEntryUsage;
import com.liferay.asset.service.AssetEntryUsageLocalService;
import com.liferay.counter.kernel.service.CounterLocalService;
import com.liferay.fragment.model.FragmentEntryLink;
import com.liferay.fragment.service.FragmentEntryLinkLocalService;
import com.liferay.layout.page.template.model.LayoutPageTemplateStructure;
import com.liferay.layout.page.template.service.LayoutPageTemplateEntryLocalService;
import com.liferay.layout.page.template.service.LayoutPageTemplateStructureLocalService;
import com.liferay.layout.seo.model.LayoutSEOEntry;
import com.liferay.layout.seo.service.LayoutSEOEntryLocalService;
import com.liferay.layout.util.LayoutCopyHelper;
import com.liferay.message.boards.service.MBMessageLocalService;
import com.liferay.portal.kernel.comment.CommentManager;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.json.JSONArray;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.json.JSONUtil;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.Image;
import com.liferay.portal.kernel.model.Layout;
import com.liferay.portal.kernel.model.LayoutConstants;
import com.liferay.portal.kernel.model.Portlet;
import com.liferay.portal.kernel.model.PortletPreferences;
import com.liferay.portal.kernel.service.ImageLocalService;
import com.liferay.portal.kernel.service.LayoutLocalService;
import com.liferay.portal.kernel.service.PortletLocalService;
import com.liferay.portal.kernel.service.PortletPreferencesLocalService;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceContextThreadLocal;
import com.liferay.portal.kernel.transaction.Propagation;
import com.liferay.portal.kernel.transaction.TransactionConfig;
import com.liferay.portal.kernel.transaction.TransactionInvokerUtil;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.CopyLayoutThreadLocal;
import com.liferay.portal.kernel.util.Portal;
import com.liferay.portal.kernel.util.PortletKeys;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portlet.exportimport.staging.StagingAdvicesThreadLocal;
import com.liferay.segments.constants.SegmentsExperienceConstants;
import com.liferay.segments.model.SegmentsExperience;
import com.liferay.segments.model.SegmentsExperienceModel;
import com.liferay.segments.service.SegmentsExperienceLocalService;
import com.liferay.sites.kernel.util.Sites;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
 * @author Pavel Savinov
 */
@Component(immediate = true, service = LayoutCopyHelper.class)
public class LayoutCopyHelperImpl implements LayoutCopyHelper {

	@Override
	public Layout copyLayout(Layout sourceLayout, Layout targetLayout)
		throws Exception {

		Callable<Layout> callable = new CopyLayoutCallable(
			sourceLayout, targetLayout);

		boolean copyLayout = CopyLayoutThreadLocal.isCopyLayout();
		boolean stagingAdvicesThreadLocalEnabled =
			StagingAdvicesThreadLocal.isEnabled();

		ServiceContext currentServiceContext =
			ServiceContextThreadLocal.getServiceContext();

		try {
			CopyLayoutThreadLocal.setCopyLayout(true);
			StagingAdvicesThreadLocal.setEnabled(false);

			return TransactionInvokerUtil.invoke(_transactionConfig, callable);
		}
		catch (Throwable t) {
			throw new Exception(t);
		}
		finally {
			CopyLayoutThreadLocal.setCopyLayout(copyLayout);
			StagingAdvicesThreadLocal.setEnabled(
				stagingAdvicesThreadLocalEnabled);

			ServiceContextThreadLocal.pushServiceContext(currentServiceContext);
		}
	}

	private void _copyAssetCategoryIdsAndAssetTagNames(
			Layout sourceLayout, Layout targetLayout)
		throws PortalException {

		if (_isDraft(sourceLayout)) {
			return;
		}

		long[] assetCategoryIds = _assetCategoryLocalService.getCategoryIds(
			Layout.class.getName(), sourceLayout.getPlid());

		String[] assetTagNames = _assetTagLocalService.getTagNames(
			Layout.class.getName(), sourceLayout.getPlid());

		Layout layout = targetLayout;

		if (_isDraft(targetLayout)) {
			layout = _layoutLocalService.getLayout(targetLayout.getClassPK());
		}

		_layoutLocalService.updateAsset(
			layout.getUserId(), layout, assetCategoryIds, assetTagNames);
	}

	private void _copyLayoutPageTemplateStructure(
			Layout sourceLayout, Layout targetLayout)
		throws Exception {

		if (Objects.equals(
				sourceLayout.getType(), LayoutConstants.TYPE_PORTLET)) {

			return;
		}

		long classNameId = _portal.getClassNameId(Layout.class);

		LayoutPageTemplateStructure layoutPageTemplateStructure =
			_layoutPageTemplateStructureLocalService.
				fetchLayoutPageTemplateStructure(
					sourceLayout.getGroupId(), classNameId,
					sourceLayout.getPlid());

		if (layoutPageTemplateStructure == null) {
			LayoutPageTemplateStructure targetLayoutPageTemplateStructure =
				_layoutPageTemplateStructureLocalService.
					fetchLayoutPageTemplateStructure(
						targetLayout.getGroupId(), classNameId,
						targetLayout.getPlid());

			if (targetLayoutPageTemplateStructure != null) {
				_layoutPageTemplateStructureLocalService.
					deleteLayoutPageTemplateStructure(
						targetLayoutPageTemplateStructure);
			}

			_fragmentEntryLinkLocalService.
				deleteLayoutPageTemplateEntryFragmentEntryLinks(
					targetLayout.getGroupId(), classNameId,
					targetLayout.getPlid());

			layoutPageTemplateStructure =
				_layoutPageTemplateStructureLocalService.
					rebuildLayoutPageTemplateStructure(
						sourceLayout.getGroupId(), classNameId,
						sourceLayout.getPlid());
		}

		ServiceContext serviceContext = Optional.ofNullable(
			ServiceContextThreadLocal.getServiceContext()
		).orElse(
			new ServiceContext()
		);

		List<FragmentEntryLink> fragmentEntryLinks =
			_fragmentEntryLinkLocalService.getFragmentEntryLinks(
				sourceLayout.getGroupId(), classNameId, sourceLayout.getPlid());

		Stream<FragmentEntryLink> stream = fragmentEntryLinks.stream();

		Map<Long, FragmentEntryLink> fragmentEntryLinkMap = stream.collect(
			Collectors.toMap(
				FragmentEntryLink::getFragmentEntryLinkId,
				fragmentEntryLink -> fragmentEntryLink));

		_fragmentEntryLinkLocalService.
			deleteLayoutPageTemplateEntryFragmentEntryLinks(
				targetLayout.getGroupId(), classNameId, targetLayout.getPlid());

		LayoutPageTemplateStructure targetLayoutPageTemplateStructure =
			_layoutPageTemplateStructureLocalService.
				fetchLayoutPageTemplateStructure(
					targetLayout.getGroupId(), classNameId,
					targetLayout.getPlid());

		if (targetLayoutPageTemplateStructure == null) {
			_layoutPageTemplateStructureLocalService.
				addLayoutPageTemplateStructure(
					targetLayout.getUserId(), targetLayout.getGroupId(),
					classNameId, targetLayout.getPlid(), null, serviceContext);
		}

		for (Long segmentsExperienceId :
				_getSegmentsExperienceIds(
					sourceLayout.getGroupId(), classNameId,
					sourceLayout.getPlid())) {

			_copyLayoutPageTemplateStructureExperience(
				layoutPageTemplateStructure, segmentsExperienceId, classNameId,
				targetLayout, fragmentEntryLinkMap, serviceContext);
		}

		_assetEntryUsageLocalService.deleteAssetEntryUsagesByPlid(
			targetLayout.getPlid());

		List<AssetEntryUsage> assetEntryUsages =
			_assetEntryUsageLocalService.getAssetEntryUsagesByPlid(
				sourceLayout.getPlid());

		for (AssetEntryUsage assetEntryUsage : assetEntryUsages) {
			_assetEntryUsageLocalService.addAssetEntryUsage(
				assetEntryUsage.getGroupId(), assetEntryUsage.getAssetEntryId(),
				assetEntryUsage.getContainerType(),
				assetEntryUsage.getContainerKey(), targetLayout.getPlid(),
				serviceContext);
		}
	}

	private void _copyLayoutPageTemplateStructureExperience(
			LayoutPageTemplateStructure layoutPageTemplateStructure,
			long segmentsExperienceId, long classNameId, Layout targetLayout,
			Map<Long, FragmentEntryLink> fragmentEntryLinkMap,
			ServiceContext serviceContext)
		throws Exception {

		String data = layoutPageTemplateStructure.getData(segmentsExperienceId);

		if (Validator.isNull(data)) {
			return;
		}

		JSONObject dataJSONObject = JSONFactoryUtil.createJSONObject(data);

		JSONArray structureJSONArray = dataJSONObject.getJSONArray("structure");

		if (structureJSONArray == null) {
			return;
		}

		Map<String, String> fragmentEntryLinkIdsMap = new HashMap<>();

		for (int i = 0; i < structureJSONArray.length(); i++) {
			JSONObject rowJSONObject = structureJSONArray.getJSONObject(i);

			JSONArray columnsJSONArray = rowJSONObject.getJSONArray("columns");

			for (int j = 0; j < columnsJSONArray.length(); j++) {
				JSONObject columnJSONObject = columnsJSONArray.getJSONObject(j);

				JSONArray fragmentEntryLinkIdsJSONArray =
					columnJSONObject.getJSONArray("fragmentEntryLinkIds");

				JSONArray newFragmentEntryLinkIdsJSONArray =
					JSONFactoryUtil.createJSONArray();

				for (int k = 0; k < fragmentEntryLinkIdsJSONArray.length();
					 k++) {

					FragmentEntryLink fragmentEntryLink =
						fragmentEntryLinkMap.get(
							fragmentEntryLinkIdsJSONArray.getLong(k));

					if (fragmentEntryLink == null) {
						continue;
					}

					FragmentEntryLink newFragmentEntryLink =
						(FragmentEntryLink)fragmentEntryLink.clone();

					newFragmentEntryLink.setUuid(serviceContext.getUuid());
					newFragmentEntryLink.setFragmentEntryLinkId(
						_counterLocalService.increment());
					newFragmentEntryLink.setUserId(targetLayout.getUserId());
					newFragmentEntryLink.setUserName(
						targetLayout.getUserName());
					newFragmentEntryLink.setCreateDate(
						serviceContext.getCreateDate(new Date()));
					newFragmentEntryLink.setModifiedDate(
						serviceContext.getModifiedDate(new Date()));
					newFragmentEntryLink.setOriginalFragmentEntryLinkId(0);
					newFragmentEntryLink.setClassNameId(classNameId);
					newFragmentEntryLink.setClassPK(targetLayout.getPlid());
					newFragmentEntryLink.setLastPropagationDate(
						serviceContext.getCreateDate(new Date()));

					newFragmentEntryLink =
						_fragmentEntryLinkLocalService.addFragmentEntryLink(
							newFragmentEntryLink);

					fragmentEntryLinkIdsMap.put(
						String.valueOf(
							fragmentEntryLink.getFragmentEntryLinkId()),
						String.valueOf(
							newFragmentEntryLink.getFragmentEntryLinkId()));

					newFragmentEntryLinkIdsJSONArray.put(
						newFragmentEntryLink.getFragmentEntryLinkId());

					_commentManager.copyDiscussion(
						targetLayout.getUserId(), targetLayout.getGroupId(),
						FragmentEntryLink.class.getName(),
						fragmentEntryLink.getFragmentEntryLinkId(),
						newFragmentEntryLink.getFragmentEntryLinkId(),
						className -> serviceContext);
				}

				columnJSONObject.put(
					"fragmentEntryLinkIds", newFragmentEntryLinkIdsJSONArray);
			}
		}

		JSONArray newNonIndexableFragmentEntryLinkIdsJSONArray =
			JSONFactoryUtil.createJSONArray();

		JSONArray nonIndexableFragmentEntryLinkIdsJSONArray =
			dataJSONObject.getJSONArray("nonIndexableFragmentEntryLinkIds");

		List<String> nonIndexableFragmentEntryLinkIds = JSONUtil.toStringList(
			nonIndexableFragmentEntryLinkIdsJSONArray);

		for (String fragmentEntryLinkId : nonIndexableFragmentEntryLinkIds) {
			String id = fragmentEntryLinkIdsMap.get(fragmentEntryLinkId);

			if (Validator.isNotNull(id)) {
				newNonIndexableFragmentEntryLinkIdsJSONArray.put(id);
			}
		}

		dataJSONObject.put(
			"nonIndexableFragmentEntryLinkIds",
			newNonIndexableFragmentEntryLinkIdsJSONArray);

		_layoutPageTemplateStructureLocalService.
			updateLayoutPageTemplateStructure(
				targetLayout.getGroupId(), classNameId, targetLayout.getPlid(),
				segmentsExperienceId, dataJSONObject.toString());
	}

	private void _copyPortletPreferences(
			Layout sourceLayout, Layout targetLayout)
		throws Exception {

		List<PortletPreferences> portletPreferencesList =
			_portletPreferencesLocalService.getPortletPreferences(
				PortletKeys.PREFS_OWNER_ID_DEFAULT,
				PortletKeys.PREFS_OWNER_TYPE_LAYOUT, sourceLayout.getPlid());

		List<PortletPreferences> targetPortletPreferencesList =
			_portletPreferencesLocalService.getPortletPreferences(
				PortletKeys.PREFS_OWNER_ID_DEFAULT,
				PortletKeys.PREFS_OWNER_TYPE_LAYOUT, targetLayout.getPlid());

		Stream<PortletPreferences> targetPortletPreferencesStream =
			targetPortletPreferencesList.stream();

		List<String> targetPortletIds = targetPortletPreferencesStream.map(
			PortletPreferences::getPortletId
		).collect(
			Collectors.toList()
		);

		for (PortletPreferences portletPreferences : portletPreferencesList) {
			Portlet portlet = _portletLocalService.getPortletById(
				portletPreferences.getPortletId());

			if ((portlet == null) || portlet.isUndeployedPortlet()) {
				continue;
			}

			targetPortletIds.remove(portletPreferences.getPortletId());

			PortletPreferences targetPortletPreferences =
				_portletPreferencesLocalService.fetchPortletPreferences(
					PortletKeys.PREFS_OWNER_ID_DEFAULT,
					PortletKeys.PREFS_OWNER_TYPE_LAYOUT, targetLayout.getPlid(),
					portletPreferences.getPortletId());

			if (targetPortletPreferences != null) {
				_portletPreferencesLocalService.updatePreferences(
					targetPortletPreferences.getOwnerId(),
					targetPortletPreferences.getOwnerType(),
					targetPortletPreferences.getPlid(),
					targetPortletPreferences.getPortletId(),
					portletPreferences.getPreferences());
			}
			else {
				_portletPreferencesLocalService.addPortletPreferences(
					targetLayout.getCompanyId(),
					PortletKeys.PREFS_OWNER_ID_DEFAULT,
					PortletKeys.PREFS_OWNER_TYPE_LAYOUT, targetLayout.getPlid(),
					portletPreferences.getPortletId(),
					_portletLocalService.getPortletById(
						portletPreferences.getPortletId()),
					portletPreferences.getPreferences());
			}
		}

		for (String portletId : targetPortletIds) {
			try {
				_portletPreferencesLocalService.deletePortletPreferences(
					PortletKeys.PREFS_OWNER_ID_DEFAULT,
					PortletKeys.PREFS_OWNER_TYPE_LAYOUT, targetLayout.getPlid(),
					portletId);
			}
			catch (Exception exception) {
				if (_log.isDebugEnabled()) {
					_log.debug(
						"Unable to delete portlet preferences for portlet " +
							portletId,
						exception);
				}
			}
		}
	}

	private long[] _getSegmentsExperienceIds(
		long groupId, long classNameId, long classPK) {

		List<SegmentsExperience> segmentsExperiences =
			_segmentsExperienceLocalService.getSegmentsExperiences(
				groupId, classNameId, classPK);

		Stream<SegmentsExperience> stream = segmentsExperiences.stream();

		return ArrayUtil.append(
			stream.mapToLong(
				SegmentsExperienceModel::getSegmentsExperienceId
			).toArray(),
			new long[] {SegmentsExperienceConstants.ID_DEFAULT});
	}

	private boolean _isDraft(Layout layout) {
		if (layout.getClassPK() <= 0) {
			return false;
		}

		if (layout.getClassNameId() != _portal.getClassNameId(
				Layout.class.getName())) {

			return false;
		}

		return true;
	}

	private static final Log _log = LogFactoryUtil.getLog(
		LayoutCopyHelperImpl.class);

	private static final TransactionConfig _transactionConfig =
		TransactionConfig.Factory.create(
			Propagation.REQUIRED, new Class<?>[] {Exception.class});

	@Reference
	private AssetCategoryLocalService _assetCategoryLocalService;

	@Reference
	private AssetEntryUsageLocalService _assetEntryUsageLocalService;

	@Reference
	private AssetTagLocalService _assetTagLocalService;

	@Reference
	private CommentManager _commentManager;

	@Reference
	private CounterLocalService _counterLocalService;

	@Reference
	private FragmentEntryLinkLocalService _fragmentEntryLinkLocalService;

	@Reference
	private ImageLocalService _imageLocalService;

	@Reference
	private LayoutLocalService _layoutLocalService;

	@Reference
	private LayoutPageTemplateEntryLocalService
		_layoutPageTemplateEntryLocalService;

	@Reference
	private LayoutPageTemplateStructureLocalService
		_layoutPageTemplateStructureLocalService;

	@Reference
	private LayoutSEOEntryLocalService _layoutSEOEntryLocalService;

	@Reference
	private MBMessageLocalService _mbMessageLocalService;

	@Reference
	private Portal _portal;

	@Reference
	private PortletLocalService _portletLocalService;

	@Reference
	private PortletPreferencesLocalService _portletPreferencesLocalService;

	@Reference
	private SegmentsExperienceLocalService _segmentsExperienceLocalService;

	@Reference
	private Sites _sites;

	private class CopyLayoutCallable implements Callable<Layout> {

		@Override
		public Layout call() throws Exception {
			_sites.copyExpandoBridgeAttributes(_sourceLayout, _targetLayout);
			_sites.copyLookAndFeel(_targetLayout, _sourceLayout);
			_sites.copyPortletSetups(_sourceLayout, _targetLayout);
			_sites.copyPortletPermissions(_targetLayout, _sourceLayout);

			_copyAssetCategoryIdsAndAssetTagNames(_sourceLayout, _targetLayout);

			_copyLayoutPageTemplateStructure(_sourceLayout, _targetLayout);

			_copyPortletPreferences(_sourceLayout, _targetLayout);

			Image image = _imageLocalService.getImage(
				_sourceLayout.getIconImageId());

			byte[] imageBytes = null;

			if (image != null) {
				imageBytes = image.getTextObj();
			}

			ServiceContext serviceContext =
				ServiceContextThreadLocal.getServiceContext();

			if (serviceContext == null) {
				serviceContext = new ServiceContext();
			}

			serviceContext.setAttribute(
				"layout.instanceable.allowed", Boolean.TRUE);

			_layoutLocalService.updateLayout(
				_targetLayout.getGroupId(), _targetLayout.isPrivateLayout(),
				_targetLayout.getLayoutId(), _sourceLayout.getTypeSettings());

			LayoutSEOEntry layoutSEOEntry =
				_layoutSEOEntryLocalService.fetchLayoutSEOEntry(
					_sourceLayout.getGroupId(), _sourceLayout.isPrivateLayout(),
					_sourceLayout.getLayoutId());

			if (layoutSEOEntry == null) {
				LayoutSEOEntry targetLayoutSEOEntry =
					_layoutSEOEntryLocalService.fetchLayoutSEOEntry(
						_targetLayout.getGroupId(),
						_targetLayout.isPrivateLayout(),
						_targetLayout.getLayoutId());

				if (targetLayoutSEOEntry != null) {
					_layoutSEOEntryLocalService.deleteLayoutSEOEntry(
						_targetLayout.getGroupId(),
						_targetLayout.isPrivateLayout(),
						_targetLayout.getLayoutId());
				}
			}
			else {
				_layoutSEOEntryLocalService.updateLayoutSEOEntry(
					_targetLayout.getUserId(), _targetLayout.getGroupId(),
					_targetLayout.isPrivateLayout(),
					_targetLayout.getLayoutId(), layoutSEOEntry.isEnabled(),
					layoutSEOEntry.getCanonicalURLMap(), serviceContext);
			}

			return _layoutLocalService.updateIconImage(
				_targetLayout.getPlid(), imageBytes);
		}

		private CopyLayoutCallable(Layout sourceLayout, Layout targetLayout) {
			_sourceLayout = sourceLayout;
			_targetLayout = targetLayout;
		}

		private final Layout _sourceLayout;
		private final Layout _targetLayout;

	}

}