package picard.illumina.parser;

import htsjdk.samtools.util.IOUtil;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import picard.PicardException;
import picard.illumina.parser.IlluminaFileUtil.SupportedIlluminaFormat;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static htsjdk.samtools.util.CollectionUtil.makeList;

public class IlluminaFileUtilTest {
    private static final int DEFAULT_LANE = 7;
    private static final List<Integer> DEFAULT_TILES = makeList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
    private static final List<Integer> DEFAULT_TILE_TEST_SUBSET = makeList(1, 4, 5, 6, 9, 10);
    private static final int[] DEFAULT_CYCLES = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
    private static final int DEFAULT_LAST_CYCLE = 20;

    // We have data for these that should agree
    private static final List<SupportedIlluminaFormat> FORMATS_TO_TEST = Arrays.asList(
            SupportedIlluminaFormat.Bcl,
            SupportedIlluminaFormat.Locs,
            SupportedIlluminaFormat.Clocs,
            SupportedIlluminaFormat.Pos,
            SupportedIlluminaFormat.Filter,
            SupportedIlluminaFormat.Barcode);


    private File intensityDir;
    private File basecallDir;

    @BeforeMethod
    private void setUp() throws Exception {
        intensityDir = IOUtil.createTempDir("ift_test", "Intensities");
        basecallDir = new File(intensityDir, "BaseCalls");
        if (!basecallDir.mkdir()) {
            throw new RuntimeException("Couldn't make basecalls dir " + basecallDir.getAbsolutePath());
        }
    }

    @AfterMethod
    private void tearDown() {
        IOUtil.deleteDirectoryTree(intensityDir);
    }

    @DataProvider(name = "validLanes")
    public Object[][] validLanes() {
        return new Object[][]{
                {0, "s_0_1111.test"},
                {1, "s_1_23.test"},
                {10, "s_10_1.test"}
        };
    }

    public void regexMatches(final String regex, final String toMatch) {
        regexMatches(regex, toMatch, true);
    }

    public void regexMatches(final String regex, final String toMatch, final boolean expectedResult) {
        final Pattern pt = Pattern.compile(regex);
        final Matcher ma = pt.matcher(toMatch);
        Assert.assertEquals(ma.matches(), expectedResult);
    }

    @Test(dataProvider = "validLanes")
    public void regexTests(final int lane, final String ltExample) {
        regexMatches(ParameterizedFileUtil.makeLaneTileRegex(".test", lane), ltExample);
    }

    @DataProvider(name = "validLanesInvalidRegexes")
    public Object[][] validLanesInvalidRegexes() {
        return new Object[][]{
                {0, "s_-0_111"},
                {1, "s_1_A3"},
                {10, "s_-100_1"},
                {20, "s_21_1"}
        };
    }

    @Test(dataProvider = "validLanesInvalidRegexes")
    public void notMatchingRegexTest(final int lane, final String ltExample) {
        regexMatches(ParameterizedFileUtil.makeLaneTileRegex(".test", lane) , ltExample, false);
    }

    @DataProvider(name = "invalidLanes")
    public Object[][] invalidLanes() {
        return new Object[][]{
                {-1000},
                {-10},
                {-1}
        };
    }

    @Test(dataProvider = "invalidLanes", expectedExceptions = PicardException.class)
    public void invalidLaneForLTRegex(final int lane) {
        ParameterizedFileUtil.makeLaneTileRegex(".test", lane);
    }

    public void assertDefaults(final IlluminaFileUtil fileUtil, final Integer lane, final List<SupportedIlluminaFormat> formatsToTest) {
        if (lane == null) {
            Assert.assertEquals(fileUtil.getLane(), DEFAULT_LANE);
        } else {
            Assert.assertEquals(new Integer(fileUtil.getLane()), lane);
        }

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Barcode).getTiles(), DEFAULT_TILES);

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Bcl).getTiles(), DEFAULT_TILES);

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Pos).getTiles(), DEFAULT_TILES);

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Locs).getTiles(), DEFAULT_TILES);

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Clocs).getTiles(), DEFAULT_TILES);

        Assert.assertEquals(fileUtil.getUtil(SupportedIlluminaFormat.Filter).getTiles(), DEFAULT_TILES);

        final Set<Integer> detectedCycles = ((PerTilePerCycleFileUtil) fileUtil.getUtil(SupportedIlluminaFormat.Bcl)).getDetectedCycles();
        Assert.assertEquals(detectedCycles.size(), DEFAULT_CYCLES.length);
        int i = 0;
        for(final Integer cycle : detectedCycles){
            Assert.assertEquals(cycle.intValue(), DEFAULT_CYCLES[i++], "Elements differ at index " + i);
        }

        Assert.assertEquals(fileUtil.getActualTiles(formatsToTest), DEFAULT_TILES);
    }

    @Test
    public void passNewUtilTest() {
        for (final SupportedIlluminaFormat format : SupportedIlluminaFormat.values()) {
            makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, DEFAULT_CYCLES);
            makeFiles(format, intensityDir, DEFAULT_LANE + 1, DEFAULT_TILES, DEFAULT_CYCLES, ".gz");
            makeFiles(format, intensityDir, DEFAULT_LANE + 2, DEFAULT_TILES, DEFAULT_CYCLES, ".bz2");
        }

        final Set<SupportedIlluminaFormat> formatsToTest = new HashSet<SupportedIlluminaFormat>();
        // TODO: I can't be bothered to build files for these.  AW
        Collections.addAll(formatsToTest, SupportedIlluminaFormat.values());
        formatsToTest.remove(SupportedIlluminaFormat.MultiTileBcl);
        formatsToTest.remove(SupportedIlluminaFormat.MultiTileFilter);
        formatsToTest.remove(SupportedIlluminaFormat.MultiTileLocs);
        final ArrayList<SupportedIlluminaFormat> formatsList = new ArrayList<SupportedIlluminaFormat>(formatsToTest);

        for (int i = 0; i < 3; i++) {
            final IlluminaFileUtil fileUtil = new IlluminaFileUtil(new File(intensityDir, "BaseCalls"), DEFAULT_LANE + i);
            Assert.assertEquals(fileUtil.getActualTiles(formatsList), DEFAULT_TILES);
            assertDefaults(fileUtil, DEFAULT_LANE + i, formatsList);
        }
    }

    @Test
    public void passingVerifyTest() {
        for (final SupportedIlluminaFormat format : SupportedIlluminaFormat.values()) {
            makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, DEFAULT_CYCLES);
            makeFiles(format, intensityDir, DEFAULT_LANE + 1, DEFAULT_TILES, DEFAULT_CYCLES, ".gz");
            makeFiles(format, intensityDir, DEFAULT_LANE + 2, DEFAULT_TILES, DEFAULT_CYCLES, ".bz2");
        }

        for (int i = 0; i < 3; i++) {
            final IlluminaFileUtil fileUtil = new IlluminaFileUtil(new File(intensityDir, "BaseCalls"), DEFAULT_LANE + i);


            for (final SupportedIlluminaFormat format : FORMATS_TO_TEST) {
                Assert.assertEquals(new ArrayList<String>(), fileUtil.getUtil(format).verify(DEFAULT_TILES, DEFAULT_CYCLES));
            }
        }
    }

    @DataProvider(name = "missingTileFormats")
    public Object[][] missingTileFormats() {
        return new Object[][]{
                {
                        1,
                        makeList(SupportedIlluminaFormat.Bcl, SupportedIlluminaFormat.Barcode),
                        makeList(SupportedIlluminaFormat.Bcl, SupportedIlluminaFormat.Barcode),
                        makeList("BaseCalls/s_1_0007_barcode.txt.gz"),
                        ".gz"
                },

                {
                        2,
                        Arrays.asList(SupportedIlluminaFormat.values()),
                        Arrays.asList(SupportedIlluminaFormat.values()),
                        makeCycleFileList(new File("BaseCalls"), ".bcl", 2, DEFAULT_CYCLES, 2),
                        ".gz"
                },
                {
                        3,
                        Arrays.asList(SupportedIlluminaFormat.values()),
                        Arrays.asList(SupportedIlluminaFormat.values()),
                        makeList("BaseCalls/L003/C1.1/s_3_2.bcl"),
                        ".bz2"
                },
                {
                        4,
                        Arrays.asList(SupportedIlluminaFormat.values()),
                        Arrays.asList(SupportedIlluminaFormat.Pos, SupportedIlluminaFormat.Locs),
                        makeList("s_4_10_pos.txt", "L004/s_4_2.locs"),
                        null
                }
        };
    }

    public static void emptyRelativeFiles(final File baseFile, final List<String> relativeFilesToDelete) {
        for (final String relativeFile : relativeFilesToDelete) {
            final File actualFile = new File(baseFile, relativeFile);


            if (!actualFile.exists()) {
                throw new RuntimeException("Trying to empty a non-existent file" + actualFile.getAbsolutePath());
            }

            if (actualFile.isDirectory()) {
                throw new RuntimeException("Trying to empty a directory(" + actualFile.getAbsolutePath() + ")");
            } else {
                if (!actualFile.delete()) {
                    throw new RuntimeException("Couldn't remove previous file when emptying(" + actualFile.getAbsolutePath() + ")");
                } else {
                    try {
                        if (!actualFile.createNewFile()) {
                            throw new RuntimeException("Couldn't create empty file: " + actualFile.getAbsolutePath() + ")");
                        }
                    } catch (final IOException ioe) {
                        throw new RuntimeException(ioe);
                    }
                }
            }
            if (!actualFile.exists()) {
                throw new PicardException("File should exist: " + actualFile);
            }
        }
    }

    public static void deleteRelativeFiles(final File baseFile, final List<String> relativeFilesToDelete) {
        for (final String relativeFile : relativeFilesToDelete) {
            final File actualFile = new File(baseFile, relativeFile);


            if (!actualFile.exists()) {
                throw new RuntimeException("Trying to delete a non-existent file" + actualFile.getAbsolutePath());
            }

            if (actualFile.isDirectory()) {
                IOUtil.deleteDirectoryTree(actualFile);
            } else {
                IOUtil.deleteFiles(actualFile);
            }
            if (actualFile.exists()) {
                throw new RuntimeException("File still exists after calling delete: " + actualFile);
            }
        }
    }

    public final void deleteRelativeFiles(final List<String> relativeFilesToDelete) {
        deleteRelativeFiles(intensityDir, relativeFilesToDelete);
    }

    @Test(dataProvider = "missingTileFormats")
    public void missingTileTest(final int lane,
                                final List<SupportedIlluminaFormat> formats,
                                final List<SupportedIlluminaFormat> formatsToGetTiles,
                                final List<String> relativeFilesToDelete,
                                final String compression) {
        for (final SupportedIlluminaFormat format : formats) {
            makeFiles(format, intensityDir, lane, DEFAULT_TILES, DEFAULT_CYCLES, compression);
        }

        deleteRelativeFiles(relativeFilesToDelete);

        PicardException pExc = null;
        try {
            final IlluminaFileUtil fUtil = new IlluminaFileUtil(new File(intensityDir, "BaseCalls"), lane);
            fUtil.getActualTiles(formatsToGetTiles);
        } catch (final PicardException exception) {
            pExc = exception;
        }

        Assert.assertNotNull(pExc, "Didn't raise a Picard Exception for missing tile!");
        Assert.assertTrue(pExc.getMessage().contains("Formats do not have the same number of tiles! "), "Wrong exception thrown for missing tile!");
    }

    @DataProvider(name = "perTileFileFormats")
    public Object[][] perTileFileUtils() {
        return new Object[][]{
                {SupportedIlluminaFormat.Locs, null, false, laneDir(DEFAULT_LANE)},
                {SupportedIlluminaFormat.Clocs, null, false, laneDir(DEFAULT_LANE)},
                {SupportedIlluminaFormat.Pos, ".gz", false, null},
                {SupportedIlluminaFormat.Pos, null, false, null},
                {SupportedIlluminaFormat.Filter, null, true, "BaseCalls/" + laneDir(DEFAULT_LANE)},
                {SupportedIlluminaFormat.Barcode, ".bz2", true, "BaseCalls"}
        };
    }

    public File makePerTileFile(final File parentDir, final int lane, final int tile, final String extension, final String compression, final boolean longFormat) {
        return new File(parentDir, "s_" + lane + "_" + longTile(tile, longFormat) + extension + (compression != null ? compression : ""));
    }

    public void testDefaultPerTileUtil(final PerTileFileUtil ptfu, final String compression, final boolean longFormat, final File parentDir) {
        final IlluminaFileMap fm = ptfu.getFiles();
        final IlluminaFileMap fmWTiles = ptfu.getFiles(DEFAULT_TILES);

        Assert.assertEquals(fm.size(), DEFAULT_TILES.size());

        for (final Integer tile : DEFAULT_TILES) {
            final File tFile = fm.get(tile);
            final File tFile2 = fmWTiles.get(tile);
            Assert.assertEquals(tFile.getAbsolutePath(), tFile2.getAbsolutePath());
            Assert.assertEquals(tFile, makePerTileFile(parentDir, DEFAULT_LANE, tile, ptfu.extension, compression, longFormat));
            Assert.assertTrue(tFile.exists());
            Assert.assertTrue(tFile.length() > 0);
        }

        final List<Integer> tiles = new ArrayList<Integer>(DEFAULT_TILE_TEST_SUBSET);
        final IlluminaFileMap subsetMap = ptfu.getFiles(DEFAULT_TILE_TEST_SUBSET);
        for (final Integer tile : subsetMap.keySet()) {
            tiles.remove(tile);
            Assert.assertTrue(DEFAULT_TILE_TEST_SUBSET.contains(tile));
            final File tFile = subsetMap.get(tile);
            Assert.assertEquals(tFile, makePerTileFile(parentDir, DEFAULT_LANE, tile, ptfu.extension, compression, longFormat));
            Assert.assertTrue(tFile.exists());
            Assert.assertTrue(tFile.length() > 0);
        }

        Assert.assertTrue(tiles.isEmpty());
    }

    @Test(dataProvider = "perTileFileFormats")
    public void perTileFileUtilsTest(final SupportedIlluminaFormat format, final String compression, final boolean longFormat, final String parentDir) {
        makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, DEFAULT_CYCLES, compression);

        final IlluminaFileUtil fileUtil = new IlluminaFileUtil(basecallDir, DEFAULT_LANE);
        final PerTileFileUtil ptfu = (PerTileFileUtil) fileUtil.getUtil(format);

        Assert.assertTrue(ptfu.filesAvailable());
        testDefaultPerTileUtil(ptfu, compression, longFormat, (parentDir == null) ? intensityDir : new File(intensityDir, parentDir));

        final IlluminaFileUtil noFilesFu = new IlluminaFileUtil(basecallDir, DEFAULT_LANE + 20);
        final PerTileFileUtil noFilesPtfu = (PerTileFileUtil) noFilesFu.getUtil(format);
        Assert.assertFalse(noFilesPtfu.filesAvailable());
        Assert.assertTrue(noFilesPtfu.getFiles().isEmpty());
        Assert.assertTrue(noFilesPtfu.getFiles(DEFAULT_TILES).isEmpty());
    }

    public File makePerTilePerCycleFilePath(final File parentDir, final int lane, final int tile, final int cycle, final String extension) {
        return new File(parentDir, "C" + cycle + ".1/s_" + lane + "_" + tile + extension);
    }

    public void testDefaultPerTilePerCycleUtil(final PerTilePerCycleFileUtil pcfu, final File parentDir, final int[] cycles) {
        final CycleIlluminaFileMap cfm = pcfu.getFiles(cycles);
        final CycleIlluminaFileMap cfmWTiles = pcfu.getFiles(DEFAULT_TILES, cycles);
        final CycleIlluminaFileMap cfmNoCycles;
        if (Arrays.equals(cycles, DEFAULT_CYCLES)) {
            cfmNoCycles = pcfu.getFiles();
        } else {
            cfmNoCycles = null;
        }

        Assert.assertEquals(cfm.size(), cycles.length);

        for (final int cycle : cycles) {
            final IlluminaFileMap tFileIter = cfm.get(cycle);
            final IlluminaFileMap tFileIter2 = cfmWTiles.get(cycle);
            final IlluminaFileMap tFileIter3;
            if (cfmNoCycles != null) {
                tFileIter3 = cfmNoCycles.get(cycle);
            } else {
                tFileIter3 = null;
            }

            for (final Integer tile : DEFAULT_TILES) {
                final File tcFile = tFileIter.get(tile);
                final File tcFile2 = tFileIter2.get(tile);

                Assert.assertEquals(tcFile.getAbsolutePath(), tcFile2.getAbsolutePath());
                if (tFileIter3 != null) {
                    final File tfFile3 = tFileIter3.get(tile);
                    Assert.assertEquals(tcFile.getAbsolutePath(), tfFile3.getAbsolutePath());
                }

                Assert.assertEquals(tcFile, makePerTilePerCycleFilePath(parentDir, DEFAULT_LANE, tile, cycle, pcfu.extension));
                Assert.assertTrue(tcFile.exists());
                Assert.assertTrue(tcFile.length() > 0);
            }
        }
    }


    public void testSubsetDefaultPerTilePerCycleUtil(final PerTilePerCycleFileUtil pcfu, final File parentDir, final int[] cycles) {
        final List<Integer> tiles = new ArrayList<Integer>(DEFAULT_TILE_TEST_SUBSET);
        final CycleIlluminaFileMap subsetMap = pcfu.getFiles(DEFAULT_TILE_TEST_SUBSET, cycles);
        final CycleIlluminaFileMap cfmNoCycles;
        if (Arrays.equals(cycles, DEFAULT_CYCLES)) {
            cfmNoCycles = pcfu.getFiles(DEFAULT_TILE_TEST_SUBSET);
        } else {
            cfmNoCycles = null;
        }

        for (final int cycle : cycles) {
            final IlluminaFileMap tFileIter = subsetMap.get(cycle);
            final IlluminaFileMap tFileIter2;
            if (cfmNoCycles != null) {
                tFileIter2 = cfmNoCycles.get(cycle);
            } else {
                tFileIter2 = null;
            }


            for (final Integer tile : subsetMap.get(cycle).keySet()) {
                Assert.assertTrue(DEFAULT_TILE_TEST_SUBSET.contains(tile));
                tiles.remove(tile);
                final File tcFile = tFileIter.get(tile);
                if (tFileIter2 != null) {
                    Assert.assertEquals(tcFile, tFileIter2.get(tile));
                }
                Assert.assertEquals(tcFile, makePerTilePerCycleFilePath(parentDir, DEFAULT_LANE, tile, cycle, pcfu.extension));
                Assert.assertTrue(tcFile.exists());
                Assert.assertTrue(tcFile.length() > 0);
            }
        }

        Assert.assertTrue(tiles.isEmpty());
    }

    public static int[] cycleRange(final Range range) {
        return cycleRange(range.start, range.end);
    }

    public static int[] cycleRange(final int start, final int end) {
        final int[] cycles = new int[end - start + 1];
        for (int i = 0; i < cycles.length; i++) {
            cycles[i] = start + i;
        }

        return cycles;
    }

    public static int[] cycleRange(final int end) {
        return cycleRange(1, end);
    }

    @DataProvider(name = "perTilePerCycleFileFormats")
    public Object[][] perTilePerCycleFileFormats() {
        return new Object[][]{
                {SupportedIlluminaFormat.Bcl, "BaseCalls/" + laneDir(DEFAULT_LANE), DEFAULT_CYCLES, false, false},
                {SupportedIlluminaFormat.Bcl, "BaseCalls/" + laneDir(DEFAULT_LANE), cycleRange(4), true, true},
        };
    }

    @Test(dataProvider = "perTilePerCycleFileFormats")
    public void perTilePerCycleFileUtilsTest(final SupportedIlluminaFormat format, final String parentDir,
                                             final int[] cycles, final boolean createEarlySkippedCycles,
                                             final boolean createLateSkippedCycles) {
        if (createEarlySkippedCycles) {
            makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, cycleRange(1, cycles[0]), null);
        }

        makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, cycles, null);

        if (createLateSkippedCycles) {
            makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, cycleRange(cycles[cycles.length - 1] + 1, DEFAULT_LAST_CYCLE), null);
        }

        final IlluminaFileUtil fileUtil = new IlluminaFileUtil(basecallDir, DEFAULT_LANE);
        final PerTilePerCycleFileUtil pcfu = (PerTilePerCycleFileUtil) fileUtil.getUtil(format);

        Assert.assertTrue(pcfu.filesAvailable());
        testDefaultPerTilePerCycleUtil(pcfu, (parentDir == null) ? intensityDir : new File(intensityDir, parentDir), cycles);
        testSubsetDefaultPerTilePerCycleUtil(pcfu, (parentDir == null) ? intensityDir : new File(intensityDir, parentDir), cycles);

        final IlluminaFileUtil noFilesFu = new IlluminaFileUtil(basecallDir, DEFAULT_LANE + 20);
        final PerTilePerCycleFileUtil noFilesPcfu = (PerTilePerCycleFileUtil) noFilesFu.getUtil(format);

        Assert.assertFalse(noFilesPcfu.filesAvailable());
        Assert.assertTrue(noFilesPcfu.getFiles().isEmpty());
        Assert.assertTrue(noFilesPcfu.getFiles(DEFAULT_TILES).isEmpty());
    }

    @DataProvider(name = "missingCycleDataRanges")
    public Object[][] missingCycleDataRanges() {
        return new Object[][]{
                {makeList(new Range(10, 15))},
                {makeList(new Range(9, 12), new Range(14, 15))}
        };
    }

    @Test(expectedExceptions = PicardException.class, dataProvider = "missingCycleDataRanges")
    public void perTilePerCycleFileUtilsMissingCycleTest(final List<Range> cycleRangesToMake) {
        final SupportedIlluminaFormat format = SupportedIlluminaFormat.Bcl;

        for (final Range range : cycleRangesToMake) {
            makeFiles(format, intensityDir, DEFAULT_LANE, DEFAULT_TILES, cycleRange(range), null);
        }

        final IlluminaFileUtil fileUtil = new IlluminaFileUtil(basecallDir, DEFAULT_LANE);
        final PerTilePerCycleFileUtil pcfu = (PerTilePerCycleFileUtil) fileUtil.getUtil(format);

        Assert.assertTrue(pcfu.filesAvailable());
        final int[] cycles = cycleRange(9, 16);
        final CycleIlluminaFileMap cfm = pcfu.getFiles(cycles);
        cfm.assertValid(DEFAULT_TILES, cycles);
    }


    public static void makeFiles(final SupportedIlluminaFormat format, final File intensityDir, final int lane,
                                 final List<Integer> tiles, final int[] cycles) {
        makeFiles(format, intensityDir, lane, tiles, cycles, null);
    }

    public static void makeFiles(final SupportedIlluminaFormat format, final File intensityDir, final int lane,
                                 final List<Integer> tiles, final int[] cycles, final String compression) {
        String laneDir = String.valueOf(lane);
        while (laneDir.length() < 3) {
            laneDir = "0" + laneDir;
        }
        laneDir = "L" + laneDir;


        final File basecallDir = new File(intensityDir, "BaseCalls");
        final File basecallLaneDir = new File(basecallDir, laneDir);
        final File intensityLaneDir = new File(intensityDir, laneDir);

        switch (format) {
            //per tile formats
            case Barcode:
                makePerTileFiles(basecallDir, lane, tiles, maybeAddExt("_barcode.txt", compression), true);
                break;

            case Pos:
                makePerTileFiles(intensityDir, lane, tiles, maybeAddExt("_pos.txt", compression), false);
                break;

            case Locs:
                makePerTileFiles(intensityLaneDir, lane, tiles, maybeAddExt(".locs", null), false);
                break;

            case Clocs:
                makePerTileFiles(intensityLaneDir, lane, tiles, maybeAddExt(".clocs", null), false);
                break;

            case Filter:
                makePerTileFiles(basecallLaneDir, lane, tiles, maybeAddExt(".filter", null), true);
                break;

            //per tile per cycle formats
            case Bcl:
                makePerTilePerCycleFiles(basecallLaneDir, lane, tiles, cycles, ".bcl");
                break;
        }
    }

    private static void makePerTileFiles(final File parentDir, final int lane, final List<Integer> tiles, final String ext, final boolean longName) {
        if (!parentDir.exists()) {
            if (!parentDir.mkdir()) {
                throw new RuntimeException("Couldn't create directory " + parentDir.getAbsolutePath());
            }
        }

        for (final Integer tile : tiles) {
            writeNonEmptyFile(new File(parentDir, "s_" + lane + "_" + longTile(tile, longName) + ext));
        }
    }

    private static void makePerTilePerCycleFiles(final File parentDir, final int lane, final List<Integer> tiles, final int[] cycles, final String ext) {
        if (!parentDir.exists()) {
            if (!parentDir.mkdir()) {
                throw new RuntimeException("Couldn't create directory " + parentDir.getAbsolutePath());
            }
        }

        for (final int cycle : cycles) {
            final File cycleDir = new File(parentDir, "C" + cycle + ".1");
            if (!cycleDir.exists()) {
                if (!cycleDir.mkdir()) {
                    throw new RuntimeException("Couldn't create directory " + cycleDir.getAbsolutePath());
                }
            }

            for (final Integer tile : tiles) {
                writeNonEmptyFile(new File(cycleDir, "s_" + lane + "_" + tile + ext));
            }
        }
    }

    private static List<String> makeCycleFileList(final File dir, final String ext, final int lane, final int[] cycles, final int... tiles) {
        return makeCycleFileList(dir, ext, lane, cycles, false, tiles);
    }

    private static List<String> makeCycleFileList(final File dir, final String ext, final int lane, final int[] cycles, final boolean longFmt, final int... tiles) {
        final List<String> files = new ArrayList<String>();
        final File laneDir = new File(dir, laneDir(lane));

        for (final int cycle : cycles) {
            final File cycleDir = new File(laneDir, "C" + cycle + ".1");
            for (final Integer tile : tiles) {
                files.add(cycleDir + "/s_" + lane + "_" + longTile(tile, longFmt) + ext);
            }
        }

        return files;
    }

    private static void writeNonEmptyFile(final File file) {
        try {
            final OutputStream outputStream = new DataOutputStream(new FileOutputStream(file));
            final int expectedLength = 10;
            outputStream.write(expectedLength);
            // The negative beginning index is to accommodate the header. Fancy. Ever so fancy.
            for (int i = -3; i < expectedLength; i++) outputStream.write(0x0);
            outputStream.close();
        } catch (final IOException e) {
            throw new RuntimeException("Exception trying to create non-empty file!", e);
        }
    }

    private static String laneDir(final int lane) {
        String ldir = String.valueOf(lane);
        while (ldir.length() < 3) {
            ldir = "0" + ldir;
        }
        return "L" + ldir;
    }

    private static String longTile(final int tile, final boolean makeLong) {
        if (makeLong) {
            String lt = String.valueOf(tile);
            while (lt.length() < 4) {
                lt = "0" + lt;
            }
            return lt;
        } else {
            return String.valueOf(tile);
        }
    }

    private static String maybeAddExt(final String fileExt, final String compressionExt) {
        if (compressionExt != null) {
            return fileExt + compressionExt;
        } else {
            return fileExt;
        }
    }
}
