package svm;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;

import base.DenseVector;
import base.Example;
import base.Structure;

public class SVMStruct<X extends Structure, Y extends Structure>
{
	public DenseVector w;
	public double slack;
	public double loss;

	public HashMap<String, Integer> wMap;
	public Mode m;
	public double C;
	public double eps;
	public SVMStructInstance<X, Y> instance;

	public SVMStruct(SVMStructInstance<X, Y> instance, double C, double eps, Mode m)
	{
		this.C = C;
		this.eps = eps;
		this.m = m;
		this.instance = instance;
	}

	private Y mostViolatedConstraint(X x, Y y)
	{
		if (m == Mode.MARGIN_RESCALING)
			return instance.marginRescaling(x, y, this);
		if (m == Mode.SLACK_RESCALING)
			return instance.slackRescaling(x, y, this);
		return null;
	}

	public void train(List<Example<X, Y>> training)
	{
		System.out.println("C: " + this.C + "\tEpsilon: " + this.eps);
		long t = System.currentTimeMillis();
		System.out.println("Number of Training Examples: " + training.size());
		QPSolver<X, Y> solver = new QPSolver<X, Y>();
		System.out.println("Initializing Feature Space (May Involve Precomputing Feature Maps)");
		int phiSize = initializeFeatureSpace(training);

		this.w = new DenseVector(phiSize);
		ArrayList<Constraint<X, Y>> constraints = new ArrayList<Constraint<X, Y>>();
		int iter = 0;
		long solvertime = 0;
		long oracletime = 0;
		while (true)
		{
			iter++;
			System.out.print(iter + "\n");
			// System.out.println((iter)+". Iteration");
			System.out.println("Number of Constraints: " + constraints.size());

			long ttmp = System.currentTimeMillis();
			if (constraints.size() > 0)
				slack = solver.solve(constraints, this.w, C, iter);
			else
				slack = 0;
			solvertime += System.currentTimeMillis() - ttmp;
			System.out.println("Computing new Cuttingplane");
			Constraint<X, Y> nc = new Constraint<X, Y>(this.w.length());

			// Java 8 Magic! OMG this is awesome :D
			ttmp = System.currentTimeMillis();
			AtomicInteger progress = new AtomicInteger();
			// TODO: partition -> constraint -> reduce/merge -> no locking
			training.parallelStream().forEach((e) ->
			{
				Y ybar = mostViolatedConstraint(e.x, e.y);
				int p = progress.incrementAndGet();
				if (p % 1000 == 0)
					System.out.print(p + "\t");
				// System.out.println(e.y+" "+ybar);
					double loss = instance.loss(e.y, ybar);

					nc.addConstraint(e.x, e.y, ybar, m == Mode.MARGIN_RESCALING ? 1 : loss, loss, this, instance);
				});
			oracletime += System.currentTimeMillis() - ttmp;
			nc.finalizeConstraint(training.size());
			nc.lastUsed = iter;

			constraints.add(nc);

			// cleanup unused constraints with a crude heuristic
			// cleanup unused constraints with a crude heuristic
			for (int i = constraints.size() - 2; i >= 0; i--)
			{
				Constraint<X, Y> c = constraints.get(i);
				if (iter - c.lastUsed > 50)
				{
					constraints.remove(i);
					solver.pruneConstraint(i);
					// System.out.println("cleanup");
				}
			}

			double c = nc.phi.multiply(w);

			double tmp = nc.loss - c;
			System.out.println("Loss: " + nc.loss);
			System.out.println("Margin: " + c);
			System.out.println("L-M: " + tmp);
			System.out.println("Slack: " + slack);
			System.out.println("Solver: " + solvertime + "\tOracle: " + oracletime);
			if (iter % 20 == 0)
			{
				System.out.println("Training Error: " + empiricalError(training));
			}
			// System.out.println(nc.loss+"\t"+c+"\t"+slack);
			if (tmp <= slack + eps)
			{
				System.out.println();

				System.out.println("Error: " + empiricalError(training));
				System.out.println("L2: " + w.multiply(w));
				System.out.println("Number of selected features: " + w.countNonZero());
				System.out.println("");
				System.out.println("Time:" + (System.currentTimeMillis() - t));
				break;
			}
		}
	}

	private int initializeFeatureSpace(List<Example<X, Y>> training)
	{
		this.wMap = new HashMap<String, Integer>();
		HashMap<String, Integer> pruner = new HashMap<String, Integer>();
		for (Example<X, Y> e : training)
		{
			for (String feature : instance.features(e.x, e.y))
			{
				if (!wMap.containsKey(feature))
				{
					wMap.put(feature, wMap.size());
					pruner.put(feature, 1);
				}
				else
					pruner.put(feature, 1 + pruner.get(feature));
			}
		}
		System.out.println("Number of Features before Pruning: " + pruner.size());
		int i = 0;
		for (Entry<String, Integer> e : pruner.entrySet())
		{
			if (e.getValue() < 5)
				wMap.remove(e.getKey());
			else
			{
				wMap.put(e.getKey(), i);
				i++;
			}
		}
		System.out.println("Number of Features after Pruning: " + this.wMap.keySet().size());
		Iterator<String> iterator = this.wMap.keySet().iterator();
		for (int j = 0; j < Math.min(200, this.wMap.size()); j++)
		{
			String l = iterator.next();
			System.out.print(l + ", ");
		}
		System.out.println("...");
		System.out.println("Example Feature Vector:");
		System.out.println(instance.phi(training.get(0).x, training.get(0).y));
		return wMap.size();
	}

	public double empiricalError(List<Example<X, Y>> training)
	{
		return training.parallelStream().mapToDouble((e) ->
		{
			Y ybar = predict(e.x);
			double l = instance.loss(e.y, ybar);// 0.5 * (outputKernel.k(e.y, e.y) - 2 * outputKernel.k(e.y, ybar) + outputKernel.k(ybar, ybar));
				return l;
			}).average().getAsDouble();

	}

	public Y predict(X x)
	{
		return this.instance.predict(x, this);
	}

	public double testError(List<Example<X, Y>> test)
	{
		// TODO Auto-generated method stub
		double mean = 0;
		for (Example<X, Y> e : test)
		{
			Y pred = predict(e.x);
			double l = instance.loss(e.y, pred);
			mean += l;
			// System.out.println("L("+e.y+"\t"+pred+")="+l);
		}
		return mean / test.size();
	}
}
