/*
 * Copyright (C) 2014 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Assert;

import lombok.core.LombokImmutableList;
import lombok.core.configuration.BubblingConfigurationResolver;
import lombok.core.configuration.ConfigurationProblemReporter;
import lombok.core.configuration.ConfigurationResolver;
import lombok.core.configuration.StringConfigurationSource;

public class LombokTestSource {
	private final File file;
	private final String content;
	private final LombokImmutableList<CompilerMessageMatcher> messages;
	private final Map<String, String> formatPreferences;
	private final boolean ignore;
	private final boolean skipCompareContent;
	private final int versionLowerLimit, versionUpperLimit;
	private final ConfigurationResolver configuration;
	private final String specifiedEncoding;

	public boolean versionWithinLimit(int version) {
		return version >= versionLowerLimit && version <= versionUpperLimit;
	}
	
	public File getFile() {
		return file;
	}
	
	public String getContent() {
		return content;
	}
	
	public LombokImmutableList<CompilerMessageMatcher> getMessages() {
		return messages;
	}
	
	public boolean isIgnore() {
		return ignore;
	}
	
	public boolean isSkipCompareContent() {
		return skipCompareContent;
	}
	
	public String getSpecifiedEncoding() {
		return specifiedEncoding;
	}
	
	public ConfigurationResolver getConfiguration() {
		return configuration;
	}
	
	public Map<String, String> getFormatPreferences() {
		return formatPreferences;
	}
	
	private static final Pattern VERSION_STYLE_1 = Pattern.compile("^(\\d+)$");
	private static final Pattern VERSION_STYLE_2 = Pattern.compile("^\\:(\\d+)$");
	private static final Pattern VERSION_STYLE_3 = Pattern.compile("^(\\d+):$");
	private static final Pattern VERSION_STYLE_4 = Pattern.compile("^(\\d+):(\\d+)$");
	
	private int[] parseVersionLimit(String spec) {
		/* Single version: '5' */ {
			Matcher m = VERSION_STYLE_1.matcher(spec);
			if (m.matches()) {
				int v = Integer.parseInt(m.group(1));
				return new int[] {v, v};
			}
		}
		
		/* Upper bound: ':5' (inclusive) */ {
			Matcher m = VERSION_STYLE_2.matcher(spec);
			if (m.matches()) return new int[] {0, Integer.parseInt(m.group(1))};
		}
		
		/* Lower bound '5:' (inclusive) */ {
			Matcher m = VERSION_STYLE_3.matcher(spec);
			if (m.matches()) return new int[] {Integer.parseInt(m.group(1)), Integer.MAX_VALUE};
		}
		
		/* Range '7:8' (inclusive) */ {
			Matcher m = VERSION_STYLE_4.matcher(spec);
			if (m.matches()) return new int[] {Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2))};
		}
		
		return null;
	}
	
	private static final Pattern IGNORE_PATTERN = Pattern.compile("^\\s*ignore\\s*(?:[-:].*)?$", Pattern.CASE_INSENSITIVE);
	private static final Pattern SKIP_COMPARE_CONTENT_PATTERN = Pattern.compile("^\\s*skip[- ]?compare[- ]?content\\s*(?:[-:].*)?$", Pattern.CASE_INSENSITIVE);
	
	private LombokTestSource(File file, String content, List<CompilerMessageMatcher> messages, List<String> directives) {
		this.file = file;
		this.content = content;
		this.messages = messages == null ? LombokImmutableList.<CompilerMessageMatcher>of() : LombokImmutableList.copyOf(messages);
		
		StringBuilder conf = new StringBuilder();
		int versionLower = 0;
		int versionUpper = Integer.MAX_VALUE;
		boolean ignore = false;
		boolean skipCompareContent = false;
		String encoding = null;
		Map<String, String> formats = new HashMap<String, String>();
		
		for (String directive : directives) {
			directive = directive.trim();
			String lc = directive.toLowerCase();
			if (IGNORE_PATTERN.matcher(directive).matches()) {
				ignore = true;
				continue;
			}
			
			if (SKIP_COMPARE_CONTENT_PATTERN.matcher(directive).matches()) {
				skipCompareContent = true;
				continue;
			}
			
			if (lc.startsWith("version ")) {
				int[] limits = parseVersionLimit(lc.substring(7).trim());
				if (limits == null) {
					Assert.fail("Directive line \"" + directive + "\" in '" + file.getAbsolutePath() + "' invalid: version must be followed by a single integer.");
					throw new RuntimeException();
				}
				versionLower = limits[0];
				versionUpper = limits[1];
				continue;
			}
			
			if (lc.startsWith("conf:")) {
				String confLine = directive.substring(5).trim();
				conf.append(confLine).append("\n");
				continue;
			}
			
			if (lc.startsWith("encoding:")) {
				encoding = directive.substring(9).trim();
				continue;
			}
			
			if (lc.startsWith("format:")) {
				String formatLine = directive.substring(7).trim();
				int idx = formatLine.indexOf('=');
				if (idx == -1) throw new IllegalArgumentException("To add a format directive, use: \"//FORMAT: javaLangAsFQN = skip\"");
				String key = formatLine.substring(0, idx).trim();
				String value = formatLine.substring(idx + 1).trim();
				formats.put(key.toLowerCase(), value);
				continue;
			}
			
			if (lc.startsWith("issue ")) continue;
			
			Assert.fail("Directive line \"" + directive + "\" in '" + file.getAbsolutePath() + "' invalid: unrecognized directive.");
			throw new RuntimeException();
		}
		this.specifiedEncoding = encoding;
		this.versionLowerLimit = versionLower;
		this.versionUpperLimit = versionUpper;
		this.ignore = ignore;
		this.skipCompareContent = skipCompareContent;
		ConfigurationProblemReporter reporter = new ConfigurationProblemReporter() {
			@Override public void report(String sourceDescription, String problem, int lineNumber, CharSequence line) {
				Assert.fail("Problem on directive line: " + problem + " at conf line #" + lineNumber + " (" + line + ")");
			}
		};
		
		this.configuration = new BubblingConfigurationResolver(Collections.singleton(StringConfigurationSource.forString(conf, reporter, file.getAbsolutePath())));
		this.formatPreferences = Collections.unmodifiableMap(formats);
	}
	
	public static LombokTestSource readDirectives(File file) throws IOException {
		List<String> directives = new ArrayList<String>();
		
		{
			@Cleanup val rawIn = new FileInputStream(file);
			BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, "UTF-8"));
			for (String i = in.readLine(); i != null; i = in.readLine()) {
				if (i.isEmpty()) continue;
				
				if (i.startsWith("//")) {
					directives.add(i.substring(2));
				} else {
					break;
				}
			}
			in.close();
			rawIn.close();
		}
		
		return new LombokTestSource(file, "", null, directives);
	}
	
	public static LombokTestSource read(File sourceFolder, File messagesFolder, String fileName) throws IOException {
		return read0(sourceFolder, messagesFolder, fileName, "UTF-8");
	}
	
	private static LombokTestSource read0(File sourceFolder, File messagesFolder, String fileName, String encoding) throws IOException {
		StringBuilder content = null;
		List<String> directives = new ArrayList<String>();
		
		File sourceFile = new File(sourceFolder, fileName);
		if (sourceFile.exists()) {
			@Cleanup val rawIn = new FileInputStream(sourceFile);
			BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, encoding));
			for (String i = in.readLine(); i != null; i = in.readLine()) {
				if (content != null) {
					content.append(i).append("\n");
					continue;
				}
				
				if (i.isEmpty()) continue;
				
				if (i.startsWith("//")) {
					directives.add(i.substring(2));
				} else {
					content = new StringBuilder();
					content.append(i).append("\n");
				}
			}
			in.close();
			rawIn.close();
		}
		
		if (content == null) content = new StringBuilder();
		
		List<CompilerMessageMatcher> messages = null;
		if (messagesFolder != null) {
			File messagesFile = new File(messagesFolder, fileName + ".messages");
			try {
				@Cleanup val rawIn = new FileInputStream(messagesFile);
				messages = CompilerMessageMatcher.readAll(rawIn);
				rawIn.close();
			} catch (FileNotFoundException e) {
				messages = null;
			}
		}
		
		LombokTestSource source = new LombokTestSource(sourceFile, content.toString(), messages, directives);
		String specifiedEncoding = source.getSpecifiedEncoding();
		
		// The source file has an 'encoding' header to test encoding issues. Of course, reading the encoding header
		// requires knowing the encoding of the file first. In practice we get away with it, because UTF-8 and US-ASCII are compatible enough.
		// The fix is therefore to read in as UTF-8 initially, and if the file requests that it should be read as another encoding, toss it all
		// and reread that way.
		
		if (specifiedEncoding == null || specifiedEncoding.equalsIgnoreCase(encoding)) return source;
		return read0(sourceFolder, messagesFolder, fileName, specifiedEncoding);
	}
}
