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

import * as path from "path";
import * as Q from "q";
import * as XDL from "xdl";

import {FileSystem} from "../node/fileSystem";
import {Package} from "../node/package";
import {ReactNativeProjectHelper} from "../reactNativeProjectHelper";
import {CommandExecutor} from "../commandExecutor";
import {Log} from "../log/log";

const VSCODE_EXPONENT_JSON = "vscodeExponent.json";
const EXPONENT_ENTRYPOINT = "exponentEntrypoint.js";
const EXPONENT_INDEX = "exponentIndex.js";
const DEFAULT_IOS_INDEX = "index.ios.js";
const DEFAULT_ANDROID_INDEX = "index.android.js";
const EXP_JSON = "exp.json";

enum ReactNativePackageStatus {
    FACEBOOK_PACKAGE,
    EXPONENT_PACKAGE,
    UNKNOWN
}

export class ExponentHelper {
    private rootPath: string;
    private fileSystem: FileSystem;
    private commandExecutor: CommandExecutor;

    private expSdkVersion: string;
    private entrypointFilename: string;
    private entrypointComponentName: string;

    private dependencyPackage: ReactNativePackageStatus;

    public constructor(projectRootPath: string) {
        this.rootPath = projectRootPath;
        this.fileSystem = new FileSystem();
        this.commandExecutor = new CommandExecutor(this.rootPath);
        this.dependencyPackage = ReactNativePackageStatus.UNKNOWN;
        XDL.Config.validation.reactNativeVersionWarnings = false;
    }

    /**
     * Create exp.json file in the workspace root
     */
    public createExpJson(): Q.Promise<void> {
        let defaultSettings = {
            "sdkVersion": "",
            "entryPoint": EXPONENT_INDEX,
            "slug": "",
        };
        return this.readVscodeExponentSettingFile()
            .then(exponentJson => {
                if (exponentJson.createOrOverwriteExpJson) {
                    return this.getPackageName()
                        .then(name => {
                            defaultSettings.slug = name;
                            return this.exponentSdk();
                        })
                        .then(exponentVersion => {
                            if (!exponentVersion) {
                                return XDL.Versions.facebookReactNativeVersionsAsync()
                                    .then((versions) => {
                                        return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
                                    });
                            }
                            defaultSettings.sdkVersion = exponentVersion;
                            return this.fileSystem.writeFile(this.pathToFileInWorkspace(EXP_JSON), JSON.stringify(defaultSettings, null, 4));
                        });
                }
            });
    }

    /**
     * File used as an entrypoint for exponent. This file's component should be registered as "main"
     * in the AppRegistry and should only render a entrypoint component.
     */
    public createIndex(): Q.Promise<void> {
        const pkg = require("../../../package.json");
        const extensionVersionNumber = pkg.version;
        const extensionName = pkg.name;

        return this.entrypointComponent()
            .then((componentName) => {
                const fileContents =
                    `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
// Please do not modify it manually. All changes will be lost.
var React = require('react');
var {Component} = React;

var ReactNative = require('react-native');
var {AppRegistry} = ReactNative;

var EntryPoint = require('./exponentEntrypoint.js');
var {${componentName}} = EntryPoint;

class ExponentVSCodeEntryPoint extends Component {
  render() {
    return (
      <${componentName} />
    );
  }
}
AppRegistry.registerComponent('main', () => ExponentVSCodeEntryPoint);`;
                return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_INDEX), fileContents);
            });
    }

    /**
     * Entrypoint file for exponent. Should copy the file used by the user as an entrypoint and modify it so that it can
     * live inside .vscode and can be imported by exponentIndex.
     * More specifically it will export the class and update all the relative references.
     */
    public createExponentEntrypoint(): Q.Promise<void> {
        const pkg = require("../../../package.json");
        const extensionVersionNumber = pkg.version;
        const extensionName = pkg.name;
        let entrypointComponentName = "";

        const fileHeader =
            `// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
// Please do not modify it manually. All changes will be lost.\n`;

        return this.entrypointComponent()
            .then(component => {
                entrypointComponentName = component;
                return this.entrypoint();
            })
            .then(filename => {
                const pathToEntrypoint = this.pathToFileInWorkspace(filename);
                return this.fileSystem.readFile(pathToEntrypoint, "utf-8");
            })
            .then(entrypointContents => {
                // Export class
                let modifiedContents = fileHeader + entrypointContents;
                modifiedContents = modifiedContents.replace(`class ${entrypointComponentName}`, `export class ${entrypointComponentName}`);

                // Prepend ../ to relative imports that refer to something in a parent directory
                modifiedContents = modifiedContents.replace(/\.\.\/.*/, (substr) => { return `../${substr}`; });

                // Replace ./ with ../ in relative imports that refer to something in the same directory
                modifiedContents = modifiedContents.replace(/\.\/.*/, (substr) => { return `.${substr}`; });

                return this.fileSystem.writeFile(this.dotvscodePath(EXPONENT_ENTRYPOINT), modifiedContents);
            }).catch(() => {
                return Q.reject<void>(new Error("Unable to read entrypoint. Make sure the file exists and it's under the root directory."));
            });
    }

    /**
     * Convert react native project to exponent.
     * This consists on three steps:
     * 1. Change the dependency from facebook's react-native to exponent's fork
     * 2. Create exp.json
     * 3. Create index and entrypoint for exponent
     */
    public configureExponentEnvironment(): Q.Promise<void> {
        Log.logMessage("Making sure your project uses the correct dependencies for exponent. This may take a while...");
        return this.changeReactNativeToExponent()
            .then(() => {
                Log.logMessage("Dependencies are correct. Making sure you have any necesary configuration file.");
                return this.createExpJson();
            }).then(() => {
                Log.logMessage("Project setup is correct. Generating entrypoint code.");
                return this.createExponentEntrypoint()
                    .then(() =>
                        this.createIndex());
            });
    }

    /**
     * Change dependencies to point to original react-native repo
     */
    public configureReactNativeEnvironment(): Q.Promise<void> {
        Log.logMessage("Checking react native is correctly setup. This may take a while...");
        return this.changeExponentToReactNative();
    }

    /**
     * Changes npm dependency from react native to exponent's fork
     */
    private changeReactNativeToExponent(): Q.Promise<void> {
        return this.usingReactNativeExponent()
            .then(usingExponent => {
                if (usingExponent) {
                    return Q.resolve<void>(void 0);
                }
                return this.exponentSdk()
                    .then(sdkVersion => {
                        if (!sdkVersion) {
                            return XDL.Versions.facebookReactNativeVersionsAsync()
                                .then((versions) => {
                                    return Q.reject<void>(new Error(`React Native version not supported by exponent. Major versions supported: ${versions.join(", ")}`));
                                });
                        }
                        const exponentFork = `github:exponentjs/react-native#sdk-${sdkVersion}`;
                        return this.commandExecutor.execute("npm uninstall react-native", { silent: true })
                            .then(() =>
                                this.commandExecutor.execute(`npm install ${exponentFork}`, { silent: true }));
                    });
            })
            .then(() => {
                this.dependencyPackage = ReactNativePackageStatus.EXPONENT_PACKAGE;
            });
    }

    /**
     * Changes npm dependency from exponent's fork to react native
     */
    private changeExponentToReactNative(): Q.Promise<void> {
        return this.usingReactNativeExponent()
            .then(usingExponent => {
                if (!usingExponent) {
                    return Q.resolve<void>(void 0);
                }
                return this.commandExecutor.execute("npm uninstall react-native", { silent: true })
                    .then(() =>
                        this.commandExecutor.execute("npm install react-native", { silent: true }));
            })
            .then(() => {
                this.dependencyPackage = ReactNativePackageStatus.FACEBOOK_PACKAGE;
            });
    }

    /**
     * Reads VSCODE_EXPONENT Settings file. If it doesn't exists it creates one by
     * guessing which entrypoint and filename to use.
     */
    private readVscodeExponentSettingFile(): Q.Promise<any> {
        let defaultSettings = {
            "entryPointFilename": "",
            "entryPointComponent": "",
            "createOrOverwriteExpJson": true,
        };
        // Only create a new one if there is not one already
        return this.fileSystem.exists(this.dotvscodePath(VSCODE_EXPONENT_JSON))
            .then((vscodeExponentExists: boolean) => {
                if (vscodeExponentExists) {
                    return this.fileSystem.readFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), "utf-8")
                        .then(function (jsonContents: string): Q.Promise<any> {
                            return JSON.parse(jsonContents);
                        });
                } else {
                    return this.getPackageName()
                        .then(packageName => {
                            // By default react-native uses the package name for the entry component. This is our safest guess.
                            defaultSettings.entryPointComponent = packageName;
                            this.entrypointComponentName = defaultSettings.entryPointComponent;
                            return this.fileSystem.exists(this.pathToFileInWorkspace(DEFAULT_IOS_INDEX));
                        })
                        .then((indexIosExists: boolean) => {
                            // If there is an ios entrypoint we want to use that, if not let's go with android
                            defaultSettings.entryPointFilename = indexIosExists ? DEFAULT_IOS_INDEX : DEFAULT_ANDROID_INDEX;
                            this.entrypointFilename = defaultSettings.entryPointFilename;
                            return this.fileSystem.writeFile(this.dotvscodePath(VSCODE_EXPONENT_JSON), JSON.stringify(defaultSettings, null, 4));
                        })
                        .then(() => {
                            return defaultSettings;
                        });
                }
            });
    }

    /**
     * Exponent sdk version that maps to the current react-native version
     * If react native version is not supported it returns null.
     */
    private exponentSdk(): Q.Promise<string> {
        if (this.expSdkVersion) {
            return Q(this.expSdkVersion);
        }
        return this.readFromExpJson("sdkVersion")
            .then((sdkVersion) => {
                if (sdkVersion) {
                    this.expSdkVersion = sdkVersion;
                    return this.expSdkVersion;
                }
                let reactNativeProjectHelper = new ReactNativeProjectHelper(this.rootPath);
                return reactNativeProjectHelper.getReactNativeVersion()
                    .then(version => {
                        return XDL.Versions.facebookReactNativeVersionToExponentVersionAsync(version)
                            .then(exponentVersion => {
                                this.expSdkVersion = exponentVersion;
                                return this.expSdkVersion;
                            });
                    });
            });
    }

    /**
     * Returns the specified setting from exp.json if it exists
     */
    private readFromExpJson(setting: string): Q.Promise<string> {
        return this.fileSystem.exists(this.pathToFileInWorkspace(EXP_JSON))
            .then((exists: boolean) => {
                if (!exists) {
                    return null;
                }
                return this.fileSystem.readFile(this.pathToFileInWorkspace(EXP_JSON), "utf-8")
                    .then(function (jsonContents: string): Q.Promise<any> {
                        return JSON.parse(jsonContents)[setting];
                    });
            });
    }

    /**
     * Looks at the _from attribute in the package json of the react-native dependency
     * to figure out if it's using exponent.
     */
    private usingReactNativeExponent(): Q.Promise<boolean> {
        if (this.dependencyPackage !== ReactNativePackageStatus.UNKNOWN) {
            return Q(this.dependencyPackage === ReactNativePackageStatus.EXPONENT_PACKAGE);
        }
        // Look for the package.json of the dependecy
        const pathToReactNativePackageJson = path.resolve(this.rootPath, "node_modules", "react-native", "package.json");
        return this.fileSystem.readFile(pathToReactNativePackageJson, "utf-8")
            .then(jsonContents => {
                const packageJson = JSON.parse(jsonContents);
                const isExp = /\bexponentjs\/react-native\b/.test(packageJson._from);
                this.dependencyPackage = isExp ? ReactNativePackageStatus.EXPONENT_PACKAGE : ReactNativePackageStatus.FACEBOOK_PACKAGE;
                return isExp;
            }).catch(() => {
                // Not in a react-native project
                return false;
            });
    }

    /**
     * Name of the file (we assume it lives in the workspace root) that should be used as entrypoint.
     * e.g. index.ios.js
     */
    private entrypoint(): Q.Promise<string> {
        if (this.entrypointFilename) {
            return Q(this.entrypointFilename);
        }
        return this.readVscodeExponentSettingFile()
            .then((settingsJson) => {
                // Let's load both to memory to make sure we are not reading from memory next time we query for this.
                this.entrypointFilename = settingsJson.entryPointFilename;
                this.entrypointComponentName = settingsJson.entryPointComponent;
                return this.entrypointFilename;
            });
    }

    /**
     * Name of the component used as an entrypoint for the app.
     */
    private entrypointComponent(): Q.Promise<string> {
        if (this.entrypointComponentName) {
            return Q(this.entrypointComponentName);
        }
        return this.readVscodeExponentSettingFile()
            .then((settingsJson) => {
                // Let's load both to memory to make sure we are not reading from memory next time we query for this.
                this.entrypointComponentName = settingsJson.entryPointComponent;
                this.entrypointFilename = settingsJson.entrypointFilename;
                return this.entrypointComponentName;
            });
    }

    /**
     * Path to the a given file inside the .vscode directory
     */
    private dotvscodePath(filename: string): string {
        return path.join(this.rootPath, ".vscode", filename);
    }

    /**
     * Path to the a given file from the workspace root
     */
    private pathToFileInWorkspace(filename: string): string {
        return path.join(this.rootPath, filename);
    }

    /**
     * Name specified on user's package.json
     */
    private getPackageName(): Q.Promise<string> {
        return new Package(this.rootPath, { fileSystem: this.fileSystem }).name();
    }
}
