import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Timer;
import java.util.TimerTask;

public class TypingUI extends JFrame implements KeyListener {
    private final Statistic statistic;
    private long lastTime;
    private JLabel textDisplay;
    private JLabel timerDisplay;
    private JLabel wordCountDisplay;
    private JPanel statisticsDisplay;
    private JButton restartButton;
    private static final int numberOfWordsInLine = 13;
    private static String text = "";
    private int position = 0;
    private int charCount = 0;
    private int timeRemaining;
    private int elapsedTime = 0;
    private int timeToType = 60;  // Default time
    private Timer timer;
    private TimerTask task;
    private boolean isWaitingToStart = true;
    private boolean isOptimized = true;

    public TypingUI() {
        super("Touch Typing Practice");
        statistic = new Statistic();
        TextGenerator.setStatistic(statistic);
        initializeText();
        createUI();
        restartTest();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setSize(new Dimension(1000, 400));
        this.centerFrame();
        this.setVisible(true);
        this.setFocusable(true);
        this.requestFocusInWindow();
        this.addKeyListener(this);
    }

    private static void initializeText() {
        text = "";
        for (int i = 0; i < numberOfWordsInLine; i++)
            text += TextGenerator.getRandomWord() + " ";
    }

    private void centerFrame() {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        int x = (screenSize.width - this.getWidth()) / 2;
        int y = (screenSize.height - this.getHeight()) / 2;
        this.setLocation(x, y);
    }

    private void createUI() {
        this.setLayout(new BorderLayout(10, 10));
        this.getContentPane().setBackground(new Color(240, 240, 240));
        this.setPadding(this.getContentPane(), 10);

        JPanel centerPanel = new JPanel(new GridBagLayout());
        centerPanel.setOpaque(false);

        JPanel timerPanel = new JPanel(new FlowLayout());
        timerPanel.setOpaque(false);

        statisticsDisplay = new JPanel(new GridLayout(2, 26, 5, 10));
        statisticsDisplay.setFont(new Font("Arial", Font.PLAIN, 12));
        statisticsDisplay.setForeground(new Color(60, 60, 60));

        GridBagConstraints gbcStats = new GridBagConstraints();
        gbcStats.gridx = 0;
        gbcStats.gridy = 1;
        gbcStats.anchor = GridBagConstraints.NORTH;
        centerPanel.add(statisticsDisplay, gbcStats);

        timerDisplay = new JLabel(timeRemaining + "s");
        timerDisplay.setFont(new Font("Arial", Font.PLAIN, 24));
        timerDisplay.setForeground(new Color(60, 60, 60));
        timerPanel.add(timerDisplay);

        GridBagConstraints gbcTimer = new GridBagConstraints();
        gbcTimer.gridx = 0;
        gbcTimer.gridy = 0;
        gbcTimer.anchor = GridBagConstraints.NORTH;
        centerPanel.add(timerPanel, gbcTimer);

        textDisplay = new JLabel("<html>" + colorText(text, 0, true) + "</html>");
        textDisplay.setFont(new Font("Arial", Font.PLAIN, 18));
        GridBagConstraints gbcText = new GridBagConstraints();
        gbcText.gridx = 0;
        gbcText.gridy = 2;
        gbcText.anchor = GridBagConstraints.CENTER;
        gbcText.insets = new Insets(10, 0, 0, 0);
        centerPanel.add(textDisplay, gbcText);

        wordCountDisplay = new JLabel("Type to start");
        wordCountDisplay.setFont(new Font("Arial", Font.PLAIN, 18));
        wordCountDisplay.setForeground(new Color(60, 60, 60));

        JPanel topPanel = new JPanel(new GridLayout(1, 6, 10, 10));
        topPanel.setOpaque(false);
        topPanel.setBorder(BorderFactory.createEmptyBorder(30, 40, 0, 40));
        topPanel.add(wordCountDisplay);

        JToggleButton switchButton = new JToggleButton("Training", true);
        switchButton.setFont(new Font("Arial", Font.PLAIN, 18));
        switchButton.addActionListener(e -> toggleOptimized());
        topPanel.add(switchButton);

        JButton time15Button = new JButton("15");
        time15Button.setFont(new Font("Arial", Font.PLAIN, 18));
        time15Button.addActionListener(e -> setTimeToType(15));
        topPanel.add(time15Button);

        JButton time30Button = new JButton("30");
        time30Button.setFont(new Font("Arial", Font.PLAIN, 18));
        time30Button.addActionListener(e -> setTimeToType(30));
        topPanel.add(time30Button);

        JButton time60Button = new JButton("60");
        time60Button.setFont(new Font("Arial", Font.PLAIN, 18));
        time60Button.addActionListener(e -> setTimeToType(60));
        topPanel.add(time60Button);

        JButton time120Button = new JButton("120");
        time120Button.setFont(new Font("Arial", Font.PLAIN, 18));
        time120Button.addActionListener(e -> setTimeToType(120));
        topPanel.add(time120Button);

        restartButton = new JButton("Restart");
        restartButton.setFont(new Font("Arial", Font.PLAIN, 18));
        restartButton.addActionListener(e -> restartTest());
        //topPanel.add(restartButton);

        this.add(topPanel, BorderLayout.NORTH);
        this.add(centerPanel, BorderLayout.CENTER);
    }

    private void setTimeToType(int time) {
        this.timeToType = time;
        restartTest();
    }

    private void toggleOptimized() {
        isOptimized = !isOptimized;
        initializeTextOptimized();
        restartTest();
    }

    private void setPadding(Container container, int padding) {
        if (container.getLayout() instanceof BorderLayout) {
            ((BorderLayout) container.getLayout()).setHgap(padding);
            ((BorderLayout) container.getLayout()).setVgap(padding);
        }
    }

    private void startTimer() {
        lastTime = System.currentTimeMillis();
        timeRemaining = timeToType;
        timer = new Timer();
        task = new TimerTask() {
            @Override
            public void run() {
                if (timeRemaining > 0) {
                    timeRemaining--;
                    elapsedTime++;
                    displayHeader();
                } else {
                    timer.cancel();
                    JOptionPane.showMessageDialog(null, "Time's up! You typed " + calculateWPM(elapsedTime) + " words per minute.");
                    restartTest();
                }
            }
        };
        timer.scheduleAtFixedRate(task, 1000, 1000);
    }

    private void restartTest() {
        displayHeader();
        isWaitingToStart = true;
        if (timer != null)
            timer.cancel();
        charCount = 0;
        position = 0;
        if (isOptimized) {
            initializeTextOptimized(); // Use optimized text generation
        } else {
            initializeText(); // Use random text generation
        }
        displayHeader();
        this.requestFocusInWindow();
    }

    private void initializeTextOptimized() {
        text = "";
        for (int i = 0; i < numberOfWordsInLine; i++)
            text += TextGenerator.getOptimalWord() + " ";
    }

    private String colorText(String text, int position, boolean isCorrect) {
        String before = text.substring(0, position);
        int currentIndex = Math.min(position + 1, text.length());
        String current = text.substring(position, currentIndex);
        String after = text.substring(currentIndex);
        if (isCorrect) {
            return before + "<span style='background-color:rgb(100, 100, 100); color:white;'>" + current + "</span>" + after;
        }
        return before + "<span style='background-color:rgb(200, 0, 0); color:white;'>" + current + "</span>" + after;
    }

    private void updateWPM() {
        int wpm = calculateWPM(elapsedTime);
        wordCountDisplay.setText(wpm + "wpm");
    }

    private int calculateWPM(int elapsedTime) {
        if (elapsedTime == 0) return 0;
        return (int) (((double) charCount / 5) / elapsedTime * 60);
    }

    @Override
    public void keyTyped(KeyEvent e) {
        if (isWaitingToStart) {
            isWaitingToStart = false;
            lastTime = 0;
            elapsedTime = 0;
            charCount = 0;
            startTimer();
        }
        if (position < text.length() && timeRemaining > 0) {
            char typedChar = e.getKeyChar();
            long currentTime = System.currentTimeMillis();
            if (typedChar == text.charAt(position)) {
                if (lastTime != 0) {
                    long timeTaken = currentTime - lastTime;
                    statistic.updateStats(typedChar, timeTaken);
                    displayStatistics();
                }
                lastTime = currentTime;
                charCount++;
                if (typedChar == ' ') {
                    if (isOptimized) {
                        text += TextGenerator.getOptimalWord() + " "; // Use optimized word generation
                    } else {
                        text += TextGenerator.getRandomWord() + " "; // Use random word generation
                    }
                }
                textDisplay.setText("<html>" + colorText(text, position + 1, true) + "</html>");
                if (typedChar == ' ') {
                    text = text.substring(position + 1);
                    position = -1;
                }
                position++;
            } else {
                textDisplay.setText("<html>" + colorText(text, position, false) + "</html>");
            }
        }
    }

    @Override
    public void keyPressed(KeyEvent e) {
        // No action needed here
    }

    @Override
    public void keyReleased(KeyEvent e) {
        // No action needed here
    }

    public static void start() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            SwingUtilities.invokeLater(TypingUI::new);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void displayHeader() {
        if (isWaitingToStart) {
            timerDisplay.setText(timeToType + "s: Type to start");
            wordCountDisplay.setText("");
            textDisplay.setText("<html>" + colorText(text, 0, true) + "</html>");
            return;
        }
        timerDisplay.setText(timeRemaining + "s");
        updateWPM();
    }

    private void displayStatistics() {
        statisticsDisplay.removeAll();
        statisticsDisplay.setLayout(new GridLayout(2, 26, 5, 10));

        // Create a fixed size for the JLabels
        Dimension labelSize = new Dimension(20, 25); // Increased height for better appearance

        statistic.updateMaxSpeed();
        double maxSpeed = statistic.getMaxSpeed(); // Get the max speed from Statistic

        for (char letter = 'a'; letter <= 'z'; letter++) {
            JLabel letterLabel = new JLabel(String.valueOf(letter), SwingConstants.CENTER);
            letterLabel.setFont(new Font("Arial", Font.PLAIN, 12));
            letterLabel.setPreferredSize(labelSize);
            double avgTime = statistic.getStats(letter);
            double wpm = (60.0 / (avgTime / 1000.0)) / 5.0;
            if (avgTime > 0) {
                double normalizedSpeed = Math.max(0, avgTime / maxSpeed);

                int green = (int) ((1 - normalizedSpeed) * 255);
                int red = (int) (normalizedSpeed * 255);
                Color color = new Color(red, green, 135);

                letterLabel.setOpaque(true);
                letterLabel.setBackground(color);
                letterLabel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1, true)); // Add rounded border
            } else {
                letterLabel.setOpaque(true);
                letterLabel.setBackground(Color.GRAY);
                letterLabel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1, true)); // Add rounded border
            }
            statisticsDisplay.add(letterLabel);
        }
        for (char letter = 'a'; letter <= 'z'; letter++) {
            double avgTime = statistic.getStats(letter);
            JLabel speedLabel;
            if (avgTime > 0) {
                double wpm = (60.0 / (avgTime / 1000.0)) / 5.0;
                speedLabel = new JLabel(String.valueOf((int) wpm), SwingConstants.CENTER);
            } else {
                speedLabel = new JLabel("-", SwingConstants.CENTER);
            }
            speedLabel.setFont(new Font("Arial", Font.PLAIN, 9));
            speedLabel.setPreferredSize(labelSize); // Set preferred size
            statisticsDisplay.add(speedLabel);
        }
        statisticsDisplay.revalidate();
        statisticsDisplay.repaint();
    }

    public static void main(String[] args) {
        start();
    }
}import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;

public class TextGenerator {
    static ArrayList<String> list_of_words;
    private static Statistic statistic;
    private static final Queue<String> lastUsedWords = new LinkedList<>();
    private static final int MAX_RECENT_WORDS = 100;

    private static void generate_text() {
        list_of_words = new ArrayList<>();
        try {
            File myObj = new File("file.txt");
            Scanner myReader = new Scanner(myObj);
            while (myReader.hasNextLine()) {
                String data = myReader.nextLine();
                list_of_words.add(data.trim());
            }
            myReader.close();
        } catch (FileNotFoundException e) {
            System.out.println("An error occurred.");
            e.printStackTrace();
        }
    }

    public static String getRandomWord() {
        if (list_of_words == null) generate_text();
        return list_of_words.get((new Random()).nextInt(list_of_words.size()));
    }

    public static String getOptimalWord() {
        if (list_of_words == null) generate_text();
        if (statistic == null) throw new IllegalStateException("Statistic object is not set");

        Set<String> recentWordsSet = new HashSet<>(lastUsedWords);
        Set<Character> typedLetters = statistic.getTypedLetters();

        double maxCombinedValue = -1;
        String optimalWord = "";
        ArrayList<String> untypedLetterWords = new ArrayList<>();

        for (String word : list_of_words) {
            if (recentWordsSet.contains(word)) {
                continue;
            }

            boolean hasUntypedLetter = false;
            double totalRelativeSpeed = 0;
            double totalRelativeFrequency = 0;
            int letterCount = 0;

            for (char letter : word.toCharArray()) {
                double relativeSpeed = statistic.getRelativeSpeed(letter);
                double relativeFrequency = statistic.getRelativeFrequency(letter);
                if (relativeSpeed > 0) {
                    totalRelativeSpeed += relativeSpeed;
                    totalRelativeFrequency += relativeFrequency;
                    letterCount++;
                } else if (!typedLetters.contains(letter)) {
                    untypedLetterWords.add(word);
                }
            }

            if (letterCount > 0) {
                double combinedValue = (totalRelativeSpeed + totalRelativeFrequency) / letterCount;
                if (combinedValue > maxCombinedValue) {
                    maxCombinedValue = combinedValue;
                    optimalWord = word;
                }
            }
        }

        if (!untypedLetterWords.isEmpty()) {
            optimalWord = untypedLetterWords.remove(new Random().nextInt(untypedLetterWords.size()));
        } else if (optimalWord.isEmpty()) {
            return getRandomWord();
        }

        lastUsedWords.add(optimalWord);
        if (lastUsedWords.size() > MAX_RECENT_WORDS) {
            lastUsedWords.poll();
        }

        return optimalWord;
    }

    public static void setStatistic(Statistic stat) {
        statistic = stat;
    }
}
write readme for this project in Github