package us.deathmarine.luyten;

import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.core.StringUtilities;
import com.strobel.decompiler.DecompilationOptions;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.PlainTextOutput;

import us.deathmarine.luyten.ConfigSaver;
import us.deathmarine.luyten.MainWindow;
import us.deathmarine.luyten.Model;

import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

import javax.swing.*;
import javax.swing.GroupLayout.Alignment;

/**
 * 
 * this is the Find All Dialog
 * <p>
 * Change with 1.1
 * Adjust the find all box width
 * </p>
 * 
 * @author clevertension
 * @version 1.1
 */
public class FindAllBox extends JDialog {
	private static final long serialVersionUID = -4125409760166690462L;
	private static final int MIN_WIDTH = 640;
	private boolean searching;

	private JButton findButton;
	private JTextField textField;
	private JCheckBox mcase;
	private JCheckBox regex;
	private JCheckBox wholew;
	private JCheckBox classname;
	private JList<String> list;
	private JProgressBar progressBar;
	boolean locked;

	private JLabel statusLabel = new JLabel("");

	private DefaultListModel<String> classesList = new DefaultListModel<String>();

	private Thread tmp_thread;

	public FindAllBox(final MainWindow mainWindow) {
		this.setDefaultCloseOperation(HIDE_ON_CLOSE);
		this.setHideOnEscapeButton();

		progressBar = new JProgressBar(0, 100);

		JLabel label = new JLabel("Find What:");
		textField = new JTextField();
		findButton = new JButton("Find");
		findButton.addActionListener(new FindButton());

		mcase = new JCheckBox("Match Case");
		regex = new JCheckBox("Regex");
		wholew = new JCheckBox("Whole Words");
		classname = new JCheckBox("Classnames");

		this.getRootPane().setDefaultButton(findButton);

		list = new JList<String>(classesList);
		list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
		list.setLayoutOrientation(JList.VERTICAL_WRAP);
		list.setVisibleRowCount(-1);
		list.addMouseListener(new MouseAdapter() {
			public void mouseClicked(MouseEvent evt) {
				@SuppressWarnings("unchecked")
				JList<String> list = (JList<String>) evt.getSource();
				if (evt.getClickCount() == 2) {
					int index = list.locationToIndex(evt.getPoint());
					String entryName = (String) list.getModel().getElementAt(index);
					String[] array = entryName.split("/");
					if (entryName.toLowerCase().endsWith(".class")) {
						String internalName = StringUtilities.removeRight(entryName, ".class");
						TypeReference type = Model.metadataSystem.lookupType(internalName);
						try {
							mainWindow.getModel().extractClassToTextPane(type, array[array.length - 1], entryName,
									null);
						} catch (Exception e) {
							Luyten.showExceptionDialog("Exception!", e);
						}

					} else {
						try {
							JarFile jfile = new JarFile(MainWindow.model.getOpenedFile());
							mainWindow.getModel().extractSimpleFileEntryToTextPane(
									jfile.getInputStream(jfile.getEntry(entryName)), array[array.length - 1],
									entryName);
							jfile.close();
						} catch (IOException e) {
							e.printStackTrace();
						} catch (Exception e) {
							e.printStackTrace();
						}
					}

				}
			}
		});
		list.setLayoutOrientation(JList.VERTICAL);
		JScrollPane listScroller = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
				JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		int width = (int) (screenSize.width * 0.35);
		if (width < MIN_WIDTH) {
		    width = MIN_WIDTH;
		}
		final Dimension center = new Dimension(width, 500);
		final int x = (int) (center.width * 0.2);
		final int y = (int) (center.height * 0.2);
		this.setBounds(x, y, center.width, center.height);
		this.setResizable(false);

		GroupLayout layout = new GroupLayout(getRootPane());
		getRootPane().setLayout(layout);
		layout.setAutoCreateGaps(true);
		layout.setAutoCreateContainerGaps(true);

		layout.setHorizontalGroup(
				layout.createSequentialGroup().addComponent(label)
						.addGroup(
								layout.createParallelGroup(Alignment.LEADING).addComponent(statusLabel)
										.addComponent(textField)
										.addGroup(layout.createSequentialGroup()
												.addGroup(layout.createParallelGroup(Alignment.LEADING)
														.addComponent(mcase))
										.addGroup(layout.createParallelGroup(Alignment.LEADING).addComponent(wholew))
										.addGroup(layout.createParallelGroup(Alignment.LEADING).addComponent(regex))
										.addGroup(layout.createParallelGroup(Alignment.LEADING).addComponent(classname)))
				.addGroup(layout.createSequentialGroup()
						.addGroup(layout.createParallelGroup(Alignment.LEADING).addComponent(listScroller)
								.addComponent(progressBar))))
				.addGroup(layout.createParallelGroup(Alignment.LEADING).addComponent(findButton))

		);

		layout.linkSize(SwingConstants.HORIZONTAL, findButton);
		layout.setVerticalGroup(layout.createSequentialGroup()
				.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(label).addComponent(textField)
						.addComponent(findButton))
				.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(mcase).addComponent(wholew)
						.addComponent(regex).addComponent(classname))
				.addGroup(layout.createParallelGroup(Alignment.LEADING)
						.addGroup(layout.createSequentialGroup()
								.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(listScroller))))
				.addGroup(layout.createParallelGroup(Alignment.LEADING)).addComponent(statusLabel)
				.addComponent(progressBar));
		this.adjustWindowPositionBySavedState();
		this.setSaveWindowPositionOnClosing();

		this.setName("Find All");
		this.setTitle("Find All");
	}

	private class FindButton extends AbstractAction {
		private static final long serialVersionUID = 75954129199541874L;

		@Override
		public void actionPerformed(ActionEvent event) {
			tmp_thread = new Thread() {
				public void run() {
					if (findButton.getText().equals("Stop")) {
						if (tmp_thread != null)
							tmp_thread.interrupt();
						setStatus("Stopped.");
						findButton.setText("Find");
						locked = false;
					} else {
						findButton.setText("Stop");
						classesList.clear();
						ConfigSaver configSaver = ConfigSaver.getLoadedInstance();
						DecompilerSettings settings = configSaver.getDecompilerSettings();
						File inFile = MainWindow.model.getOpenedFile();
						boolean filter = ConfigSaver.getLoadedInstance().getLuytenPreferences()
								.isFilterOutInnerClassEntries();
						try {
							JarFile jfile = new JarFile(inFile);
							Enumeration<JarEntry> entLength = jfile.entries();
							initProgressBar(Collections.list(entLength).size());
							Enumeration<JarEntry> ent = jfile.entries();
							while (ent.hasMoreElements() && findButton.getText().equals("Stop")) {
								JarEntry entry = ent.nextElement();
								String name = entry.getName();
								setStatus(name);
								if (filter && name.contains("$"))
									continue;
								if(locked || classname.isSelected()){
									locked = true;
									if(search(entry.getName()))
										addClassName(entry.getName());
								}else{
									if (entry.getName().endsWith(".class")) {
										synchronized (settings) {
											String internalName = StringUtilities.removeRight(entry.getName(), ".class");
											TypeReference type = Model.metadataSystem.lookupType(internalName);
											TypeDefinition resolvedType = null;
											if (type == null || ((resolvedType = type.resolve()) == null)) {
												throw new Exception("Unable to resolve type.");
											}
											StringWriter stringwriter = new StringWriter();
											DecompilationOptions decompilationOptions;
											decompilationOptions = new DecompilationOptions();
											decompilationOptions.setSettings(settings);
											decompilationOptions.setFullDecompilation(true);
											PlainTextOutput plainTextOutput = new PlainTextOutput(stringwriter);
											plainTextOutput.setUnicodeOutputEnabled(
													decompilationOptions.getSettings().isUnicodeOutputEnabled());
											settings.getLanguage().decompileType(resolvedType, plainTextOutput,
													decompilationOptions);
											if (search(stringwriter.toString()))
												addClassName(entry.getName());
										}
									} else {

										StringBuilder sb = new StringBuilder();
										long nonprintableCharactersCount = 0;
										try (InputStreamReader inputStreamReader = new InputStreamReader(
												jfile.getInputStream(entry));
												BufferedReader reader = new BufferedReader(inputStreamReader);) {
											String line;
											while ((line = reader.readLine()) != null) {
												sb.append(line).append("\n");

												for (byte nextByte : line.getBytes()) {
													if (nextByte <= 0) {
														nonprintableCharactersCount++;
													}
												}

											}
										}
										if (nonprintableCharactersCount < 5 && search(sb.toString()))
											addClassName(entry.getName());
									}
								}
							}
							setSearching(false);
							if (findButton.getText().equals("Stop")) {
								setStatus("Done.");
								findButton.setText("Find");
								locked = false;
							}
							jfile.close();
							locked = false;
						} catch (Exception e) {
							Luyten.showExceptionDialog("Exception!", e);
						}

					}
				}
			};
			tmp_thread.start();

		}

	}

	private boolean search(String bulk) {
		String a = textField.getText();
		String b = bulk;
		if (regex.isSelected())
			return Pattern.matches(a, b);
		if (wholew.isSelected())
			a = " " + a + " ";
		if (!mcase.isSelected()) {
			a = a.toLowerCase();
			b = b.toLowerCase();
		}
		if (b.contains(a))
			return true;
		return false;
	}

	private void setHideOnEscapeButton() {
		Action escapeAction = new AbstractAction() {
			private static final long serialVersionUID = 6846566740472934801L;

			@Override
			public void actionPerformed(ActionEvent e) {
				FindAllBox.this.setVisible(false);
			}
		};

		KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
		this.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE");
		this.getRootPane().getActionMap().put("ESCAPE", escapeAction);
	}

	private void adjustWindowPositionBySavedState() {
		WindowPosition windowPosition = ConfigSaver.getLoadedInstance().getFindWindowPosition();

		if (windowPosition.isSavedWindowPositionValid()) {
			this.setLocation(windowPosition.getWindowX(), windowPosition.getWindowY());
		}
	}

	private void setSaveWindowPositionOnClosing() {
		this.addWindowListener(new WindowAdapter() {
			@Override
			public void windowDeactivated(WindowEvent e) {
				WindowPosition windowPosition = ConfigSaver.getLoadedInstance().getFindWindowPosition();
				windowPosition.readPositionFromDialog(FindAllBox.this);
			}
		});
	}

	public void showFindBox() {
		this.setVisible(true);
		this.textField.requestFocus();
	}

	public void hideFindBox() {
		this.setVisible(false);
	}

	public void setStatus(String text) {
		if (text.length() > 25) {
			this.statusLabel.setText("Searching in file: ..." + text.substring(text.length() - 25));
		} else {
			this.statusLabel.setText("Searching in file: " + text);
		}

		progressBar.setValue(progressBar.getValue() + 1);
	}

	public void addClassName(String className) {
		this.classesList.addElement(className);
	}

	public void initProgressBar(Integer length) {
		progressBar.setMaximum(length);
		progressBar.setValue(0);
		progressBar.setStringPainted(true);
	}

	public boolean isSearching() {
		return searching;
	}

	public void setSearching(boolean searching) {
		this.searching = searching;
	}
}