/**
 * 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.fragment.internal.renderer;

import com.liferay.fragment.constants.FragmentEntryLinkConstants;
import com.liferay.fragment.contributor.FragmentCollectionContributorTracker;
import com.liferay.fragment.model.FragmentEntry;
import com.liferay.fragment.model.FragmentEntryLink;
import com.liferay.fragment.processor.DefaultFragmentEntryProcessorContext;
import com.liferay.fragment.processor.FragmentEntryProcessorRegistry;
import com.liferay.fragment.processor.PortletRegistry;
import com.liferay.fragment.renderer.FragmentRenderer;
import com.liferay.fragment.renderer.FragmentRendererContext;
import com.liferay.fragment.renderer.constants.FragmentRendererConstants;
import com.liferay.fragment.util.FragmentEntryConfigUtil;
import com.liferay.petra.reflect.ReflectionUtil;
import com.liferay.petra.string.StringBundler;
import com.liferay.petra.string.StringPool;
import com.liferay.petra.string.StringUtil;
import com.liferay.portal.kernel.cache.MultiVMPool;
import com.liferay.portal.kernel.cache.PortalCache;
import com.liferay.portal.kernel.cluster.ClusterInvokeAcceptor;
import com.liferay.portal.kernel.cluster.ClusterInvokeThreadLocal;
import com.liferay.portal.kernel.cluster.ClusterableInvokerUtil;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.io.unsync.UnsyncStringWriter;
import com.liferay.portal.kernel.json.JSONFactoryUtil;
import com.liferay.portal.kernel.json.JSONObject;
import com.liferay.portal.kernel.model.Layout;
import com.liferay.portal.kernel.model.Portlet;
import com.liferay.portal.kernel.module.framework.service.IdentifiableOSGiService;
import com.liferay.portal.kernel.service.PortletLocalService;
import com.liferay.portal.kernel.servlet.taglib.util.OutputData;
import com.liferay.portal.kernel.theme.ThemeDisplay;
import com.liferay.portal.kernel.util.PortalUtil;
import com.liferay.portal.kernel.util.PropsUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.kernel.util.WebKeys;
import com.liferay.segments.util.SegmentsExperiencePortletUtil;
import com.liferay.taglib.servlet.PipingServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

import java.lang.reflect.Method;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

/**
 * @author Jorge Ferrer
 * @author Pablo Molina
 */
@Component(
	property = "fragment.renderer=fragmentEntryFragmentRenderer",
	service = FragmentRenderer.class
)
public class FragmentEntryFragmentRenderer
	implements FragmentRenderer, IdentifiableOSGiService {

	@Override
	public void clearCache(long fragmentEntryLinkId) {
		StringBundler cacheKeySB = new StringBundler(2);

		cacheKeySB.append(fragmentEntryLinkId);
		cacheKeySB.append(StringPool.DASH);

		for (String key : _portalCache.getKeys()) {
			if (key.startsWith(cacheKeySB.toString())) {
				_portalCache.remove(key);
			}
		}

		if (ClusterInvokeThreadLocal.isEnabled()) {
			try {
				ClusterableInvokerUtil.invokeOnCluster(
					ClusterInvokeAcceptor.class, this,
					_clearFragmentEntryLinksCacheMethod,
					new Object[] {fragmentEntryLinkId});
			}
			catch (Throwable t) {
				ReflectionUtil.throwException(t);
			}
		}
	}

	@Override
	public void clearWidgetCache() {
		_widgetPortalCache.removeAll();

		if (ClusterInvokeThreadLocal.isEnabled()) {
			try {
				ClusterableInvokerUtil.invokeOnCluster(
					ClusterInvokeAcceptor.class, this, _clearWidgetCacheMethod,
					new Object[0]);
			}
			catch (Throwable t) {
				ReflectionUtil.throwException(t);
			}
		}
	}

	@Override
	public void clearWidgetCache(long fragmentEntryLinkId) {
		StringBundler cacheKeySB = new StringBundler(2);

		cacheKeySB.append(fragmentEntryLinkId);
		cacheKeySB.append(StringPool.DASH);

		for (String key : _widgetPortalCache.getKeys()) {
			if (key.startsWith(cacheKeySB.toString())) {
				_widgetPortalCache.remove(key);
			}
		}

		if (ClusterInvokeThreadLocal.isEnabled()) {
			try {
				ClusterableInvokerUtil.invokeOnCluster(
					ClusterInvokeAcceptor.class, this,
					_clearFragmentEntryLinksWidgetCacheMethod,
					new Object[] {fragmentEntryLinkId});
			}
			catch (Throwable t) {
				ReflectionUtil.throwException(t);
			}
		}
	}

	@Override
	public String getCollectionKey() {
		return StringPool.BLANK;
	}

	@Override
	public String getConfiguration(
		FragmentRendererContext fragmentRendererContext) {

		FragmentEntryLink fragmentEntryLink =
			fragmentRendererContext.getFragmentEntryLink();

		return fragmentEntryLink.getConfiguration();
	}

	@Override
	public String getKey() {
		return FragmentRendererConstants.FRAGMENT_ENTRY_FRAGMENT_RENDERER_KEY;
	}

	@Override
	public String getOSGiServiceIdentifier() {
		return FragmentEntryFragmentRenderer.class.getName();
	}

	@Override
	public boolean isSelectable(HttpServletRequest httpServletRequest) {
		return false;
	}

	@Override
	public void render(
			FragmentRendererContext fragmentRendererContext,
			HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse)
		throws IOException {

		try {
			PrintWriter printWriter = httpServletResponse.getWriter();

			printWriter.write(
				_renderFragmentEntryLink(
					fragmentRendererContext, httpServletRequest,
					httpServletResponse));
		}
		catch (PortalException portalException) {
			throw new IOException(portalException);
		}
	}

	@Activate
	protected void activate() {
		_portalCache = (PortalCache<String, String>)_multiVMPool.getPortalCache(
			FragmentEntryLink.class.getName() + ".List1");
		_widgetPortalCache =
			(PortalCache<String, String>)_multiVMPool.getPortalCache(
				FragmentEntryLink.class.getName() + ".List2");
	}

	private String _getCacheKey(
		FragmentRendererContext fragmentRendererContext,
		HttpServletRequest httpServletRequest) {

		FragmentEntryLink fragmentEntryLink =
			fragmentRendererContext.getFragmentEntryLink();

		StringBundler cacheKeySB = new StringBundler(7);

		cacheKeySB.append(fragmentEntryLink.getFragmentEntryLinkId());
		cacheKeySB.append(StringPool.DASH);
		cacheKeySB.append(fragmentRendererContext.getLocale());
		cacheKeySB.append(StringPool.DASH);
		cacheKeySB.append(PortalUtil.getHost(httpServletRequest));
		cacheKeySB.append(StringPool.DASH);
		cacheKeySB.append(
			StringUtil.merge(
				fragmentRendererContext.getSegmentsExperienceIds(),
				StringPool.SEMICOLON));

		return cacheKeySB.toString();
	}

	private FragmentEntry _getContributedFragmentEntry(
		FragmentEntryLink fragmentEntryLink) {

		Map<String, FragmentEntry> fragmentCollectionContributorEntries =
			_fragmentCollectionContributorTracker.getFragmentEntries();

		return fragmentCollectionContributorEntries.get(
			fragmentEntryLink.getRendererKey());
	}

	private boolean _isCacheable(FragmentEntryLink fragmentEntryLink) {
		if (fragmentEntryLink.getFragmentEntryLinkId() == 0) {
			return false;
		}

		if (_cacheableFragmentEntries.contains(
				String.valueOf(fragmentEntryLink.getFragmentEntryId()))) {

			return true;
		}

		if (Validator.isNull(fragmentEntryLink.getRendererKey())) {
			return false;
		}

		FragmentEntry fragmentEntry = _getContributedFragmentEntry(
			fragmentEntryLink);

		if (fragmentEntry == null) {
			return false;
		}

		return fragmentEntry.isCacheable();
	}

	private String _renderFragmentEntry(
		long fragmentEntryId, String css, String html, String js,
		String configuration, String namespace, String mode,
		HttpServletRequest httpServletRequest) {

		StringBundler sb = new StringBundler(16);

		sb.append("<div id=\"");

		StringBundler fragmentIdSB = new StringBundler(4);

		fragmentIdSB.append("fragment-");
		fragmentIdSB.append(fragmentEntryId);
		fragmentIdSB.append("-");
		fragmentIdSB.append(namespace);

		sb.append(fragmentIdSB.toString());

		sb.append("\" >");
		sb.append(html);
		sb.append("</div>");

		if (Validator.isNotNull(css)) {
			String outputKey = fragmentEntryId + "_CSS";

			OutputData outputData = (OutputData)httpServletRequest.getAttribute(
				WebKeys.OUTPUT_DATA);

			boolean cssLoaded = false;

			if (outputData != null) {
				Set<String> outputKeys = outputData.getOutputKeys();

				cssLoaded = outputKeys.contains(outputKey);

				StringBundler cssSB = outputData.getDataSB(
					outputKey, StringPool.BLANK);

				if (cssSB != null) {
					cssLoaded = Objects.equals(cssSB.toString(), css);
				}
			}
			else {
				outputData = new OutputData();
			}

			if (!cssLoaded ||
				Objects.equals(mode, FragmentEntryLinkConstants.EDIT)) {

				sb.append("<style>");
				sb.append(css);
				sb.append("</style>");

				outputData.addOutputKey(outputKey);

				outputData.setDataSB(
					outputKey, StringPool.BLANK, new StringBundler(css));

				httpServletRequest.setAttribute(
					WebKeys.OUTPUT_DATA, outputData);
			}
		}

		if (Validator.isNotNull(js)) {
			sb.append("<script>(function() {");
			sb.append("var fragmentElement = document.querySelector('#");
			sb.append(fragmentIdSB.toString());
			sb.append("'); var configuration = ");
			sb.append(configuration);
			sb.append(";");
			sb.append(js);
			sb.append(";}());</script>");
		}

		return sb.toString();
	}

	private String _renderFragmentEntryLink(
			FragmentRendererContext fragmentRendererContext,
			HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse)
		throws PortalException {

		FragmentEntryLink fragmentEntryLink =
			fragmentRendererContext.getFragmentEntryLink();

		String content = StringPool.BLANK;

		Layout draftLayout = (Layout)httpServletRequest.getAttribute(
			"DRAFT_LAYOUT");

		JSONObject jsonObject = JSONFactoryUtil.createJSONObject(
			fragmentEntryLink.getEditableValues());

		String portletId = jsonObject.getString("portletId");

		boolean hasCacheableWidget = false;

		if (Validator.isNotNull(portletId)) {
			Portlet portlet = _portletLocalService.getPortletById(
				SegmentsExperiencePortletUtil.decodePortletName(portletId));

			if (portlet.isLayoutCacheable()) {
				hasCacheableWidget = true;
			}
		}
		else {
			List<String> fragmentEntryLinkPortletIds =
				_portletRegistry.getFragmentEntryLinkPortletIds(
					fragmentEntryLink);

			if (!fragmentEntryLinkPortletIds.isEmpty() &&
				_isCacheable(fragmentEntryLink)) {

				hasCacheableWidget = true;
			}
		}

		String cacheKey = _getCacheKey(
			fragmentRendererContext, httpServletRequest);

		if (!hasCacheableWidget) {
			if (Objects.equals(
					fragmentRendererContext.getMode(),
					FragmentEntryLinkConstants.VIEW) &&
				_isCacheable(fragmentEntryLink) && (draftLayout == null)) {

				content = _portalCache.get(cacheKey);

				if (Validator.isNotNull(content)) {
					return content;
				}
			}
		}
		else {
			if (Objects.equals(
					fragmentRendererContext.getMode(),
					FragmentEntryLinkConstants.VIEW)) {

				ThemeDisplay themeDisplay =
					(ThemeDisplay)httpServletRequest.getAttribute(
						WebKeys.THEME_DISPLAY);

				if ((themeDisplay != null) && !themeDisplay.isSignedIn()) {
					content = _widgetPortalCache.get(cacheKey);
				}

				if (Validator.isNotNull(content)) {
					return content;
				}
			}
		}

		DefaultFragmentEntryProcessorContext
			defaultFragmentEntryProcessorContext =
				new DefaultFragmentEntryProcessorContext(
					httpServletRequest, httpServletResponse,
					fragmentRendererContext.getMode(),
					fragmentRendererContext.getLocale());

		Optional<Map<String, Object>> fieldValuesOptional =
			fragmentRendererContext.getFieldValuesOptional();

		defaultFragmentEntryProcessorContext.setFieldValues(
			fieldValuesOptional.orElse(null));

		defaultFragmentEntryProcessorContext.setPreviewClassPK(
			fragmentRendererContext.getPreviewClassPK());
		defaultFragmentEntryProcessorContext.setPreviewType(
			fragmentRendererContext.getPreviewType());
		defaultFragmentEntryProcessorContext.setSegmentsExperienceIds(
			fragmentRendererContext.getSegmentsExperienceIds());

		String css =
			_fragmentEntryProcessorRegistry.processFragmentEntryLinkCSS(
				fragmentEntryLink, defaultFragmentEntryProcessorContext);

		String html =
			_fragmentEntryProcessorRegistry.processFragmentEntryLinkHTML(
				fragmentEntryLink, defaultFragmentEntryProcessorContext);

		if (Objects.equals(
				defaultFragmentEntryProcessorContext.getMode(),
				FragmentEntryLinkConstants.EDIT)) {

			html = _writePortletPaths(
				fragmentEntryLink, html, httpServletRequest,
				httpServletResponse);
		}

		JSONObject configurationJSONObject =
			FragmentEntryConfigUtil.getConfigurationJSONObject(
				fragmentEntryLink.getConfiguration(),
				fragmentEntryLink.getEditableValues(),
				fragmentRendererContext.getSegmentsExperienceIds());

		content = _renderFragmentEntry(
			fragmentEntryLink.getFragmentEntryId(), css, html,
			fragmentEntryLink.getJs(), configurationJSONObject.toString(),
			fragmentEntryLink.getNamespace(), fragmentRendererContext.getMode(),
			httpServletRequest);

		if (!hasCacheableWidget) {
			if (Objects.equals(
					fragmentRendererContext.getMode(),
					FragmentEntryLinkConstants.VIEW) &&
				_isCacheable(fragmentEntryLink) && (draftLayout == null)) {

				_portalCache.put(cacheKey, content);
			}
		}
		else {
			if (Objects.equals(
					fragmentRendererContext.getMode(),
					FragmentEntryLinkConstants.VIEW)) {

				ThemeDisplay themeDisplay =
					(ThemeDisplay)httpServletRequest.getAttribute(
						WebKeys.THEME_DISPLAY);

				if ((themeDisplay != null) && !themeDisplay.isSignedIn()) {
					_widgetPortalCache.put(cacheKey, content);
				}
			}
		}

		return content;
	}

	private String _writePortletPaths(
			FragmentEntryLink fragmentEntryLink, String html,
			HttpServletRequest httpServletRequest,
			HttpServletResponse httpServletResponse)
		throws PortalException {

		UnsyncStringWriter unsyncStringWriter = new UnsyncStringWriter();

		_portletRegistry.writePortletPaths(
			fragmentEntryLink, httpServletRequest,
			new PipingServletResponse(httpServletResponse, unsyncStringWriter));

		unsyncStringWriter.append(html);

		return unsyncStringWriter.toString();
	}

	private static final Set<String> _cacheableFragmentEntries =
		new HashSet<>();
	private static final Method _clearFragmentEntryLinksCacheMethod;
	private static final Method _clearFragmentEntryLinksWidgetCacheMethod;
	private static final Method _clearWidgetCacheMethod;
	private static PortalCache<String, String> _portalCache;
	private static PortalCache<String, String> _widgetPortalCache;

	static {
		for (String cacheableId :
				PropsUtil.getArray(
					"site.navigation.cacheable.fragment.entry.ids")) {

			_cacheableFragmentEntries.add(cacheableId);
		}

		try {
			_clearFragmentEntryLinksCacheMethod =
				FragmentEntryFragmentRenderer.class.getMethod(
					"clearCache", long.class);
		}
		catch (NoSuchMethodException noSuchMethodException) {
			throw new ExceptionInInitializerError(noSuchMethodException);
		}

		try {
			_clearFragmentEntryLinksWidgetCacheMethod =
				FragmentEntryFragmentRenderer.class.getMethod(
					"clearWidgetCache", long.class);
		}
		catch (NoSuchMethodException noSuchMethodException) {
			throw new ExceptionInInitializerError(noSuchMethodException);
		}

		try {
			_clearWidgetCacheMethod =
				FragmentEntryFragmentRenderer.class.getMethod(
					"clearWidgetCache");
		}
		catch (NoSuchMethodException noSuchMethodException) {
			throw new ExceptionInInitializerError(noSuchMethodException);
		}
	}

	@Reference
	private FragmentCollectionContributorTracker
		_fragmentCollectionContributorTracker;

	@Reference
	private FragmentEntryProcessorRegistry _fragmentEntryProcessorRegistry;

	@Reference
	private MultiVMPool _multiVMPool;

	@Reference
	private PortletLocalService _portletLocalService;

	@Reference
	private PortletRegistry _portletRegistry;

}