package pl.edu.pw.mini.jk.sg.gtopt;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.gaffer.GafferConfigurator;
import com.lexicalscope.jewel.cli.CliFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.lang.Override;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.collections4.Factory;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import org.slf4j.LoggerFactory;
import pl.edu.pw.mini.jk.sg.game.DefenderAttackerGame;
import pl.edu.pw.mini.jk.sg.game.GameVariantRunner;
import pl.edu.pw.mini.jk.sg.game.NormalizableFactory;
import pl.edu.pw.mini.jk.sg.game.NormalizableFactoryAction;
import pl.edu.pw.mini.jk.sg.game.common.Cloneable;
import pl.edu.pw.mini.jk.sg.game.common.DefenderMove;
import pl.edu.pw.mini.jk.sg.game.common.SecurityGame;
import pl.edu.pw.mini.jk.sg.game.common.advanceable.AdvanceableAttackerState;
import pl.edu.pw.mini.jk.sg.game.common.advanceable.AdvanceableDefenderState;

import pl.edu.pw.mini.jk.sg.game.configuration.StaticGameConfigurationReader;
import pl.edu.pw.mini.jk.sg.game.graph.GraphGameVariantRunner;
import pl.edu.pw.mini.jk.sg.game.graph.advanceable.AdvanceableGraphGameConfiguration;
import pl.edu.pw.mini.jk.sg.game.graph.NormalizableAdvanceableStateGraphGameFactory;
import pl.edu.pw.mini.jk.sg.game.graph.advanceable.AdvanceableStateGraphGame;
import pl.edu.pw.mini.jk.sg.game.graph.moves.single.DesiredConfigurationMove;
import pl.edu.pw.mini.jk.sg.gteval.config.PluginLibrary;
import pl.edu.pw.mini.jk.sg.opt.util.CodeUtils;
import pl.edu.pw.mini.jk.sg.opt.util.GamePayoff;
import pl.edu.pw.mini.jk.sg.opt.util.GameUtil;
import pl.edu.pw.mini.jk.sg.opt.util.ScipUtil;
import pl.edu.pw.mini.sg.movingtargets.MovingTargetsGameConfig;
import pl.edu.pw.mini.sg.movingtargets.MovingTargetsGameConfigReader;
import pl.edu.pw.mini.sg.movingtargets.MovingTargetsVariantRunner;
import scala.Some;

public class App {
    private final static double M = 1000000;
    private final static double smallM = 10000;

    public static void main(String[] args) throws IOException {
	Locale.setDefault(Locale.US);
	final PluginLibrary gamePlugins = PluginLibrary.defaultLibrary();

	LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
	GafferConfigurator configurator = new GafferConfigurator(context);
	// Call context.reset() to clear any previous configuration, e.g. default 
	// configuration. For multi-step configuration, omit calling context.reset().
	context.reset(); 
	configurator.run(App.class.getResource("logback.groovy"));
	
	final CmdlineOptions cmdline = CliFactory.createCli(CmdlineOptions.class).parseArguments(args);

	final boolean pesimistic = false;
	final Cache<Object, ImmutableList<DesiredConfigurationMove>> moveCache = CacheBuilder.newBuilder().build();

	final GameVariantRunner gvr; 

	if(gamePlugins.isFileSupported(cmdline.getGameConfig())) {
	    if(cmdline.isGameTransformer())
		gvr = gamePlugins.getGameVariantRunner(cmdline.getGameConfig(), cmdline.getGameTransformer());
	    else 
	        gvr = gamePlugins.getGameVariantRunner(cmdline.getGameConfig());
	} else {
	    final AdvanceableGraphGameConfiguration gc = StaticGameConfigurationReader.readAagcFromFile(cmdline.getGameConfig());
	    gvr = new GraphGameVariantRunner(gc, cmdline.getRoundLimit());
	}
		
	

	final NormalizableFactoryAction<Void> action = new NormalizableFactoryAction<Void>() {
		@Override
		public <AM, DM extends DefenderMove,
				       AS extends AdvanceableAttackerState<AM>, DS extends AdvanceableDefenderState<DM>,
											   G extends SecurityGame & Cloneable<G> & DefenderAttackerGame<AM, DM, AS, DS, ?>> Void runWithNormalizableFactory(final NormalizableFactory<AM,DM,G> factory2, final int roundLimit) {
		    try {
		    final NormalizableFactory<AM,DM,G> factory = factory2.denormalize();
		    final G game = factory.create();
		    final List<ImmutableList<AM>> attackerSequences = GameUtil
		    .getAllPossiblePlayerMoveSequences(game.getAttackerInitialState(), roundLimit)
		    .collect(Collectors.toList());

		    System.out.println("Attacker sequences: done");
		    final List<ImmutableList<DM>> defenderSequences = GameUtil
		    .getAllPossiblePlayerMoveSequences(game.getDefenderInitialState(), roundLimit)
		    .collect(Collectors.toList());
		    System.out.println("Defender sequences: done");
		    
		    double[][] defenderMatrix = new double[attackerSequences.size()][defenderSequences.size()];
		    double[][] attackerMatrix = new double[attackerSequences.size()][defenderSequences.size()];
		    initializeMatrices(factory, attackerSequences, defenderSequences, defenderMatrix, attackerMatrix);
		    System.out.println("Matrices: done");
		    
		    final OutputStream os;
		    if(cmdline.getOutputFile().toString().endsWith(".gz")) {
			os = new GZIPOutputStream(new FileOutputStream(cmdline.getOutputFile()));
		    } else {
			os = new FileOutputStream(cmdline.getOutputFile());
		    }
		    PrintStream outStream = new PrintStream(os);
		    if(cmdline.isZimplOutput()){
			writeZimplProblem(cmdline.getGameConfig().getName(), attackerSequences.size(), defenderSequences.size(),
					  defenderMatrix, attackerMatrix, outStream);
		    } else {
			writeMpsProblem(cmdline.getGameConfig().getName(), pesimistic, attackerSequences.size(), defenderSequences.size(),
					defenderMatrix, attackerMatrix, outStream);
		    }

		    outStream.close();

		    if(cmdline.isMoveCodesPath()) {
			CodeUtils.writeMoveCodes(cmdline.getMoveCodesPath(), attackerSequences, defenderSequences);
		    }

		    } catch (IOException ex) {
			throw new UncheckedIOException(ex);
		    }
		    return null;
		}
	    };

	gvr.runWithCompactGameFactory(action);
    }

    private static void writeZimplProblem(final String problemName,
					  final int attackerMovesCount, final int defenderMovesCount, double[][] defenderMatrix,
					  double[][] attackerMatrix, PrintStream outStream) {
	// 1) Zbiory
	outStream.println(Zimpl.intSet("AM", 0, attackerMovesCount-1));
	outStream.println(Zimpl.intSet("DM", 0, defenderMovesCount-1));
	// 2) parametry (macierze)
	Zimpl.printParamMatrix(outStream, "AR", "AM", "DM", attackerMatrix);
	Zimpl.printParamMatrix(outStream, "DR", "AM", "DM", defenderMatrix);
	// 3) zmienne
	outStream.println(
			  Zimpl.variable("a", Collections.emptyList(), ZimplVarType.REAL, Optional.empty(), Optional.empty()));
	outStream.println(
			  Zimpl.variable("q", Arrays.asList("AM"), ZimplVarType.BINARY, Optional.empty(), Optional.empty()));
	outStream.println(
			  Zimpl.variable("z", Arrays.asList("DM","AM"), ZimplVarType.REAL, Optional.of(0.0), Optional.of(1.0)));
	// 4) F-ja celu
	outStream.println("maximize payoff: sum <i,j> in DM cross AM : z[i,j] * DR[j,i];");
	// 5) ograniczenia
	outStream.println("subto probsum: sum <i,j> in DM cross AM : z[i,j] == 1;");
	outStream.println("subto aprobsum: forall <i> in DM do (sum <j> in AM: z[i,j]) <= 1;");
	outStream.println("subto onear: forall <j> in AM do q[j] <= sum <i> in DM: z[i,j];");
	outStream.println("subto oenal: forall <j> in AM do (sum <i> in DM: z[i,j]) <= 1;");
	outStream.println("subto oneaq: sum <j> in AM: q[j] == 1;");
	outStream.println("subto tiel: forall <j> in AM do 0 <= a - (sum <i> in DM: AR[j,i] * (sum <h> in AM: z[i,h]));");
	outStream.println("subto tier: forall <j> in AM do a - (sum <i> in DM: AR[j,i] * (sum <h> in AM: z[i,h])) <= (1 - q[j]) *"+M+";");
    }

    private static void writeMpsProblem(final String problemName, final boolean pesimistic,
					final int attackerMovesCount, final int defenderMovesCount, double[][] defenderMatrix,
					double[][] attackerMatrix, PrintStream outStream) {
	outStream.format("%-15s%s\n", "NAME", problemName);
	outStream.println("ROWS");
	outStream.println(" N Obj");
	outStream.println(" E Proball");
	for (int i = 0; i < defenderMovesCount; i++) {
	    outStream.println(" L Psd" + i);
	}
	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.println(" L Psa" + i);
	}
	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.println(" G Psar" + i);
	}

	outStream.println(" E qsum");
	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.println(" G Atpg" + i);
	    outStream.println(" L Atpl" + i);
	}
	outStream.println("COLUMNS");
	printZCoefficients(outStream, defenderMatrix, attackerMatrix, pesimistic);
	printQCoefficients(outStream, attackerMatrix.length, M);
	printACoefficients(outStream, attackerMatrix.length);

	outStream.println("RHS");
	outStream.format("    RHS       %-15s%10f\n", "Proball", 1.);
	for (int i = 0; i < defenderMovesCount; i++) {
	    outStream.format("    RHS       %-15s%10f\n", "Psd" + i, 1.);
	}
	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.format("    RHS       %-15s%10f\n", "Psa" + i, 1.);
	}
	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.format("    RHS       %-15s%10f\n", "Psar" + i, 0.);
	}
	outStream.format("    RHS       %-15s%10f\n", "qsum", 1.);

	for (int i = 0; i < attackerMovesCount; i++) {
	    outStream.format("    RHS       %-15s%10f\n", "Atpg" + i, 0.);
	    outStream.format("    RHS       %-15s%10f\n", "Atpl" + i, M);
	}

	outStream.println("BOUNDS");

	printZTypes(outStream, attackerMovesCount, defenderMovesCount);
	printQTypes(outStream, attackerMovesCount);
	outStream.format(" FR Bound     a\nENDATA");
    }

    protected static <AM, DM extends DefenderMove> void initializeMatrices(final NormalizableFactory<AM, DM, ?> gameFactory,
					     final List<ImmutableList<AM>> attackerSequences,
					     final List<ImmutableList<DM>> defenderSequences, double[][] defenderMatrix,
					     double[][] attackerMatrix) {
	double mind = 0;
	double mina = 0;

	for (int i = 0; i < attackerSequences.size(); i++) {
	    for (int j = 0; j < defenderSequences.size(); j++) {
		GamePayoff p = GameUtil.getPayoff(
												       defenderSequences.get(j), attackerSequences.get(i), gameFactory);
		defenderMatrix[i][j] = p.getDefenderPayoff();
		attackerMatrix[i][j] = p.getAttackerPayoff();
		mind = Math.min(p.getDefenderPayoff(), mind);
		mina = Math.min(p.getAttackerPayoff(), mina);
	    }
	}

	for (int i = 0; i < attackerSequences.size(); i++) {
	    for (int j = 0; j < defenderSequences.size(); j++) {
		defenderMatrix[i][j] -= mind;
		attackerMatrix[i][j] -= mina;
	    }
	}
    }

    private static void printACoefficients(PrintStream out, final int attackerSequencesCount) {
	final Map<String, Double> coefficients = new HashMap<>();
	final String name = "a";
	for (int i = 0; i < attackerSequencesCount; i++) {
	    coefficients.put("Atpg" + i, 1.);
	}
	for (int i = 0; i < attackerSequencesCount; i++) {
	    coefficients.put("Atpl" + i, 1.);
	}

	ScipUtil.printColumnLimits(out, name, coefficients);
    }

    private static void printQTypes(PrintStream out, final int attackerSequencesCount) {
	for (int j = 0; j < attackerSequencesCount; j++) {
	    final String columnName = "q" + j;
	    out.format(" BV Bound     %s\n", columnName);
	}
    }

    private static void printQCoefficients(PrintStream out, final int attackerSequencesCount, double m) {
	for (int j = 0; j < attackerSequencesCount; j++) {
	    final String columnName = "q" + j;
	    final Map<String, Double> coefficients = new HashMap<>();

	    coefficients.put("qsum", 1.);

	    coefficients.put("Psar" + j, -1.);
	    coefficients.put("Atpl" + j, m);

	    ScipUtil.printColumnLimits(out, columnName, coefficients);
	}

    }

    private static void printZTypes(final PrintStream out, final int attackerSequencesCount,
				    final int defenderSequencesCount) {
	for (int am = 0; am < attackerSequencesCount; am++) {
	    for (int dm = 0; dm < defenderSequencesCount; dm++) {
		final String columnName = "z_" + dm + "_" + am;

		out.format(" UP Bound     %-15s%10f\n", columnName, 1.);
		out.format(" LO Bound     %-15s%10f\n", columnName, 0.);
	    }
	}
    }

    private static void printZCoefficients(final PrintStream out, final double[][] defenderMatrix,
					   final double[][] attackerMatrix, final boolean pesimistic) {
	final int attackerSequencesCount = defenderMatrix.length;
	for (int am = 0; am < defenderMatrix.length; am++) {
	    for (int dm = 0; dm < defenderMatrix[am].length; dm++) {
		final String columnName = "z_" + dm + "_" + am;
		final Map<String, Double> coefficients = new HashMap<>();

		coefficients.put("Obj", -defenderMatrix[am][dm]);
		coefficients.put("Proball", 1.);
		coefficients.put("Psd" + dm, 1.);
		coefficients.put("Psa" + am, 1.);
		coefficients.put("Psar" + am, 1.);

		for (int i = 0; i < attackerSequencesCount; i++) {
		    coefficients.put("Atpg" + i, -attackerMatrix[i][dm] * (pesimistic ? smallM : 1)
				     + (pesimistic ? defenderMatrix[i][dm] : 0));
		    coefficients.put("Atpl" + i, -attackerMatrix[i][dm] * (pesimistic ? smallM : 1)
				     + (pesimistic ? defenderMatrix[i][dm] : 0));
		}

		ScipUtil.printColumnLimits(out, columnName, coefficients);
	    }
	}
    }

}
