package com.quui.tm2.agents;

import java.io.File;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import com.quui.tm2.Agent;
import com.quui.tm2.Annotation;
import com.quui.tm2.AnnotationReader;
import com.quui.tm2.Evaluation;
import com.quui.tm2.ImmutableAnnotation;
import com.quui.tm2.Model;
import com.quui.tm2.util.Preferences;
import com.quui.tm2.util.Preferences.Default;
import com.quui.tm2.util.TM2Logger;

/**
 * @author Fabian Steeg (fsteeg)
 */
public class SimpleEvaluation<E extends Comparable<E> & Serializable> implements Agent<E, String>, Model<E, E>, Evaluation {

  /***/
  static class Result {
    /***/
    private float f;
    /***/
    private float r;
    /***/
    private float p;

    /**
     * @param p Precision
     * @param r Recall
     * @param f F-Measure
     */
    Result(final float p, final float r, final float f) {
      this.p = p;
      this.r = r;
      this.f = f;
    }

    /**
     * @return Returns a human-readable representation of the evaluation result, containing
     *         precision, recall and f-measure. The exact format is subject to change. If you need
     *         particular values, use the accessors.
     * @see java.lang.Object#toString()
     */
    public String toString() {
      return String.format("f: %.2f (p: %.2f, r: %.2f)", f, p, r);
    }
  }

  /** The result of this evaluation. */
  private Result result = null;
  /***/
  private String goldLocation;

  /***/
  public String resultFile;
  String agent;

  /**
   * Sets the GOLD file.
   * @param gold The gold annotations file to evaluate against
   */
  public SimpleEvaluation(final String gold) {
    this.goldLocation = gold;
  }

  /**
   * Uses default GOLD file.
   */
  public SimpleEvaluation() {
    this.goldLocation = Preferences.get(Default.GOLD);
  }

  public final <T extends Comparable<T> & Serializable> void evaluateWithDefaultGold(
      final Class<? extends Agent<?, T>> agentClass) {
    evaluate(Preferences.get(Preferences.Default.GOLD), agentClass);
  }

  /**
   * Evaluates the results of the experiment associated with this runner against the given gold
   * standard, using the given agent and the given annotation type.
   * @param <T> The annotation value type
   * @param goldFileLocation The location of the gold standard annotations
   * @param agentClass The class for the agent to use
   */
  public final <T extends Comparable<T> & Serializable> void evaluate(
      final String goldFileLocation, final Class<? extends Agent<?, T>> agentClass) {
    try {
      if (!new File(new URI(resultFile)).exists()) {
        throw new IllegalStateException("Result file not found: " + resultFile);
      }
    } catch (URISyntaxException e) {
      e.printStackTrace();
    }
    this.agent = agentClass.getSimpleName();
    List<Annotation<T>> gold = new AnnotationReader(goldFileLocation).readAnnotations(agentClass);
    List<Annotation<T>> resultToEvaluate = new AnnotationReader(resultFile)
        .readAnnotations(agentClass);
    evaluateAgainst(resultToEvaluate, gold);
    this.goldLocation = goldFileLocation;
    /* Print the evaluation result: */
    TM2Logger.singleton().info(
        String.format("%s for evaluation of '%s' annotations against '%s'", getResultString(),
            agentClass.getSimpleName(), gold));
  }

  /**
   * @param <T> The value types of the annotations to compare
   * @param resultAnnotations The annotations to evaluate against the gold annotations
   * @param goldAnnotations The gold standard annotations to evaluate the result against
   */
  public final <T extends Comparable<T>> void evaluateAgainst(
      final List<Annotation<T>> resultAnnotations, final List<Annotation<T>> goldAnnotations) {
    float tp = 0f;
    /* True positives: result and gold: */
    for (Annotation<?> a : resultAnnotations) {
      if (goldAnnotations.contains(a)) {
        tp++;
      }
    }
    /* Precision: tp / (tp + fp) (i.e. all result annotations): */
    float p = tp / resultAnnotations.size();
    /* Recall, tp / (tp + fn) (i.e. all gold annotations): */
    float r = tp / goldAnnotations.size();
    float f = 0f;
    if (p == 0 && r == 0) {
      f = 0;
    } else {
      f = 2 * p * r / (p + r);
    }
    if (f < 0 || f > 1.0f) {
      throw new IllegalStateException("f should be between 0 and 1 but is: " + f);
    }
    this.result = new Result(p, r, f);
  }

  /**
   * @return Returns true if this evaluation is finished and a result is available
   */
  public final boolean finished() {
    return this.result != null;
  }

  /**
   * @return The location of the gold standard file
   */
  public final String getGoldLocation() {
    return goldLocation;
  }

  /**
   * @return Returns a human-readable representation of the evaluation result, containing precision,
   *         recall and f-measure. The exact format is subject to change. If you need particular
   *         values, use the accessors.
   */
  public final String getResultString() {
    if (!finished()) {
      return String.format("Unfinished %s", this.getClass().getSimpleName());
    }
    return result.toString();
  }

  /**
   * @return Returns the f-measure
   */
  public final float getF() {
    checkFinished();
    return result.f;
  }

  /**
   * @return Returns the precision
   */
  public final float getPrecision() {
    checkFinished();
    return result.p;
  }

  /**
   * @return Returns the recall
   */
  public final float getRecall() {
    checkFinished();
    return result.r;
  }

  /**
   * Throws an IllegalArgumentException if the evaluation is not finished yet.
   */
  private void checkFinished() {
    if (!finished()) {
      throw new IllegalStateException("Evaluation is not finished!");
    }
  }

  @Override
  public String toString() {
    return String.format("%s using gold: %s, result: %s", getClass().getSimpleName(), goldLocation
        .substring(goldLocation.lastIndexOf('/') + 1), getResultString());
  }

  public List<Annotation<String>> process(final List<Annotation<E>> input) {
    // Generics with ? here work with the Eclipse compiler but not Sun's
    final Class author = input.get(0).author();
    List<Annotation<E>> gold = new AnnotationReader(goldLocation)
        .readAnnotations((Class<? extends Agent<?, E>>) author);
    return evaluate(input, author, gold);
  }

  private List<Annotation<String>> evaluate(final List<Annotation<E>> input,
      final Class<? extends Agent<?, ?>> author, List<Annotation<E>> gold) {
    evaluateAgainst(input, gold);
    try {
		final Class c = Class.forName(getClass().getName());
		return new ArrayList<Annotation<String>>() {
		  {
		    add(ImmutableAnnotation.getInstance(c, String.format("%s = %s;[%s]",
		        author.getSimpleName(), getResultString(), goldLocation), input.get(0).getStart(),
		        input.get(input.size() - 1).getEnd()));
		  }
		};
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
		return null;
	}
  }

  public void setResultFile(String outputAnnotationLocation) {
    this.resultFile = outputAnnotationLocation;

  }

  public Model<E, E> train(List<Annotation<E>> values,
      List<Annotation<E>> correct) {
    // Generics with ? here work with the Eclipse compiler but not Sun's
    final Class author = values.get(0).author();
    // TODO use result here?
    List<Annotation<String>> result = evaluate(values, author, correct);
    evaluateAgainst(values, correct);
    return this;
  }
}
