/*-
 * Copyright (C) 2007 Erik Larsson
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.catacombae.storage.io.win32;

import org.catacombae.io.RandomAccessStream;
import org.catacombae.util.Util;

/** BUG: Writing at the end of a file will always expand it to fit the sector size! Not very good. Fixit.
    Note: Is this fixed? TODO ?! */

/**
 * @author <a href="https://catacombae.org" target="_top">Erik Larsson</a>
 */
public class Win32FileStream extends ReadableWin32FileStream implements RandomAccessStream {
    private byte[] sectorBuffer;
    public Win32FileStream(String filename) {
        super(filename);
        sectorBuffer = new byte[sectorSize];
    }

    public void write(byte[] b) { write(b, 0, b.length); }
    public void write(byte[] b, int pos, int len) {
	/* NOTE: The reason for all this code is that we have to align write operations to sectors on the disk. */
	if(fileHandle != null) {
	    final long fileLength = length(fileHandle); // We only record the length of the file at the beginning
	    // Will we allow writing beyond this file (extending it)? We currently do (but maybe windows prohibits it)

	    System.out.println("write(b, " + pos + ", " + len + ");");
	    long startFP = filePointer;
	    long endFP = filePointer+len;
	    long startSectorFP = (startFP/sectorSize)*sectorSize;
	    int startSectorFPOffset = (int)(startFP-startSectorFP);
	    long endSectorFP = (endFP/sectorSize)*sectorSize;
	    int endSectorFPLength = (int)(endFP-endSectorFP);

	    int inputPos = pos;

	    /* First make sure that we are at the beginning of the sector containing
	       filePointer. */
	    seek(startSectorFP, fileHandle);

	    /* Read the contents of the start sector. */
	    int bytesToRead = (int)((sectorBuffer.length > fileLength) ? fileLength : sectorBuffer.length);
	    if(read(sectorBuffer, 0, bytesToRead, fileHandle) != bytesToRead)
		throw new RuntimeException("Could not read contents of starting sector!");

	    System.out.println("Writing first bytes.");

	    /* Replace the contents starting at filePointer, seek back to start pos and write to disk. */
	    seek(startSectorFP, fileHandle);
	    int remainingSpaceInBuffer = sectorBuffer.length-startSectorFPOffset;
	    int bytesToReplace = (len < remainingSpaceInBuffer) ? len : remainingSpaceInBuffer;
	    System.arraycopy(b, inputPos, sectorBuffer, startSectorFPOffset, bytesToReplace);
	    inputPos += bytesToReplace;
	    write(sectorBuffer, 0, startSectorFPOffset+bytesToReplace, fileHandle);


	    /* Burst all non-complicated sectors... */
	    long currentSectorFP = startSectorFP+sectorSize;
	    while(currentSectorFP < endSectorFP) {
		System.out.println("Writing non-complicated sector " + ((currentSectorFP-startSectorFP)/sectorSize) + "...");
		System.arraycopy(b, inputPos, sectorBuffer, 0, sectorBuffer.length);
		inputPos += sectorBuffer.length;
		write(sectorBuffer, 0, sectorBuffer.length, fileHandle);

		currentSectorFP += sectorSize;
	    }


	    /* If start and end sector are the same, the work has already been done (before the loop). */
	    if(startSectorFP != endSectorFP && endSectorFPLength > 0) {
		int finalWriteLength = sectorSize;

		if(fileLength < endSectorFP+sectorSize)
		    finalWriteLength = endSectorFPLength; // The file isn't sector aligned, so we'll just write the final bytes.

		/* Read the contents of the end sector. */
		if(read(sectorBuffer, 0, finalWriteLength, fileHandle) != finalWriteLength)
		    throw new RuntimeException("Could not read contents of end sector!");

		System.arraycopy(b, inputPos, sectorBuffer, 0, endSectorFPLength);

		seek(endSectorFP, fileHandle);
		write(sectorBuffer, 0, finalWriteLength, fileHandle);
	    }

	    /* That should be all. Restore state and update variables. */
	    seek(endFP); // Since we have gone beyond it when writing the last sector
	}
	else
	    throw new RuntimeException("File closed!");

    }
    public void write(int b) { write(new byte[] { (byte)(b & 0xFF) }); }

    /** We override the open method in order to get the file opened in write mode */
    protected byte[] open(String filename) {
	//System.out.println("Java: WritableWin32File.open(" + filename + ");");
	return openNative(filename);
    }
    protected static native byte[] openNative(String filename);
    /** Will throw exception whenever all bytes can't be written. */
    protected static native void write(byte[] data, int off, int len, byte[] handle);

    /*--------------------- TEST CODE FOLLOWS ---------------------*/
    private static java.io.BufferedReader stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
    private static Win32FileStream wwf;
    private static java.io.RandomAccessFile ref;
    private static long currentSeekPos;
    private static int currentBufferSize;
    public static void main(String[] args) {
	if(args.length != 2) {
	    System.out.println("The program takes two different files with identical contents as arguments.");
	    return;
	}
	try {
	    /* Bashing test. Run this on a test file for some time. If the test file doesn't get corrupted,
	       we probably have a winner. */
	    wwf = new Win32FileStream(args[0]);
	    ref = new java.io.RandomAccessFile(args[1], "rw");
	    java.util.Random rnd = new java.util.Random();
	    if(wwf.length() != ref.length()) {
		System.out.println("The two files must be equal in length! (" + wwf.length() + " != " + ref.length() + ")");
		return;
	    }

	    java.io.BufferedReader stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
	    int maxBufferSize = 100000;

	    int constantBufferSize = 3912;
	    byte[] constantBufferLeft = new byte[constantBufferSize];
	    byte[] constantBufferRight = new byte[constantBufferSize];
	    while(true) {
		wwf.seek(0);

		/* For each iteration: randomize the position and read length. Read and write back. */
		long currentSeekPos = (long)((wwf.length()-1)*rnd.nextDouble());
		int currentBufferSize = rnd.nextInt(maxBufferSize);
		if(currentSeekPos+currentBufferSize > wwf.length())
		    currentBufferSize = (int)(wwf.length()-currentSeekPos);

		/*    - Seek to random position
		 *    - Read random amount of data, check if both equal
		 *    - Read constant amount of data, check if both equal
		 *    - Seek back to 0
		 *    - Seek to same position
		 *    - Write data back
		 *    - Read data again, check if equal
		 */
		byte[] currentBufferLeft = new byte[currentBufferSize];
		byte[] currentBufferRight = new byte[currentBufferSize];
		wwf.seek(currentSeekPos);
		ref.seek(currentSeekPos);

		wwf.readFully(currentBufferLeft);
		ref.readFully(currentBufferRight);
		testEquality(currentBufferLeft, currentBufferRight, "(1) Data not equal after reads!");

		if(currentSeekPos+currentBufferSize+constantBufferSize <= wwf.length()) {
		    wwf.readFully(constantBufferLeft);
		    ref.readFully(constantBufferRight);
		    testEquality(currentBufferLeft, currentBufferRight, "(2) Data following reads not equal!");
		}

		wwf.seek(0);
		ref.seek(0);

		wwf.seek(currentSeekPos);
		ref.seek(currentSeekPos);

		wwf.write(currentBufferLeft);
		ref.write(currentBufferRight);

		wwf.seek(currentSeekPos);
		ref.seek(currentSeekPos);

		wwf.readFully(currentBufferLeft);
		ref.readFully(currentBufferRight);

		testEquality(currentBufferLeft, currentBufferRight, "(3) Data not equal after writes!");
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	}
    }

    private static void testEquality(byte[] a, byte[] b, String idMessage) throws Exception {
	if(!Util.arraysEqual(a, b)) {
	    System.out.println(idMessage);
	    System.out.println("  currentSeekPos=" + currentSeekPos);
	    System.out.println("  currentBufferSize=" + currentBufferSize);
	    System.out.println("  wwf.length()=" + wwf.length());
	    System.out.println("  ref.length()=" + ref.length());
	    System.out.println("  wwf.getFilePointer()=" + wwf.getFilePointer());
	    System.out.println("  ref.getFilePointer()=" + ref.getFilePointer());
// 		    System.out.println("
// 		    System.out.println("
// 		    System.out.println("
	    System.out.print("Press enter to continue: ");
	    stdin.readLine();
	}
    }
}
