// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

import * as net from "net";
import * as Q from "q";
import * as vscode from "vscode";

import * as em from "../common/extensionMessaging";
import {Log} from "../common/log/log";
import {LogLevel} from "../common/log/logHelper";
import {Packager, PackagerRunAs} from "../common/packager";
import {PackagerStatus, PackagerStatusIndicator} from "./packagerStatusIndicator";
import {LogCatMonitor} from "./android/logCatMonitor";
import {FileSystem} from "../common/node/fileSystem";
import {ConfigurationReader} from "../common/configurationReader";
import {SettingsHelper} from "./settingsHelper";
import {Telemetry} from "../common/telemetry";
import {ExponentHelper} from "../common/exponent/exponentHelper";

export class ExtensionServer implements vscode.Disposable {
    private serverInstance: net.Server | null = null;
    private messageHandlerDictionary: { [id: number]: ((...argArray: any[]) => Q.Promise<any>) } = {};
    private reactNativePackager: Packager;
    private reactNativePackageStatusIndicator: PackagerStatusIndicator;
    private pipePath: string;
    private logCatMonitor: LogCatMonitor | null = null;
    private exponentHelper: ExponentHelper;

    public constructor(projectRootPath: string, reactNativePackager: Packager, packagerStatusIndicator: PackagerStatusIndicator, exponentHelper: ExponentHelper) {

        this.pipePath = new em.MessagingChannel(projectRootPath).getPath();
        this.reactNativePackager = reactNativePackager;
        this.reactNativePackageStatusIndicator = packagerStatusIndicator;
        this.exponentHelper = exponentHelper;

        /* register handlers for all messages */
        this.messageHandlerDictionary[em.ExtensionMessage.START_PACKAGER] = this.startPackager;
        this.messageHandlerDictionary[em.ExtensionMessage.STOP_PACKAGER] = this.stopPackager;
        this.messageHandlerDictionary[em.ExtensionMessage.RESTART_PACKAGER] = this.restartPackager;
        this.messageHandlerDictionary[em.ExtensionMessage.PREWARM_BUNDLE_CACHE] = this.prewarmBundleCache;
        this.messageHandlerDictionary[em.ExtensionMessage.START_MONITORING_LOGCAT] = this.startMonitoringLogCat;
        this.messageHandlerDictionary[em.ExtensionMessage.STOP_MONITORING_LOGCAT] = this.stopMonitoringLogCat;
        this.messageHandlerDictionary[em.ExtensionMessage.GET_PACKAGER_PORT] = this.getPackagerPort;
        this.messageHandlerDictionary[em.ExtensionMessage.SEND_TELEMETRY] = this.sendTelemetry;
        this.messageHandlerDictionary[em.ExtensionMessage.OPEN_FILE_AT_LOCATION] = this.openFileAtLocation;
        this.messageHandlerDictionary[em.ExtensionMessage.START_EXPONENT_PACKAGER] = this.startExponentPackager;
        this.messageHandlerDictionary[em.ExtensionMessage.SHOW_INFORMATION_MESSAGE] = this.showInformationMessage;
        this.messageHandlerDictionary[em.ExtensionMessage.GET_APPLICATION_TARGET] = this.getApplicationTarget;
    }

    /**
     * Starts the server.
     */
    public setup(): Q.Promise<void> {

        let deferred = Q.defer<void>();

        let launchCallback = (error: any) => {
            Log.logInternalMessage(LogLevel.Info, `Extension messaging server started at ${this.pipePath}.`);
            if (error) {
                deferred.reject(error);
            } else {
                deferred.resolve(void 0);
            }
        };

        this.serverInstance = net.createServer(this.handleSocket.bind(this));
        this.serverInstance.on("error", this.recoverServer.bind(this));
        this.serverInstance.listen(this.pipePath, launchCallback);

        return deferred.promise;
    }

    /**
     * Stops the server.
     */
    public dispose(): void {
        if (this.serverInstance) {
            this.serverInstance.close();
            this.serverInstance = null;
        }

        this.stopMonitoringLogCat();
    }

    /**
     * Message handler for GET_PACKAGER_PORT.
     */
    private getPackagerPort(): Q.Promise<number> {
        return Q(SettingsHelper.getPackagerPort());
    }

    /**
     * Message handler for START_PACKAGER.
     */
    private startPackager(): Q.Promise<any> {
        return this.reactNativePackager.isRunning().then((running) => {
            if (running) {
                if (this.reactNativePackager.getRunningAs() !== PackagerRunAs.REACT_NATIVE) {
                    return this.reactNativePackager.stop().then(() =>
                        this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED)
                    );
                }

                Log.logMessage("Attaching to running React Native packager");
            }
            return void 0;
        })
        .then(() => {
            return this.reactNativePackager.startAsReactNative();
        })
        .then(() =>
                this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
    }

    /**
     * Message handler for START_EXPONENT_PACKAGER.
     */
    private startExponentPackager(): Q.Promise<any> {
        return this.reactNativePackager.isRunning().then((running) => {
            if (running) {
                if (this.reactNativePackager.getRunningAs() !== PackagerRunAs.EXPONENT) {
                    return this.reactNativePackager.stop().then(() =>
                        this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED));
                }

                Log.logMessage("Attaching to running Exponent packager");
            }
            return void 0;
        }).then(() =>
            this.exponentHelper.configureExponentEnvironment()
            ).then(() =>
                this.exponentHelper.loginToExponent(
                    (message, password) => {
                        return Q.Promise((resolve, reject) => {
                            vscode.window.showInputBox({ placeHolder: message, password: password })
                                .then(resolve, reject);
                        });
                    },
                    (message) => {
                        return Q.Promise((resolve, reject) => {
                            vscode.window.showInformationMessage(message)
                                .then(resolve, reject);
                        });
                    }
                ))
            .then(() => {
                return this.reactNativePackager.startAsExponent();
            })
            .then(exponentUrl => {
                vscode.commands.executeCommand("vscode.previewHtml", vscode.Uri.parse(exponentUrl), 1, "Expo QR code");
                this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.EXPONENT_PACKAGER_STARTED);
                return exponentUrl;
            });
    }

    /**
     * Message handler for STOP_PACKAGER.
     */
    private stopPackager(): Q.Promise<any> {
        return this.reactNativePackager.stop()
            .then(() => this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED));
    }

    /**
     * Message handler for RESTART_PACKAGER.
     */
    private restartPackager(port?: any): Q.Promise<any> {
        const portToUse = ConfigurationReader.readIntWithDefaultSync(port, SettingsHelper.getPackagerPort());
        return this.reactNativePackager.restart(portToUse)
            .then(() =>
                this.reactNativePackageStatusIndicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED));
    }

    /**
     * Message handler for PREWARM_BUNDLE_CACHE.
     */
    private prewarmBundleCache(platform: string): Q.Promise<any> {
        return this.reactNativePackager.prewarmBundleCache(platform);
    }

    /**
     * Message handler for START_MONITORING_LOGCAT.
     */
    private startMonitoringLogCat(deviceId: string, logCatArguments: string): Q.Promise<any> {
        this.stopMonitoringLogCat(); // Stop previous logcat monitor if it's running

        // this.logCatMonitor can be mutated, so we store it locally too
        const logCatMonitor = this.logCatMonitor = new LogCatMonitor(deviceId, logCatArguments);
        logCatMonitor.start() // The LogCat will continue running forever, so we don't wait for it
            .catch(error =>
                Log.logWarning("Error while monitoring LogCat", error))
            .done();

        return Q.resolve<void>(void 0);
    }

    /**
     * Message handler for OPEN_FILE_AT_LOCATION
     */
    private openFileAtLocation(filename: string, lineNumber: number): Q.Promise<PromiseLike<void>> {
        return Q(vscode.workspace.openTextDocument(vscode.Uri.file(filename)).then((document: vscode.TextDocument) => {
            return vscode.window.showTextDocument(document).then((editor: vscode.TextEditor) => {
                let range = editor.document.lineAt(lineNumber - 1).range;
                editor.selection = new vscode.Selection(range.start, range.end);
                editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
            });
        }));
    }

    private stopMonitoringLogCat(): Q.Promise<void> {
        if (this.logCatMonitor) {
            this.logCatMonitor.dispose();
            this.logCatMonitor = null;
        }

        return Q.resolve<void>(void 0);
    }

    /**
     * Sends telemetry
     */
    private sendTelemetry(extensionId: string, extensionVersion: string, appInsightsKey: string, eventName: string, properties: {[key: string]: string}, measures: {[key: string]: number}): Q.Promise<any> {
        Telemetry.sendExtensionTelemetry(extensionId, extensionVersion, appInsightsKey, eventName, properties, measures);
        return Q.resolve({});
    }

    /**
     * Extension message handler.
     */
    private handleExtensionMessage(messageWithArgs: em.MessageWithArguments): Q.Promise<any> {
        let handler = this.messageHandlerDictionary[messageWithArgs.message];
        if (handler) {
            Log.logInternalMessage(LogLevel.Info, "Handling message: " + em.ExtensionMessage[messageWithArgs.message]);
            return handler.apply(this, messageWithArgs.args);
        } else {
            return Q.reject("Invalid message: " + messageWithArgs.message);
        }
    }

    /**
     * Handles connections to the server.
     */
    private handleSocket(socket: net.Socket): void {
        let handleError = (e: any) => {
            Log.logError(e);
            socket.end(em.ErrorMarker);
        };

        let dataCallback = (data: any) => {
            try {
                let messageWithArgs: em.MessageWithArguments = JSON.parse(data);
                this.handleExtensionMessage(messageWithArgs)
                    .then(result => {
                        socket.end(JSON.stringify(result));
                    })
                    .catch((e) => { handleError(e); })
                    .done();
            } catch (e) {
                handleError(e);
            }
        };

        socket.on("data", dataCallback);
    }

    /**
     * Recovers the server in case the named socket we use already exists, but no other instance of VSCode is active.
     */
    private recoverServer(error: any): void {
        let errorHandler = (e: any) => {
            /* The named socket is not used. */
            if (e.code === "ECONNREFUSED") {
                new FileSystem().removePathRecursivelyAsync(this.pipePath)
                    .then(() => {
                        if (this.serverInstance) {
                            this.serverInstance.listen(this.pipePath);
                        }
                    })
                    .done();
            }
        };

        /* The named socket already exists. */
        if (error.code === "EADDRINUSE") {
            let clientSocket = new net.Socket();
            clientSocket.on("error", errorHandler);
            clientSocket.connect(this.pipePath, function() {
                clientSocket.end();
            });
        }
    }

    /**
     * Message handler for SHOW_INFORMATION_MESSAGE
     */
    private showInformationMessage(message: string): Q.Promise<void> {
        return Q(vscode.window.showInformationMessage(message)).then(() => {});
    }
    private getApplicationTarget(platform: string, targetType: string): Q.Promise<string> {
        return Q.resolve(SettingsHelper.getApplicationTarget(platform, targetType));
    }
}
