////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package org.apache.royale.formatter;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.royale.compiler.clients.problems.CompilerProblemCategorizer;
import org.apache.royale.compiler.clients.problems.ProblemFormatter;
import org.apache.royale.compiler.clients.problems.ProblemPrinter;
import org.apache.royale.compiler.clients.problems.ProblemQuery;
import org.apache.royale.compiler.clients.problems.WorkspaceProblemFormatter;
import org.apache.royale.compiler.common.VersionInfo;
import org.apache.royale.compiler.config.ConfigurationPathResolver;
import org.apache.royale.compiler.exceptions.ConfigurationException;
import org.apache.royale.compiler.filespecs.FileSpecification;
import org.apache.royale.compiler.internal.config.localization.LocalizationManager;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.CompilerProblemSeverity;
import org.apache.royale.compiler.problems.ConfigurationProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.UnexpectedExceptionProblem;
import org.apache.royale.formatter.config.CommandLineConfigurator;
import org.apache.royale.formatter.config.Configuration;
import org.apache.royale.formatter.config.ConfigurationBuffer;
import org.apache.royale.formatter.config.ConfigurationValue;
import org.apache.royale.formatter.config.Configurator;
import org.apache.royale.formatter.config.Semicolons;
import org.apache.royale.utils.FilenameNormalization;

/**
 * Formats .as and .mxml source files.
 */
public class FORMATTER {
	private static final String NEWLINE = System.getProperty("line.separator");
	private static final String DEFAULT_VAR = "files";
	private static final String L10N_CONFIG_PREFIX = "org.apache.royale.compiler.internal.config.configuration";

	static enum ExitCode {
		SUCCESS(0), PRINT_HELP(1), FAILED_WITH_ERRORS(2), FAILED_WITH_EXCEPTIONS(3), FAILED_WITH_CONFIG_PROBLEMS(4);

		ExitCode(int code) {
			this.code = code;
		}

		final int code;

		int getCode() {
			return code;
		}
	}

	/**
	 * Java program entry point.
	 * 
	 * @param args command line arguments
	 */
	public static void main(final String[] args) {
		FORMATTER formatter = new FORMATTER();
		int exitCode = formatter.execute(args);
		System.exit(exitCode);
	}

	public FORMATTER() {

	}

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public int tabSize = 4;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaces = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertFinalNewLine = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean placeOpenBraceOnNewLine = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceAfterSemicolonInForStatements = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceAfterKeywordsInControlFlowStatements = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceAfterFunctionKeywordForAnonymousFunctions = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceBeforeAndAfterBinaryOperators = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceAfterCommaDelimiter = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceBetweenMetadataAttributes = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean insertSpaceAtStartOfLineComment = true;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public int maxPreserveNewLines = 2;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public Semicolons semicolons = Semicolons.INSERT;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean ignoreProblems = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean collapseEmptyBlocks = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean mxmlAlignAttributes = false;

	/**
	 * @deprecated Use an FormatterSettings instead
	 */
	@Deprecated
	public boolean mxmlInsertNewLineBetweenAttributes = false;

	private FormatterSettings settings = null;

	private ProblemQuery problemQuery;
	private List<File> inputFiles = new ArrayList<File>();
	private boolean writeBackToInputFiles = false;
	private boolean listChangedFiles = false;
	private Configuration configuration;
	private ConfigurationBuffer configBuffer;

	public int execute(String[] args) {
		ExitCode exitCode = ExitCode.SUCCESS;
		problemQuery = new ProblemQuery();

		try {
			boolean continueFormatting = configure(args, problemQuery);
			if (continueFormatting) {
				if (inputFiles.size() == 0) {
					StringBuilder builder = new StringBuilder();
					Scanner sysInScanner = new Scanner(System.in);
					try {
						while (sysInScanner.hasNext()) {
							builder.append(sysInScanner.next());
						}
					} finally {
						IOUtils.closeQuietly(sysInScanner);
					}
					String filePath = FilenameNormalization.normalize("stdin.as");
					String fileText = builder.toString();
					String formattedText = formatFileText(filePath, fileText, problemQuery.getProblems());
					if (!fileText.equals(formattedText)) {
						if (listChangedFiles) {
							System.out.println(filePath);
						}
					}
					if (!listChangedFiles) {
						System.out.println(formattedText);
					}
				} else {
					for (File inputFile : inputFiles) {
						String filePath = FilenameNormalization.normalize(inputFile.getAbsolutePath());
						FileSpecification fileSpec = new FileSpecification(filePath);
						String fileText = IOUtils.toString(fileSpec.createReader());
						String formattedText = formatFileText(filePath, fileText, problemQuery.getProblems());
						if (!fileText.equals(formattedText)) {
							if (listChangedFiles) {
								System.out.println(filePath);
							}
							if (writeBackToInputFiles) {
								FileUtils.write(inputFile, formattedText, "utf8");
							}
						}
						if (!listChangedFiles && !writeBackToInputFiles) {
							System.out.println(formattedText);
						}
					}
				}
			} else if (problemQuery.hasFilteredProblems()) {
				exitCode = ExitCode.FAILED_WITH_CONFIG_PROBLEMS;
			} else {
				exitCode = ExitCode.PRINT_HELP;
			}
		} catch (Exception e) {
			problemQuery.add(new UnexpectedExceptionProblem(e));
			System.err.println(e.getMessage());
			exitCode = ExitCode.FAILED_WITH_EXCEPTIONS;
		} finally {
			final CompilerProblemCategorizer categorizer = new CompilerProblemCategorizer();
			ArrayList<ICompilerProblem> filteredProblems = new ArrayList<ICompilerProblem>();
			for (ICompilerProblem problem : problemQuery.getFilteredProblems()) {
        		CompilerProblemSeverity severity = categorizer.getProblemSeverity(problem);
				// filter out everything that isn't an error
				if (!CompilerProblemSeverity.ERROR.equals(severity)) {
					continue;
				}
				filteredProblems.add(problem);
			}
			if (filteredProblems.size() > 0) {
				final Workspace workspace = new Workspace();
				final ProblemFormatter formatter = new WorkspaceProblemFormatter(workspace, categorizer);
				final ProblemPrinter printer = new ProblemPrinter(formatter);
				printer.printProblems(filteredProblems);
			}
		}
		return exitCode.getCode();
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatFile(File file, Collection<ICompilerProblem> problems) throws IOException {
		String filePath = FilenameNormalization.normalize(file.getAbsolutePath());
		FileSpecification fileSpec = new FileSpecification(filePath);
		String fileText = IOUtils.toString(fileSpec.createReader());
		return formatFileText(filePath, fileText, problems);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatFile(File file) throws IOException {
		return formatFile(file, null);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatFileText(String filePath, String text, Collection<ICompilerProblem> problems) {
		if (settings == null) {
			settings = getLegacyFormatterSettings();
		}
		filePath = FilenameNormalization.normalize(filePath);
		String result = null;
		if (filePath.endsWith(".mxml")) {
			result = formatMXMLTokens(filePath, text, problems);
		} else {
			result = formatASTokens(filePath, text, problems);
		}
		if (insertFinalNewLine && result.charAt(result.length() - 1) != '\n') {
			return result + '\n';
		}
		return result;
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatFileText(String filePath, String text) {
		return formatFileText(filePath, text, null);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatActionScriptText(String text, Collection<ICompilerProblem> problems) {
		if (settings == null) {
			settings = getLegacyFormatterSettings();
		}
		String filePath = FilenameNormalization.normalize("stdin.as");
		return formatASTokens(filePath, text, problems);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatActionScriptText(String text) {
		return formatActionScriptText(text, null);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatMXMLText(String text, Collection<ICompilerProblem> problems) {
		if (settings == null) {
			settings = getLegacyFormatterSettings();
		}
		String filePath = FilenameNormalization.normalize("stdin.mxml");
		return formatMXMLTokens(filePath, text, problems);
	}

	/**
	 * @deprecated Use an ASTokenFormatter or MXMLTokenFormatter instead
	 */
	@Deprecated
	public String formatMXMLText(String text) {
		return formatMXMLText(text, null);
	}

	private String formatASTokens(String filePath, String text, Collection<ICompilerProblem> problems) {
		ASTokenFormatter asFormatter = new ASTokenFormatter(settings);
		return asFormatter.format(filePath, text, problems);
	}

	private String formatMXMLTokens(String filePath, String text, Collection<ICompilerProblem> problems) {
		MXMLTokenFormatter mxmlFormatter = new MXMLTokenFormatter(settings);
		return mxmlFormatter.format(filePath, text, problems);
	}

	private FormatterSettings getLegacyFormatterSettings() {
		FormatterSettings result = new FormatterSettings();
		result.tabSize = tabSize;
		result.insertSpaces = insertSpaces;
		result.insertFinalNewLine = insertFinalNewLine;
		result.placeOpenBraceOnNewLine = placeOpenBraceOnNewLine;
		result.insertSpaceAfterSemicolonInForStatements = insertSpaceAfterSemicolonInForStatements;
		result.insertSpaceAfterKeywordsInControlFlowStatements = insertSpaceAfterKeywordsInControlFlowStatements;
		result.insertSpaceAfterFunctionKeywordForAnonymousFunctions = insertSpaceAfterFunctionKeywordForAnonymousFunctions;
		result.insertSpaceBeforeAndAfterBinaryOperators = insertSpaceBeforeAndAfterBinaryOperators;
		result.insertSpaceAfterCommaDelimiter = insertSpaceAfterCommaDelimiter;
		result.insertSpaceBetweenMetadataAttributes = insertSpaceBetweenMetadataAttributes;
		result.insertSpaceAtStartOfLineComment = insertSpaceAtStartOfLineComment;
		result.maxPreserveNewLines = maxPreserveNewLines;
		result.semicolons = semicolons;
		result.ignoreProblems = ignoreProblems;
		result.collapseEmptyBlocks = collapseEmptyBlocks;
		result.mxmlAlignAttributes = mxmlAlignAttributes;
		result.mxmlInsertNewLineBetweenAttributes = mxmlInsertNewLineBetweenAttributes;
		return result;
	}

	/**
	 * Get the start up message that contains the program name with the copyright
	 * notice.
	 * 
	 * @return The startup message.
	 */
	protected String getStartMessage() {
		// This message should not be localized.
		String message = "Apache Royale ActionScript Formatter (asformat)" + NEWLINE + VersionInfo.buildMessage()
				+ NEWLINE;
		return message;
	}

	/**
	 * Get my program name.
	 * 
	 * @return always "asformat".
	 */
	protected String getProgramName() {
		return "asformat";
	}

	/**
	 * Print detailed help information if -help is provided.
	 */
	private void processHelp(final List<ConfigurationValue> helpVar) {
		final Set<String> keywords = new LinkedHashSet<String>();
		if (helpVar != null) {
			for (final ConfigurationValue val : helpVar) {
				for (final Object element : val.getArgs()) {
					String keyword = (String) element;
					while (keyword.startsWith("-"))
						keyword = keyword.substring(1);
					keywords.add(keyword);
				}
			}
		}

		if (keywords.size() == 0)
			keywords.add("help");

		final String usages = CommandLineConfigurator.usage(getProgramName(), DEFAULT_VAR, configBuffer, keywords,
				LocalizationManager.get(), L10N_CONFIG_PREFIX);
		System.out.println(getStartMessage());
		System.out.println(usages);
	}

	private boolean configure(String[] args, ProblemQuery problems) {
		try {
			Configurator configurator = new Configurator();
			ConfigurationPathResolver resolver = new ConfigurationPathResolver(System.getProperty("user.dir")); 
			configurator.setConfigurationPathResolver(resolver);
			configurator.setConfiguration(args, "files");
			configuration = configurator.getConfiguration();
			configBuffer = configurator.getConfigurationBuffer();

			problems.addAll(configurator.getConfigurationProblems());

			if (configBuffer.getVar("version") != null) {
				System.out.println(VersionInfo.buildMessage());
				return false;
			}

			// Print help if "-help" is present.
			final List<ConfigurationValue> helpVar = configBuffer.getVar("help");
			if (helpVar != null || (args.length == 0 && configuration.getFiles().size() == 0)) {
				processHelp(helpVar);
				return false;
			}

			if (problems.hasErrors()) {
				return false;
			}

			settings = FormatterUtils.configurationToFormatterSettings(configuration);
			listChangedFiles = configuration.getListFiles();
			writeBackToInputFiles = configuration.getWriteFiles();
			for (String filePath : configuration.getFiles()) {
				File inputFile = new File(filePath);
				if (!inputFile.exists()) {
					throw new ConfigurationException("Input file does not exist: " + filePath, null, -1);
				}
				if (inputFile.isDirectory()) {
					addDirectory(inputFile);
				} else {
					inputFiles.add(inputFile);
				}
			}
			if (inputFiles.size() == 0 && listChangedFiles) {
				throw new ConfigurationException("Cannot use -list-files with standard input", null, -1);
			}
			if (writeBackToInputFiles) {
				if (inputFiles.size() == 0) {
					throw new ConfigurationException("Cannot use -write-files with standard input", null, -1);
				}
				for (File inputFile : inputFiles) {
					if (!inputFile.canWrite()) {
						throw new ConfigurationException("File is read-only: " + inputFile.getPath(), null, -1);
					}
				}
			}
			return true;
		} catch (ConfigurationException e) {
			final ICompilerProblem problem = new ConfigurationProblem(e);
			problems.add(problem);
			return false;
		} catch (Exception e) {
			final ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, e.getMessage());
			problems.add(problem);
			return false;
		}
	}

	private void addDirectory(File inputFile) {
		for (File file : inputFile.listFiles()) {
			String fileName = file.getName();
			if (fileName.startsWith(".")) {
				continue;
			}
			if (file.isDirectory()) {
				addDirectory(file);
			} else if (fileName.endsWith(".as") || fileName.endsWith(".mxml")) {
				inputFiles.add(file);
			}
		}
	}
}