public interface ColourChoiceStrategy {
	
	/**
	 * Method to return the RGB int colour (Spectrum colour) that should be
	 * used for a given pixel based on the Spectrum colours
	 * passed in as mostPopularColour and secondMostPopularColour.
	 * originalRgb is either the original pixel colour or
	 * part converted colour (e.g. a pixel that has already undergone
	 * colour processing). 
	 * 
	 * @param originalRgb the source rgb value
	 * @param mostPopularRgbColours the rgb values from which to choose the closest match
	 * @return the closest colour from the most popular array
	 */
	int chooseBestPaletteMatch(int originalRgb, int[] mostPopularRgbColours);

	int chooseBestPaletteMatch(int rgb);

	int[] getPalette();

	BufferedImage colourAttributes(BufferedImage image);

} public class FullPaletteStrategy implements ColourChoiceStrategy {
	
	public String toString() {
		return getCaption("colour_mode_full");
	}
	
	/*
	 * {@inheritDoc}
	 */
	@Override
	public int chooseBestPaletteMatch(int originalRgb, int[] mostPopularRgbColours) {
		return ColourHelper.getClosestColour(originalRgb, mostPopularRgbColours);
	}

	@Override
	public int chooseBestPaletteMatch(int rgb) {
		return ColourHelper.getClosestColour(rgb, SpectrumDefaults.SPECTRUM_COLOURS_ALL);
	}

	@Override
	public int[] getPalette() {
		return SpectrumDefaults.SPECTRUM_COLOURS_ALL;
	}

	@Override
	public BufferedImage colourAttributes(BufferedImage image) {
		return ColourHelper.colourAttributes(image, this);
	}
} public class GigaScreenPaletteStrategy implements ColourChoiceStrategy {

	private static final int CACHE_TIME_SECONDS = 10;

	private static final Cache<String, GigaScreenAttribute[][]> CACHE = Caffeine.newBuilder().expireAfterAccess(CACHE_TIME_SECONDS, TimeUnit.SECONDS).build();

	public String toString() {
		return getCaption("colour_mode_gigascreen");
	}

	/**
	 * Processing of the colours cannot be applied to both Spectrum screens used in Gigascreen at the same time.
	 * If this method is called then the wrong processor implementation is being used. 
	 */
	@Override
	public int chooseBestPaletteMatch(int originalRgb, int[] mostPopularRgbColours) {
		throw new UnsupportedOperationException("GigaScreen palette colouring cannot be applied to dither image processors - this is a placeholder class to allow identification during getClosestColour conversions for the GigaScreenConverter.");
	}

	@Override
	public int chooseBestPaletteMatch(int rgb) {
		return ColourHelper.getClosestColour(rgb, SpectrumDefaults.GIGASCREEN_COLOURS_ALL);
	}

	@Override
	public int[] getPalette() {
		return SpectrumDefaults.GIGASCREEN_COLOURS_ALL;
	}

	@Override
	public BufferedImage colourAttributes(BufferedImage output) {
		// Algorithm replaces each pixel with the colour from the closest matching
		// 4 colour GigaScreen attribute block.
		GigaScreenAttribute[] palette = OptionsObject.getInstance().getGigaScreenAttributeStrategy().getPalette();
		GigaScreenAttribute[][] quad = getGigaScreenAttributes(output, palette);
		GigaScreenAttribute currentGigaScreenAttribute = null;
		for (int y = 0; y < output.getHeight(); ++y) {
			for (int x = 0; x < output.getWidth(); ++x) {
				if (x % ATTRIBUTE_BLOCK_SIZE == 0) {
					currentGigaScreenAttribute = quad[x / ATTRIBUTE_BLOCK_SIZE][y / ATTRIBUTE_BLOCK_SIZE];
				}
				GigaScreenAttribute.GigaScreenColour c = ColourHelper.getClosestGigaScreenColour(output.getRGB(x, y), currentGigaScreenAttribute);
				output.setRGB(x, y, c.getGigascreenColour());
			}
		}
		return output;

	}

	/**
	 * Creates a map of the 32x24 Spectrum attribute set for two screens.
	 * For each attribute block it finds the closest (best fitting) gigascreen
	 * palette of 4 colours.
	 *
	 * @param original the original image
	 * @return the giga screen attribute array
	 */
	public GigaScreenAttribute[][] getGigaScreenAttributes(BufferedImage original, GigaScreenAttribute[] palette) {
		String key = getKey(original, palette, OptionsObject.getInstance().getGigaScreenAttributeStrategy());
		GigaScreenAttribute[][] entries = CACHE.getIfPresent(key);
		if (entries != null) {
			return entries;
		}
		entries = new GigaScreenAttribute[original.getWidth() / ATTRIBUTE_BLOCK_SIZE][original.getHeight() / ATTRIBUTE_BLOCK_SIZE];
		for (int y = 0; y + ATTRIBUTE_BLOCK_SIZE <= original.getHeight(); y += ATTRIBUTE_BLOCK_SIZE) {
			for (int x = 0; x + ATTRIBUTE_BLOCK_SIZE <= original.getWidth() && y + ATTRIBUTE_BLOCK_SIZE <= original.getHeight(); x += ATTRIBUTE_BLOCK_SIZE) {
				int[] outRgb = original.getRGB(x, y, ATTRIBUTE_BLOCK_SIZE, ATTRIBUTE_BLOCK_SIZE, null, 0, ATTRIBUTE_BLOCK_SIZE);
				double lowest = Double.MAX_VALUE;
				GigaScreenAttribute chosen = palette[0];
				for (GigaScreenAttribute combo : palette) {
					double score = combo.getScoreForAttributeBlock(outRgb);
					if (score < lowest) {
						lowest = score;
						chosen = combo;
					}
				}
				entries[x / ATTRIBUTE_BLOCK_SIZE][y / ATTRIBUTE_BLOCK_SIZE] = chosen;
			}
		}
		CACHE.put(key, entries);
		return entries;
	}

	private String getKey(BufferedImage original, GigaScreenAttribute[] palette, GigaScreenAttributeStrategy attributeStrategy) {
		return original.hashCode()+"-"+ Arrays.hashCode(palette)+"-"+attributeStrategy.hashCode();
	}
} public class MonochromePaletteStrategy implements ColourChoiceStrategy {

	public String toString() {
		return getCaption("colour_mode_mono");
	}

	@Override
	public int chooseBestPaletteMatch(int originalRgb, int[] mostPopularRgbColours){
		return getMonoColour(originalRgb);
	}

	@Override
	public int chooseBestPaletteMatch(int rgb) {
		return getMonoColour(rgb);
	}

	private int getMonoColour(int rgb) {
		OptionsObject oo = OptionsObject.getInstance();
		return getMonochromeColour(rgb,
				SPECTRUM_COLOURS_BRIGHT[oo.getMonochromeInkIndex()],
				SPECTRUM_COLOURS_BRIGHT[oo.getMonochromePaperIndex()]);
	}

	/**
	 * Based on the darkness of the pixel colour determines whether a pixel is
	 * ink or paper and returns that colour. Used for converting colour to
	 * monochrome based on whether a pixel can be considered black using the
	 * isBlack threshold.
	 *
	 * @param rgb the rgb colour to get the monochrome colour from
	 * @param ink the spectrum ink colour
	 * @param paper the spectrum paper colour
	 * @return the ink colour if black, otherwise paper colour
	 */
	public static int getMonochromeColour(int rgb, int ink, int paper) {
		int[] comps = intToRgbComponents(rgb);
		if (rgb == ink || isBlack(comps[0], comps[1], comps[2]))
			return ink;
		return paper;
	}

	/**
	 * Determines whether a pixel is closer to black (than white)
	 *
	 * @param red the red component
	 * @param green the green component
	 * @param blue the blue component
	 * @return whether this component is closer to black than white
	 */
	private static boolean isBlack(int red, int green, int blue) {
		int threshold = OptionsObject.getInstance().getBlackThreshold();
		return red < threshold && green < threshold && blue < threshold;
	}

	@Override
	public int[] getPalette() {
		return SPECTRUM_COLOURS_BRIGHT;
	}

	@Override
	public BufferedImage colourAttributes(BufferedImage image) {
		return ColourHelper.colourAttributes(image, this);
	}
} public class CharacterDitherStrategy implements DitherStrategy {

	@Override
	public String toString() {
		return getCaption("character_dither");
	}

	@Override
	public ImageConverter createImageConverter() {
		return new CharacterConverterImpl();
	}

	@Override
	public ImageConverter createPreviewImageConverter(ColourChoiceStrategy colourChoiceStrategy) {
		return new CharacterConverterImpl(this, colourChoiceStrategy);
	}
} public interface DitherStrategy {

    ImageConverter createImageConverter();

    ImageConverter createPreviewImageConverter(ColourChoiceStrategy colourChoiceStrategy);
} public class HumbleVideoImportEngine implements VideoImportEngine {

	private final Logger log = LoggerFactory.getLogger(this.getClass());

	private volatile boolean cancel = false;

	/*
	 * {@inheritDoc}
	 */
	@Override
	public void convertVideoToImages(File f, boolean singleImage, BlockingQueue<Image> sharedQueue, VideoLoadedLock videoLoadedLock) throws Exception {
		cancel = false;
		Demuxer demuxer = Demuxer.make();
		demuxer.open(f.getAbsolutePath(), null, false, true, null, null);
		long streamStartTime = Global.NO_PTS;
		int videoStreamId = -1;
		Decoder videoDecoder = null;
		Rational streamTimeBase = Global.getDefaultTimeBase();
		for (int i = 0; i < demuxer.getNumStreams(); i++) {
			final DemuxerStream stream = demuxer.getStream(i);
			streamStartTime = stream.getStartTime();
			final Decoder decoder = stream.getDecoder();
			if (decoder != null && decoder.getCodecType() == Type.MEDIA_VIDEO) {
				videoStreamId = i;
				videoDecoder = decoder;
				streamTimeBase = stream.getTimeBase();
				log.debug("Timebase {}", stream.getTimeBase());
				// stop at the first one.
				break;
			}
		}
		videoDecoder.open(null, null);

		final MediaPicture picture = MediaPicture.make(videoDecoder.getWidth(), videoDecoder.getHeight(), videoDecoder.getPixelFormat());
		final MediaPictureConverter converter = MediaPictureConverterFactory.createConverter(MediaPictureConverterFactory.HUMBLE_BGR_24, picture);
		final MediaPacket packet = MediaPacket.make();
		videoLoadedLock.preloadFinished();
		Long nextSnapShotTime = null;
		long sampleRateNanos = Math.round(1000000000d/OptionsObject.getInstance().getVideoFramesPerSecond());
		log.debug("Sampling every {}ns", sampleRateNanos);
		final Rational systemTimeBase = Rational.make(1, 1000000000);
		log.debug("Stream start time {}", streamStartTime);
		try {
			while (demuxer.read(packet) >= 0) {
				// Now we have a packet, let's see if it belongs to our video stream
				if (packet.getStreamIndex() == videoStreamId) {
					/*
					  A packet can actually contain multiple sets of samples (or
					  frames of samples in decoding speak). So, we may need to call
					  decode multiple times at different offsets in the packet's
					  data. We capture that here.
					 */
					int offset = 0;
					int bytesRead = 0;
					do {
						bytesRead += videoDecoder.decode(picture, packet, offset);
						nextSnapShotTime = snapshotImageIfAtSampleTime(picture,systemTimeBase, streamStartTime, streamTimeBase, nextSnapShotTime, sharedQueue, converter, sampleRateNanos);
						if (cancel || singleImage) {
							return;
						}
						offset += bytesRead;
					} while (offset < packet.getSize());
				}
			}	
		} finally {
			do {
				videoDecoder.decode(picture, null, 0);
				snapshotImageIfAtSampleTime(picture,systemTimeBase, streamStartTime, streamTimeBase, nextSnapShotTime, sharedQueue, converter, sampleRateNanos);
			} while (picture.isComplete());
			demuxer.close();
			cancel = false;
		}
	}
	
	/**
	 * Snapshots an image to the sharedQueue if at the requested time interval
	 * 
	 * @param picture from which to get the timestamp
	 * @param systemTimeBase the time base for this system
	 * @param streamStartTime the start time for the stream
	 * @param streamTimeBase the stream time base
	 * @param nextSnapShotTime the time at which the snapshot is expected
	 * @param sharedQueue the shared processing queue
	 * @param converter the media converter for making the snampshot
	 * @param sampleRateNanos the sampling rate for the video in nanoseconds
	 * @return the next snapshot time (current snapshot time  + sampleRateNanos)
	 * @throws InterruptedException if the snapshot cannot be added to the shared queue
	 */
	private Long snapshotImageIfAtSampleTime(MediaPicture picture, Rational systemTimeBase, long streamStartTime, Rational streamTimeBase, Long nextSnapShotTime, 
			BlockingQueue<Image> sharedQueue, MediaPictureConverter converter, long sampleRateNanos) throws InterruptedException {
		if (picture.isComplete()) {
			long streamTimestamp = picture.getTimeStamp();
		    
			// Convert streamTimestamp into system units (i.e. nano-seconds)
			streamTimestamp = systemTimeBase.rescale(streamTimestamp-streamStartTime, streamTimeBase);
		    if (nextSnapShotTime == null || streamTimestamp >= nextSnapShotTime) {
				BufferedImage image = converter.toImage(null, picture);
				boolean added = sharedQueue.offer(image);
				if (!added && !cancel) {
					log.debug("Video queue full at {}... blocking", streamTimestamp);
					sharedQueue.put(image);
				} else {
					log.debug("Video queue had space");
				}
				
				nextSnapShotTime = streamTimestamp+sampleRateNanos;
				log.debug("Timestamp {}, Next snapshot {}", streamTimestamp, nextSnapShotTime);
			}
		}
		return nextSnapShotTime;
	}

	/*
	 * {@inheritDoc}
	 */
	@Override
	public void cancel() {
		this.cancel = true;
	}

	/*
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		return "Humble Video";
	}

	/*
	 * {@inheritDoc}
	 */
	@Override
	public void initVideoImportEngine(Optional<String> pathToLibrary) {
		Thread t = new Thread(() -> {
            try {
                log.debug("Path to library {}", pathToLibrary);
                if (SystemUtils.IS_OS_WINDOWS) {
                    log.debug("Windows OS");
                    exposeNativeLibrary("libhumblevideo-0.dll");
                } else if (SystemUtils.IS_OS_MAC_OSX) {
                    log.debug("Mac OS");
                    exposeNativeLibrary("libhumblevideo.dylib");
                } else if (SystemUtils.IS_OS_LINUX) {
                    log.debug("Linux OS");
                    exposeNativeLibrary("libhumblevideo.so");
                } else {
                    log.error("Unknown OS");
                    throw new IllegalStateException("Unable to determine OS");
                }
                /*
                  Effectively refreshing the library path "stirs the tanks" and preloads the library
                  before its actual usage by JNI in humble video - note the loadClass doesn't actually
                  need doing manually but if we don't the initialisation later of the video decoder
                  is delayed by 5 to 10 seconds and the UI may appear to be frozen.
                 */
                ClassLoader.getSystemClassLoader().loadClass("io.humble.ferry.FerryJNI").newInstance();
            } catch (Throwable t1) {
                log.error("Unable to load native library", t1);
            }
        });
		t.setDaemon(true);
		// This method is usually quite CPU intensive and can even block the GUI rendering 
		// despite being in another thread so ensure it is minimum priority
		t.setPriority(Thread.MIN_PRIORITY);
		t.start();		
	}	
	
	/**
	 * Java cannot load native libraries from jars so we need to expose the library
	 * first by copying it to an external folder and then refreshing the Java library path.
	 * 
	 * @param resourceName the name of the library resource to expose
	 * @return whether the library was successfully exposed
	 */
	private boolean exposeNativeLibrary(String resourceName) {
		try {
			Path tempDir = Files.createTempDirectory("native-", new FileAttribute[0]);
			log.debug("Resource name: {}", resourceName);
			copyLibraryResources(tempDir, resourceName);
			refreshLibraryPath(tempDir);
		} catch (Throwable t) {
			log.warn("Unable to expose library {}", resourceName, t);
			return false;
		}
		return true;
	}
	
	/**
	 * Copies the given jar resource into the temporary directory
	 * 
	 * @param tempDir the temporary directory from which to load the native library
	 * @param resourceName the resource name to get from the jar
	 * @throws IOException if the library copy fails
	 */
	private void copyLibraryResources(Path tempDir, String resourceName) throws IOException {
		String fileSeparator = System.getProperty("file.separator");
		try (InputStream in = ClassLoader.getSystemResourceAsStream(resourceName)) {
			Files.copy(in, new File(tempDir.toFile().getAbsolutePath()+fileSeparator+resourceName).toPath());
		}
	}

	/**
	 * Refreshes the Java library path
	 * 
	 * @param tempDir the temporary directory containing the libraries we want to add
	 * 
	 * @throws IllegalAccessException if the classloader cannot be reset
	 * @throws NoSuchFieldException if the classloader's system path field is not accessible
	 */
	private void refreshLibraryPath(Path tempDir) throws IllegalAccessException, NoSuchFieldException, SecurityException {
		String pathSeparator = System.getProperty("path.separator");
		String oldPath = System.getProperty("java.library.path");
		System.setProperty("java.library.path",  oldPath+pathSeparator+tempDir.toFile().getAbsolutePath());
		Field sysPaths = ClassLoader.class.getDeclaredField("sys_paths");
		sysPaths.setAccessible(true);
		sysPaths.set(null,  null);
	}
} public class VLCVideoImportEngine implements VideoImportEngine {

	private Logger log = LoggerFactory.getLogger(this.getClass());
	
	/**
	 * Amount of time as a minimum before we can take a single image from the
	 * video in seconds
	 */
	private static final int MINIMUM_INTRO_WAIT = (int)TimeUnit.SECONDS.toMillis(3);

	/**
	 * Amount of (up to) random time we add to MINIMUM_INTRO_WAIT before we can
	 * take a single image from the video in seconds.
	 */
	private static final int RANDOM_INTRO_WAIT = (int)TimeUnit.SECONDS.toMillis(5);
	
	/**
	 * Amount of time to wait between checks to see if the video has loaded (there
	 * is no other way unless the VLC UI is displayed).
	 */
	private static final int MEDIA_PRELOAD_WAIT = (int)TimeUnit.SECONDS.toMillis(2);
	
	private volatile boolean cancel = false;
	
	/**
	 * Converts the specified video file to a series of images and adds them to
	 * the given shared queue. 
	 * 
	 * @param f the video file
	 * @param singleImage whether just a single preview image is required
	 * @param sharedQueue the shared processing queue
	 * @throws IOException if there is a problem loading the media file
	 * @throws InterruptedException if loading is interupted
	 */
	public void convertVideoToImages(File f, boolean singleImage, final BlockingQueue<Image> sharedQueue, VideoLoadedLock videoLoadedLock) throws IOException, InterruptedException {
		EmbeddedMediaPlayerComponent mediaPlayerComponent = new EmbeddedMediaPlayerComponent();
		JWindow frame = new JWindow();
		frame.setBounds(0,0,1,1);
		frame.add(mediaPlayerComponent);
		frame.setVisible(true);
		final MediaPlayer player = mediaPlayerComponent.getMediaPlayer();
		player.mute();
		player.playMedia(f.getAbsolutePath());
		long len = player.getLength();
		if (len == 0) {
			Thread.sleep(MEDIA_PRELOAD_WAIT);
			len = player.getLength();
		}
		int singleImageSelectionTime = -1;
		if (singleImage) {
			singleImageSelectionTime = new Random().nextInt(RANDOM_INTRO_WAIT) + MINIMUM_INTRO_WAIT;
			if (singleImageSelectionTime > len) {
				singleImageSelectionTime = (int) len-1;
			}
		}
		frame.setVisible(false);
		log.debug("Player time: {}, Len: {}", player.getTime(), len);
		videoLoadedLock.preloadFinished();
		try {
			while (player.getTime() < len) {
				if (cancel) {
					break;
				}
				
				// Always add images if we don't want a single preview
				// image or when the time exceeds the point we want a
				// single image from
				if (singleImageSelectionTime == -1 || player.getTime() >= singleImageSelectionTime) {
					sharedQueue.add(player.getSnapshot());
	
					// We only want one image if single image selection
					// is turned on
					if (singleImageSelectionTime != -1 && player.getTime() >= singleImageSelectionTime) {
						break;
					}
				}
			}
		} finally {
			player.stop();
			player.release();
			mediaPlayerComponent.release();
			cancel = false;
		}
	}
	
	/*
	 * {@inheritDoc}
	 */
	@Override 
	public String toString() {
		return getCaption("VLC");
	}

	/*
	 * {@inheritDoc}
	 */
	@Override
	public void cancel() {
		this.cancel = true;
	}
	
	/*
	 * {@inheritDoc}
	 */
	@Override
	public void initVideoImportEngine(Optional<String> pathToLibrary) {
		if (pathToLibrary.isPresent()) {
			log.debug("Path to library {}", pathToLibrary);
			NativeLibrary.addSearchPath(RuntimeUtil.getLibVlcLibraryName(), pathToLibrary.get());
			Native.loadLibrary(RuntimeUtil.getLibVlcLibraryName(), LibVlc.class);
			log.debug("Library loaded");
		}
	}
} public interface VideoImportEngine {
	
	/**
	 * Converts a video file to image frames
	 * 
	 * @param f the video file
	 * @param singleImage whether we just need a single image (for preview)
	 * @param sharedQueue the concurrent queue for adding the images to
	 * @param videoLoadedLock lock to delay queue processing until the video is loaded and streaming has started
	 * @throws Exception if conversion fails for any reason
	 */
	void convertVideoToImages(File f, boolean singleImage, final BlockingQueue<Image> sharedQueue, VideoLoadedLock videoLoadedLock) throws Exception;

	/**
	 * Cancels the frame extraction
	 */
	void cancel();

	/**
	 * Initialises the video import engine with optional native library path
	 * 
	 * @param pathToLibrary path to a native library for decoding (implementation specific)
	 */
	void initVideoImportEngine(Optional<String> pathToLibrary);
} public class ClassicColourDistanceStrategy implements ColourDistanceStrategy {

    @Override
    public double getColourDistance(int red, int green, int blue, int[] colourSetComps) {
        return Math.abs(red - colourSetComps[0]) + Math.abs(green - colourSetComps[1]) + Math.abs(blue - colourSetComps[2]);
    }

    @Override
    public String toString() {
        return getCaption("adv_colour_dist_classic") ;
    }
} public interface ColourDistanceStrategy {
    double getColourDistance(int red, int green, int blue, int[] paletteComps);
} public class CompuphaseColourDistanceStrategy implements ColourDistanceStrategy {

    @Override
    public double getColourDistance(int red, int green, int blue, int[] colourSetComps) {
        long rmean = ((long) colourSetComps[0] + (long) red) / 2;
        long r = (long) colourSetComps[0] - (long) red;
        long g = (long) colourSetComps[1] - (long) green;
        long b = (long) colourSetComps[2] - (long) blue;
        return Math.sqrt((((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8));
    }

    @Override
    public String toString() {
        return getCaption("adv_colour_dist_compuphase") ;
    }
} public class EuclideanColourDistance implements ColourDistanceStrategy {

    @Override
    public double getColourDistance(int red, int green, int blue, int[] colourSetComps) {
        return Math.pow(red - colourSetComps[0], 2d) + Math.pow(green - colourSetComps[1], 2d) + Math.pow(blue - colourSetComps[2], 2d);
    }

    @Override
    public String toString() {
        return getCaption("adv_colour_dist_euclidean") ;
    }
} public static String getCaption(String key) {
	String translation = null;
	if (captions != null) {
		try {
			translation = captions.getString(key);
		} catch(MissingResourceException ignore){}
	}
	// We're missing a key's value, use the key as the value :(
	if (translation == null || translation.trim().length()==0) {
		translation=key;
	}
	return translation;
}
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.silentsoftware.core.attributestrategy.*;
import uk.co.silentsoftware.core.colourstrategy.ColourChoiceStrategy;
import uk.co.silentsoftware.core.colourstrategy.FullPaletteStrategy;
import uk.co.silentsoftware.core.colourstrategy.GigaScreenPaletteStrategy;
import uk.co.silentsoftware.core.colourstrategy.MonochromePaletteStrategy;
import uk.co.silentsoftware.core.converters.image.CharacterDitherStrategy;
import uk.co.silentsoftware.core.converters.image.DitherStrategy;
import uk.co.silentsoftware.core.converters.image.errordiffusionstrategy.*;
import uk.co.silentsoftware.core.converters.image.orderedditherstrategy.*;
import uk.co.silentsoftware.core.converters.video.HumbleVideoImportEngine;
import uk.co.silentsoftware.core.converters.video.VLCVideoImportEngine;
import uk.co.silentsoftware.core.converters.video.VideoImportEngine;
import uk.co.silentsoftware.core.helpers.colourdistance.ClassicColourDistanceStrategy;
import uk.co.silentsoftware.core.helpers.colourdistance.ColourDistanceStrategy;
import uk.co.silentsoftware.core.helpers.colourdistance.CompuphaseColourDistanceStrategy;
import uk.co.silentsoftware.core.helpers.colourdistance.EuclideanColourDistance;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static uk.co.silentsoftware.config.LanguageSupport.getCaption;
/* Image to ZX Spec
 * Copyright (C) 2020 Silent Software (Benjamin Brown)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.co.silentsoftware.config;




/**
 * The monolithic backing object behind the OptionDialog, holding the particular user
 * configuration. This object's marked @PreferencesFields are primitives which are 
 * persisted after modification by the UI.
 */
public class OptionsObject {

	private static final Logger log = LoggerFactory.getLogger(OptionsObject.class);
	
	/**
	 * The version of preferences
	 */
	@PreferencesField
	private int prefsVersion = 1;
	
	/**
	 * The number of starts this app has had
	 */
	@PreferencesField
	private int starts = 0;
	
	/**
	 * Path to the VLC binary
	 */
	@PreferencesField
	private String pathToVideoEngineLibrary = null;

	/**
	 * The number of frames to sample from the video per second
	 */
	@PreferencesField
	private volatile double videoFramesPerSecond = 10;

	/**
	 * The number of milliseconds delay for each gif frame
	 */
	@PreferencesField
	private volatile int gifDisplayTimeMillis = 100;

	/**
	 * CPU time is diverted more to processing than rendering or other less important tasks
	 */
	@PreferencesField
	private volatile boolean turboMode = false;

	/**
	 * Prefix identifier for custom basic loaders
	 */
	public final static String CUSTOM_LOADER_PREFIX = getCaption("loader_custom") + " ";

	/**
	 * Basic loader for slideshows/video (tap output)
	 */
	private final List<BasicLoader> basicLoaders;
	{
		basicLoaders = new ArrayList<>();
		basicLoaders.add(new BasicLoader(getCaption("loader_simple"), "/simple.tap"));
		basicLoaders.add(new BasicLoader(getCaption("loader_buffered"), "/buffered.tap"));
		basicLoaders.add(new BasicLoader(getCaption("loader_gigascreen"), "/gigascreen.tap"));
		basicLoaders.add(new BasicLoader(CUSTOM_LOADER_PREFIX, null));
	}

	/**
	 * The chosen basic loader
	 */
	@PreferencesField
	private volatile int basicLoader = 0;

	/**
	 * Error diffusion dither strategies available
	 */
	private final List<ErrorDiffusionDitherStrategy> errorDithers;
	{
		errorDithers = new ArrayList<>();
		errorDithers.add(new AtkinsonDitherStrategy());
		errorDithers.add(new BurkesDitherStrategy());
		errorDithers.add(new FloydSteinbergDitherStrategy());
		errorDithers.add(new JarvisJudiceNinkeDitherStrategy());
		errorDithers.add(new LowErrorAtkinsonDitherStrategy());
		errorDithers.add(new NoDitherStrategy());
		errorDithers.add(new SierraFilterLightStrategy());
		errorDithers.add(new StuckiDitherStrategy());
	}

	/**
	 * Ordered dither strategies available
	 */
	private final List<OrderedDitherStrategy> orderedDithers;
	{
		orderedDithers = new ArrayList<>();
		orderedDithers.add(new BayerTwoByOneOrderedDitherStrategy());
		orderedDithers.add(new BayerTwoByTwoOrderedDitherStrategy());
		orderedDithers.add(new OmegaOrderedDitherStrategy()); 
		orderedDithers.add(new BayerFourByFourDitherStrategy());
		orderedDithers.add(new BayerEightByEightDitherStrategy());
		orderedDithers.add(new MagicSquareDitherStrategy());
		orderedDithers.add(new NasikMagicSquareDitherStrategy());
	}

	/**
	 * Any non standard dither strategies (e.g character dither)
	 */
	private final List<DitherStrategy> otherDithers;
	{
		otherDithers = new ArrayList<>();
		otherDithers.add(new CharacterDitherStrategy());
	}
	
	/**
	 * The chosen dither strategy
	 */
	@PreferencesField
	private volatile int selectedDitherStrategy = 0;

	/**
	 * The chosen dither strategy type
	 */
	@PreferencesField
	private volatile String selectedDitherStrategyType = ErrorDiffusionDitherStrategy.class.getName();

	public final static ScalingObject INTERLACED = new ScalingObject(getCaption("scaling_interlace"),
			SpectrumDefaults.SCREEN_WIDTH, SpectrumDefaults.SCREEN_HEIGHT*2);

	/**
	 * Scaling modes available
	 */
	private final List<ScalingObject> scalings;
	{
		scalings = new ArrayList<>();
		scalings.add(new ScalingObject(getCaption("scaling_default"), SpectrumDefaults.SCREEN_WIDTH, SpectrumDefaults.SCREEN_HEIGHT));
		scalings.add(INTERLACED);
		scalings.add(new ScalingObject(getCaption("scaling_none"), -1, -1));
		scalings.add(new ScalingObject(getCaption("scaling_width_prop"), SpectrumDefaults.SCREEN_WIDTH, -1));
		scalings.add(new ScalingObject(getCaption("scaling_height_prop"), -1, SpectrumDefaults.SCREEN_HEIGHT));
	}

	/**
	 * ZX Spectrum scaling mode
	 */
	public final ScalingObject zxScaling = scalings.get(0);	
	
	/**
	 * Currently selected scaling mode
	 */
	@PreferencesField
	public volatile int scaling = 0;

	public static final GigaScreenPaletteStrategy GIGASCREEN_PALETTE_STRATEGY = new GigaScreenPaletteStrategy();

	/**
	 * Pixel colouring strategy - akin to screen modes on the Spectrum
	 * i.e. 2, 15 or 102 colours.
	 */
	private final List<ColourChoiceStrategy> colourModes;
	{
		colourModes = new ArrayList<>();
		colourModes.add(new FullPaletteStrategy());
		colourModes.add(GIGASCREEN_PALETTE_STRATEGY);
		colourModes.add(new MonochromePaletteStrategy());
	}

	/**
	 * Currently selected pixel colouring strategy
	 */
	@PreferencesField
	private volatile int colourMode = 0;

	/**
	 * Attribute favouritism choice - when colours need to be changed to a two
	 * colour attribute block of 8x8 what is favoured?
	 */
	private final List<AttributeStrategy> attributeModes;
	{
		attributeModes = new ArrayList<>();
		attributeModes.add(new FavourHalfBrightAttributeStrategy());
		attributeModes.add(new FavourBrightAttributeStrategy());
		attributeModes.add(new FavourMostPopularAttributeStrategy());
		attributeModes.add(new ForceHalfBrightAttributeStrategy()); 
		attributeModes.add(new ForceBrightAttributeStrategy());
		attributeModes.add(new ForceReducedHalfBrightAttributeStrategy());
	};

	/**
	 * Currently selected attribute favouritism mode
	 */
	@PreferencesField
	private volatile int attributeMode = 0;

	/**
	 * Attribute favouritism choice for Gigascreen. This is similar to the regular
	 * attribute modes but needs to take account for the fact that the extra
	 * colours are created by using two Spectrum screens (e.g. black+white full bright = grey)
	 */
	private final List<GigaScreenAttributeStrategy> gigaScreenAttributeModes;
	{
		gigaScreenAttributeModes = new ArrayList<>();
		gigaScreenAttributeModes.add(new GigaScreenHalfBrightPaletteStrategy());
		gigaScreenAttributeModes.add(new GigaScreenBrightPaletteStrategy());
		gigaScreenAttributeModes.add(new GigaScreenMixedPaletteStrategy());
	};

	private final List<ColourDistanceStrategy> colourDistancesModes;