/**
 * Copyright 2009 - 2014 Jörgen Lundgren
 * 
 * This file is part of org.macroing.diagnostics.
 * 
 * org.macroing.diagnostics is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * org.macroing.diagnostics is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with org.macroing.diagnostics. If not, see <http://www.gnu.org/licenses/>.
 */
package org.macroing.diagnostics;

import static org.macroing.common.lang.Throwables.doAssertObjectIsNotNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * A class for registering {@code Diagnosable}s.
 * 
 * @since 1.0.0
 * @author Jörgen Lundgren
 */
public final class DiagnosticsManager {
	private static final DiagnosticsManager INSTANCE = new DiagnosticsManager();
	private final List<Diagnosable> diagnosables = new ArrayList<>();
	
	private DiagnosticsManager() {
		
	}
	
	/**
	 * Returns a {@code Set} with all currently added {@code Diagnosable}s. If the returned {@code Set} is modified, it will not affect this
	 * {@code DiagnosticsManager} instance.
	 * 
	 * @return a {@code Set} with all currently added {@code Diagnosable}s
	 */
	public Set<Diagnosable> getDiagnosables() {
		return doGetDiagnosables();
	}
	
	/**
	 * Adds a {@code Diagnosable} to this {@code DiagnosticsManager} instance. If {@code diagnosable} is {@code null}, a
	 * {@code NullPointerException} will be thrown.
	 * 
	 * @param diagnosable the {@code Diagnosable} instance to add
	 * @throws NullPointerException thrown if, and only if, {@code diagnosable} is {@code null}
	 */
	public void addDiagnosable(Diagnosable diagnosable) throws NullPointerException {
		doAssertObjectIsNotNull(diagnosable, "diagnosable");
		doAddDiagnosable(diagnosable);
	}

	/**
	 * Removes a {@code Diagnosable} from this {@code DiagnosticsManager} instance. If {@code diagnosable} is {@code null}, a
	 * {@code NullPointerException} will be thrown.
	 * 
	 * @param diagnosable the {@code Diagnosable} instance to remove
	 * @throws NullPointerException thrown if, and only if, {@code diagnosable} is {@code null}
	 */
	public void removeDiagnosable(Diagnosable diagnosable) throws NullPointerException {
		doAssertObjectIsNotNull(diagnosable, "diagnosable");
		doRemoveDiagnoable(diagnosable);
	}
	
	/**
	 * Returns the default instance of this {@code DiagnosticsManager} class.
	 * 
	 * @return the default instance of this {@code DiagnosticsManager} class
	 */
	public static DiagnosticsManager getDefaultInstance() {
		return doGetDefaultInstance();
	}
	
	/**
	 * Returns a new {@code DiagnosticsManager} instance.
	 * 
	 * @return a new {@code DiagnosticsManager} instance
	 */
	public static DiagnosticsManager newInstance() {
		return doNewInstance();
	}
	
	private Set<Diagnosable> doGetDiagnosables() {
		return new LinkedHashSet<>(diagnosables);
	}
	
	private void doAddDiagnosable(Diagnosable diagnosable) {
		boolean shouldAdd = true;
		
		for(int i = 0; i < diagnosables.size(); i++) {
			Diagnosable diagnosable0 = diagnosables.get(i);
			
			Name name = diagnosable.getName();
			Name name0 = diagnosable0.getName();
			
			if(diagnosable != diagnosable0 && name.equals(name0)) {
				if(diagnosable0 instanceof CompoundDiagnosable) {
					CompoundDiagnosable compoundDiagnosable = CompoundDiagnosable.class.cast(diagnosable0);
					compoundDiagnosable.addDiagnosable(diagnosable);
				} else {
					CompoundDiagnosable compoundDiagnosable = new CompoundDiagnosable(name, diagnosable0, diagnosable);
					
					diagnosables.set(i, compoundDiagnosable);
				}
				
				shouldAdd = false;
				
				break;
			}
		}
		
		if(shouldAdd) {
			diagnosables.add(diagnosable);
			
			Collections.sort(diagnosables, new DiagnosableComparator());
		}
	}
	
	private void doRemoveDiagnoable(Diagnosable diagnosable) {
		diagnosables.remove(diagnosable);
	}
	
	private static DiagnosticsManager doGetDefaultInstance() {
		return INSTANCE;
	}
	
	private static DiagnosticsManager doNewInstance() {
		return new DiagnosticsManager();
	}
	
	private static final class CompoundDiagnosable implements Diagnosable {
		private final Name name;
		private final List<Diagnosable> diagnosables;
		
		private CompoundDiagnosable(Name name, Diagnosable... diagnosables) {
			this.name = name;
			this.diagnosables = new LinkedList<>(Arrays.asList(diagnosables));
		}
		
		@Override
		public Diagnosis diagnose() {
			Diagnosis.Builder diagnosis_Builder = Diagnosis.Builder.newInstance();
			
			for(Diagnosable diagnosable : diagnosables) {
				Diagnosis diagnosis = diagnosable.diagnose();
				
				for(Problem problem : diagnosis.getProblems()) {
					diagnosis_Builder.addProblem(problem);
				}
			}
			
			Diagnosis diagnosis = diagnosis_Builder.build();
			
			return diagnosis;
		}
		
		@Override
		public Name getName() {
			return name;
		}
		
		private void addDiagnosable(Diagnosable diagnosable) {
			diagnosables.add(diagnosable);
		}
	}
	
	private static final class DiagnosableComparator implements Comparator<Diagnosable> {
		private DiagnosableComparator() {
			
		}
		
		@Override
		public int compare(Diagnosable a, Diagnosable b) {
			return a.getName().toString().compareToIgnoreCase(b.getName().toString());
		}
	}
}