/*
 * Copyright (C) 2004-2013 L2J Server
 * 
 * This file is part of L2J Server.
 * 
 * L2J Server 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.
 * 
 * L2J Server 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 com.l2jserver.tools.configurator;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIDefaults;
import javax.swing.UIManager;

import javolution.util.FastList;

import com.l2jserver.tools.configurator.ConfigUserInterface.ConfigFile.ConfigComment;
import com.l2jserver.tools.configurator.ConfigUserInterface.ConfigFile.ConfigProperty;
import com.l2jserver.tools.i18n.LanguageControl;
import com.l2jserver.tools.images.ImagesTable;

/**
 * @author KenM
 */
public class ConfigUserInterface extends JFrame implements ActionListener
{
	private static final long serialVersionUID = 2609592249095305857L;
	
	public static final String EOL = System.lineSeparator();
	
	private final JTabbedPane _tabPane = new JTabbedPane();
	
	private List<ConfigFile> _configs = new FastList<>();
	
	private ResourceBundle _bundle;
	
	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		try
		{
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		}
		catch (Exception e)
		{
			// couldn't care less
		}
		
		final ResourceBundle bundle = ResourceBundle.getBundle("configurator.Configurator", Locale.getDefault(), LanguageControl.INSTANCE);
		
		SwingUtilities.invokeLater(new Runnable()
		{
			@Override
			public void run()
			{
				ConfigUserInterface cui = new ConfigUserInterface(bundle);
				cui.setVisible(true);
			}
		});
	}
	
	public ConfigUserInterface(ResourceBundle bundle)
	{
		setBundle(bundle);
		
		//[JOJO]-------------------------------------------------
		try {
			String fontName = bundle.getString("fontName");
			String fontSize = bundle.getString("fontSize");
			Font font = new Font(fontName, Font.PLAIN, Integer.parseInt(fontSize));
			UIDefaults defaultTable = UIManager.getLookAndFeelDefaults();
			for (Object o : defaultTable.keySet())
				if (o.toString().endsWith(".font"))
					UIManager.put(o, font);
		}
		catch (Exception e) {}
		//-------------------------------------------------------
		
		setTitle(bundle.getString("toolName"));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setSize(750, 500);
		setLayout(new GridBagLayout());
		
		setDefaultLookAndFeelDecorated(true);
		setIconImage(ImagesTable.getImage("l2j.png").getImage());
		
		GridBagConstraints cons = new GridBagConstraints();
		cons.fill = GridBagConstraints.HORIZONTAL;
		cons.gridx = 0;
		cons.gridy = 0;
		cons.weighty = 0;
		cons.weightx = 1;
		
		JMenuBar menubar = new JMenuBar();
		
		JMenu fileMenu = new JMenu(bundle.getString("fileMenu"));
		JMenu helpMenu = new JMenu(bundle.getString("helpMenu"));
		
		JMenuItem exitItem = new JMenuItem(bundle.getString("exitItem"));
		exitItem.setActionCommand("exit");
		exitItem.addActionListener(this);
		fileMenu.add(exitItem);
		
		JMenuItem aboutItem = new JMenuItem(bundle.getString("aboutItem"));
		aboutItem.setActionCommand("about");
		aboutItem.addActionListener(this);
		helpMenu.add(aboutItem);
		
		menubar.add(fileMenu);
		menubar.add(helpMenu);
		
		setJMenuBar(menubar);
		
		JToolBar toolBar = new JToolBar();
		toolBar.setFloatable(false);
		toolBar.setRollover(true);
		toolBar.add(createToolButton("disk.png", bundle.getString("save"), "save"));
		this.add(toolBar, cons);
		
		cons.gridy++;
		cons.fill = GridBagConstraints.BOTH;
		cons.weighty = 1;
		loadConfigs();
		buildInterface();
		this.add(_tabPane, cons);
	}
	
	private JButton createToolButton(String image, String text, String action)
	{
		JButton button = new JButton(text, ImagesTable.getImage(image));
		button.setActionCommand(action);
		button.addActionListener(this);
		return button;
	}
	
	private void buildInterface()
	{
		ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
		ToolTipManager.sharedInstance().setInitialDelay(0);
		ToolTipManager.sharedInstance().setReshowDelay(0);
		
		GridBagConstraints cons = new GridBagConstraints();
		cons.fill = GridBagConstraints.NONE;
		cons.anchor = GridBagConstraints.FIRST_LINE_START;
		cons.insets = new Insets(2, 2, 2, 2);
		for (ConfigFile cf : getConfigs())
		{
			JPanel panel = new JPanel()
			{
				private static final long serialVersionUID = -323928678804839054L;
				
				@Override
				public void scrollRectToVisible(Rectangle r)
				{
				}
			};
			panel.setLayout(new GridBagLayout());
			
			cons.gridy = 0;
			cons.weighty = 0;
			for (ConfigComment cc : cf.getConfigProperties())
			{
				if (!(cc instanceof ConfigProperty))
				{
					continue;
				}
				
				ConfigProperty cp = (ConfigProperty) cc;
				cons.gridx = 0;
				
				JLabel keyLabel = new JLabel(cp.getDisplayName() + ':', ImagesTable.getImage("help.png"), SwingConstants.LEFT);
				String comments = "<b>" + cp.getName() + ":</b><br>" + cp.getComments();
				comments = comments.replaceAll("(?:\r\n|\n|\r)", "<br>");	//[JOJO]/*テスト済*/
				comments = "<html>" + comments + "</html>";
				keyLabel.setToolTipText(comments);
				cons.weightx = 0;
				panel.add(keyLabel, cons);
				cons.gridx++;
				
				JComponent valueComponent = cp.getValueComponent();
				valueComponent.setToolTipText(comments);
				cons.weightx = 1;
				panel.add(valueComponent, cons);
				cons.gridx++;
				cons.gridy++;
			}
			cons.gridy++;
			cons.weighty = 1;
			panel.add(new JLabel(), cons); // filler
			_tabPane.addTab(cf.getName(), new JScrollPane(panel));
		}
		/////////////////////////////////////////////////////
		// [JOJO]
		for (int index = _tabPane.getTabCount(); --index >= 0; ) {
			String title = _tabPane.getTitleAt(index);
			if (title.endsWith("server")) {
		//	if (title.equals("server") || title.equals("loginserver")) {
				_tabPane.setSelectedIndex(index);
				break;
			}
		}
		/////////////////////////////////////////////////////
	}
	
	private void loadConfigs()
	{
		File configsDir = new File("config");
		for (File file : configsDir.listFiles())
		{
			if (file.getName().endsWith(".properties") && file.isFile() && file.canWrite())
			{
				try
				{
					parsePropertiesFile(file);
				}
				catch (IOException e)
				{
					JOptionPane.showMessageDialog(ConfigUserInterface.this, getBundle().getString("errorReading") + file.getName(), getBundle().getString("error"), JOptionPane.ERROR_MESSAGE);
					System.exit(3);
				}
			}
		}
	}
	
	/**
	 * @param file
	 * @throws IOException
	 */
	private void parsePropertiesFile(File file) throws IOException
	{
		try (FileInputStream fis = new FileInputStream(file);
			InputStreamReader isr = new InputStreamReader(fis, UTF_8);	//[JOJO] UTF-8
			LineNumberReader lnr = new LineNumberReader(isr))
		{
			String line;
			StringBuilder commentBuffer = new StringBuilder();
			ConfigFile cf = new ConfigFile(file);
			while ((line = lnr.readLine()) != null)
			{
				line = line.trim();
				if (line.isEmpty())
				{
					// blank line, reset comments
					if (commentBuffer.length() > 0)
					{
						cf.addConfigComment(commentBuffer.toString());
					}
					commentBuffer.setLength(0);
				}
				else if (line.charAt(0) == '#')
				{
					if (commentBuffer.length() > 0)
					{
						commentBuffer.append(cf.FILE_EOL);
					}
					commentBuffer.append(line.substring(1));
				}
				else if (line.indexOf('=') >= 0)
				{
					String[] kv = line.split("=", 2);	//[JOJO]
					String key = kv[0].trim();
					StringBuilder value = new StringBuilder();
					if (kv.length > 1)
					{
						value.append(kv[1].trim());
					}
					
					while (line.endsWith("\\"))	//[JOJO]
					{
						if ((line = lnr.readLine()) == null)
							break;
						value.append(cf.FILE_EOL).append(line);
					}
					
					String comments = commentBuffer.toString();
					commentBuffer.setLength(0); // reset
					
					cf.addConfigProperty(key, parseValue(value.toString()), comments);
				}
			}
			getConfigs().add(cf);
		}
	}
	
	/**
	 * @param value
	 * @return
	 */
	private Object parseValue(String value)
	{
		if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("true"))
		{
			return Boolean.parseBoolean(value);
		}
		
		/*
		 * try { double parseDouble = Double.parseDouble(value); return parseDouble; } catch (NumberFormatException e) { // not a double, ignore }
		 */
		
		// localhost -> 127.0.0.1
		if (value.equals("localhost"))
		{
			value = "127.0.0.1";
		}
		
		String[] parts = value.split("\\.");
		if (parts.length == 4)
		{
			boolean ok = true;
			for (int i = 0; (i < 4) && ok; i++)
			{
				try
				{
					int parseInt = Integer.parseInt(parts[i]);
					if ((parseInt < 0) || (parseInt > 255))
					{
						ok = false;
					}
				}
				catch (NumberFormatException e)
				{
					ok = false;
				}
			}
			
			if (ok)
			{
				try
				{
					InetAddress address = InetAddress.getByName(value);
					return address;
				}
				catch (UnknownHostException e)
				{
					// ignore
				}
			}
		}
		
		return value;
	}
	
	static class ConfigFile
	{
		private final File _file;
		private final String _name;
		public final String FILE_EOL;	//[JOJO]
		private final List<ConfigComment> _configs = new FastList<>();
		
		public ConfigFile(File file)
		{
			_file = file;
			int lastIndex = file.getName().lastIndexOf('.');
			_name = file.getName().substring(0, lastIndex);
			FILE_EOL = detectEOL(file);	//[JOJO]
		}
		
		//[JOJO]-------------------------------------------------
		private String detectEOL(File file)
		{
			try (FileInputStream in = new FileInputStream(file))
			{
				int ch1, ch2;
				while ((ch1 = in.read()) != -1)
				{
					if (ch1 == '\r')
					{
						ch2 = in.read();
						if (ch2 == '\n') return "\r\n";
						else return "\r";
					}
					else if (ch1 == '\n')
					{
						return "\n";
					}
				}
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
			return System.lineSeparator();
		}
		//-------------------------------------------------------
		
		public void addConfigProperty(String name, Object value, ValueType type, String comments)
		{
			_configs.add(new ConfigProperty(name, value, type, comments));
		}
		
		public void addConfigComment(String comment)
		{
			_configs.add(new ConfigComment(comment));
		}
		
		public void addConfigProperty(String name, Object value, String comments)
		{
			this.addConfigProperty(name, value, ValueType.firstTypeMatch(value), comments);
		}
		
		public List<ConfigComment> getConfigProperties()
		{
			return _configs;
		}
		
		/**
		 * @return Returns the name.
		 */
		public String getName()
		{
			return _name;
		}
		
		public void save() throws IOException
		{
			try (BufferedWriter bufWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(_file), UTF_8)))	//[JOJO] UTF-8 /*テスト済*/
			{
				for (ConfigComment cc : _configs)
				{
					cc.save(bufWriter);
				}
			}
		}
		
		class ConfigComment
		{
			
			private String _comments;
			
			/**
			 * @param comments
			 */
			public ConfigComment(String comments)
			{
				_comments = comments;
			}
			
			/**
			 * @return Returns the comments.
			 */
			public String getComments()
			{
				return _comments;
			}
			
			/**
			 * @param comments The comments to set.
			 */
			public void setComments(String comments)
			{
				_comments = comments;
			}
			
			public void save(Writer writer) throws IOException
			{
				StringBuilder sb = new StringBuilder();
				sb.append(Pattern.compile("^", Pattern.MULTILINE).matcher(getComments()).replaceAll("#"));	//[JOJO]/*テスト済*/
				sb.append(FILE_EOL);
				sb.append(FILE_EOL);
				writer.write(sb.toString());
			}
		}
		
		class ConfigProperty extends ConfigComment
		{
			private String _propname;
			private Object _value;
			private ValueType _type;
			private JComponent _component;
			
			/**
			 * @param name
			 * @param value
			 * @param type
			 * @param comments
			 */
			public ConfigProperty(String name, Object value, ValueType type, String comments)
			{
				super(comments);
				if (!type.getType().isAssignableFrom(value.getClass()))
				{
					throw new IllegalArgumentException("Value Instance Type doesn't match the type argument.");
				}
				_propname = name;
				_type = type;
				_value = value;
			}
			
			/**
			 * @return Returns the name.
			 */
			public String getName()
			{
				return _propname;
			}
			
			/**
			 * @return Returns the name.
			 */
			public String getDisplayName()
			{
				return unCamelize(_propname);
			}
			
			/**
			 * @param name The name to set.
			 */
			public void setName(String name)
			{
				_propname = name;
			}
			
			/**
			 * @return Returns the value.
			 */
			public Object getValue()
			{
				return _value;
			}
			
			/**
			 * @param value The value to set.
			 */
			public void setValue(String value)
			{
				_value = value;
			}
			
			/**
			 * @return Returns the type.
			 */
			public ValueType getType()
			{
				return _type;
			}
			
			/**
			 * @param type The type to set.
			 */
			public void setType(ValueType type)
			{
				_type = type;
			}
			
			public JComponent getValueComponent()
			{
				if (_component == null)
				{
					_component = createValueComponent();
				}
				return _component;
			}
			
			public JComponent createValueComponent()
			{
				switch (getType())
				{
					case BOOLEAN:
						boolean bool = (Boolean) getValue();
						JCheckBox checkBox = new JCheckBox();
						checkBox.setSelected(bool);
						return checkBox;
					case IPv4:
						return new JIPTextField((Inet4Address) getValue());
					case DOUBLE:
					case INTEGER:
					case STRING:
					default:
						String val = getValue().toString();
						JTextArea textArea = new JTextArea(val);
						textArea.setFont(UIManager.getFont("TextField.font"));
						int rows = 1;
						for (int i = 0; i < val.length(); i++)
						{
							if (val.charAt(i) == '\\')
							{
								rows++;
							}
						}
						textArea.setRows(rows);
						textArea.setColumns(Math.max(val.length() / rows, 20));
						return textArea;
				}
			}
			
			@Override
			public void save(Writer writer) throws IOException
			{
				String value;
				if (getValueComponent() instanceof JCheckBox)
				{
					value = Boolean.toString(((JCheckBox) getValueComponent()).isSelected());
					value = value.substring(0, 1).toUpperCase() + value.substring(1);
				}
				else if (getValueComponent() instanceof JIPTextField)
				{
					value = ((JIPTextField) getValueComponent()).getText();
				}
				else if (getValueComponent() instanceof JTextArea)
				{
					value = ((JTextArea) getValueComponent()).getText();
				}
				else
				{
					throw new IllegalStateException("Unhandled component value");
				}
				
				StringBuilder sb = new StringBuilder();
				sb.append(Pattern.compile("^", Pattern.MULTILINE).matcher(getComments()).replaceAll("#"));	//[JOJO]/*テスト済*/
				sb.append(FILE_EOL);
				sb.append(getName());
				sb.append(" = ");
				sb.append(value);
				sb.append(FILE_EOL);
				sb.append(FILE_EOL);
				writer.write(sb.toString());
			}
		}
	}
	
	public static enum ValueType
	{
		BOOLEAN(Boolean.class),
		DOUBLE(Double.class),
		INTEGER(Integer.class),
		IPv4(Inet4Address.class),
		STRING(String.class);
		
		private final Class<?> _type;
		
		private ValueType(Class<?> type)
		{
			_type = type;
		}
		
		/**
		 * @return Returns the type.
		 */
		public Class<?> getType()
		{
			return _type;
		}
		
		public static ValueType firstTypeMatch(Object value)
		{
			for (ValueType vt : ValueType.values())
			{
				if (vt.getType() == value.getClass())
				{
					return vt;
				}
			}
			throw new NoSuchElementException("No match for: " + value.getClass().getName());
		}
	}
	
	@Override
	public void actionPerformed(ActionEvent e)
	{
		String cmd = e.getActionCommand();
		
		StringBuilder errors = new StringBuilder();
		
		if (cmd.equals("save"))
		{
			for (ConfigFile cf : ConfigUserInterface.this.getConfigs())
			{
				try
				{
					cf.save();
				}
				catch (Exception e1)
				{
					e1.printStackTrace();
					errors.append(getBundle().getString("errorSaving") + cf.getName() + ".properties. " + getBundle().getString("reason") + e1.getLocalizedMessage() + EOL);
				}
			}
			if (errors.length() == 0)
			{
				JOptionPane.showMessageDialog(ConfigUserInterface.this, getBundle().getString("success"), "OK", JOptionPane.INFORMATION_MESSAGE);
			}
			else
			{
				JOptionPane.showMessageDialog(ConfigUserInterface.this, errors, getBundle().getString("error"), JOptionPane.ERROR_MESSAGE);
				System.exit(2);
			}
		}
		else if (cmd.equals("exit"))
		{
			System.exit(0);
		}
		else if (cmd.equals("about"))
		{
			JOptionPane.showMessageDialog(ConfigUserInterface.this, getBundle().getString("credits") + EOL + "http://www.l2jserver.com" + EOL + EOL + getBundle().getString("icons") + EOL + EOL + getBundle().getString("langText") + EOL + getBundle().getString("translation"), getBundle().getString("aboutItem"), JOptionPane.INFORMATION_MESSAGE, ImagesTable.getImage("l2jserverlogo.png"));
		}
	}
	
	/**
	 * @param configs The configuration to set.
	 */
	public void setConfigs(List<ConfigFile> configs)
	{
		_configs = configs;
	}
	
	/**
	 * @return Returns the configuration.
	 */
	public List<ConfigFile> getConfigs()
	{
		return _configs;
	}
	
	/**
	 * @param keyName
	 * @return Returns the configuration setting name in a human readable form.
	 */
	public static String unCamelize(final String keyName)
	{
		Pattern p = Pattern.compile("\\p{Lu}");
		Matcher m = p.matcher(keyName);
		StringBuffer sb = new StringBuffer();
		int last = 0;
		while (m.find())
		{
			if (m.start() != (last + 1))
			{
				m.appendReplacement(sb, " " + m.group());
			}
			last = m.start();
		}
		m.appendTail(sb);
		return sb.toString().trim();
	}
	
	/**
	 * @param bundle The bundle to set.
	 */
	public void setBundle(ResourceBundle bundle)
	{
		_bundle = bundle;
	}
	
	/**
	 * @return Returns the bundle.
	 */
	public ResourceBundle getBundle()
	{
		return _bundle;
	}
}
