/**
 * tectonic1.java
 * auteur : Jean-Paul Quelen
 * date : 31/08/22
 */

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.io.*;

public class tectonic extends JPanel implements ActionListener {
	static final long serialVersionUID = 220831L;
	JTextField jtfnl = new JTextField ("5", 5);
	JTextField jtfnc = new JTextField ("4", 5);
	JButton jbng = new JButton ("Nouvelle grille");
	JButton jbof = new JButton ("Ouvrir un fichier");
	JButton jbgc = new JButton ("Créer une grille complète");
	String listenbres = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	static String sng = "Nouvelle grille";
	static String sngc = "Nouvelle grille complète";
	static String sgv = "Grille vierge";

	public tectonic() {
		add (new JLabel ("nl : "));
		add (jtfnl);
		add (new JLabel ("nc : "));
		add (jtfnc);
		add (jbng);
		jbng.addActionListener (this);
		add (jbgc);
		jbgc.addActionListener (this);
		add (jbof);
		jbof.addActionListener (this);
	}

/*
 * convertit le caractère alphanumérique présent en position j dans la chaîne s
 * en un entier ; retourne -1 si pas trouvé
 */

	private int indStr2int (String s, int j) {
		try {
			return listenbres.indexOf (s.toUpperCase().charAt (j));
		}
		catch (StringIndexOutOfBoundsException e) {
			return -1;
		}
	}
	
/*
 * convertit un entier n avec 0 <= n < 36 en une chaîne de caractères
 * réduite à un caractère alphanumérique
 * retourne " " si n n'est pas dans l'intervalle [0, 36[
 */

	private String index2char (int i) {
		try {
			return listenbres.substring (i, i + 1);
		}
		catch (IndexOutOfBoundsException ioobe) {
			return " ";
		}
	}

	private void affiche_message (String s) {
		JOptionPane.showMessageDialog (null, s, "", JOptionPane.ERROR_MESSAGE);
	}

// commentaire = ligne démarrant par #
	private String ouvrir_ligne(BufferedReader br) {
		String s;
		try {
			do {
				s = br.readLine(); // s == null si fin du fichier
				if (s == null)
					return null;
				if (s.length() == 0)
					s = " ";
			}
			while (s.charAt (0) == '#');
			return s;
		}
		catch (java.io.IOException e) {
			affiche_message ("problème de lecture");
			return null;
		}
	}

// ouverture d'un fichier
	private Grille ouvrir_fichier() {
		Grille g;
		File repertoireCourant = null;
		try {
			repertoireCourant = new File(".").getCanonicalFile();
		}
		catch(IOException e) {}
		JFileChooser jfc = new JFileChooser (repertoireCourant);
		jfc.setDialogTitle ("Ouvrir...");
		jfc.showOpenDialog(null);
		File gsf = jfc.getSelectedFile();
		if (gsf != null) {
			try {
				BufferedReader br = new BufferedReader (new FileReader (gsf.toString()));
// lecture du nombre de lignes à lire
				String snl = ouvrir_ligne(br);
				if (snl == null) {
					return null;
				}
				int nl = Integer.valueOf (snl);
				int nc = 0;
// lecture des indicateurs de zone présents dans chaque cellule
				String s = ouvrir_ligne (br);
				if (s == null || s.length() == 0)
					return null;
				else {
					nc = s.length();
					g = new Grille (nl, nc, gsf.toString());
					g.zones = new int [nl][nc];
					g.jbok.setEnabled (false);
				}
				for (int j = 0; j < nc; j++) {
					g.zones[0][j] = indStr2int (s, j);
				}
				for (int i = 1; i < nl; i++) {
					s = ouvrir_ligne (br);
					if (s == null || s.length() == 0)
						return null;
					else {
						if (i == 0) {
							nc = s.length();
							g = new Grille (nl, nc, gsf.toString());
							g.zones = new int [nl][nc];
						}
						for (int j = 0; j < nc; j++) {
							g.zones[i][j] = indStr2int (s, j);
						}
					}
				}
// lecture des valeurs de chaque cellule
				for (int i = 0; i < nl; i++) {
					s = ouvrir_ligne (br);
					if (s != null && s.length() != 0) {
						for (int j = 0; j < nc; j++) {
							String c = s.toUpperCase().substring (j, j + 1);
							g.jtfgrille[i][j].setText((listenbres.indexOf(c) != -1)? c : " ");
						}
					}
				}
// lecture éventuelle de chaque élément de la grille
				s = null;
				for (int i = 0; i < nl; i++) {
					for (int j = 0; j < nc; j++) {
						s = ouvrir_ligne (br);
						if (s == null) {
							break;
						} else {
							try {
								int sio = s.indexOf ("|");
								String texte = s.substring (0, sio++);
								String c =s.substring (sio);
								g.jtfgrille[i][j].setText(texte);
								Color couleur = Color.BLACK; // noirs par défaut
								if (c.compareTo ("B") == 0) {
									couleur = Color.BLUE;
								} else if (c.compareTo ("V") == 0) {
									couleur = Color.GREEN;
								} else if (c.compareTo ("R") == 0) {
									couleur = Color.RED;
								}
								g.jtfgrille[i][j].setForeground (couleur);
							}
							catch (IndexOutOfBoundsException e) {
								g.jtfgrille[i][j].setText (s);
							}
						}
					}
					if (s == null) {
						break;
					}
				}
			}
			catch (java.io.IOException e) {
				affiche_message ("IOException");
				return null;
			}
			catch (java.lang.SecurityException e) {
				affiche_message ("SecurityException");
				return null;
			}
		} else {
			affiche_message ("Pas de fichier sélectionné");
			return null;
		}
		g.delimite_zones();
		return g;
	}

	public void actionPerformed (ActionEvent e) {
		if (e.getSource() == jbng || e.getSource() == jbgc) {
// Nouvelle grille ou créer une nouvelle grille complète
			String titre = (e.getSource() == jbgc)? sngc : sng;
			Grille g = new Grille (Integer.parseInt(jtfnl.getText()), Integer.parseInt(jtfnc.getText()), titre);
			g.jbf.setEnabled (false);
			g.jbfo.setEnabled (false);
			g.jbe.setEnabled (false);
			g.jbv.setEnabled (false);
		} else if (e.getSource() == jbof) {
// oouverture d'un fichier
			ouvrir_fichier();
		}
	}

	public static void main (String [] args) {
		tectonic t = new tectonic();
		JFrame jf = new JFrame ("Tectonic");
		jf.add (t);
		jf.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
		jf.setSize (700, 80);
		jf.setVisible (true);
	}


/**
 * classe : grille
 * date : 31/08/22
 */

protected class Grille extends JFrame implements ActionListener, ComponentListener, FocusListener {
	static final long serialVersionUID = 220831L;
	int nl, nc;
	JTextField jtfgrilleij = new JTextField();
	JTextField [][] jtfgrille;
	JButton jbok = new JButton ("Ok");
	JButton jbf = new JButton ("Fixe");
	JButton jbfo = new JButton ("Formate");
	JButton jbv = new JButton ("Vérifie");
	JButton jbe = new JButton ("Enregistre");
	JRadioButton jrbn = new JRadioButton ("", true);
	JRadioButton jrbb = new JRadioButton ("", false);
	JRadioButton jrbr = new JRadioButton ("", false);
	JRadioButton jrbv = new JRadioButton ("", false);
	Font F, Fs2;
	JPanel jpg;
	int[][] zones;
	Color couleur = Color.BLACK;
	boolean bbgc;
	boolean bfg = false;

	public Grille (int nl, int nc, String titre) {
		super (titre);
		try {
			this.nl = nl;
			this.nc = nc;
			bbgc = (titre.compareTo (sng) == 0) ? false : true;
			JPanel jp = new JPanel();
			add (jp);
			jp.setLayout (new BorderLayout());

			JPanel jpn = new JPanel();
			jp.add (jpn, BorderLayout.NORTH);

			jpn.add (jbf);
			jbf.addActionListener (this);
			jpn.add (jbfo);
			jbfo.addActionListener (this);
			jpn.add (jbv);
			jbv.addActionListener (this);
			jpn.add (jbe);
			jbe.addActionListener (this);

			ButtonGroup bgc = new ButtonGroup();
			bgc.add (jrbn);
			jpn.add (jrbn);
			jrbn.setBackground (Color.BLACK);
			jrbn.addActionListener (this);
			bgc.add (jrbb);
			jpn.add (jrbb);
			jrbb.setBackground (Color.BLUE);
			jrbb.addActionListener (this);
			bgc.add (jrbr);
			jpn.add (jrbr);
			jrbr.setBackground (Color.RED);
			jrbr.addActionListener (this);
			bgc.add (jrbv);
			jpn.add (jrbv);
			jrbv.setBackground (Color.GREEN);
			jrbv.addActionListener (this);

			jpn.add (jbok);
			jbok.addActionListener (this);

			jpg = new JPanel();
			jp.add (jpg, BorderLayout.CENTER);
			jpg.setLayout (new GridLayout(nl, nc));
			jpg.addComponentListener (this);
			jtfgrille = new JTextField [nl][nc];
			for (int i = 0; i < nl; i ++) {
				for (int j = 0; j < nc; j ++) {
					JTextField jtfgrilleij = new JTextField(" ");
					jtfgrilleij.addFocusListener (this);
					jpg.add (jtfgrilleij);
					jtfgrille[i][j] = jtfgrilleij;
				}
			}
		}
		catch (NegativeArraySizeException e) {
			affiche_message (e.toString());
		}
		catch (NumberFormatException e) {
			affiche_message (e.toString());
		}
		setSize (Integer.max (500, 80 * nc), Integer.max (700, 100 * nl));
		setVisible (true);
	}

// détermine les bords des différentes zones et calcule le nombre de cases par zone
	private void delimite_zones() {
		int bzoneslen = 0;
		int[] bzones = new int [nl * nc];
		for (int i = 0; i < nl; i++) {
			for (int j = 0; j < nc; j++) {
				int bzij = zones[i][j];
				if (bzones [bzij] == 0)
					bzoneslen++;
				bzones [bzij] ++;
				int h = 1, g = 1, b = 1, d	= 1;
				int ep = 3;
// teste si c'est un bord
				if (i == 0)
					h = ep;
				if (i == nl - 1)
					b = ep;
				if (j == 0)
					g = ep;
				if (j == nc - 1)
					d = ep;
// teste si les cellules voisines ont des éléments d'une autre zone
				if (i > 0 && bzij != zones[i-1][j])
					h = ep;
				if (j > 0 && bzij != zones [i][j-1])
					g = ep;
				if (i < nl - 1 && bzij != zones[i+1][j])
					b = ep;
				if (j < nc - 1 && bzij != zones [i][j+1])
					d = ep;
				jtfgrille[i][j].setBorder (BorderFactory.createMatteBorder(h, g, b, d, Color.BLACK));
			}
		}
	}

// Applique la Fonte F aux cellules de la grille si la valeur est fixée et unique dans sa zone,
// Fs2 sinon

	private void formate() {
		int w = getWidth();
		int h = getHeight();
		int npx = w / nc / 2;
		F =  new Font ("Arial, Helvetica, sans-serif", Font.PLAIN, npx);
		Fs2 = new Font ("Arial, Helvetica, sans-serif", Font.PLAIN, npx / 2);
		for (int i = 0; i < nl; i++) {
			for (int j = 0; j < nc; j++) {
				JTextField jtfgij = jtfgrille[i][j];
				String sjtfgij = jtfgij.getText().trim();
				boolean unique = false;
				if (sjtfgij.length() == 1 && indStr2int (sjtfgij, 0) >= 0) {
					unique = true;
					int zij = zones [i][j];
					for (int i1 = 0; i1 < nl; i1++) {
						for (int j1 = 0; j1 < nc; j1++) {
							if ((i1 != i || j1 != j) && zij == zones[i1][j1]
							&& sjtfgij.compareTo (jtfgrille[i1][j1].getText().trim()) == 0) {
								unique = false;
								break;
							}
						}
						if (!unique)
							break;
					}
				}
				if (unique) {
					jtfgij.setFont (F);
					jtfgij.setText (sjtfgij);
				} else {
					jtfgij.setFont (Fs2);
					jtfgij.setText (sjtfgij);
				}
			}
		}
	}

// teste les voisins
	private boolean verifie_voisins() {
		for (int i = 0; i < nl; i++) {
				for (int j = 0; j < nc; j++) {
					String sjtfgij = jtfgrille[i][j].getText().trim();
					if (sjtfgij.length() == 0)
						return false;
					if (i > 0) {
						if (sjtfgij.compareTo (jtfgrille[i-1][j].getText().trim()) == 0)
							return false;
					}
					if (j > 0) {
						if (sjtfgij.compareTo (jtfgrille[i][j-1].getText().trim()) == 0)
							return false;
					}
					if (i > 0 && j > 0) {
						if (sjtfgij.compareTo (jtfgrille[i-1][j-1].getText().trim()) == 0)
							return false;
					}						
				}
		}
		return true;
	}

// test la cohérence des valeurs dans chaque zone.
	private boolean verifie_coherence() {
		int[] bzones = new int [nl * nc];
		int bzoneslen = 0;
		for (int i = 0; i < nl; i++) {
			for (int j = 0; j < nc; j++) {
				int k = zones [i][j];
				if (bzones[k] == 0)
					bzoneslen++;
				bzones[k]++;
			}
		}
		
		for (int k = 0; k < bzoneslen; k++) {
			String listevaleurs = "";
			for (int i = 0; i < nl; i++) {
				for (int j = 0; j < nc; j++) {
					if (zones [i][j] == k) {
						listevaleurs += jtfgrille[i][j].getText().trim();;
					}
				}
			}
			if (listevaleurs.length() != bzones [k])
				return false;
			for (int i = 1; i <= bzones [k]; i++) {
				if (listevaleurs.indexOf (index2char (i)) < 0)
					return false;
			}
		}						
		return true;
	}

	private void enregistre () {
		File repertoireCourant = null;
		try {
			repertoireCourant = new File(".").getCanonicalFile();
		}
		catch(IOException e) {}
		JFileChooser jfc = new JFileChooser (repertoireCourant);
		jfc.setDialogTitle("Enregistrer...");
		String gt = getTitle();
		if (gt.compareTo (sngc) != 0 && gt.compareTo (sgv) != 0) {
			File fc = new File (getTitle());
			jfc.setSelectedFile (fc);
		}
		jfc.showSaveDialog(null);
		File gsf = jfc.getSelectedFile();
		if (gsf != null) {
			try {
				String sgsf = gsf.toString();
				PrintWriter pw = new PrintWriter (new BufferedWriter (new FileWriter (sgsf)));
				pw.println ("# " + sgsf);
				pw.println ("# nombre de lignes");
				pw.println (Integer.toString (nl));
				pw.println ("# n° de la zone pour chaque case");
				for (int i = 0; i < nl; i++) {
					String szi = "";
					for (int j = 0; j < nc; j++) {
						szi += index2char (zones [i][j]);
					}
					pw.println (szi);
				}
				pw.println ("# valeurs");
				for (int i = 0; i < nl; i++) {
					for (int j = 0; j < nc; j++) {
						String s = jtfgrille[i][j].getText().trim();
						if (s.length() == 1)
							pw.print (s);
						else
							pw.print ("*");
					}
					pw.println ();
				}
				pw.println ("# grille en l'état");
				for (int i = 0; i < nl; i++) {
					for (int j = 0; j < nc; j++) {
						jtfgrilleij = jtfgrille[i][j];
						String scouleur = "N";
						Color couleur_cellule = jtfgrilleij.getForeground();
						if (couleur_cellule.equals (Color.BLUE)) {
							scouleur = "B";
						} else if (couleur_cellule.equals (Color.RED)) {
							scouleur = "R";
						} else if (couleur_cellule.equals (Color.GREEN)) {
							scouleur = "V";
						}
						pw.println (jtfgrilleij.getText().trim() + "|" + scouleur);
					}
				}
				pw.close();
				setTitle (sgsf);
			}
			catch (java.io.IOException e) {
				affiche_message ("IOException");
			}
			catch (java.lang.SecurityException e) {
				affiche_message ("SecurityException");
			}
		} else {
			affiche_message ("nom de fichier invalide ou absent");
		}
	}

// parcourt la partie connexe et met à jour éventuellement le numéro de zone
	private boolean cs (int i, int j, int anz, int nnz, boolean[][] tbz) {
		if (i >= 0 && i < nl && j >= 0 && j < nc && tbz[i][j] && anz == zones[i][j]) {
			tbz [i][j] = false;
			zones [i][j] = nnz;
			while (cs (i, j + 1, anz, nnz, tbz)) {}
			while (cs (i, j - 1, anz, nnz, tbz)) {}
			while (cs (i + 1, j, anz, nnz, tbz)) {}
			while (cs (i - 1, j, anz, nnz, tbz)) {}
			return true;
		}
		return false;
	}


	public void actionPerformed (ActionEvent e) {
		if (e.getSource() == jbok) {
// bouton "Ok"
			if (!bbgc)
				setTitle (sgv);
			jbok.setEnabled (false);
			jbf.setEnabled (true);
			jbfo.setEnabled (true);
			jbe.setEnabled (true);
			jbv.setEnabled (true);
			int bzoneslen = 0;
			zones = new int [nl][nc];
			String[] szones = new String [nl * nc];
			for (int i = 0; i < nl; i ++) {
				for (int j = 0; j < nc; j ++) {
					JTextField jtfgrilleij = jtfgrille [i][j];
					String sz = null;
					if (bbgc) {
						sz = jtfgrilleij.getForeground().toString();
					} else {
						sz = jtfgrilleij.getText().trim();
						jtfgrilleij.setText("");
					}
					int k = 0;
					while (szones[k] != null && szones[k].compareTo (sz) != 0) {
						k++;
					}
					if (szones[k] == null) {
						szones[k] = sz;
						bzoneslen++;
					}
					zones [i][j] = k;
				}
			}

			boolean[] bz = new boolean [nl * nc];
			for (int k = 0; k < bzoneslen; k++) {
				bz [k] = true;
			}

			boolean[][] tbz = new boolean [nl][nc];
			for (int i = 0; i < nl; i ++) {
				for (int j = 0; j < nc; j ++) {
					tbz [i][j] = true;
				}
			}


			for (int i = 0; i < nl; i ++) {
				for (int j = 0; j < nc; j ++) {
					if (tbz [i][j])	{
						int anz = zones [i][j];
						int nnz = anz;
						if (bz[nnz]) {
							bz[nnz] = false;
						} else {
							nnz = bzoneslen;
							bzoneslen++;
						}
						cs (i, j, anz, nnz, tbz);
					}
				}
			}
			delimite_zones();
// on bloque focusGained sinon problème avec la couleur dans la case [0][0]
			bfg = true;
		} else if (e.getSource() == jbf) {
// bouton "Fixe"
			jtfgrilleij.setFont (F);
			jtfgrilleij.setText (jtfgrilleij.getText().trim());
		} else if (e.getSource() == jbfo) {
// bouton "Formate"
			formate();
		} else if (e.getSource() == jbv) {
			if (!verifie_voisins())
				affiche_message ("cellules voisines identiques ou incohérence dans le contenu d'un cellule");
			else if (!verifie_coherence())
				affiche_message ("contenu de cellule(s) incohérent");
			else
				affiche_message ("grille Ok");
// bouton "Vérifie"
		} else if (e.getSource() == jbe) {
// bouton "Enregistre"
			enregistre();
// Radio boutons
		} else if (e.getSource() == jrbb) {
			couleur = Color.BLUE;
		} else if (e.getSource() == jrbn) {
			couleur = Color.BLACK;
		} else if (e.getSource() == jrbr) {
			couleur = Color.RED;
		} else if (e.getSource() == jrbv) {
				couleur = Color.GREEN;
		}
	}

	public void focusGained (FocusEvent e) {
		if (bfg)
			bfg = false;
		else {
			jtfgrilleij = (JTextField) e.getSource ();
			jtfgrilleij.setForeground (couleur);
		}
	}

	public void focusLost (FocusEvent e) {}

	public void componentResized (ComponentEvent e) {
		if (e.getSource() == jpg) {
			formate();
		}
	}

	public void componentMoved (ComponentEvent e) {}
	public void componentShown (ComponentEvent e) {}
	public void componentHidden (ComponentEvent e) {}
}

}
