package us.parr.bookish.semantics;

import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroup;
import org.stringtemplate.v4.STGroupFile;
import us.parr.bookish.Tool;
import us.parr.bookish.entity.ExecutableCodeDef;
import us.parr.bookish.entity.PyFigDef;
import us.parr.bookish.parse.BookishParser;
import us.parr.bookish.parse.ChapDocInfo;
import us.parr.bookish.translate.HTMLEscaper;
import us.parr.lib.ParrtCollections;
import us.parr.lib.ParrtIO;
import us.parr.lib.ParrtSys;
import us.parr.lib.collections.MultiMap;
import us.parr.lib.collections.MultiMapOfLists;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import static us.parr.lib.ParrtStrings.md5hash;
import static us.parr.lib.ParrtSys.execCommandLine;

public class PySnippetExecutor {
	public STGroup pycodeTemplates = new STGroupFile("templates/pyeval.stg");

	public Tool tool;

	public PySnippetExecutor(Tool tool) {
		this.tool = tool;
		pycodeTemplates = new STGroupFile("templates/pyeval.stg");
		pycodeTemplates.registerRenderer(String.class, new HTMLEscaper());
	}

	public void prepExecutionEnv(ChapDocInfo doc) {
		String snippetsDir = tool.getBuildDir()+"/snippets";

		// prepare snippets, images, and notebooks directories
		String basename = doc.getSourceBaseName();
		String chapterSnippetsDir = snippetsDir+"/"+basename;
		ParrtIO.mkdir(chapterSnippetsDir);
		ParrtIO.mkdir(tool.outputDir+"/images/"+basename);
		String outputChapNotebooksDir = tool.outputDir+"/notebooks/"+basename;
		ParrtIO.mkdir(outputChapNotebooksDir);

		// every chapter snippets/notebooks dir gets a data link to book data directory
		if ( !Files.isSymbolicLink(Paths.get(chapterSnippetsDir+"/data")) ) {
//			System.err.println("ln -s "+doc.artifact.dataDir+" "+chapterSnippetsDir+"/data");
			execCommandLine("ln -s "+doc.artifact.dataDir+" "+chapterSnippetsDir+"/data");
		}
		if ( !Files.isSymbolicLink(Paths.get(outputChapNotebooksDir+"/data")) ) {
//			System.err.println("ln -s "+doc.artifact.dataDir+" "+outputChapNotebooksDir+"/data");
			execCommandLine("ln -s ../../data "+outputChapNotebooksDir+"/data"); // relative link
		}

		// Copy resource to output notebook dir
		for (String notebookResource : doc.artifact.notebookResources) {
			execCommandLine(String.format("cp %s/%s %s", tool.inputDir, notebookResource, chapterSnippetsDir));
			execCommandLine(String.format("cp %s/%s %s", tool.inputDir, notebookResource, outputChapNotebooksDir));
		}
	}

	// combine list of code snippets for each label into file
	public void execSnippets(ChapDocInfo doc) {
		String basename = doc.getSourceBaseName();

		MultiMap<String, ExecutableCodeDef> labelToDefs = collectSnippetsByLabel(doc);

		// for each group of code with same label, create executable py file
		for (String label : labelToDefs.keySet()) {
			List<ExecutableCodeDef> defs = (List<ExecutableCodeDef>)labelToDefs.get(label);
			List<ST> snippets = getSnippetTemplates(doc, label, pycodeTemplates, defs);
			ST file = pycodeTemplates.getInstanceOf("pyfile");
			file.add("snippets", snippets);
			file.add("buildDir", tool.getBuildDir());
			file.add("outputDir", tool.outputDir);
			file.add("basename", basename);
			file.add("label", label);
			String pycode = file.render();

			execAndSaveOutput(doc, label, pycode);
		}
	}

	public void execAndSaveOutput(ChapDocInfo doc, String label, String pycode) {
		String snippetsDir = tool.getBuildDir()+"/snippets";
		String basename = doc.getSourceBaseName();

		String chapterSnippetsDir = snippetsDir+"/"+basename;
		String snippetHashFilename = chapterSnippetsDir+"/"+basename+"_"+label+"-"+md5hash(pycode)+".hash";
		String snippetFilename = basename+"_"+label+".py";
		if ( !Files.exists(Paths.get(snippetHashFilename)) ) { // check whether we've generated before
			System.err.println("BUILDING "+snippetFilename);
			ParrtIO.save(snippetHashFilename, ""); // save empty hash marker file
			ParrtIO.save(chapterSnippetsDir+"/"+snippetFilename, pycode);
			// EXEC!
			String[] result = ParrtSys.execInDir(chapterSnippetsDir, "pythonw", snippetFilename);
			if ( result[1]!=null && result[1].length()>0 ) {
				System.err.println(result[1]); // errors during python compilation not exec
			}
		}
	}

	public void saveNotebooks(ChapDocInfo doc) {
		MultiMap<String, ExecutableCodeDef> labelToDefs = collectSnippetsByLabel(doc);
		for (String label : labelToDefs.keySet()) {
			List<ExecutableCodeDef> defs = (List<ExecutableCodeDef>)labelToDefs.get(label);
			saveNotebook(doc, label, defs);
		}
	}

	// Generate and exec .py file that creates .ipynb file
	public void saveNotebook(ChapDocInfo doc, String label, List<ExecutableCodeDef> defs) {
		String basename = doc.getSourceBaseName();
		String snippetsDir = tool.getBuildDir()+"/snippets";
		String chapterSnippetsDir = snippetsDir+"/"+basename;

		ST nbwriter = pycodeTemplates.getInstanceOf("noteBookWriter");
		List<String> codeBlks = ParrtCollections.map(defs, d -> d.code);
		nbwriter.add("snippets", codeBlks);
		nbwriter.add("outputDir", tool.outputDir);
		nbwriter.add("basename", basename);
		nbwriter.add("label", label);
		nbwriter.add("title", "Notebook "+label+" from Chap "+doc.docNumber+" "+defs.get(0).enclosingChapter.title);
		String nbcode = nbwriter.render();
		String nbWriterFilename = "mk_ipynb_"+basename+"_"+label+".py";
		ParrtIO.save(chapterSnippetsDir+"/"+nbWriterFilename, nbcode);

//		System.err.println("### "+chapterSnippetsDir+"/"+nbWriterFilename);
		String[] result = ParrtSys.execInDir(chapterSnippetsDir, "pythonw", nbWriterFilename);
		if ( result[1]!=null && result[1].length()>0 ) {
			System.err.println(result[1]); // errors during python compilation not exec
		}
	}

	public void createNotebooksIndexFile() {
		ST indexFile = tool.artifact.templates.getInstanceOf("NotebooksIndexFile");
		indexFile.add("file", tool.artifact);
		indexFile.add("booktitle", tool.artifact.title);

		for (ChapDocInfo doc : tool.artifact.docs) {
			String basename = doc.getSourceBaseName();

			MultiMap<String, ExecutableCodeDef> labelToDefs = collectSnippetsByLabel(doc);
			for (String label : labelToDefs.keySet()) {
				indexFile.add("names", basename+"/"+label);
			}
		}
		ParrtIO.save(tool.outputDir+"/notebooks/index.html", indexFile.render());
	}

	public void annotateTreesWithOutput(ChapDocInfo doc) {
		String basename = doc.getSourceBaseName();
		String snippetsDir = tool.getBuildDir()+"/snippets";
		String chapterSnippetsDir = snippetsDir+"/"+basename;

		MultiMap<String, ExecutableCodeDef> labelToDefs = collectSnippetsByLabel(doc);

		// For each group of snippets associated with a label
		for (String label : labelToDefs.keySet()) {
			List<ExecutableCodeDef> defs = (List<ExecutableCodeDef>) labelToDefs.get(label);
			// For each snippet
			for (ExecutableCodeDef def : defs) {
				String errFilename = chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".err";

				File f = new File(errFilename);
				if ( !f.exists() ) {
					if ( def.tree instanceof BookishParser.PyevalContext ) {
						BookishParser.PyevalContext tree = (BookishParser.PyevalContext) def.tree;
						tree.stderr = "Can't find stderr file "+(basename+"_"+label+"_"+def.index)+" for this code:\n"+def.code;
						System.err.println(tree.stderr);
					}
					else {
						BookishParser.Inline_pyContext tree = (BookishParser.Inline_pyContext) def.tree;
						tree.stderr = "Can't find stderr file "+(basename+"_"+label+"_"+def.index)+" for this code:\n"+def.code;
						System.err.println(tree.stderr);
					}
					continue;
				}
				String stderr = ParrtIO.load(chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".err");
				if ( def instanceof PyFigDef ) {
					((PyFigDef) def).generatedFilenameNoSuffix = "images/"+basename+"/"+basename+"_"+label+"_"+def.index;
				}
				if ( stderr.trim().length()>0 ) {
					System.err.println(stderr);
				}
				if ( def.isOutputVisible ) {
					if ( def.tree instanceof BookishParser.PyevalContext ) {
						BookishParser.PyevalContext tree = (BookishParser.PyevalContext) def.tree;
						tree.stdout = ParrtIO.load(chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".out");
						tree.stderr = stderr.trim();
						if ( tree.stdout.length()==0 ) tree.stdout = null;
						if ( tree.stderr.length()==0 ) tree.stderr = null;
//						System.out.println("stdout: "+tree.stdout);
//						System.out.println("stderr: "+tree.stderr);
						if ( def.displayExpr!=null ) {
							String dataFilename = basename+"_"+label+"_"+def.index+".csv";
							tree.displayData = ParrtIO.load(chapterSnippetsDir+"/"+dataFilename);
//							System.out.println("data: "+tree.displayData);
						}
					}
					else {
						BookishParser.Inline_pyContext tree = (BookishParser.Inline_pyContext) def.tree;
						tree.stdout = ParrtIO.load(chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".out");
						tree.stderr = stderr.trim();
						if ( tree.stdout.length()==0 ) tree.stdout = null;
						if ( tree.stderr.length()==0 ) tree.stderr = null;
						String dataFilename = basename+"_"+label+"_"+def.index+".csv";
						tree.displayData = ParrtIO.load(chapterSnippetsDir+"/"+dataFilename);
					}
				}
			}
		}
	}

	public List<ST> getSnippetTemplates(ChapDocInfo doc,
	                                    String label,
	                                    STGroup pycodeTemplates,
	                                    List<ExecutableCodeDef> defs)
	{
		List<ST> snippets = new ArrayList<>();
		for (ExecutableCodeDef def : defs) {
			// avoid generating output for disable=true snippets if output available
			String basename = doc.getSourceBaseName();
			String snippetsDir = tool.getBuildDir()+"/snippets";
			String chapterSnippetsDir = snippetsDir+"/"+basename;
			String errFilename = chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".err";
			String outFilename = chapterSnippetsDir+"/"+basename+"_"+label+"_"+def.index+".out";
			if ( ((new File(errFilename)).exists()&&(new File(outFilename)).exists()) &&
				 !def.isEnabled ) {
				continue;
			}

			String tname = def.isOutputVisible ? "pyeval" : "pyfig";
			ST snippet = pycodeTemplates.getInstanceOf(tname);
			snippet.add("def",def);
			// Don't allow "plt.show()" to execute, strip it
			String code = null;
			if ( def.code!=null ) {
				code = def.code.replace("plt.show()", "");
			}
			if ( code!=null && code.trim().length()==0 ) {
				code = null;
			}
			code = HTMLEscaper.stripBangs(code);
			snippet.add("code", code);
			snippets.add(snippet);
		}
		return snippets;
	}

	// get mapping from label (or index if no label) to list of snippets
	public MultiMap<String, ExecutableCodeDef> collectSnippetsByLabel(ChapDocInfo doc) {
		MultiMap<String, ExecutableCodeDef> labelToDefs = new MultiMapOfLists<>();
		for (ExecutableCodeDef codeDef : doc.codeBlocks) { // for each code blob
			String label = codeDef.label!=null ? codeDef.label : String.valueOf(codeDef.index);
			labelToDefs.put(label, codeDef);
		}
		return labelToDefs;
	}
}