/*
 * @(#)PutFiles.java
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 1999-2004 Jörg P. M. Haeger
 *
 * webCDwriter is free software. See CDcreator.java for details.
 */

import java.io.*;
import java.net.*;
import java.security.MessageDigest;
import javax.swing.*;

/**
 * Execution of "putFile" commands to copy a directory tree
 * to the server.
 *
 * @version 20040318
 * @author Jörg P. M. Haeger
 */
public class PutFiles extends Command {
	PutDialog view;
	protected DataOutputStream outStream;
	DirTreeNode CDRootNode;
	Object MD5Sum;
	ByteArrayOutputStream MD5Sums;
	long maxCommandDataSize = 64 * 1024;
	int sessionSizeKB;
	int transmittedKB;
	boolean doNotShorten = false;
	boolean ignoreUnreadable;
	boolean shorten;
	boolean localServer = false;

	public PutFiles(DirTreeNode CDRootNode, int sessionSizeKB) {
		view = new PutDialog();
		outStream = CDcreator.outStream;
		this.CDRootNode = CDRootNode;
		try {
			if (Options.MD5File && !CDcreator.isNetscapeVM()) {
				MD5Sum = MessageDigest.getInstance("MD5");
				MD5Sums = new ByteArrayOutputStream();
			}
			else
				MD5Sum = null;
		}
		catch(java.security.NoSuchAlgorithmException e) {
			MD5Sum = null;
		}
		this.sessionSizeKB = sessionSizeKB;
		transmittedKB = 0;

		if (CDcreator.socket != null) {
//			String name = CDcreator.socket.getInetAddress().getHostName();
			String name = CDcreator.serverName;
			localServer = name.equals("127.0.0.1")
				|| name.equals("localhost");
		}
	}

	static String bytesToHex(byte[] bs) {
		String hexStr = "";
		for (int i = 0; i < bs.length; i++) {
			int c1 = (bs[i] & 0xF0) >> 4;
			if (c1 < 10)
				c1 += '0';
			else
				c1 += 'a' - 10;

			int c2 = bs[i] & 0x0F;
			if (c2 < 10)
				c2 += '0';
			else
				c2 += 'a' - 10;

			hexStr = hexStr + (char)c1 + (char)c2;
		}
		return hexStr;
	}

	private void calculateMD5(InputStream inFile, long bytesToWrite)
			throws java.io.IOException {
		while (bytesToWrite > 0) {
			int blockSize = 64 * 1024;
			if (blockSize > bytesToWrite)
				blockSize = (int) bytesToWrite;
			byte bs[] = new byte[blockSize];
			int len = inFile.read(bs);
			if (len == -1)
				break;
			((MessageDigest) MD5Sum).update(bs, 0, len);
			bytesToWrite -= len;
			transmittedKB += (len + 1023) / 1024;
			view.setProgress(transmittedKB);
		}
	}

	private void copy(InputStream inFile, long bytesToWrite)
			throws java.io.IOException {
		try {
			while (bytesToWrite > 0) {
				int blockSize = 64 * 1024;
				if (blockSize > bytesToWrite)
					blockSize = (int) bytesToWrite;
				byte bs[] = new byte[blockSize];
				int len = inFile.read(bs);
				if (len == -1)
					break;
				outStream.write(bs, 0, len);
				if (MD5Sum != null)
					((MessageDigest) MD5Sum).update(bs, 0, len);
				bytesToWrite -= len;
				transmittedKB += (len + 1023) / 1024;
				view.setProgress(transmittedKB);
			}
		}
		finally {
			while (bytesToWrite-- > 0)
				outStream.write(0);
		}
	}

	protected void editFileName(DirTreeNode node, int index)
		throws Exception {
		to64Characters(node, index);
	}

	protected void execute(String command)
			throws java.io.IOException, CommandError, Exception {
		outStream.writeBytes(command + "\n");
		readResult();
		if (!lastResultOK())
			throw new CommandError();
	}

	protected void execute(String command,
			InputStream inFile, long bytesToWrite)
			throws java.io.IOException, CommandError, Exception {
		long t0 = new java.util.Date().getTime();

		outStream.writeBytes(command + "\n");
		copy(inFile, bytesToWrite);
		readResult();
		if (!lastResultOK())
			throw new CommandError();

		long deltaT = new java.util.Date().getTime() - t0;
		if (deltaT > 0) {
			long bytesPerSecond = (1000 * bytesToWrite) / deltaT;
			if (2 * bytesPerSecond > maxCommandDataSize) {
				maxCommandDataSize = 2 * bytesPerSecond;
				maxCommandDataSize
					= 1024 * ((maxCommandDataSize + 1023) / 1024);
				Version.debug("bytesPerSecond", "" + bytesPerSecond);
			}
		}
	}

	private boolean ignoreFile(final String name) {
		if (view.ignoreAllUnreadableBox.isSelected())
			return true;

		try {
			ignoreUnreadable = false;
			SwingUtilities.invokeAndWait(new Runnable() {
				public void run() {
					String[] options = {
						i18n("PutFiles.ignore"),
						i18n("PutFiles.ignoreAll"),
						i18n("cancel") };
					JPanel panel = new JPanel();
					panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
					JLabel l = new JLabel(i18n("PutFiles.cannotRead"));
					l.setAlignmentX(JLabel.LEFT_ALIGNMENT);
					panel.add(l);
					JTextArea t = new JTextArea(name);
					t.setAlignmentX(JTextArea.LEFT_ALIGNMENT);
					t.setEditable(false);
					panel.add(t);
					int result = JOptionPane.showOptionDialog(
						CDcreator.mainWin,
						panel,
						i18n("error"),
						JOptionPane.DEFAULT_OPTION,
						JOptionPane.ERROR_MESSAGE,
						null, options, options[0]);
					if (result == 0)
						ignoreUnreadable = true;
					else
					if (result == 1)
						view.ignoreAllUnreadableBox.setSelected(true);
				}
			});
		}
		catch (Exception e2) {
		}

		if (ignoreUnreadable || view.ignoreAllUnreadableBox.isSelected())
			return true;

		return false;
	}

	FileInputStream open(File file) throws Exception {
		try {
			FileInputStream inFile = new FileInputStream(file);
			return inFile;
		}
		catch (java.io.IOException e) {
		}

		if (view.ignoreAllUnreadableBox.isSelected())
			return null;

//		System.out.println("cannot read " + file);
		final String name = file.getPath();
		ignoreFile(name);

		if (ignoreUnreadable || view.ignoreAllUnreadableBox.isSelected())
			return null;

		throw new Exception("open cancelled");
	}

	protected void process(int code, String text) {
		switch (code) {
			case 711:
			try {
				int p = text.indexOf(' ');
				text = text.substring(0, p);
				transmittedKB += Integer.parseInt(text);
				view.setProgress(transmittedKB);
			}
			catch (Exception e) {
			}
			break;
			default:
				;
		}
	}

	protected boolean processInternal(int code, String text) {
		switch (code) {
			case 710:
			try {
				if (MD5Sum != null) {
					text = removeEscChars(text);
					MD5Sums.write((text + "\n").getBytes("UTF-8"));
				}
			}
			catch (java.io.IOException e) {
			}
			return true;
			default:
				;
		}
		return false;
	}

	protected void putFile(String dirName, DirTreeNode node)
		throws java.io.IOException, CommandError, Exception {
		File file = node.getFile();
		String fileName = dirName + "/" + node.toString();
		Version.showDebugInfo(1, fileName + " " + file);
		view.setMessage(fileName);

		if (file instanceof ServerFile)
			try {
				execute("linkFile"
					+ " " + spaceToEsc(file.getPath())
					+ " " + spaceToEsc(fileName)
					+ (MD5Sum != null? " md5": ""));
			}
			catch (CommandError e) {
				if (!ignoreFile(fileName))
					throw e;
			}
		else {
			long bytesToWrite = file.length();
			FileInputStream inFile = null;
			String referenceArg = "";
			if (bytesToWrite > 0) {
				inFile = open(file);
				if (inFile == null)
					return;
				if (localServer)
					referenceArg = " " + spaceToEsc(file.getPath());
			}
			if (MD5Sum != null)
				((MessageDigest) MD5Sum).reset();

			int mode = 0444;
			if (node.isExecutable())
				mode |= 0111;

			if (Mode.saveImage && bytesToWrite > 2 * 1024) {
				execute("putDummy"
					+ " " + spaceToEsc(fileName)
					+ " " + bytesToWrite
					+ " " + Integer.toOctalString(mode)
					+ " " + file.lastModified() / 1000
					+ " " + spaceToEsc(file.getPath()));
				if (MD5Sum != null)
					calculateMD5(inFile, bytesToWrite);
			}
			else
			if (bytesToWrite <= maxCommandDataSize
					&& referenceArg.length() == 0)
				execute("putFile"
					+ " " + spaceToEsc(fileName)
					+ " " + bytesToWrite
					+ " " + Integer.toOctalString(mode)
					+ " " + file.lastModified() / 1000,
					inFile, bytesToWrite);
			else {
				execute("putFileStart"
					+ " " + spaceToEsc(fileName) + referenceArg);
				while (!cancel && bytesToWrite > 0) {
					long block = maxCommandDataSize;
					if (block > bytesToWrite)
						block = bytesToWrite;
					execute("putFileContinue"
						+ " " + block,
						inFile, block);
					bytesToWrite -= block;
				}
				execute("putFileEnd"
					+ " " + Integer.toOctalString(mode)
					+ " " + file.lastModified() / 1000);
			}

			if (MD5Sum != null)
				MD5Sums.write((
					bytesToHex(((MessageDigest) MD5Sum).digest())
					+ "  " + fileName.substring(1) + "\n")
						.getBytes("UTF-8"));

			if (inFile != null)
				inFile.close();
		}

		view.setProgress(transmittedKB);
		State.numOfFiles++;

		if (node == MainWin.bootImageNode)
			Options.bootImage = fileName;
	}

	void putTree(String dirName, DirTreeNode node)
		throws java.io.IOException, CommandError, Exception {
		String dirName2;
		if (dirName.equals("/"))
			dirName2 = "";
		else
			dirName2 = dirName + '/' + node.toString();

		File dir = node.getFile();
		if (dir instanceof ServerFile) {
			try {
				// just make a link to the file on the server
				execute("linkFile"
					+ " " + spaceToEsc(dir.getPath())
					+ " " + spaceToEsc(dirName2)
					+ (MD5Sum != null? " md5": ""));
			}
			catch (CommandError e) {
				if (!ignoreFile(dirName2))
					throw e;
			}
			return;
		}

		// transfer the files and directories contained
		// in this directory
		for (int i = 0; i < node.getChildCount(); i++) {
			if (cancel)
				return;
			editFileName(node, i);
			DirTreeNode child =
				(DirTreeNode)node.getChildAt(i);
			if (child.isLeaf())
				putFile(dirName2, child);
			else
				putTree(dirName2, child);
		}

		if (dir != null && dirName2.length() != 0)
			// create the directory and set the modification time
			execCommand("mkdir "
				+ spaceToEsc(dirName2) + " "
				+ "555 "
				+ dir.lastModified() / 1000);
	}

	public void run() {
//		System.out.println("PutFiles");
		view.setTitle(i18n("titlePutFiles"));
		view.setRange(0, sessionSizeKB);
		showDialog(view);

		// for netscape
		if (CDcreator.isNetscapeVM())
		try {
			netscape.security.PrivilegeManager
				.enablePrivilege("UniversalFileRead");
		}
		catch (Exception e) {
		}

		boolean error = false;
		doNotShorten = false;
		view.ignoreAllUnreadableBox.setSelected(false);
		view.shortenAllBox.setSelected(false);
		try {
			putTree("/", CDRootNode);

			if (MD5Sum != null) {
				java.util.Calendar c = new java.util.GregorianCalendar();
				execute("putFile"
					+ " /MD5Sums/" + spaceToEsc(
						Format.right(c.get(c.YEAR), 4, '0')
						+ Format.right(
							c.get(c.MONTH) - c.JANUARY + 1, 2, '0')
						+ Format.right(c.get(c.DAY_OF_MONTH), 2, '0')
						+ "_" + Format.right(c.get(c.HOUR_OF_DAY), 2, '0')
						+ Format.right(c.get(c.MINUTE), 2, '0')
						+ ".md5")
					+ " " + MD5Sums.size()
					+ " " + Integer.toOctalString(0444)
					+ " " + new java.util.Date().getTime() / 1000,
					new ByteArrayInputStream(MD5Sums.toByteArray()),
					MD5Sums.size());

				execute("linkFile"
					+ " " + spaceToEsc("/Server/tools/MD5Verify.jar")
					+ " " + spaceToEsc("/MD5Verify.jar"));
			}
		}
		catch (java.io.IOException e) {
			status = ERROR;
			System.out.println("PutFiles: " + e);
		}
		catch (CommandError cmdErr) {
			status = ERROR;
			error = true;
		}
		catch (Exception e) {
			status = ERROR;
		}

		hideDialog();
		if (error)
			showInfoWin(new JOptionPane(
				lastResult,
				JOptionPane.INFORMATION_MESSAGE,
				JOptionPane.DEFAULT_OPTION,
				null, okButton).createDialog(
				CDcreator.mainWin, i18n("error")));

		super.run();
//		System.out.println("PutFiles end");
	}

	private void to64Characters(DirTreeNode node, int index)
			throws Exception {
		final int max = 64;
		final String name = node.getChildAt(index).toString();
		int len = name.length();
		int max2;
		try {
			max2 = max - (name.getBytes("UTF-8").length - len);
		}
		catch (Exception e) {
			max2 = max;
		}
		if (len <= max2)
			return;

		if (!doNotShorten && !view.shortenAllBox.isSelected())
		try {
			shorten = false;
			SwingUtilities.invokeAndWait(new Runnable() {
				public void run() {
					String[] options = {
						i18n("PutFiles.shorten"),
						i18n("PutFiles.shortenAll"),
						i18n("PutFiles.ignore"),
						i18n("PutFiles.ignoreAlways"),
						i18n("cancel") };
					JPanel panel = new JPanel();
					panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
					JLabel l = new JLabel(i18n("PutFiles.above64characters"));
					l.setAlignmentX(JLabel.LEFT_ALIGNMENT);
					panel.add(l);
					JTextArea t = new JTextArea(name);
					t.setAlignmentX(JTextArea.LEFT_ALIGNMENT);
					t.setEditable(false);
					panel.add(t);
					t.setAlignmentX(JLabel.LEFT_ALIGNMENT);
					l = new JLabel(i18n("PutFiles.problem"));
					panel.add(l);
					int result = JOptionPane.showOptionDialog(
						CDcreator.mainWin,
						panel,
						i18n("warning"),
						JOptionPane.DEFAULT_OPTION,
						JOptionPane.WARNING_MESSAGE,
						null, options, options[0]);
					if (result == 0)
						shorten = true;
					else
					if (result == 1)
						view.shortenAllBox.setSelected(true);
					else
					if (result == 3)
						doNotShorten = true;
					else
					if (result == 4)
						shorten = doNotShorten = true;
				}
			});
		}
		catch (Exception e) {
		}

		if (shorten && doNotShorten)
			throw new Exception("shorten cancelled");

		if (!shorten && !view.shortenAllBox.isSelected())
			return;

		int dot = name.lastIndexOf('.');
		if (dot < len - 5)
			dot = len;
		int suffixLen = len - dot;

		int j0 = 1;
		for (int i = 2; i <= max2; i++) {
			for (int j = j0; j < 10 * j0; j++) {
				String name2 = name.substring(0, max2 - i - suffixLen)
					+ "~" + j
					+ name.substring(len - suffixLen);
				int k;
				for (k = 0; k < node.getChildCount(); k++)
					if (node.getChildAt(k).toString().equals(name2))
						break;
				if (k == node.getChildCount()) {
					DirTreeNode child
						= (DirTreeNode)node.getChildAt(index);
					child.setUserObject(name2);
//					System.out.println(name2);
					return;
				}
			}
			j0 *= 10;
		}
	}
}
