/*
 * QuasselDroid - Quassel client for Android
 * Copyright (C) 2016 Janne Koschinski
 * Copyright (C) 2016 Ken Børge Viktil
 * Copyright (C) 2016 Magnus Fjell
 * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
 *
 * 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 de.kuschku.libquassel.client;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import de.kuschku.libquassel.BusProvider;
import de.kuschku.libquassel.events.ConnectionChangeEvent;
import de.kuschku.libquassel.events.CriticalErrorEvent;
import de.kuschku.libquassel.events.LagChangedEvent;
import de.kuschku.libquassel.events.PasswordChangeEvent;
import de.kuschku.libquassel.events.StatusMessageEvent;
import de.kuschku.libquassel.functions.types.InitRequestFunction;
import de.kuschku.libquassel.functions.types.SyncFunction;
import de.kuschku.libquassel.localtypes.NotificationManager;
import de.kuschku.libquassel.localtypes.backlogstorage.BacklogStorage;
import de.kuschku.libquassel.message.Message;
import de.kuschku.libquassel.objects.types.CoreStatus;
import de.kuschku.libquassel.objects.types.SessionState;
import de.kuschku.libquassel.primitives.QMetaTypeRegistry;
import de.kuschku.libquassel.primitives.types.BufferInfo;
import de.kuschku.libquassel.syncables.types.SyncableObject;
import de.kuschku.libquassel.syncables.types.impl.AliasManager;
import de.kuschku.libquassel.syncables.types.impl.BacklogManager;
import de.kuschku.libquassel.syncables.types.impl.BufferSyncer;
import de.kuschku.libquassel.syncables.types.impl.BufferViewManager;
import de.kuschku.libquassel.syncables.types.impl.CoreInfo;
import de.kuschku.libquassel.syncables.types.impl.Identity;
import de.kuschku.libquassel.syncables.types.impl.IgnoreListManager;
import de.kuschku.libquassel.syncables.types.impl.NetworkConfig;
import de.kuschku.libquassel.syncables.types.interfaces.QAliasManager;
import de.kuschku.libquassel.syncables.types.interfaces.QBacklogManager;
import de.kuschku.libquassel.syncables.types.interfaces.QBufferSyncer;
import de.kuschku.libquassel.syncables.types.interfaces.QBufferViewConfig;
import de.kuschku.libquassel.syncables.types.interfaces.QBufferViewManager;
import de.kuschku.libquassel.syncables.types.interfaces.QIgnoreListManager;
import de.kuschku.libquassel.syncables.types.interfaces.QNetwork;
import de.kuschku.libquassel.syncables.types.interfaces.QNetworkConfig;

import static de.kuschku.util.AndroidAssert.assertNotNull;

public class Client extends AClient {

// synced

    @NonNull
    private final NetworkManager networkManager;
    @NonNull
    private final BufferManager bufferManager;
    @NonNull
    private final IdentityManager identityManager;
    @NonNull
    private final BacklogStorage backlogStorage;
    @NonNull
    private final NotificationManager notificationManager;
    private final List<String> initRequests = new LinkedList<>();
    private final List<Integer> backlogRequests = new LinkedList<>();
    private final QBacklogManager backlogManager;
    private final Map<String, List<SyncFunction>> bufferedSyncs = new HashMap<>();
    private final Map<Integer, Pair<QBufferViewConfig, Integer>> bufferedBuffers = new HashMap<>();
    private QBufferViewManager bufferViewManager;
    // local
    private QBufferSyncer bufferSyncer;
    private QAliasManager aliasManager;
    private QIgnoreListManager ignoreListManager;
    private QNetworkConfig globalNetworkConfig;
    private CoreStatus core;
    private CoreInfo coreInfo;
    private long latency;
    private ConnectionChangeEvent.Status connectionStatus;

    public Client(@NonNull BusProvider provider, @NonNull BacklogStorage backlogStorage) {
        this.provider = provider;
        this.networkManager = new NetworkManager(this);
        this.bufferManager = new BufferManager(this);
        this.identityManager = new IdentityManager(this);
        this.backlogStorage = backlogStorage;
        backlogStorage.setClient(this);
        this.backlogManager = new BacklogManager(this, backlogStorage);
        this.backlogManager.init("", provider, this);
        this.notificationManager = new NotificationManager(this);
        this.initialized = true;
    }

    public QBufferViewManager bufferViewManager() {
        return bufferViewManager;
    }

    public QBufferSyncer bufferSyncer() {
        return bufferSyncer;
    }

    public QAliasManager aliasManager() {
        return aliasManager;
    }

    public QBacklogManager backlogManager() {
        return backlogManager;
    }

    public QIgnoreListManager ignoreListManager() {
        return ignoreListManager;
    }

    public QNetworkConfig globalNetworkConfig() {
        return globalNetworkConfig;
    }

    @Override
    public void _displayMsg(Message msg) {
        backlogStorage.insertMessages(msg);
    }

    @Override
    public void _displayStatusMsg(String network, String message) {
        assertNotNull(provider);

        provider.sendEvent(new StatusMessageEvent(network, message));
    }

    @Override
    public void _bufferInfoUpdated(BufferInfo bufferInfo) {
        bufferManager.updateBufferInfo(bufferInfo);
    }

    @Override
    public void _identityCreated(Identity identity) {
        identityManager.createIdentity(identity);
    }

    @Override
    public void _identityRemoved(int id) {
        identityManager.removeIdentity(id);
    }

    @Override
    public void _networkCreated(int network) {
        networkManager.createNetwork(network);
    }

    @Override
    public void _networkRemoved(int network) {
        networkManager.removeNetwork(network);
    }

    @Override
    public void _passwordChanged(long peerPtr, boolean success) {
        assertNotNull(provider);

        if (peerPtr != 0x0000000000000000L)
            provider.sendEvent(new CriticalErrorEvent("Your core has a critical vulnerability. Please update it."));
        provider.sendEvent(new PasswordChangeEvent(success));
    }

    @Override
    public void ___objectRenamed__(String type, String oldName, String newName) {

    }

    public ConnectionChangeEvent.Status connectionStatus() {
        return connectionStatus;
    }

    public void setConnectionStatus(@NonNull ConnectionChangeEvent.Status connectionStatus) {
        assertNotNull(provider);

        this.connectionStatus = connectionStatus;
        if (connectionStatus == ConnectionChangeEvent.Status.LOADING_BACKLOG) {
            for (QNetwork network : networkManager().networks()) {
                Log.d("libquassel", String.valueOf(network.channels()));
            }
            setConnectionStatus(ConnectionChangeEvent.Status.CONNECTED);
        }
        provider.sendEvent(new ConnectionChangeEvent(connectionStatus));
    }

    @Nullable
    public Object unsafe_getObjectByIdentifier(@NonNull String className, @NonNull String objectName) {
        switch (className) {
            case "AliasManager": {
                assertNotNull(aliasManager);
                return aliasManager;
            }
            case "BacklogManager": {
                assertNotNull(backlogManager);
                return backlogManager;
            }
            case "BufferSyncer": {
                assertNotNull(bufferSyncer);
                return bufferSyncer;
            }
            case "BufferViewConfig": {
                assertNotNull(bufferViewManager);
                return bufferViewManager.bufferViewConfig(Integer.parseInt(objectName));
            }
            case "BufferViewManager": {
                assertNotNull(bufferViewManager);
                return bufferViewManager;
            }
            case "CoreInfo": {
                assertNotNull(coreInfo);
                return coreInfo;
            }
            case "Identity": {
                return identityManager.identity(Integer.parseInt(objectName));
            }
            case "IgnoreListManager": {
                assertNotNull(ignoreListManager);
                return ignoreListManager;
            }
            case "IrcChannel": {
                String[] split = objectName.split("/");
                if (split.length != 2) {
                    Log.w("libquassel", "malformatted object name: " + objectName);
                    return null;
                }
                QNetwork network = networkManager.network(Integer.parseInt(split[0]));
                if (network == null) {
                    Log.w("libquassel", "Network doesn’t exist yet: " + objectName);
                    return null;
                }
                return network.ircChannel(split[1]);
            }
            case "IrcUser": {
                String[] split = objectName.split("/");
                if (split.length != 2) {
                    Log.w("libquassel", "malformatted object name: " + objectName);
                    return null;
                }
                QNetwork network = networkManager.network(Integer.parseInt(split[0]));
                if (network == null) {
                    Log.w("libquassel", "Network doesn’t exist yet: " + objectName);
                    return null;
                }
                return network.ircUser(split[1]);
            }
            case "Network": {
                return networkManager.network(Integer.parseInt(objectName));
            }
            case "NetworkConfig": {
                assertNotNull(globalNetworkConfig);
                return globalNetworkConfig;
            }
            case "NetworkInfo": {
                QNetwork network = networkManager().network(Integer.parseInt(objectName));
                if (network == null)
                    return null;
                else
                    return network.networkInfo();
            }
            default: {
                Log.w("libquassel", "Unknown type: " + className + " : " + objectName);
                return null;
            }
        }
    }

    @Nullable
    public <T> T getObjectByIdentifier(@NonNull String className, @NonNull String objectName) {
        Class<T> cl = QMetaTypeRegistry.<T>getType(className).cl;
        return getObjectByIdentifier(cl, className, objectName);
    }

    @Nullable
    public <T> T getObjectByIdentifier(@NonNull Class<T> cl, @NonNull String objectName) {
        return getObjectByIdentifier(cl, cl.getSimpleName(), objectName);
    }

    @SuppressWarnings("unchecked")
    @Nullable
    public <T> T getObjectByIdentifier(@NonNull Class<T> cl, @NonNull String className, @NonNull String objectName) {
        Object obj = unsafe_getObjectByIdentifier(className, objectName);
        // The fancy version of "instanceof" that works with erased types, too
        if (obj == null || !cl.isAssignableFrom(obj.getClass()))
            return null;
        else
            return (T) obj;
    }

    public void init(@NonNull SessionState sessionState) {
        networkManager.init(sessionState.NetworkIds);
        identityManager.init(sessionState.Identities);
        bufferManager.init(sessionState.BufferInfos);

        requestInitObject("BufferSyncer", "");
        requestInitObject("BufferViewManager", "");
        requestInitObject("AliasManager", "");
        requestInitObject("NetworkConfig", "GlobalNetworkConfig");
        requestInitObject("IgnoreListManager", "");
        //sendInitRequest("TransferManager", "");
        // This thing never gets sent...
    }

    @NonNull
    public NetworkManager networkManager() {
        return networkManager;
    }

    @NonNull
    public BufferManager bufferManager() {
        return bufferManager;
    }

    @NonNull
    public IdentityManager identityManager() {
        return identityManager;
    }

    public void requestInitObject(@NonNull String className, String objectName) {
        assertNotNull(provider);

        if (connectionStatus() == ConnectionChangeEvent.Status.INITIALIZING_DATA)
            initRequests.add(hashName(className, objectName));

        provider.dispatch(new InitRequestFunction(className, objectName));
    }

    public void initObject(String className, @NonNull String objectName, @NonNull SyncableObject object) {
        assertNotNull(provider);

        if (connectionStatus() == ConnectionChangeEvent.Status.INITIALIZING_DATA) {
            initRequests.remove(hashName(className, objectName));
            if (initRequests.isEmpty()) {
                setConnectionStatus(ConnectionChangeEvent.Status.LOADING_BACKLOG);
            }
        }

        object.init(objectName, provider, this);

        // Execute cached sync requests
        if (bufferedSyncs.size() > 0) {
            String key = hashName(className, objectName);
            if (bufferedSyncs.containsKey(key)) {
                List<SyncFunction> functions = bufferedSyncs.get(key);
                for (SyncFunction function : functions)
                    provider.handle(function);
                bufferedSyncs.remove(key);
            }
        }
    }

    @NonNull
    private String hashName(String className, String objectName) {
        return className + ":" + objectName;
    }

    public void initBacklog(int id) {
        backlogRequests.remove((Integer) id);
        if (backlogRequests.isEmpty())
            setConnectionStatus(ConnectionChangeEvent.Status.CONNECTED);
    }

    public void requestInitBacklog(int id, int amount) {
        backlogRequests.add(id);
        backlogManager.requestBacklogInitial(id, amount);
    }

    public void setLatency(long latency) {
        assertNotNull(provider);

        this.latency = latency;
        provider.sendEvent(new LagChangedEvent(latency));
    }

    public CoreInfo coreInfo() {
        return coreInfo;
    }

    public void setCoreInfo(CoreInfo coreInfo) {
        this.coreInfo = coreInfo;
    }

    public CoreStatus core() {
        return core;
    }

    public void setCore(CoreStatus core) {
        this.core = core;
    }

    public long latency() {
        return latency;
    }

    @NonNull
    public NotificationManager notificationManager() {
        return notificationManager;
    }

    public void setBufferSyncer(BufferSyncer bufferSyncer) {
        this.bufferSyncer = bufferSyncer;
    }

    public void setBufferViewManager(BufferViewManager bufferViewManager) {
        this.bufferViewManager = bufferViewManager;
    }

    public void setAliasManager(AliasManager aliasManager) {
        this.aliasManager = aliasManager;
    }

    public void setIgnoreListManager(IgnoreListManager ignoreListManager) {
        this.ignoreListManager = ignoreListManager;
    }

    public void setGlobalNetworkConfig(NetworkConfig globalNetworkConfig) {
        this.globalNetworkConfig = globalNetworkConfig;
    }


    @NonNull
    public BacklogStorage backlogStorage() {
        return backlogStorage;
    }

    public void bufferSync(@NonNull SyncFunction packedFunc) {
        String key = hashName(packedFunc.className, packedFunc.objectName);
        if (connectionStatus() == ConnectionChangeEvent.Status.CONNECTED)
            Log.d("libquassel", "Queueing sync: " + packedFunc);
        if (!bufferedSyncs.containsKey(key))
            bufferedSyncs.put(key, new LinkedList<>());
        bufferedSyncs.get(key).add(packedFunc);
    }

    public void bufferBuffer(QBufferViewConfig bufferViewConfig, int bufferId, int pos) {
        bufferedBuffers.put(bufferId, Pair.create(bufferViewConfig, pos));
        Log.d("libquassel", "Queueing buffer: " + bufferId);
    }

    public void unbufferBuffer(BufferInfo info) {
        if (!bufferManager().exists(info)) {
            bufferManager().createBuffer(info);
            Log.d("libquassel", "Creating buffer from message info: " + info.id());
        }
        if (bufferedBuffers.containsKey(info.id())) {
            Pair<QBufferViewConfig, Integer> pair = bufferedBuffers.get(info.id());
            pair.first._addBuffer(info.id(), pair.second);
            Log.d("libquassel", "Un-Queueing buffer: " + info.id());
        }
    }
}