/*-
 * Copyright (C) 2008 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.hfsexplorer.testcode;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import org.catacombae.storage.ps.gpt.types.GPTHeader;
import org.catacombae.storage.ps.gpt.types.GUIDPartitionTable;
import org.catacombae.storage.ps.gpt.types.MutableGPTEntry;
import org.catacombae.storage.ps.gpt.types.MutableGPTHeader;
import org.catacombae.storage.ps.gpt.types.MutableGUIDPartitionTable;
import org.catacombae.storage.io.win32.Win32FileStream;
import org.catacombae.io.FileStream;
import org.catacombae.io.RandomAccessStream;
import org.catacombae.io.TruncatableRandomAccess;
import org.catacombae.util.Util;

/**
 * This application reads the primary GPT header from the specified disk and writes a
 * corresponding backup header at the end of the disk. Useful for when a disk image
 * does not contain the entire length of the drive, due to read errors and such.
 *
 * @author <a href="https://catacombae.org" target="_top">Erik Larsson</a>
 */

public class RepairMyBackupGPT {
    private static BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));

    public static void main(String[] args) throws Exception {
	long runTimeStamp = System.currentTimeMillis();
	RandomAccessStream llf;
	if(System.getProperty("os.name").toLowerCase().startsWith("windows") &&
	   !new File(args[0]).exists()) {
	    System.out.println("Win32 extended stream loading...");
	    llf = new Win32FileStream(args[0]); // In case we have specified a windows device path
	}
	else
	    llf = new FileStream(args[0]);

	final GUIDPartitionTable originalGpt = new GUIDPartitionTable(llf, 0);
	MutableGUIDPartitionTable gpt = new MutableGUIDPartitionTable(originalGpt);

	if(!originalGpt.isValid() && !gpt.isValid()) {
	    final int blockSize = 512;
	    GPTHeader hdr = gpt.getHeader();
	    GPTHeader backupHdr = gpt.getBackupHeader();
	    if(!hdr.isValid()) {
		System.out.println("The GPT primary header is not valid! Exiting...");
		System.exit(-1);
		return;
	    }
	    else if(backupHdr.isValid()) {
		System.out.println("The backup header is already valid. Exiting...");
		System.exit(-1);
		return;
	    }

	    // Backup the GPT backup table at the end of the disk.
	    String backupFilename2 = null;
	    {
		long backupGPTPos = hdr.getBackupLBA()*blockSize - hdr.getNumberOfPartitionEntries()*hdr.getSizeOfPartitionEntry();
		int backupGPTLen = hdr.getNumberOfPartitionEntries()*hdr.getSizeOfPartitionEntry() + blockSize;

		byte[] backup2 = new byte[backupGPTLen];
		llf.seek(backupGPTPos);
		int bytesRead = llf.read(backup2);
		if(bytesRead > 0) {
		    backupFilename2 = "gpt_backup_table-" + runTimeStamp + ".backup";
		    System.out.print("Backing up GPT backup header and table to \"" + backupFilename2 + "\"...");
		    FileOutputStream backupFile2 = new FileOutputStream(backupFilename2);
		    backupFile2.write(backup2, 0, bytesRead);
		    backupFile2.close();
		    System.out.println("done!");
		}
		else {
		    System.out.println("Could not read the backup GPT table data area!");
		    String s = "";
		    while(!(s.equals("y") || s.equals("n"))) {
			System.out.print("Continue without backup (y/n)? ");
			s = stdin.readLine();
		    }
		    if(s.equals("n")) {
			System.exit(-1);
			return;
		    }
		}
	    }

	    // Now let's modify the table in memory

	    System.out.println("Modifying GPT data in memory:");

	    System.out.print("  - Creating a GPT backup header from the primary header...");
	    GPTHeader newBackupHeader = hdr.createValidBackupHeader();
	    System.out.println("done.");

	    System.out.print("  - Setting contents of mutable backup header to newly created backup header...");
	    MutableGPTHeader backupHeader = gpt.getMutableBackupHeader();
	    backupHeader.setFields(newBackupHeader);
	    System.out.println("done.");

	    System.out.print("  - Setting contents of all backup table entries to match primary entries...");
	    MutableGPTEntry[] primaryEntries = gpt.getMutablePrimaryEntries();
	    MutableGPTEntry[] backupEntries = Util.arrayCopy(primaryEntries, new MutableGPTEntry[primaryEntries.length]);
	    gpt.setMutableBackupEntries(backupEntries);
	    System.out.println("done.");

	    // At this point, all data in gpt should have been set to intended values.
	    // Finalizing and checking the resulting table...

	    System.out.print("  - Checking if gpt.isValid() == true as it now should be...");
	    if(!gpt.isValid()) {
		System.out.println("failed! Halting program.");
		System.exit(0);
	    }
	    System.out.println("yes.");

	    // If we have got to this point, the table should be valid and ready to be written to disk!
	    System.out.println("The backup table is now ready to be written down to disk.");

	    System.out.print("Press enter to view the original table:");
	    stdin.readLine();
	    originalGpt.printBackupFields(System.out, "");

	    System.out.print("Press enter to view the modified table:");
	    stdin.readLine();
	    gpt.printBackupFields(System.out, "");

	    System.out.print("If you want to write this table to disk, type \"yes\" here: ");
	    String answer = stdin.readLine();
	    if(answer.equals("yes")) {
		System.out.print("Getting binary data for backup table...");
		byte[] newBackupGPT = gpt.getBackupTableBytes();
		System.out.println("done.");

		// Write the new backup GPT data to a file.
		String newdataFilename2 = "gpt_backup_table-" + runTimeStamp + ".new";
		System.out.print("Writing new GPT backup header and table to \"" + newdataFilename2 + "\"...");
		FileOutputStream newdataFile2 = new FileOutputStream(newdataFilename2);
		newdataFile2.write(newBackupGPT);
		newdataFile2.close();
		System.out.println("done!");

		// Write to disk! Dangerous stuff...

		System.out.print("Writing backup table...");
		long backupTableLocation = gpt.getBackupTableBytesOffset();
		int backupTableSize = newBackupGPT.length;
		long targetLength = backupTableLocation + backupTableSize;
		if(targetLength > llf.length()) {
		    System.out.println("The target storage size is reported to be");
		    System.out.println(llf.length() + " bytes, which is smaller than what the GPT specifies");
		    System.out.println("(" + (backupTableLocation+backupTableSize) + " bytes).");
		    System.out.println("This may be because you are accessing a block device, or an incomplete disk");
		    System.out.println("image. If you proceed, the disk image file, if any, may be extended, or the");
		    System.out.println("write will fail.");
		    String s = "";
		    while(!(s.equals("y") || s.equals("n"))) {
			System.out.print("Proceed anyway (y/n)? ");
			s = stdin.readLine();
		    }
		    if(s.equals("n")) {
			System.exit(-1);
			return;
		    }
		    else if(llf instanceof TruncatableRandomAccess) {
			System.out.println("Setting length of file to: " + targetLength);
			((TruncatableRandomAccess)llf).setLength(targetLength);
		    }
		}
		else if(targetLength < llf.length()) {
		    System.out.println("Warning: The backup table is not situated at the end of the target storage.");
		    String s = "";
		    while(!(s.equals("y") || s.equals("n"))) {
			System.out.print("Proceed anyway (y/n)? ");
			s = stdin.readLine();
		    }
		    if(s.equals("n")) {
			System.exit(-1);
			return;
		    }
		}

		System.out.print("Seeking to " + backupTableLocation + "...");
		llf.seek(backupTableLocation);
		System.out.println("done!");
		System.out.print("Writing backup GPT data...");
		llf.write(newBackupGPT);
		System.out.println("done!");

		// Check to see if we have succeeded.
		System.out.println();
		System.out.println("Checking the newly written GPT...");
		GUIDPartitionTable newGpt = new GUIDPartitionTable(llf, 0);
		newGpt.print(System.out, "");
		if(newGpt.isValid())
		    System.out.println("The GPT on disk is valid!");
		else {
		    System.out.println("INVALID GPT ON DISK! FATAL ERROR!");
		    System.out.println("Try to restore the original GPT tables from the backup files:");
		    if(backupFilename2 != null)
			System.out.println("  " + backupFilename2);
		    System.out.println("(dd in linux can probably do the job)");
		}
	    }
	    else
		System.out.println("Exiting program without modifying anything.");

	}
	else
	    System.out.println("The GUID Partition Table on disk seems to be valid. No changes will be made.");
	llf.close();
    }

}
