package application;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.imageio.ImageIO;

import input.FileFilter;
import timedistortion.ITimeMapping;
import timedistortion.ITransform;
import timedistortion.Ictx;
import timedistortion.PixelData;
import timedistortion.Point;

public class TD {

    private static final int AP = Runtime.getRuntime().availableProcessors();
	private static CountDownLatch loading;
	private static CountDownLatch processing;

	private static File[] in_files;
	private static PixelData[] pd_cube;
	private static Ictx ictx;
	private static ITransform transform;
	private static ITimeMapping time_mapper;
	private static AtomicBoolean fatal = new AtomicBoolean();
	
	private static StringBuilder video_job;
	@SuppressWarnings("rawtypes")
	private static List<Class> tf_classes = new ArrayList<Class>();
//	@SuppressWarnings("rawtypes")
//	private static Class tf_classes[] = {
////			transformer.Average.class,
////			transformer.Copy.class,
//			transformer.HalfReverse.class,
////			transformer.HalfVelocity.class,
////			transformer.Horizontal.class,
////			transformer.Vertical.class,
////			transformer.Max.class,
////			transformer.TriChrome.class,
////			transformer.TriChromeSum.class,
////			transformer.Radial.class,
//			transformer.Manhattan.class,
////			transformer.Curtain.class,
//	};

	@SuppressWarnings("rawtypes")
	private static List<Class> tm_classes = new ArrayList<Class>();
//	@SuppressWarnings("rawtypes")
//	private static Class tm_classes[] = {
//			timemapper.Linear.class,
////			timemapper.OneMinusLinear.class,
////			timemapper.Sinus.class,
////			timemapper.OneMinusSinus.class,
//	};

	@SuppressWarnings("rawtypes")
	public static void setTasks(List<Class> tf, List<Class> tm) {
		tf_classes = tf;
		tm_classes = tm;
	}

	public static boolean loadImages(final LoadAction loadAction) {
		in_files = FileFilter.listFiles(TaskEnvironment.path, TaskEnvironment.startswith, TaskEnvironment.endswith);
		if(checkInput()) {
			load_all_images(loadAction);
			return true;
		}
		else {
			System.out.println("no input images");
		}
		return false;
	}

	public static void doTransform(final TransformAction transformAction) {
		if(checkInput()) {
			video_job = new StringBuilder("#!/bin/bash\n");
			fatal.set(false);
			for (int tr = 0; tr < tf_classes.size(); tr++) {
				for (int tm = 0; tm < tm_classes.size(); tm++) {
					try {
//						@SuppressWarnings("unchecked")
						transform = (ITransform)tf_classes.get(tr).getDeclaredConstructor().newInstance();
						transform.setContext(ictx);
						time_mapper = (ITimeMapping)tm_classes.get(tm).getDeclaredConstructor().newInstance();
						time_mapper.setContext(ictx);
						process_all_frames(tf_classes.get(tr), tm_classes.get(tm), transformAction);
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
		else {
			System.out.println("no input images");
		}
	}

	public static void main(String[] args) {
		TaskEnvironment.scanProgramArgs(args);

		if (TaskEnvironment.path.length() > 0 && TaskEnvironment.startswith.length() > 0) {
			loadImages(null);
			tf_classes.add(transformer.Manhattan.class);
			tm_classes.add(timemapper.OneMinusSinus.class);
			doTransform(null);
		}

		if (TaskEnvironment.accepted) {
			System.out.println(video_job.toString());
		}
	}

//	private static void print_transformer() {
//		for (int i = 0; i < tf_classes.size(); i++) {
//			try {
//				System.out.println();
//				System.out.println(tf_classes.get(i).getCanonicalName());
//			} catch (Exception e) {
//				e.printStackTrace();
//			}
//		}
//	}

	private static void create_out_folder(String out_run) {
		File op = new File(out_run);
		if(!op.exists()) {
			op.mkdirs();
		}
		if(!op.exists() || !op.canWrite()) {
			System.out.println("can not access ouput folder '" + out_run + "'");
			System.exit(1);
		}
	}

	private static void append_video_job(StringBuilder job, String out_run, String tf_name, String tm_name) {
		job.append("\n");
		job.append("timestamp=$(date +%s)\n");
		job.append("cd " + out_run + "\n");
		job.append("ffmpeg -framerate 30 -pattern_type glob -i 'frame_*.png' -vcodec libx265 -preset slow -crf 18 "
				+ tf_name
				+ "-"
				+ tm_name 
		        + "-$timestamp.mp4\n");
		job.append("ls -lh *.mp4\n");
//		job.append("cd ../..\n");
	}

	private static boolean checkInput() {
		return in_files != null && in_files.length > 0;
	}

	private static void load_all_images(final LoadAction loadAction) {
		System.gc();
		pd_cube = new PixelData[in_files.length];
		ictx = new Ictx(pd_cube, in_files.length);

		ExecutorService service = null;
		try {
			System.out.println("loading input files " + in_files.length);
			service = Executors.newFixedThreadPool(AP);
			loading = new CountDownLatch(in_files.length);
			for (int fi = 0; fi < in_files.length; ++fi) {
				final int i = fi;
				Runnable task = new Runnable() {
					@Override
					public void run() {
						Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
						if(loadAction != null)
							loadAction.progress("(" + i + ") " + in_files[i]);
						load_single_image(i, in_files[i]);
					}
				};
				service.execute(task);
			}
			
			System.out.println("loading count down " + loading.getCount());
			loading.await();
			System.out.println("loading count down " + loading.getCount());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally {
			if(service != null)
				service.shutdown();
		}
	}
	
	private static void load_single_image(int i, File file) {
		if(fatal.get())
			return;

		Raster raster = null;
		int[] argbArray = null;
		try {
			BufferedImage in_image = ImageIO.read(file);
			final int in_width = in_image.getWidth();
			final int in_height = in_image.getHeight();
			ictx.width = in_width; //TODO
			ictx.height = in_height; //TODO
			final int size = 3 * in_width * in_height;
			argbArray = new int[size];
			raster = in_image.getData();
			raster.getPixels(0, 0, in_width, in_height, argbArray);
			raster = null;
			in_image.flush();
			pd_cube[i] = new PixelData(size, argbArray);
			System.out.print("loaded " + file + '\r');
		} catch (Throwable e) {
			e.printStackTrace();
			fatal.set(true);
			System.exit(1);
		}
		finally {
			loading.countDown();
			argbArray = null;
		}
		System.out.println();
	}

	@SuppressWarnings("rawtypes")
	private static void process_all_frames(Class tf_class, Class tm_class, final TransformAction transformAction) {
		ExecutorService service = null;
		try {
			String tf_name = tf_class.toString().replace("class ", "");
			String tm_name = tm_class.toString().replace("class ", "");
			String out_run = TaskEnvironment.out_path + tf_name + "/" + tm_name + "/";
			create_out_folder(out_run);
			System.out.println("processing all frames");
			service = Executors.newFixedThreadPool(AP);
			int out_len = time_mapper.len();
			processing = new CountDownLatch(out_len);
			for (int di = 0; di < out_len; ++di) {
				final int fdi = di;
				final float fti = time_mapper.di_to_ti(di);
				Runnable task = new Runnable() {
					@Override
					public void run() {
						Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
						process_frame(fdi, fti, out_run, transformAction);
					}
				};
				service.execute(task);
			}
			
			System.out.println("processing count down " + processing.getCount());
			append_video_job(video_job, out_run, tf_name, tm_name);
			processing.await();
			TaskEnvironment.accepted = true;
			System.out.println("processing count down " + processing.getCount());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally {
			if(service != null)
				service.shutdown();
		}
	}

	
	private static void process_frame(int di, float ti, String out_run, final TransformAction transformAction) {
		if(fatal.get())
			return;

		try {
			BufferedImage out_image = new BufferedImage(ictx.width, ictx.height, BufferedImage.TYPE_INT_RGB);
			Point p = new Point();
			p.di = di;
			p.ti = ti;
			for(int y=0; y<ictx.height; ++y) {
				p.dy = y;
				for(int x=0; x<ictx.width; ++x) {
					p.dx = x;
					transform.t(p);					
					int r = clamp255(p.r);
					int g = clamp255(p.g);
					int b = clamp255(p.b);
					out_image.setRGB(x, y, ((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff));
				}
			}

			String output_name = out_run + "frame_" + String.format("%06d", di) + ".png";
			ImageIO.write(out_image, "png", new File(output_name));
			System.out.print("output  = " + output_name + '\r');
			if(transformAction != null)
				transformAction.progress("(" + processing.getCount() +  ") " + output_name);
		} catch (Throwable e) {
			e.printStackTrace();
			fatal.set(true);
			System.exit(2);
		}
		finally {
			processing.countDown();
		}
		System.out.println();
	}

	private static int clamp255(float v) {
		if(v <= 0.0f)
			return 0;
		if(v >= 255.0f)
			return 255;
		return ((int)v) & 0xff;
	}

	public static boolean setInputPath(File path) {
		TaskEnvironment.path = path.toString();
		if(!TaskEnvironment.path.endsWith("/")) { //TODO path separator
			TaskEnvironment.path += "/";
		}
		in_files = FileFilter.listFiles(TaskEnvironment.path, TaskEnvironment.startswith, TaskEnvironment.endswith);
		boolean has_files = checkInput();
		return has_files;
	}

}
