import com.liferay.portal.kernel.dao.orm.QueryUtil
import com.liferay.portal.kernel.dao.shard.ShardUtil
import com.liferay.portal.kernel.exception.PortalException
import com.liferay.portal.kernel.exception.SystemException
import com.liferay.portal.kernel.log.Log
import com.liferay.portal.kernel.log.LogFactoryUtil
import com.liferay.portal.kernel.util.StringUtil
import com.liferay.portal.kernel.util.Validator
import com.liferay.portal.kernel.workflow.WorkflowConstants
import com.liferay.portal.kernel.xml.Attribute;
import com.liferay.portal.kernel.xml.Document
import com.liferay.portal.kernel.xml.DocumentException
import com.liferay.portal.kernel.xml.Element
import com.liferay.portal.kernel.xml.SAXReaderUtil
import com.liferay.portal.model.Group
import com.liferay.portal.service.GroupLocalServiceUtil
import com.liferay.portal.service.ServiceContext
import com.liferay.portlet.dynamicdatamapping.model.DDMStructure
import com.liferay.portlet.dynamicdatamapping.model.DDMTemplate
import com.liferay.portlet.dynamicdatamapping.service.DDMStructureLocalServiceUtil
import com.liferay.portlet.dynamicdatamapping.service.DDMTemplateLocalServiceUtil
import com.liferay.portlet.journal.NoSuchArticleException
import com.liferay.portlet.journal.NoSuchStructureException
import com.liferay.portlet.journal.model.JournalArticle
import com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil
import com.liferay.util.log4j.Log4JUtil

public class NormalizeDuplicateFields {

	public void doNormalize() throws Exception {
		setLogLevel();

		_log.info(
			"Starting NormalizeDuplicateFields process for DDM Structures " +
				"and Web Contents");

		if (_safeMode) {
			_log.info("Safe Mode Execution");
		}

		String[] shards = ShardUtil.getAvailableShardNames();

		if (shards.length == 0) {
			doNormalizeShard();
		}
		else {
			for (String shardName : ShardUtil.getAvailableShardNames()) {
				_log.info("Processing shard " + shardName);

				ShardUtil.pushCompanyService(shardName);

				try {
					doNormalizeShard();
				}
				finally {
					ShardUtil.popCompanyService();
				}
			}
		}

		_log.info("Finishing NormalizeDuplicateFields process");
	}

	public void doNormalizeShard() throws Exception {
		_ddmStructuresWithDuplicateFields = 0;
		_templatesToModifyManually = 0;
		_webContentsModifiedAutomatically = 0;
		_webContentsOutOfSync = 0;

		normalizeDDMStructures();

		printStatistics();
	}

	protected void normalizeDDMStructures() throws Exception {
		HashMap<Long, List<StructureElement>> structuresElements =
				new HashMap<Long, List<StructureElement>>();

		List<DDMStructure> structures =
				DDMStructureLocalServiceUtil.getStructures();

		_log.info(structures.size() + " DDM Structures to check");

		for (DDMStructure structure : structures) {
			_globalGroup =
				GroupLocalServiceUtil.getCompanyGroup(structure.getCompanyId());

			try {
				if (structuresElements.containsKey(
						structure.getStructureId())) {

					_log.info("Structure with ID " +
						structure.getStructureId() +  " already checked");

					continue;
				}

				normalizeDDMFields(structure, structuresElements);
			}
			catch (Exception e) {
				_log.error("Exception processing DDM Structure ", e);
			}
		}
	}

	protected List<StructureElement> normalizeDDMFields(
			DDMStructure structure,
			HashMap<Long, List<StructureElement>> structuresElements)
		throws Exception {

		_log.info("Checking DDM Structure with structure ID " +
			structure.getStructureId() + ", structure Key " +
			structure.getStructureKey() + " and name " +
			structure.getName("en_US") + " from group ID " +
			structure.getGroupId());

		List<StructureElement> structureElements =
			new ArrayList<StructureElement>();

		DDMStructure parentStructure = getParentStructure(structure);

		if (parentStructure != null) {
			List<StructureElement> parentElements =
				structuresElements.get(parentStructure.getStructureId());

			if (parentElements == null) {
				parentElements = normalizeDDMFields(
					parentStructure, structuresElements);
			}
			else {
				_log.info("Parent structure already checked");
			}

			structureElements.addAll(parentElements);
		}

		String xsd = getNormalizedDDMStructureContent(
			structure, structureElements);

		structuresElements.put(structure.getStructureId(), structureElements);

		if (hasNewNames(structureElements)) {
			_ddmStructuresWithDuplicateFields++;
			_log.info("This structure contains duplicate field names");

			ServiceContext serviceCtx = new ServiceContext();
			Date now = new Date();

			serviceCtx.setModifiedDate(now);

			if (!_safeMode) {
				DDMStructureLocalServiceUtil.updateStructure(
					structure.getStructureId(),
					structure.getParentStructureId(), structure.getNameMap(),
					structure.getDescriptionMap(), xsd, serviceCtx);

				_log.info("Structure modified");
			}

			printTemplatesLog(structure);

			normalizeWebContentsFields(structure, structureElements);
		}

		_log.info("Check for DDM Structure with ID " +
			structure.getStructureId() + " finished");

		return structureElements;
	}

	protected String getNormalizedDDMStructureContent(
			DDMStructure structure,
			List<StructureElement> structureElements)
		throws DocumentException, IOException {

		Document document = SAXReaderUtil.read(structure.getXsd());

		normalizeStructureDocument(
			document.getRootElement(), structureElements,
			_DDMSTRUCTURE_AND_JOURNAL_FIELD_TYPES);

		return document.formattedString();
	}

	protected void normalizeStructureDocument(
		Element element, List<StructureElement> structureElements,
		List<String> availableFieldTypes) {

		List<Element> dynamicElements = element.elements("dynamic-element");

		for (Element dynamicElement : dynamicElements) {
			String elementType = dynamicElement.attributeValue("type");

			if (elementType.equals("option")) {
				//See LPS-47238
				Attribute indexTypeAttribute =
						dynamicElement.attribute("index-type");

				if (indexTypeAttribute != null) {
					dynamicElement.remove(indexTypeAttribute);
				}

				continue;
			}

			if (!availableFieldTypes.contains(elementType)) {
				continue;
			}

			String elementName = dynamicElement.attributeValue("name");

			boolean elementRepeatable =
				Boolean.parseBoolean(
					dynamicElement.attributeValue("repeatable"));

			if (elementRepeatable) {
				_log.debug("Structure field " + elementName + " is repeatable");
			}

			String newElementName = null;

			if (contains(structureElements, elementName)) {
				newElementName =
					generateNewFieldName(structureElements, elementName);

				dynamicElement.attribute("name").setValue(newElementName);

				_log.info("Field " + elementName + " will be " +
					"replaced by " + newElementName);
			}

			StructureElement structureElement = new StructureElement(
				elementName, newElementName, elementRepeatable);

			structureElements.add(structureElement);

			normalizeStructureDocument(
				dynamicElement, structureElements, availableFieldTypes);
		}
	}

	protected String getNormalizedWebContentContent(
			JournalArticle article, List<StructureElement> structureElements)
		throws DocumentException, IOException {

		Document document = SAXReaderUtil.read(article.getContent());

		_pos = 0;

		if (normalizeWebContentDocument(
				document.getRootElement(), structureElements)) {

			return document.formattedString();
		}

		return null;
	}

	protected boolean normalizeWebContentDocument(
		Element rootElement, List<StructureElement> structureElements) {

		StructureElement previousSiblingElement = null;

		List<Element> dynamicElements = rootElement.elements("dynamic-element");

		for (Element dynamicElement : dynamicElements) {
			String elementType = dynamicElement.attributeValue("type");

			if (!_DDMSTRUCTURE_AND_JOURNAL_FIELD_TYPES.contains(elementType)) {
				continue;
			}

			String elementName = dynamicElement.attributeValue("name");

			StructureElement structureElement =
				getNextStructureElement(
					elementName, previousSiblingElement, structureElements);

			if (structureElement == null) {
				_webContentsOutOfSync++;
				_log.info("Expected the end of the web content but found " +
					"other fields. Please modify this web content manually");

				return false;
			}
			else if (!structureElement.getName().equals(elementName)) {
				_webContentsOutOfSync++;
				_log.info("Expected field " +
					structureElement.getName() + " but found " +
					elementName + ". Please modify this web content manually " +
					"since it is currently out of sync with its structure");

				return false;
			}

			if (structureElement.getNewName() != null) {
				dynamicElement.attribute("name").setValue(
					structureElement.getNewName());

				_log.debug("Web content field " + elementName + " will be " +
					"replaced by " + structureElement.getNewName());
			}

			_pos++;

			if (!normalizeWebContentDocument(
				dynamicElement, structureElements)) {

				return false;
			}

			previousSiblingElement = structureElement;
		}

		return true;
	}

	protected void normalizeWebContentsFields(
			DDMStructure structure,
			List<StructureElement> structureElements)
		throws Exception {

		normalizeWebContentsFieldsByGroup(
			structure.getGroupId(), structure.getStructureKey(),
			structureElements);

		if (structure.getGroupId() == _globalGroup.getGroupId()){
			List<Group> groups = GroupLocalServiceUtil.getGroups(
				QueryUtil.ALL_POS, QueryUtil.ALL_POS);

			for (Group group : groups) {
				if (group.getGroupId() == _globalGroup.getGroupId()) {
					continue;
				}

				if (!existsStructureInGroup(
						structure.getStructureKey(), group.getGroupId(),
						structure.getClassNameId())) {

					normalizeWebContentsFieldsByGroup(
						group.getGroupId(), structure.getStructureKey(),
						structureElements);
				}
			}
		}
	}

	protected void normalizeWebContentsFieldsByGroup(
			long groupId, String ddmStructureKey,
			List<StructureElement> structureElements)
		throws Exception {

		int start = 0;
		int end = 1000;

		int articlesCount =
			JournalArticleLocalServiceUtil.getStructureArticlesCount(
				groupId, ddmStructureKey);

		while (start < articlesCount) {
			List<JournalArticle> articles =
				JournalArticleLocalServiceUtil.getStructureArticles(
					groupId, ddmStructureKey, start, end, null);

			for (JournalArticle article : articles) {
				boolean isLatestApprovedVersion = false;

				try {
					isLatestApprovedVersion =
						JournalArticleLocalServiceUtil.isLatestVersion(
							article.getGroupId(),
							article.getArticleId(), article.getVersion(),
							WorkflowConstants.STATUS_APPROVED);
				}
				catch (NoSuchArticleException e) {
				}

				if (isLatestApprovedVersion ||
						JournalArticleLocalServiceUtil.isLatestVersion(
							article.getGroupId(), article.getArticleId(),
							article.getVersion())) {

					_log.info("Normalizing web content with article ID " +
						article.getArticleId() + ", version " +
						article.getVersion() + " and ID " +
						article.getStructureId() + " and group ID "+
						article.getGroupId());

					String xml = getNormalizedWebContentContent(
						article, structureElements);

					if (xml != null) {
						_webContentsModifiedAutomatically++;

						if (!_safeMode) {
							JournalArticleLocalServiceUtil.updateContent(
								article.getGroupId(), article.getArticleId(),
								article.getVersion(), xml);

							_log.info("Web content modified");
						}
					}
				}
			}

			start+=1000;
			end+=1000;
		}
	}

	protected boolean contains(List<StructureElement> elements, String name) {
		for (StructureElement element : elements) {
			if (StringUtil.toLowerCase(element.getName()).
					equals(StringUtil.toLowerCase(name))) {

				return true;
			}
		}

		return false;
	}

	protected boolean containsNewName(
		List<StructureElement> elements, String name) {

		for (StructureElement element : elements) {
			String newNameLowerCase =
				StringUtil.toLowerCase(element.getNewName());

			if ((newNameLowerCase != null) &&
					(newNameLowerCase.equals(StringUtil.toLowerCase(name)))) {

				return true;
			}
		}

		return false;
	}

	protected boolean existsStructureInGroup(String structureKey, long groupId,
			long classNameId)
		throws PortalException, SystemException {

		try {
			DDMStructureLocalServiceUtil.getStructure(
				groupId, classNameId, structureKey);
		}
		catch (NoSuchStructureException e) {
			return false;
		}

		return true;
	}

	protected String generateNewFieldName(
		List<StructureElement> elements, String oldElementName) {

		int pos = 1;
		String newElementName =
			oldElementName + _DUPLICATE_FIELD_SUFFIX + pos;

		while ((containsNewName(elements, newElementName)) ||
			   (contains(elements, newElementName))) {

			pos++;
			newElementName = oldElementName + _DUPLICATE_FIELD_SUFFIX + pos;
		}

		return newElementName;
	}

	protected int getElementPosition(Object element, List elements) {
		for (int pos = 0; pos < elements.size(); pos++){
			if (elements.get(pos) ==  element) {
				return pos;
			}
		}

		return -1;
	}

	protected StructureElement getNextStructureElement(
		String elementName, StructureElement previousSiblingElement,
		List<StructureElement> structureElements) {

		StructureElement structureElement = null;

		try {
			structureElement = structureElements.get(_pos);
		}
		catch (IndexOutOfBoundsException e) {
		}

		if ((previousSiblingElement != null) &&
			previousSiblingElement.isRepeatable() &&
			previousSiblingElement.getName().equals(elementName)) {

			// We go over the same tree since it's a repeated element
			structureElement = previousSiblingElement;

			_pos = getElementPosition(
				 previousSiblingElement, structureElements);

			_log.debug("This element with name " + elementName +
				" is a repeat from previous sibling element");
		}

		return structureElement;
	}

	protected DDMStructure getParentStructure(DDMStructure structure)
		throws PortalException, SystemException {

		long parentStructureId = structure.getParentStructureId();

		DDMStructure parentStructure = null;

		if (Validator.isNotNull(parentStructureId)) {
			parentStructure =
				DDMStructureLocalServiceUtil.fetchStructure(parentStructureId);

			if (parentStructure != null) {
				_log.info(
						"Structure with ID " + structure.getStructureId() +
						" has parent structure with ID " +
						parentStructure.getStructureId());

			}
		}

		return parentStructure;
	}

	protected boolean hasNewNames(List<StructureElement> elements) {
		for (StructureElement element : elements) {
			if (element.getNewName() != null) {
				return true;
			}
		}

		return false;
	}

	protected void printStatistics() {
		_log.info("*** RESULTS ***");

		if (_ddmStructuresWithDuplicateFields == 0) {

			_log.info("*** There are no structures with duplicate fields. You "+
				"can proceed with the upgrade ***");
		}
		else {
			_log.info("*** " + _ddmStructuresWithDuplicateFields +
				" DDM Structures with duplicate fields automatically " +
				"processed ***");

			_log.info("*** " + _templatesToModifyManually + " Templates to " +
				"modify manually ***");

			_log.info("*** " + _webContentsModifiedAutomatically + " Web " +
				"contents automatically processed ***");

			if (_webContentsOutOfSync > 0) {
				_log.info("*** " + _webContentsOutOfSync + " web contents can "+
					"not be processed since they were out of sync prior to " +
					"execute this script ***");
			}

			if (_safeMode) {
				_log.info("The script has been executed in safe mode so no " +
					"modifications have been committed. Set _safeMode to " +
					"false and execute the script again to persist the " +
					"changes.");
			}
		}
	}

	protected void printTemplatesLog(DDMStructure structure)
		throws SystemException, PortalException {

		List<DDMTemplate> templates = new ArrayList<DDMTemplate>();

		List<DDMTemplate> groupTemplates =
			DDMTemplateLocalServiceUtil.getTemplatesByClassPK(
				structure.getGroupId(), structure.getStructureId());

		templates.addAll(groupTemplates);

		if (structure.getGroupId() == _globalGroup.getGroupId()) {
			List<Group> groups = GroupLocalServiceUtil.getGroups(
				QueryUtil.ALL_POS, QueryUtil.ALL_POS);

			for (Group group : groups) {
				if (group.getGroupId() == _globalGroup.getGroupId()) {
					continue;
				}

				if (!existsStructureInGroup(
						structure.getStructureKey(), group.getGroupId(),
						structure.getClassNameId())) {

					groupTemplates =
						DDMTemplateLocalServiceUtil.getTemplatesByClassPK(
							group.getGroupId(), structure.getStructureId());

					templates.addAll(groupTemplates);
				}

			}
		}

		if (templates.size() > 0) {
			_log.info("The following associated templates have to be " +
				"manually modified: ");

			for (DDMTemplate template : templates) {
				_templatesToModifyManually++;

				_log.info("- Template ID " + template.getTemplateId() +
					" from Group ID " + template.getGroupId());
			}
		}
	}

	protected void setLogLevel() {
		if (_allLog) {
			Log4JUtil.setLevel(this.getClass().getName(), "ALL", true);
		}
		else {
			Log4JUtil.setLevel(this.getClass().getName(), "INFO", true);
		}
	}

	public class StructureElement {

		public StructureElement(
			String name, String newName, boolean repeatable) {

			_name = name;
			_newName = newName;
			_repeatable = repeatable;
		}

		public String getNewName() {
			return _newName;
		}
		public void setNewName(String _newName) {
			this._newName = _newName;
		}
		public String getName() {
			return _name;
		}
		public void setName(String _name) {
			this._name = _name;
		}
		public boolean isRepeatable() {
			return _repeatable;
		}
		public void setRepeatable(boolean _repeatable) {
			this._repeatable = _repeatable;
		}

		private String _name = null;
		private String _newName = null;
		private boolean _repeatable = false;
	}

	private static Group _globalGroup = null;
	private static int _pos = 0;
	private static int _ddmStructuresWithDuplicateFields;
	private static int _templatesToModifyManually;
	private static int _webContentsModifiedAutomatically;
	private static int _webContentsOutOfSync;

 	private static final List<String> _DDMSTRUCTURE_AND_JOURNAL_FIELD_TYPES =
 		new ArrayList<String>(Arrays.asList(
			"checkbox", "ddm-date", "ddm-decimal", "ddm-documentlibrary",
			"ddm-fileupload", "ddm-integer", "ddm-link-to-page", "ddm-number",
			"ddm-separator", "ddm-text-html", "radio", "select", "text",
			"textarea", "wcm-image", "boolean", "document_library", "image",
			"link_to_layout", "list", "multi-list", "selection_break", "text",
			"text_area", "text_box"));

	private static final boolean _allLog = false;

	private static final boolean _safeMode = true;

	private static final String _DUPLICATE_FIELD_SUFFIX = "Rev";

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

}

(new NormalizeDuplicateFields()).doNormalize();