/*
 * Copyright (c) 2019-2020 Martin Paljak
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package apdu4j;

import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.dialogs.MessageDialog;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.smartcardio.Card;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CardTerminals;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;

public class FancyChooser implements Callable<Optional<CardTerminal>> {
    private static final Logger logger = LoggerFactory.getLogger(FancyChooser.class);
    // Nice names
    static final String PRESENT = "*";
    static final String EMPTY = " ";
    static final String EXCLUSIVE = "X";

    final Terminal terminal;
    final Screen screen;
    final List<CardTerminal> initialList;

    // UI elements
    final MultiWindowTextGUI gui;
    final BasicWindow mainWindow;
    final Panel mainPanel;
    final ActionListBox mainActions;
    final Button quitButton;

    // State monitoring
    final Thread monitor;
    volatile HashMap<String, String> previousStates = new HashMap<>();
    volatile String status = "OK";

    // The Chosen One
    volatile CardTerminal chosenOne;
    final ReaderAliases aliases = ReaderAliases.getDefault();

    static {
        // Force the text based terminal on macOS
        if (isMacOS() && System.console() != null)
            System.setProperty("java.awt.headless", "true");
    }

    private FancyChooser(Terminal terminal, Screen screen, CardTerminals monitorObject, List<CardTerminal> terminals) {
        if (monitorObject != null)
            monitor = new MonitorThread(monitorObject);
        else monitor = null;
        this.terminal = terminal;
        this.screen = screen;
        gui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE));
        this.initialList = terminals;
        // Create UI elements
        mainWindow = new BasicWindow(" apdu4j ");
        mainWindow.setCloseWindowWithEscape(true);
        mainWindow.setHints(Arrays.asList(Window.Hint.FIT_TERMINAL_WINDOW, Window.Hint.CENTERED));

        mainPanel = new Panel();
        mainPanel.setLayoutManager(new BorderLayout());
        mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL));
        mainActions = new ActionListBox();
        mainActions.setLayoutData(BorderLayout.Location.CENTER);
        mainPanel.addComponent(mainActions);
        mainPanel.addComponent(new EmptySpace(new TerminalSize(0, 1)));
        quitButton = new Button("Cancel and quit", () -> mainWindow.close());
        quitButton.setLayoutData(LinearLayout.createLayoutData(LinearLayout.Alignment.End));
        mainPanel.addComponent(quitButton);
        //mainPanel.setLayoutData(LinearLayout.createLayoutData(LinearLayout.Alignment.End));
        mainWindow.setComponent(mainPanel);
    }


    public static FancyChooser forTerminals(CardTerminals terminals) throws IOException, CardException {
        List<CardTerminal> terminalList = terminals.list();
        Terminal terminal = new DefaultTerminalFactory().createTerminal();
        Screen screen = new TerminalScreen(terminal);
        return new FancyChooser(terminal, screen, terminals, terminalList);
    }


    public static FancyChooser forTerminals(List<CardTerminal> terminals) throws IOException {
        Terminal terminal = new DefaultTerminalFactory().createTerminal();
        Screen screen = new TerminalScreen(terminal);
        return new FancyChooser(terminal, screen, null, terminals);
    }

    // Returns true if terminal is in exclusive use
    private boolean isExlusive(CardTerminal t) {
        boolean result = false;
        Card c = null;
        // Try shared mode, to detect exclusive mode via exception
        try {
            c = t.connect("*");
        } catch (CardException e) {
            String err = TerminalManager.getExceptionMessage(e);
            // Detect exclusive mode. Hopes this always succeeds
            if (err.equals(SCard.SCARD_E_SHARING_VIOLATION))
                result = true;
        } finally {
            if (c != null) {
                try {
                    c.disconnect(false);
                } catch (CardException e) {
                    // FIXME: log
                }
            }
        }
        return result;
    }

    // When selection changes (callable from another thread)
    synchronized void setSelection(List<CardTerminal> terminals) {
        try {
            // -1 == Selection not made; -2 == Selection is quit
            int previouslySelected = quitButton.isFocused() ? -2 : mainActions.getSelectedIndex();
            //setStatus("set " + previouslySelected);
            int selectedIndex = previouslySelected;
            mainActions.clearItems();
            HashMap<String, String> statuses = new HashMap<>();
            int i = 0;
            for (CardTerminal t : terminals) {
                final String name = t.getName();
                final boolean present = t.isCardPresent();
                final boolean exclusive = isExlusive(t);
                String status = EMPTY;
                if (present)
                    status = PRESENT;
                if (exclusive)
                    status = EXCLUSIVE;
                statuses.put(name, status);
                mainActions.addItem(String.format("[%s] %s", status, aliases.translate(name)), () -> {
                    if (exclusive) {
                        MessageDialog warn = new MessageDialogBuilder()
                                .setTitle(" Warning! ")
                                .setText("Reader is in exclusive use by some other application")
                                .addButton(MessageDialogButton.Cancel)
                                .addButton(MessageDialogButton.Continue)
                                .build();
                        warn.setCloseWindowWithEscape(true);
                        MessageDialogButton r = warn.showDialog(gui);
                        if (r == null || r == MessageDialogButton.Cancel) {
                            return;
                        }
                        // Continue below
                    }
                    chosenOne = t;
                    mainWindow.close();
                });


                // Reset if reader becomes exclusive (unless it is the only reader)
                if (i == selectedIndex && exclusive && !previousStates.get(name).equals(EXCLUSIVE) && terminals.size() > 1)
                    selectedIndex = -1;
                // Select if only reader becomes non-exclusive
                if (!exclusive && previousStates.getOrDefault(name, "").equals(EXCLUSIVE) && terminals.size() == 1)
                    selectedIndex = i;
                // New reader connected
                if (!previousStates.containsKey(name))
                    selectedIndex = i;
                // Select first non-exclusive reader
                if (selectedIndex == -1 && !exclusive)
                    selectedIndex = i;
                // Existing reader got a card
                if (previousStates.getOrDefault(name, "").equals(EMPTY) && present && !exclusive)
                    selectedIndex = i;
                i++;
            }

            // Set title
            if (terminals.size() == 0) {
                mainWindow.setTitle(" Connect a reader ");
            } else {
                mainWindow.setTitle(" Choose a reader ");
            }

            // Update selected index and related focus
            if (selectedIndex >= 0) {
                mainActions.setSelectedIndex(selectedIndex);
                mainActions.takeFocus();
            } else {
                quitButton.takeFocus();
            }
            previousStates = statuses;
            // Refresh screen
            mainPanel.invalidate();
            //screen.refresh(Screen.RefreshType.COMPLETE);
            gui.updateScreen();
        } catch (CardException | IOException e) {
            System.err.println(e.getMessage());
            throw new RuntimeException(e);
        }
    }

    @Override
    public Optional<CardTerminal> call() {
        try {
            setSelection(initialList);

            // Start monitor thread
            if (monitor != null)
                monitor.start();

            screen.startScreen();
            gui.addWindow(mainWindow);
            gui.waitForWindowToClose(mainWindow);

            terminal.clearScreen();
            screen.stopScreen();
            terminal.close();

            if (monitor != null) {
                monitor.interrupt();
            }
            // on OSX at least this prevents the print from last column
            System.out.println();
            return Optional.ofNullable(chosenOne);
        } catch (IOException e) {
            logger.error("Could not run: " + e.getMessage());
        }
        return Optional.empty();
    }

    class MonitorThread extends Thread {
        final CardTerminals terms;

        MonitorThread(CardTerminals terminals) {
            terms = terminals;
            setName("MonitorThread");
            setDaemon(true);
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                try {
                    // To be able to interrupt this thread, we poll often
                    boolean changed = terms.waitForChange(1000);
                    List<CardTerminal> l = terms.list();
                    if (changed) {
                        setSelection(l);
                    }
                } catch (CardException e) {
                    logger.error("Failed: " + e.getMessage());
                }
            }
        }
    }

    static boolean isMacOS() {
        return System.getProperty("os.name").equalsIgnoreCase("mac os x");
    }
}