/*
 *  Kontalk Java client
 *  Copyright (C) 2016 Kontalk Devteam <[email protected]>
 *
 *  This program 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.
 *
 *  This program 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 org.kontalk.view;

import javax.swing.JDialog;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.EnumSet;
import java.util.List;
import java.util.Observable;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.alee.extended.statusbar.WebStatusBar;
import com.alee.extended.statusbar.WebStatusLabel;
import com.alee.laf.WebLookAndFeel;
import com.alee.laf.label.WebLabel;
import com.alee.laf.optionpane.WebOptionPane;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.rootpane.WebDialog;
import com.alee.laf.text.WebPasswordField;
import org.kontalk.client.FeatureDiscovery;
import org.kontalk.misc.JID;
import org.kontalk.misc.ViewEvent;
import org.kontalk.model.Contact;
import org.kontalk.model.Model;
import org.kontalk.model.chat.Chat;
import org.kontalk.persistence.Config;
import org.kontalk.system.Control;
import org.kontalk.system.Control.ViewControl;
import org.kontalk.util.EncodingUtils;
import org.kontalk.util.Tr;

/**
 * Initialize and control the user interface.
 *
 * @author Alexander Bikadorov {@literal <[email protected]>}
 */
public final class View implements ObserverTrait {
    private static final Logger LOGGER = Logger.getLogger(View.class.getName());

    static final String KONTALK_SITE = "https://www.kontalk.org";
    static final String KONTALK_RELEASES = "https://github.com/kontalk/desktopclient-java/releases";
    static final String KONTALK_FORUM = "https://forum.kontalk.org/";

    static final int LISTS_WIDTH = 300;

    static final int GAP_DEFAULT = 10;
    static final int GAP_BIG = 15;
    static final int GAP_SMALL = 5;
    static final int MARGIN_DEFAULT = 10;
    static final int MARGIN_BIG = 15;
    static final int MARGIN_SMALL = 5;
    static final int MARGIN_TINY = 2;

    static final int ROUND = 5;

    static final int FONT_SIZE_TINY = 11;
    static final int FONT_SIZE_SMALL = 12;
    static final int FONT_SIZE_NORMAL = 13;
    static final int FONT_SIZE_BIG = 14;
    static final int FONT_SIZE_HUGE = 16;

    static final int MAX_SUBJ_LENGTH = 30;
    static final int MAX_NAME_LENGTH = 60;
    static final int MAX_NAME_IN_LIST_LENGTH = 18;
    static final int MAX_NAME_IN_GROUP_LENGTH = 25;
    static final int MAX_NAME_IN_FROM_LABEL = 40;
    static final int MAX_NAME_IN_NOTIFIER_LENGTH = 20;
    static final int MAX_JID_LENGTH = 100;
    static final int MAX_JID_IN_NOTIFIER_LENGTH = 30;
    static final int MAX_USER_ID_LENGTH = 30;
    static final int MAX_XMPP_ID_LENGTH = 30;

    static final int PRETTY_JID_LENGTH = 28;

    static final Color BLUE = new Color(130, 170, 240);
    static final Color LIGHT_BLUE = new Color(220, 230, 250);
    static final Color LIGHT_GREY = new Color(240, 240, 240);
    //static final Color GREEN = new Color(83, 196, 46);
    static final Color GREEN = new Color(0, 200, 0);
    static final Color LIGHT_GREEN = new Color(220, 250, 220);
    static final Color DARK_GREEN = new Color(0, 100, 0);
    static final Color DARK_RED = new Color(196, 46, 46);

    static final int CHAT_BG_ALPHA = 30;

    static final int AVATAR_LIST_SIZE = 30;
    static final int AVATAR_CHAT_SIZE = 40;
    static final int AVATAR_DETAIL_SIZE = 60;
    static final int AVATAR_PROFILE_SIZE = 150;

    static final String THE_ME_COMMAND = "/me ";

    private final ViewControl mControl;
    private final Model mModel;

    private final TrayManager mTrayManager;
    private final Notifier mNotifier;

    private final ContactListView mContactListView;
    private final ChatListView mChatListView;
    private final Content mContent;
    private final ChatView mChatView;
    private final WebStatusLabel mStatusBarLabel;
    private final MainFrame mMainFrame;

    final String tr_remove_contact = Tr.tr("Chats and messages will not be deleted.");

    private Control.Status mCurrentStatus;
    private EnumSet<FeatureDiscovery.Feature> mServerFeatures;

    private View(ViewControl control, Model model) {
        mControl = control;
        mModel = model;

        WebLookAndFeel.install();
        ToolTipManager.sharedInstance().setInitialDelay(200);

        // chat view
        mChatView = new ChatView(this);
        // content area
        mContent = new Content(this, mChatView);

        mContactListView = new ContactListView(this, mModel);
        mChatListView = new ChatListView(this, mModel.chats());

        // search panel
        SearchPanel searchPanel = new SearchPanel(
                new ListView[]{mContactListView, mChatListView},
                mChatView);
        // status bar
        WebStatusBar statusBar = new WebStatusBar();
        mStatusBarLabel = new WebStatusLabel(" ");
        statusBar.add(mStatusBarLabel);
        // main frame
        mMainFrame = new MainFrame(this, mModel, mContactListView, mChatListView,
                mContent, searchPanel, statusBar);
        mMainFrame.addWindowFocusListener(new WindowAdapter() {
            @Override
            public void windowGainedFocus(WindowEvent e) {
                mChatView.getCurrentChat().ifPresent(Chat::setRead);
            }
        });

        // tray
        mTrayManager = new TrayManager(this, mModel, mMainFrame);
        // notifier
        mNotifier = new Notifier(this, mMainFrame);

        // register observer
        mModel.contacts().addObserver(mContactListView);
        mModel.chats().addObserver(mChatListView);
        mModel.chats().addObserver(mChatView);
        mModel.chats().addObserver(mTrayManager);

        this.setHotkeys();

        this.statusChanged(Control.Status.DISCONNECTED, EnumSet.noneOf(FeatureDiscovery.Feature.class));

        mMainFrame.setVisible(true);
    }

    public static Optional<View> create(ViewControl control, Model model) {
        View view;
        try {
            view = invokeAndWait(new Callable<View>() {
                @Override
                public View call() throws Exception {
                    return new View(control, model);
                }
            });
        } catch (ExecutionException | InterruptedException ex) {
            LOGGER.log(Level.WARNING, "can't start view", ex);
            return Optional.empty();
        }
        control.addObserver(view);
        return Optional.of(view);
    }

    void setHotkeys() {
        boolean enterSends = Config.getInstance().getBoolean(Config.MAIN_ENTER_SENDS);
        mChatView.setHotkeys(enterSends);
    }

    /**
     * Setup view on startup after model was initialized.
     */
    public void init() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                View.this.mChatListView.selectLastChat();

                if (mModel.chats().isEmpty())
                    mMainFrame.selectTab(MainFrame.Tab.CONTACT);
            }
        });
    }

    Control.Status currentStatus() {
        return mCurrentStatus;
    }

    EnumSet<FeatureDiscovery.Feature> serverFeatures() {
        return mServerFeatures;
    }

    void showConfig() {
        JDialog configFrame = new ConfigurationDialog(mMainFrame, this, mModel);
        configFrame.setVisible(true);
    }

    /* control to view */

    @Override
    public void updateOnEDT(Observable o, Object arg) {
        if (arg instanceof ViewEvent.StatusChange) {
            ViewEvent.StatusChange statChange = (ViewEvent.StatusChange) arg;
            this.statusChanged(statChange.status, statChange.features);
        } else if (arg instanceof ViewEvent.PasswordSet) {
            this.showPasswordDialog(false);
        } else if (arg instanceof ViewEvent.MissingAccount) {
            ViewEvent.MissingAccount missAccount = (ViewEvent.MissingAccount) arg;
            this.showImportWizard(missAccount.connect);
        } else if (arg instanceof ViewEvent.Exception) {
            ViewEvent.Exception exception = (ViewEvent.Exception) arg;
            mNotifier.showException(exception.exception);
        } else if (arg instanceof ViewEvent.SecurityError) {
            ViewEvent.SecurityError error = (ViewEvent.SecurityError) arg;
            mNotifier.showSecurityErrors(error.message);
        } else if (arg instanceof ViewEvent.NewMessage) {
            ViewEvent.NewMessage newMessage = (ViewEvent.NewMessage) arg;
            mNotifier.onNewMessage(newMessage.message);
        } else if (arg instanceof ViewEvent.NewKey) {
            ViewEvent.NewKey newKey = (ViewEvent.NewKey) arg;
            mNotifier.confirmNewKey(newKey.contact, newKey.key);
        } else if (arg instanceof ViewEvent.ContactDeleted) {
            ViewEvent.ContactDeleted contactDeleted = (ViewEvent.ContactDeleted) arg;
            mNotifier.confirmContactDeletion(contactDeleted.contact);
        } else if (arg instanceof ViewEvent.PresenceError) {
            ViewEvent.PresenceError presenceError = (ViewEvent.PresenceError) arg;
            mNotifier.showPresenceError(presenceError.contact, presenceError.error);
        } else if (arg instanceof ViewEvent.SubscriptionRequest) {
            mNotifier.confirmSubscription((ViewEvent.SubscriptionRequest) arg);
        } else if (arg instanceof ViewEvent.RetryTimerMessage) {
            mStatusBarLabel.setText(
                    String.format(Tr.tr("Connection failure. Retry in %1$d seconds."),
                    ((ViewEvent.RetryTimerMessage) arg).countdown));
        } else {
            LOGGER.warning("unexpected argument: "+arg);
        }
    }

    private void statusChanged(Control.Status status, EnumSet<FeatureDiscovery.Feature> features) {
        mCurrentStatus = status;
        mServerFeatures = features;

        mChatView.onStatusChange(status, features);
        mMainFrame.onStatusChanged(status);

        switch (status) {
            case CONNECTING:
                mStatusBarLabel.setText(Tr.tr("Connecting…"));
                mNotifier.hideNotifications();
                break;
            case CONNECTED:
                mStatusBarLabel.setText(Tr.tr("Connected"));
                break;
            case DISCONNECTING:
                mStatusBarLabel.setText(Tr.tr("Disconnecting…"));
                break;
            case DISCONNECTED:
                mStatusBarLabel.setText(Tr.tr("Not connected"));
                //if (mTrayIcon != null)
                //    trayIcon.setImage(updatedImage);
                break;
            case SHUTTING_DOWN:
                mMainFrame.save();
                mChatListView.save();
                mTrayManager.removeTray();
                mMainFrame.setVisible(false);
                mMainFrame.dispose();
                break;
            case FAILED:
                mStatusBarLabel.setText(Tr.tr("Connecting failed"));
                break;
            case ERROR:
                mStatusBarLabel.setText(Tr.tr("Connection error"));
                break;
        }
    }

    void showPasswordDialog(boolean wasWrong) {
        WebPanel passPanel = new WebPanel();
        WebLabel passLabel = new WebLabel(Tr.tr("Please enter your key password:"));
        passPanel.add(passLabel, BorderLayout.NORTH);
        final WebPasswordField passField = new WebPasswordField();
        passPanel.add(passField, BorderLayout.CENTER);
        if (wasWrong) {
            WebLabel wrongLabel = new WebLabel(Tr.tr("Wrong password"));
            wrongLabel.setForeground(Color.RED);
            passPanel.add(wrongLabel, BorderLayout.SOUTH);
        }
        WebOptionPane passPane = new WebOptionPane(passPanel,
                WebOptionPane.QUESTION_MESSAGE,
                WebOptionPane.OK_CANCEL_OPTION);
        JDialog dialog = passPane.createDialog(mMainFrame, Tr.tr("Enter password"));
        dialog.setModal(true);
        dialog.addWindowFocusListener(new WindowAdapter() {
            @Override
            public void windowGainedFocus(WindowEvent e) {
                passField.requestFocusInWindow();
            }
        });
        // blocking
        LOGGER.info("asking for password…");
        dialog.setVisible(true);

        Object value = passPane.getValue();
        if (value != null && value.equals(WebOptionPane.OK_OPTION))
            mControl.connect(passField.getPassword());
    }

    void showImportWizard(boolean connect) {
        WebDialog importFrame = new ImportDialog(this, connect);
        importFrame.setVisible(true);
    }

    /* view to control */

    ViewControl getControl() {
        return mControl;
    }

    void callShutDown() {
        // trigger save if contact details are shown
        mContent.showNothing();
        mControl.shutDown();
    }

    /* view internal */

    void showChat(Contact contact) {
        this.showChat(mControl.getOrCreateSingleChat(contact));
    }

    void showChat(Chat chat) {
        // show by selecting it
        mMainFrame.selectTab(MainFrame.Tab.CHATS);
        mChatListView.setSelectedItem(chat);
    }

    void onChatSelectionChanged(Optional<Chat> optChat) {
        if (mMainFrame.getCurrentTab() != MainFrame.Tab.CHATS)
            return;

        if (optChat.isPresent())
            mContent.showChat(optChat.get());
        else
            mContent.showNothing();
    }

    void onContactSelectionChanged(Optional<Contact> optContact) {
        Contact contact = optContact.orElse(null);
        if (contact == null || contact.isDeleted()) {
            mContent.showNothing();
            return;
        }

        mContent.showContact(contact);
    }

    void requestRenameFocus(Contact contact) {
        if (contact.isDeleted())
            return;

        this.showContactDetails(contact);
        mContent.requestRenameFocus();
    }

    void showContactDetails(Contact contact) {
        // show by selecting in contact list
        mMainFrame.selectTab(MainFrame.Tab.CONTACT);
        mContactListView.setSelectedItem(contact);
    }

    void tabPaneChanged(MainFrame.Tab tab) {
        if (tab == MainFrame.Tab.CHATS) {
            Chat chat = mChatListView.getSelectedValue().orElse(null);
            if (chat != null) {
                mContent.showChat(chat);
                return;
            }
        } else {
            Contact contact = mContactListView.getSelectedValue().orElse(null);
            if (contact != null) {
                mContent.showContact(contact);
                return;
            }
        }
        mContent.showNothing();
    }

    boolean chatIsVisible(Chat chat) {
        return mChatView.getCurrentChat().orElse(null) == chat && mMainFrame.isFocused();
    }

    void reloadChatBG() {
        mChatView.loadDefaultBG();
    }

    void updateContactList() {
        mContactListView.updateOnEDT(null);
    }

    void updateTray() {
        mTrayManager.setTray();
    }

    void updateMessageLists() {
        mChatView.updateMessageLists();
    }

    // TODO is this good?
    String names(List<JID> jids) {
        return Utils.displayNames(jids, mModel.contacts(), View.PRETTY_JID_LENGTH);
    }

    /* static */

    private static <T> T invokeAndWait(Callable<T> callable)
            throws InterruptedException, ExecutionException {
        FutureTask<T> task = new FutureTask<>(callable);
        SwingUtilities.invokeLater(task);
        // blocking
        return task.get();
    }

    public static void showWrongJavaVersionDialog() {
        String jVersion = System.getProperty("java.version");
        if (jVersion.length() >= 3)
            jVersion = jVersion.substring(2, 3);
        String errorText = Tr.tr("The installed Java version is too old")+": " + jVersion;
        errorText += EncodingUtils.EOL;
        errorText += Tr.tr("Please install Java 8.");
        WebOptionPane.showMessageDialog(null,
                errorText,
                Tr.tr("Unsupported Java Version"),
                WebOptionPane.ERROR_MESSAGE);
    }
}