import { workspace, window, WorkspaceConfiguration, QuickPickItem } from 'vscode';
import * as path from 'path';
import {
  runScriptInOutputWindow,
  stopScriptInOutputWindow,
  runCommandInIntegratedTerminal,
} from '../utils/run-terminal';
import * as portfinder from 'portfinder';
import { getPorts } from '../utils/ports';
import { logError, log } from '../utils/vscode-output';
import { selectCode } from '../utils/vscode-file';
import { getProjectFolder, isWorkspacesRoot, traverseSubFolders, removeRootFolder } from '../utils/folders';
import resolveFrom from 'resolve-from';
import { detectScriptsVersion } from '@sculpt-ui/server';
import telemetry from '../telemetry';

export enum DevServerStatus {
  stopped,
  starting,
  started,
  stopping,
}

const runInTerminal = true;
const terminalId = 'SculptUI';
const outputWindowId = 'SculptUI - Dev Server';
const defaultPorts = { port: 3000, highestPort: 3100 };

interface FolderOptions extends QuickPickItem {
  folder: string;
}

export default class DevServer {
  private readonly extensionPath: string;
  private status: DevServerStatus = DevServerStatus.stopped;
  private onStatusChanged?: (status: DevServerStatus) => void;
  private onStatusMessage?: (message: string, duration?: number) => void;
  private serverPId?: number;
  private url?: string;
  private readonly ideServerUrl: string;

  constructor(
    extensionPath: string,
    ideServerUrl: string,
    onStatusChanged?: (status: DevServerStatus) => void,
    onStatusMessage?: (message: string, duration?: number) => void,
  ) {
    this.ideServerUrl = ideServerUrl;
    this.extensionPath = extensionPath;
    this.onStatusChanged = onStatusChanged;
    this.onStatusMessage = onStatusMessage;
    this.setStatus(DevServerStatus.stopped);
  }

  start = async () => {
    const config = workspace.getConfiguration('sculpt-ui');
    try {
      const folder = await this.getFolder(config);
      if (!folder) {
        return;
      }
      this.setStatus(DevServerStatus.starting);
      this.setStatusMessage('dev server starting...', 3000);
      const port = await this.getPort(config);
      const useHttps = config.get<boolean>('https');
      telemetry.sendEvent('start', { port, useHttps });
      this.startServer(folder, { port, useHttps });
      return folder;
    } catch (err) {
      this.setStatus(DevServerStatus.stopped);
      telemetry.sendError(err as Error);
      this.notifyError(`SculptUI can't start: ${err}`);
    }
  };

  stop = () => {
    telemetry.sendEvent('stop');
    if (runInTerminal) {
      runCommandInIntegratedTerminal(terminalId, '\x03', [], undefined);
      this.setStatus(DevServerStatus.stopped);
    } else {
      if (this.serverPId) {
        this.setStatus(DevServerStatus.stopping);
        stopScriptInOutputWindow(this.serverPId);
      } else {
        // make sure status is notified
        this.setStatus(DevServerStatus.stopped);
      }
    }
  };

  getStatus() {
    return this.status;
  }

  getUrl() {
    return this.url;
  }

  private notifyError(message: string) {
    logError(message);
    window.showErrorMessage(message);
  }

  private getFolder(config: WorkspaceConfiguration): Promise<string | undefined> {
    return new Promise((resolve, reject) => {
      let folder = getProjectFolder(config);
      if (!folder) {
        reject(new Error(`No folder is opened in VS Code.`));
        return;
      }
      const detectedInRoot = detectScriptsVersion(folder);
      if (detectedInRoot && (detectedInRoot === 'sculpt' || !isWorkspacesRoot(folder))) {
        resolve(folder);
        return;
      }
      const subFolders: string[] = detectedInRoot && !isWorkspacesRoot(folder) ? [folder] : [];
      traverseSubFolders(
        folder,
        (dir: string) => {
          if (detectScriptsVersion(dir)) {
            subFolders.push(dir);
          }
          return false;
        },
        3,
        ['node_modules', '.vscode', '.git'],
      );
      if (subFolders.length === 0) {
        reject(new Error(`SculptUI can not start: No project folder is available.`));
      } else if (subFolders.length === 1) {
        resolve(subFolders[0]);
      } else {
        const folderTrunc = folder.endsWith(path.sep) ? folder.substring(0, folder.length - 2) : folder;
        const makeLabel = (subFolder: string) => {
          return removeRootFolder(subFolder, folderTrunc) || '\\';
        };
        const folderOptions = subFolders.map<FolderOptions>(subFolder => ({
          label: makeLabel(subFolder),
          folder: subFolder,
        }));
        folderOptions.push(
          ...subFolders.map<FolderOptions>(subFolder => ({
            label: makeLabel(subFolder),
            description: '$(symbol-property) and always use in future',
            folder: subFolder,
          })),
        );
        window.showQuickPick(folderOptions).then(
          value => {
            if (value && value.description) {
              this.saveFolder(config, value.folder);
            }
            resolve(value && value.folder);
          },
          reason => reject(reason),
        );
      }
    });
  }

  private saveFolder(config: WorkspaceConfiguration, folder: string) {
    const projectFolder = getProjectFolder();
    if (projectFolder) {
      folder = path.relative(projectFolder, folder);
    }
    config.update('folderForStarting', folder, false);
    window.showInformationMessage(
      `Folder '${folder}' has been set as the folder to always start SculptUI in. You can change this anytime in the SculptUI extension's settings.`,
    );
  }

  private getPort(config: WorkspaceConfiguration): Promise<number | undefined> {
    return new Promise((resolve, reject) => {
      try {
        let ports = getPorts(config.get('port'), undefined);
        if (!ports) {
          resolve(undefined);
        } else {
          portfinder
            .getPortPromise(ports || defaultPorts)
            .then(port => resolve(port))
            .catch(reason => reject(`Can't retrieve an avialable port: ${reason}`));
        }
      } catch (error) {
        reject(`Port is not correctly configured:\n${error}\nPlease check the extension's settings.`);
        return;
      }
    });
  }

  private startServer(folder: string, options?: { port?: number; useHttps?: boolean }) {
    const command = resolveFrom(this.extensionPath, '@sculpt-ui/server/bin/sculpt.js');
    const port = options ? options.port : undefined;
    const useHttps = options ? options.useHttps : undefined;
    //start our server
    if (runInTerminal) {
      const commandArgs = [command];
      commandArgs.push('--sculpt-ide-url', this.ideServerUrl);
      if (port) {
        commandArgs.push('--port', port.toString());
      }
      if (useHttps) {
        commandArgs.push('--https', useHttps.toString());
      }
      runCommandInIntegratedTerminal(terminalId, 'node', commandArgs, folder).then(pid => {
        this.serverPId = pid;
        this.setStatus(DevServerStatus.started);
      });
    } else {
      const env: any = { ...process.env };
      if (useHttps) {
        env.HTTPS = useHttps;
      }
      if (port) {
        env.PORT = port.toString();
      }
      this.serverPId = runScriptInOutputWindow(
        outputWindowId,
        command,
        [],
        folder,
        env,
        message => {
          if (typeof message === 'object') {
            if (message.type === 'selectInEditor') {
              if (!message.file || message.line === undefined || Number.isNaN(Number(message.line))) {
                logError(`Select in editor message with incorrect parameters ${message}`);
              } else {
                log(`Selecting in code editor ${message.file}:${message.line}:${message.column || 0}`);
                selectCode(
                  message.file,
                  { line: message.endLine ? message.endLine - 1 : 0, character: message.endColumn || 0 },
                  { line: message.line - 1, character: message.column || 0 },
                );
              }
            } else if (message.type === 'devServerURL') {
              this.setStatusMessage('opening ' + message.url, 3000);
              this.setStatus(DevServerStatus.started);
              this.url = message.url;
            }
          }
        },
        (code, signal, outputChannel) => {
          if (signal === 'SIGTERM') {
            /* window.setStatusBarMessage('SculptUI dev server terminated.', 3000);
                    window.showInformationMessage('Sculpt devserver has been terminated.'); */
            outputChannel.appendLine('Successfully stopped server');
            outputChannel.appendLine('-----------------------');
            outputChannel.appendLine('');
            this.serverPId = undefined;
            this.setStatus(DevServerStatus.stopped);
          } else {
            if (code) {
              window.showErrorMessage(
                `SculptUI dev server has exited with error code '${code}'. Please check the SculptUI dev server's output for more details.`,
              );
            }
            outputChannel.appendLine(`SculptUI dev server has exited with error code '${code}'.`);
            outputChannel.appendLine('-----------------------');
            outputChannel.appendLine('');
            this.serverPId = undefined;
            this.setStatus(DevServerStatus.stopped);
          }
        },
      );
    }
  }

  private setStatus(status: DevServerStatus) {
    this.status = status;
    if (this.onStatusChanged) {
      this.onStatusChanged(status);
    }
  }

  private setStatusMessage(message: string, duration?: number) {
    if (this.onStatusMessage) {
      this.onStatusMessage(message, duration);
    }
  }
}
