package bndtools.jareditor.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.forms.AbstractFormPart;
import org.eclipse.ui.forms.IFormPart;
import org.eclipse.ui.forms.IPartSelectionListener;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;

public class JAREntryPart extends AbstractFormPart implements IPartSelectionListener {

	private static final String	DEFAULT_CHARSET	= "UTF-8";

	private final IEditorPart	editor;

	private Text				text;
	protected ZipEntry			zipEntry		= null;
	private Job					displayJob		= null;

	protected boolean			showAsText		= true;
	protected final String[]	charsets;
	protected int				selectedCharset;

	public JAREntryPart(IEditorPart editor, Composite composite, FormToolkit toolkit) {
		this.editor = editor;

		SortedMap<String, Charset> charsetMap = Charset.availableCharsets();
		charsets = new String[charsetMap.size()];
		int i = 0;
		for (Iterator<String> iter = charsetMap.keySet()
			.iterator(); iter.hasNext(); i++) {
			charsets[i] = iter.next();
		}
		setSelectedCharset(DEFAULT_CHARSET);

		createContent(composite, toolkit);
	}

	private void createContent(Composite parent, FormToolkit toolkit) {
		Section textSection = toolkit.createSection(parent,
			ExpandableComposite.TITLE_BAR | ExpandableComposite.EXPANDED);
		textSection.setText("Entry Content");
		Composite textComposite = toolkit.createComposite(textSection);
		text = toolkit.createText(textComposite, "", SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.READ_ONLY);
		text.setFont(JFaceResources.getTextFont());
		textSection.setClient(textComposite);

		Section encodingSection = toolkit.createSection(parent,
			ExpandableComposite.TITLE_BAR | ExpandableComposite.EXPANDED);
		encodingSection.setText("Display Options");
		Composite encodingPanel = toolkit.createComposite(encodingSection);
		encodingSection.setClient(encodingPanel);
		toolkit.createLabel(encodingPanel, "Show As:");
		final Button btnText = toolkit.createButton(encodingPanel, "Text", SWT.RADIO);
		btnText.setSelection(showAsText);
		Button btnBinary = toolkit.createButton(encodingPanel, "Binary (hex)", SWT.RADIO);
		btnBinary.setSelection(!showAsText);
		toolkit.createLabel(encodingPanel, "Text Encoding:");
		final Combo encodingCombo = new Combo(encodingPanel, SWT.READ_ONLY);
		encodingCombo.setEnabled(showAsText);

		// INITIALISE
		encodingCombo.setItems(charsets);
		encodingCombo.select(selectedCharset);

		// LISTENERS
		encodingSection.addExpansionListener(new ExpansionAdapter() {
			@Override
			public void expansionStateChanged(ExpansionEvent e) {
				getManagedForm().reflow(true);
			}
		});
		SelectionListener radioListener = new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				showAsText = btnText.getSelection();
				encodingCombo.setEnabled(showAsText);
				loadContent();
			}
		};
		btnText.addSelectionListener(radioListener);
		btnBinary.addSelectionListener(radioListener);
		encodingCombo.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				selectedCharset = encodingCombo.getSelectionIndex();
				loadContent();
			}
		});

		// LAYOUT
		GridLayout layout;
		GridData gd;

		layout = new GridLayout(1, false);
		parent.setLayout(layout);

		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		textSection.setLayoutData(gd);

		layout = new GridLayout(1, false);
		textComposite.setLayout(layout);

		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		text.setLayoutData(gd);

		gd = new GridData(SWT.FILL, SWT.FILL, true, false);
		encodingSection.setLayoutData(gd);
		encodingSection.setLayout(new FillLayout());
		encodingPanel.setLayout(new GridLayout(3, false));
		encodingCombo.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 2, 1));
	}

	@Override
	public void selectionChanged(IFormPart part, ISelection selection) {
		if (selection instanceof IStructuredSelection) {
			Object element = ((IStructuredSelection) selection).getFirstElement();

			ZipEntry entry = null;
			if (element instanceof ZipEntry)
				entry = (ZipEntry) element;
			else if (element instanceof ZipTreeNode)
				entry = ((ZipTreeNode) element).getZipEntry();

			this.zipEntry = entry;
		} else {
			this.zipEntry = null;
		}
		loadContent();
	}

	private final void setSelectedCharset(String selectedCharsetName) {
		for (int i = 0; i < charsets.length; i++) {
			if (charsets[i].equals(selectedCharsetName)) {
				selectedCharset = i;
				return;
			}
		}
		throw new IllegalArgumentException("Unknown charset name: " + selectedCharsetName);
	}

	protected void loadContent() {
		if (displayJob != null && displayJob.getState() != Job.NONE)
			displayJob.cancel();

		if (zipEntry != null && !zipEntry.isDirectory()) {
			IEditorInput input = editor.getEditorInput();
			final Display display = text.getDisplay();
			final URI uri = URIHelper.retrieveFileURI(input);

			if (uri != null) {
				displayJob = new Job("Load zip content") {
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						File ioFile = new File(uri);
						try (ZipFile zipFile = new ZipFile(ioFile)) {
							final StringWriter writer = new StringWriter();
							if (showAsText)
								readAsText(zipFile, zipEntry, charsets[selectedCharset], writer, 1024 * 20, monitor);
							else
								readAsHex(zipFile, zipEntry, writer, 1024 * 20, 2, monitor);

							display.asyncExec(() -> setContent(writer.toString()));

							return Status.OK_STATUS;
						} catch (IOException e) {
							Status status = new Status(IStatus.ERROR, PluginConstants.PLUGIN_ID, 0,
								"I/O error reading JAR file contents", e);
							// ErrorDialog.openError(getManagedForm().getForm().getShell(),
							// "Error", null, status);
							return status;
						}
					}
				};
				displayJob.schedule();
			}
		} else {
			setContent("");
		}
	}

	protected void setContent(String content) {
		if (text != null && !text.isDisposed())
			text.setText(content);
	}

	private static final String pseudo[] = {
		"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"
	};

	private static SubMonitor createProgressMonitor(ZipEntry entry, long limit, IProgressMonitor monitor) {
		SubMonitor progress;
		long size = entry.getSize();
		if (size == -1) {
			progress = SubMonitor.convert(monitor);
		} else {
			long ticks = (limit == -1) ? size : Math.min(size, limit);
			progress = SubMonitor.convert(monitor, (int) ticks);
		}
		return progress;
	}

	protected static void readAsText(ZipFile zipFile, ZipEntry entry, String encoding, Writer out, long limit,
		IProgressMonitor monitor) throws IOException {
		SubMonitor progress = createProgressMonitor(entry, limit, monitor);

		boolean limitReached = false;
		try (InputStream stream = zipFile.getInputStream(entry)) {
			long total = 0;

			byte[] buffer = new byte[1024];
			while (true) {
				if (progress.isCanceled())
					return;
				int bytesRead = stream.read(buffer, 0, 1024);
				if (bytesRead < 0)
					break;
				String string = new String(buffer, 0, bytesRead, encoding);
				out.write(string);

				total += bytesRead;
				progress.worked(bytesRead);
				if (limit >= 0 && total >= limit) {
					limitReached = true;
					return;
				}
			}
		} finally {
			if (limitReached) {
				out.write("\nLimit of " + (limit >> 10) + "Kb reached, the rest of the entry is not shown.");
			}
		}
	}

	private static char byteToChar(byte b) {
		if ((b < 32) || (b == 127)) {
			return '.';
		}

		return (char) b;
	}

	protected static void readAsHex(ZipFile zipFile, ZipEntry entry, Writer out, long limit, int groupsOf8BytesPerLine,
		IProgressMonitor monitor) throws IOException {
		SubMonitor progress = createProgressMonitor(entry, limit, monitor);

		boolean limitReached = false;
		long offsetInFile = 0;
		int bytesPerLine = groupsOf8BytesPerLine * 8;
		int asciiPosition = 0;
		char[] asciiBuffer = new char[bytesPerLine + (2 * (groupsOf8BytesPerLine - 1))];
		int bytePosition = 0;
		byte[] buffer = new byte[1024];

		try (InputStream stream = zipFile.getInputStream(entry)) {
			long total = 0;

			while (true) {
				if (progress.isCanceled())
					return;
				int bytesRead = stream.read(buffer, 0, 1024);
				if (bytesRead < 0)
					break;

				for (int i = 0; i < bytesRead; i++) {
					if (bytePosition == 0) {
						String s = String.format("0x%04x ", offsetInFile);
						out.write(s);
						offsetInFile += bytesPerLine;
					}

					asciiBuffer[asciiPosition] = byteToChar(buffer[i]);
					asciiPosition++;

					out.write(pseudo[(buffer[i] & 0xf0) >>> 4]); // Convert to a
																	// string
																	// character
					out.write(pseudo[(buffer[i] & 0x0f)]); // convert the nibble
															// to a String
															// Character
					out.write(' ');
					bytePosition++;

					/* do a linebreak after the required number of bytes */
					if (bytePosition >= bytesPerLine) {
						out.write(' ');
						out.write(asciiBuffer);
						out.write('\n');
						asciiPosition = 0;
						bytePosition = 0;
					}

					/* put 2 extra spaces between bytes */
					if ((bytePosition > 0) && (bytePosition % 8 == 0)) {
						asciiBuffer[asciiPosition++] = ' ';
						asciiBuffer[asciiPosition++] = ' ';
						out.write(' ');
					}
				}

				total += bytesRead;
				progress.worked(bytesRead);
				if (limit >= 0 && total >= limit) {
					limitReached = true;
					return;
				}
			}
		} finally {
			if (bytePosition > 0) {
				while (bytePosition < bytesPerLine) {
					out.write("   ");
					bytePosition++;

					/* put 2 extra spaces between bytes */
					if ((bytePosition > 0) && (bytePosition % 8 == 0)) {
						out.write(' ');
					}
				}
				out.write(asciiBuffer, 0, asciiPosition);
			}

			if (limitReached) {
				out.write("\nLimit of " + (limit >> 10) + "Kb reached, the rest of the entry is not shown.");
			}
		}
	}
}
