/*******************************************************************************
 * Copyright (c) 2008, 2012 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.wizards.settingswizards;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICFolderDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Custom behavior for the Import wizard.
 *
 * @author Mike Kucera
 * @since 5.1
 *
 */
public class ProjectSettingsImportStrategy implements IProjectSettingsWizardPageStrategy {

	@Override
	public String getMessage(MessageType type) {
		switch (type) {
		case TITLE:
			return Messages.ProjectSettingsWizardPage_Import_title;
		case MESSAGE:
			return Messages.ProjectSettingsWizardPage_Import_message;
		case CHECKBOX:
			return Messages.ProjectSettingsWizardPage_Import_checkBox;
		case FILE:
			return Messages.ProjectSettingsWizardPage_Import_file;
		case SETTINGS:
			return Messages.ProjectSettingsWizardPage_Import_selectSettings;
		default:
			return null;
		}
	}

	/*
	 * Start with an empty list of processors.
	 */
	@Override
	public void pageCreated(IProjectSettingsWizardPage page) {
		page.setDisplayedSettingsProcessors(Collections.<ISettingsProcessor>emptyList());
	}

	/*
	 * Collects the importers that can be applied to the file and displays
	 * them to the user.
	 */
	@Override
	public void fileSelected(IProjectSettingsWizardPage page) {
		List<ImporterSectionPair> pairs = Collections.emptyList();
		try {
			pairs = extractSectionsFromFile(page);
			page.setMessage(getMessage(MessageType.MESSAGE), IMessageProvider.NONE); // its all good
		} catch (FileNotFoundException e) {
			page.setMessage(Messages.ProjectSettingsWizardPage_Import_openError, IMessageProvider.ERROR);
		} catch (SettingsImportExportException e) {
			page.setMessage(Messages.ProjectSettingsWizardPage_Import_parseError, IMessageProvider.ERROR);
		}

		List<ISettingsProcessor> importersToDisplay = new ArrayList<>();
		for (ImporterSectionPair pair : pairs) {
			importersToDisplay.add(pair.importer);
		}

		// if there was an error then importersToDisplay will be empty and this will clear the list
		page.setDisplayedSettingsProcessors(importersToDisplay);
	}

	/*
	 * Parse the file again and this time actually do the import.
	 */
	@Override
	public boolean finish(IProjectSettingsWizardPage page) {
		// get the selected project and configuration
		ICConfigurationDescription config = page.getSelectedConfiguration();
		IProject project = config.getProjectDescription().getProject();

		// get a writable copy of the project description so that we can make changes to it
		ICProjectDescription writableDescription = CoreModel.getDefault().getProjectDescription(project, true);
		ICConfigurationDescription writableConfig = writableDescription.getConfigurationById(config.getId());
		ICFolderDescription writableProjectRoot = writableConfig.getRootFolderDescription();

		List<ISettingsProcessor> selectedImporters = page.getSelectedSettingsProcessors();

		try {
			List<ImporterSectionPair> pairs = extractSectionsFromFile(page);

			for (ImporterSectionPair pair : pairs) {
				if (selectedImporters.contains(pair.importer))
					pair.importer.readSectionXML(writableProjectRoot, pair.section);
			}

		} catch (FileNotFoundException e) {
			CUIPlugin.log(e);
			page.showErrorDialog(Messages.ProjectSettingsImportStrategy_fileOpenError,
					Messages.ProjectSettingsImportStrategy_couldNotOpen);
			return false;
		} catch (SettingsImportExportException e) { // error during parsing or importing
			CUIPlugin.log(e);
			page.showErrorDialog(Messages.ProjectSettingsImportStrategy_importError,
					Messages.ProjectSettingsImportStrategy_couldNotImport);
			return false;
		}

		// only if all the importing was successful do we actually write out to the .cproject file
		try {
			CoreModel.getDefault().setProjectDescription(project, writableDescription);
		} catch (CoreException e) {
			CUIPlugin.log(e);
			page.showErrorDialog(Messages.ProjectSettingsImportStrategy_importError,
					Messages.ProjectSettingsImportStrategy_saveError);
			return false;
		}

		return true;
	}

	private static class ImporterSectionPair {
		Element section;
		ISettingsProcessor importer;

		ImporterSectionPair(ISettingsProcessor importer, Element section) {
			this.importer = importer;
			this.section = section;
		}
	}

	/*
	 * Attempts to parse the file the user selected.
	 */
	private List<ImporterSectionPair> extractSectionsFromFile(IProjectSettingsWizardPage page)
			throws FileNotFoundException, SettingsImportExportException {
		// get the file path that the user input
		String filePath = page.getDestinationFilePath();

		// get all the importers
		Map<String, ISettingsProcessor> importers = new HashMap<>();
		for (ISettingsProcessor processor : page.getSettingsProcessors()) {
			importers.put(processor.getSectionName(), processor);
		}

		FileInputStream in = new FileInputStream(filePath); // throws FileNotFoundException

		// try to parse the file as generic XML with no schema
		Document document = parse(in);

		// now try to get a list of <section> elements
		Element root = document.getDocumentElement();
		List<Element> sections = XMLUtils.extractChildElements(root, ProjectSettingsExportStrategy.SECTION_ELEMENT);

		List<ImporterSectionPair> pairs = new ArrayList<>();

		// associate an importer with each section
		for (Element section : sections) {
			String sectionName = section.getAttribute(ProjectSettingsExportStrategy.SECTION_NAME_ATTRIBUTE);
			if (sectionName != null) {
				ISettingsProcessor importer = importers.get(sectionName);

				// if there is an importer available for the section then delegate to it
				if (importer != null)
					pairs.add(new ImporterSectionPair(importer, section));
			}
		}

		return pairs;
	}

	/**
	 * An error handler that aborts the XML parse at the first sign
	 * of any kind of problem.
	 */
	private static ErrorHandler ABORTING_ERROR_HANDER = new ErrorHandler() {
		@Override
		public void error(SAXParseException e) throws SAXException {
			throw e;
		}

		@Override
		public void fatalError(SAXParseException e) throws SAXException {
			throw e;
		}

		@Override
		public void warning(SAXParseException e) throws SAXException {
			throw e;
		}
	};

	/*
	 * Uses JAXP to parse the file. Returns null if the file could
	 * not be parsed as XML.
	 *
	 * Not validating because I want to make it easy to add new settings processors.
	 * Eventually there could be an extension point for adding settings processors
	 * so I'm coding everything with the assumption that each settings processor
	 * will do its own validation programatically.
	 */
	private static Document parse(InputStream in) throws SettingsImportExportException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(false);
		factory.setNamespaceAware(false);
		factory.setIgnoringComments(true);

		try {
			DocumentBuilder parser = factory.newDocumentBuilder();
			parser.setErrorHandler(ABORTING_ERROR_HANDER); // causes SAXException to be thrown on any parse error
			InputSource input = new InputSource(in); // TODO should I be using an InputSource?
			Document doc = parser.parse(input);
			return doc;

		} catch (Exception e) {
			throw new SettingsImportExportException(e);
		}
	}

}
