import {
  chain,
  Rule,
  SchematicContext,
  Tree
} from '@angular-devkit/schematics';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

import {
  createOrUpdate,
  readJsonInTree,
  updateJsonInTree
} from '../../src/utils/ast-utils';
import { serializeJson, renameSync } from '../../src/utils/fileutils';
import { parseTarget, serializeTarget } from '../../src/utils/cli-config-utils';
import { offsetFromRoot } from '../../src/utils/common';
import { formatFiles } from '../../src/utils/rules/format-files';
import { NxJson } from '../../src/command-line/shared';

function createKarma(host: Tree, project: any) {
  const offset = offsetFromRoot(project.root);

  createOrUpdate(
    host,
    `${project.root}/karma.conf.js`,
    `
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, '${offset}coverage'),
      reports: ['html', 'lcovonly'],
      fixWebpackSourcePaths: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false
  });
};
    `
  );
}

function createProtractor(host: Tree, project: any) {
  createOrUpdate(
    host,
    `${project.root}/protractor.conf.js`,
    `
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './src/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: require('path').join(__dirname, './tsconfig.e2e.json')
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};
    `
  );
}

function createTestTs(host: Tree, project: any) {
  if (project.projectType === 'library') {
    createOrUpdate(
      host,
      `${project.sourceRoot}/test.ts`,
      `
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\\.spec\\.ts$/);
// And load the modules.
context.keys().map(context);    
    `
    );
  } else {
    createOrUpdate(
      host,
      `${project.sourceRoot}/test.ts`,
      `
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

  import 'zone.js/dist/zone-testing';
  import { getTestBed } from '@angular/core/testing';
  import {
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting
  } from '@angular/platform-browser-dynamic/testing';

  declare const require: any;

// First, initialize the Angular testing environment.
  getTestBed().initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
  );
// Then we find all the tests.
  const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
  context.keys().map(context);
    `
    );
  }
  return host;
}

function createBrowserlist(host: Tree, project: any) {
  createOrUpdate(
    host,
    `${project.root}/browserslist`,
    stripIndents`
      # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
      # For additional information regarding the format and rule options, please see:
      # https://github.com/browserslist/browserslist#queries
      # For IE 9-11 support, please uncomment the last line of the file and adjust as needed
      > 0.5%
      last 2 versions
      Firefox ESR
      not dead
      # IE 9-11
    `
  );
}

function createTsconfigSpecJson(host: Tree, project: any) {
  const files = ['src/test.ts'];

  const offset = offsetFromRoot(project.root);

  const compilerOptions = {
    outDir: `${offset}dist/out-tsc/${project.root}`,
    types: ['jasmine', 'node']
  };

  if (project.projectType === 'application') {
    files.push('src/polyfills.ts');
    compilerOptions['module'] = 'commonjs';
  }

  createOrUpdate(
    host,
    `${project.root}/tsconfig.spec.json`,
    serializeJson({
      extends: `${offset}tsconfig.json`,
      compilerOptions,
      files,
      include: ['**/*.spec.ts', '**/*.d.ts']
    })
  );
}

function createTslintJson(host: Tree, project: any) {
  const offset = offsetFromRoot(project.root);

  createOrUpdate(
    host,
    `${project.root}/tslint.json`,
    serializeJson({
      extends: `${offset}tslint.json`,
      rules: {
        'directive-selector': [true, 'attribute', project.prefix, 'camelCase'],
        'component-selector': [true, 'element', project.prefix, 'kebab-case']
      }
    })
  );
}

function createTsconfigLibJson(host: Tree, project: any) {
  const offset = offsetFromRoot(project.root);

  createOrUpdate(
    host,
    `${project.root}/tsconfig.lib.json`,
    serializeJson({
      extends: `${offset}tsconfig.json`,
      compilerOptions: {
        outDir: `${offset}out-tsc/${project.root}`,
        target: 'es2015',
        module: 'es2015',
        moduleResolution: 'node',
        declaration: true,
        sourceMap: true,
        inlineSources: true,
        emitDecoratorMetadata: true,
        experimentalDecorators: true,
        importHelpers: true,
        types: [],
        lib: ['dom', 'es2015']
      },
      angularCompilerOptions: {
        annotateForClosureCompiler: true,
        skipTemplateCodegen: true,
        strictMetadataEmit: true,
        fullTemplateTypeCheck: true,
        strictInjectionParameters: true,
        flatModuleId: 'AUTOGENERATED',
        flatModuleOutFile: 'AUTOGENERATED'
      },
      exclude: ['src/test.ts', '**/*.spec.ts']
    })
  );
}

function createAdditionalFiles(host: Tree) {
  const angularJson = readJsonInTree(host, 'angular.json');
  Object.entries<any>(angularJson.projects).forEach(([key, project]) => {
    if (project.architect.test) {
      createTsconfigSpecJson(host, project);
      createKarma(host, project);
      createTestTs(host, project);
    }

    if (project.projectType === 'application' && !project.architect.e2e) {
      createBrowserlist(host, project);
      createTslintJson(host, project);
    }

    if (project.projectType === 'application' && project.architect.e2e) {
      createProtractor(host, project);
    }

    if (project.projectType === 'library') {
      createTsconfigLibJson(host, project);
      createTslintJson(host, project);
    }
  });
  return host;
}

function moveE2eTests(host: Tree, context: SchematicContext) {
  const angularJson = readJsonInTree(host, 'angular.json');

  Object.entries<any>(angularJson.projects).forEach(([key, p]) => {
    if (p.projectType === 'application' && !p.architect.e2e) {
      renameSync(`${p.root}/e2e`, `${p.root}-e2e/src`, err => {
        if (!err) {
          context.logger.info(`Moved ${p.root}/e2e to ${p.root}-e2e/src`);
          return;
        }

        context.logger.info(
          `We could not find e2e tests for the ${key} project.`
        );
        context.logger.info(
          `Ignore this if there are no e2e tests for ${key} project.`
        );
        context.logger.warn(err.message);
        context.logger.warn(
          `If there are e2e tests for the ${key} project, we recommend manually moving them to ${
            p.root
          }-e2e/src.`
        );
      });
    }
  });
}

function deleteUnneededFiles(host: Tree) {
  try {
    host.delete('karma.conf.js');
    host.delete('protractor.conf.js');
    host.delete('tsconfig.spec.json');
    host.delete('test.js');
  } catch (e) {}
  return host;
}

function patchLibIndexFiles(host: Tree, context: SchematicContext) {
  const angularJson = readJsonInTree(host, 'angular.json');

  Object.entries<any>(angularJson.projects).forEach(([key, p]) => {
    if (p.projectType === 'library') {
      try {
        // TODO: incorporate this into fileutils.renameSync
        renameSync(p.sourceRoot, `${p.root}/lib`, err => {
          if (err) {
            context.logger.warn(
              `We could not resolve the "sourceRoot" of the ${key} library.`
            );
            throw err;
          }
          renameSync(`${p.root}/lib`, `${p.sourceRoot}/lib`, err => {
            // This should never error
            context.logger.info(`Moved ${p.sourceRoot} to ${p.sourceRoot}/lib`);
            context.logger.warn(
              'Deep imports may have been affected. Always try to import from the index of the lib.'
            );
          });
        });
        const npmScope = readJsonInTree<NxJson>(host, 'nx.json').npmScope;
        host = updateJsonInTree('tsconfig.json', json => {
          json.compilerOptions.paths = json.compilerOptions.paths || {};
          json.compilerOptions.paths[
            `@${npmScope}/${p.root.replace('libs/', '')}`
          ] = [`${p.sourceRoot}/index.ts`];
          return json;
        })(host, context) as Tree;
        const content = host.read(`${p.root}/index.ts`).toString();
        host.create(
          `${p.sourceRoot}/index.ts`,
          content.replace(new RegExp('/src/', 'g'), '/lib/')
        );
        host.delete(`${p.root}/index.ts`);
      } catch (e) {
        context.logger.warn(`Nx failed to successfully update '${p.root}'.`);
        context.logger.warn(`Error message: ${e.message}`);
        context.logger.warn(`Please update the library manually.`);
      }
    }
  });

  return host;
}

const updatePackageJson = updateJsonInTree('package.json', json => {
  json.dependencies = json.dependencies || {};
  json.devDependencies = json.devDependencies || {};

  json.scripts = {
    ...json.scripts,
    'affected:test': './node_modules/.bin/nx affected:test'
  };

  json.dependencies = {
    ...json.dependencies,
    // Migrating Angular Dependencies because ng update @angular/core doesn't work well right now
    '@angular/animations': '6.0.1',
    '@angular/common': '6.0.1',
    '@angular/compiler': '6.0.1',
    '@angular/core': '6.0.1',
    '@angular/forms': '6.0.1',
    '@angular/platform-browser': '6.0.1',
    '@angular/platform-browser-dynamic': '6.0.1',
    '@angular/router': '6.0.1',
    rxjs: '6.0.0',
    'rxjs-compat': '6.0.0',
    'zone.js': '^0.8.26',
    'core-js': '^2.5.4',
    // End Angular Versions
    '@ngrx/effects': '6.0.1',
    '@ngrx/router-store': '6.0.1',
    '@ngrx/store': '6.0.1',
    '@ngrx/store-devtools': '6.0.1',
    '@nrwl/nx': '6.0.2'
  };
  json.devDependencies = {
    ...json.devDependencies,
    // Migrating Angular Dependencies because ng update @angular/core doesn't work well right now
    '@angular/compiler-cli': '6.0.1',
    '@angular/language-service': '6.0.1',
    // End Angular Versions
    typescript: '2.7.2',
    'jasmine-marbles': '0.3.1',
    '@types/jasmine': '~2.8.6',
    '@types/jasminewd2': '~2.0.3',
    '@types/node': '~8.9.4',
    codelyzer: '~4.2.1',
    'jasmine-core': '~2.99.1',
    'jasmine-spec-reporter': '~4.2.1',
    karma: '~2.0.0',
    'karma-chrome-launcher': '~2.2.0',
    'karma-coverage-istanbul-reporter': '~1.4.2',
    'karma-jasmine': '~1.1.0',
    'karma-jasmine-html-reporter': '^0.2.2',
    protractor: '~5.3.0',
    'ts-node': '~5.0.1',
    tslint: '~5.9.1',
    prettier: '1.10.2'
  };

  if (json.dependencies['@angular/http']) {
    json.dependencies['@angular/http'] = '6.0.1';
  }

  if (json.dependencies['@angular/platform-server']) {
    json.dependencies['@angular/platform-server'] = '6.0.1';
  }

  if (json.dependencies['@angular/service-worker']) {
    json.dependencies['@angular/service-worker'] = '6.0.1';
  }

  if (json.dependencies['@angular/platform-webworker']) {
    json.dependencies['@angular/platform-webworker'] = '6.0.1';
  }

  if (json.dependencies['@angular/platform-webworker-dynamic']) {
    json.dependencies['@angular/platform-webworker-dynamic'] = '6.0.1';
  }

  if (json.dependencies['@angular/upgrade']) {
    json.dependencies['@angular/upgrade'] = '6.0.1';
  }

  return json;
});

function createDefaultAppTsConfig(host: Tree, project: any) {
  const offset = offsetFromRoot(project.root);

  const defaultAppTsConfig = {
    extends: `${offset}tsconfig.json`,
    compilerOptions: {
      outDir: `${offset}dist/out-tsc/${project.root}`,
      module: 'es2015'
    },
    include: ['**/*.ts'],
    exclude: ['src/test.ts', '**/*.spec.ts']
  };
  createOrUpdate(
    host,
    `${project.root}/tsconfig.app.json`,
    serializeJson(defaultAppTsConfig)
  );
}

function createDefaultE2eTsConfig(host: Tree, project: any) {
  const offset = offsetFromRoot(project.root);

  const defaultE2eTsConfig = {
    extends: `${offset}tsconfig.json`,
    compilerOptions: {
      outDir: `${offset}dist/out-tsc/${project.root}`,
      module: 'commonjs',
      target: 'es5',
      types: ['jasmine', 'jasminewd2', 'node']
    }
  };
  createOrUpdate(
    host,

    `${project.root}/tsconfig.e2e.json`,
    serializeJson(defaultE2eTsConfig)
  );
}

function updateTsConfigs(host: Tree) {
  const angularJson = readJsonInTree(host, 'angular.json');
  Object.entries<any>(angularJson.projects).forEach(([key, project]) => {
    if (
      project.architect.build &&
      project.architect.build.options.main.startsWith('apps')
    ) {
      const offset = offsetFromRoot(project.root);
      const originalTsConfigPath = `${project.root}/src/tsconfig.app.json`;
      if (host.exists(originalTsConfigPath)) {
        const tsConfig = readJsonInTree(host, originalTsConfigPath);
        if (!(tsConfig.exclude as string[]).includes('src/test.ts')) {
          tsConfig.exclude.push('src/test.ts');
        }

        createOrUpdate(
          host,

          `${project.root}/tsconfig.app.json`,
          serializeJson({
            ...tsConfig,
            extends: `${offset}tsconfig.json`,
            compilerOptions: {
              ...tsConfig.compilerOptions,
              outDir: `${offset}dist/out-tsc/${project.root}`
            },
            include: tsConfig.include.map((include: string) => {
              if (include.startsWith('../../../')) {
                include = include.substring(3);
              }

              if (include.includes('/libs/') && include.endsWith('index.ts')) {
                include = include.replace('index.ts', 'src/index.ts');
              }
              return include;
            })
          })
        );
        host.delete(`${project.root}/src/tsconfig.app.json`);
      } else {
        createDefaultAppTsConfig(host, project);
      }
    }

    if (project.architect.e2e) {
      const offset = offsetFromRoot(project.root);

      if (host.exists(`${project.root}/src/tsconfig.e2e.json`)) {
        const tsConfig = readJsonInTree(
          host,
          `${project.root}/src/tsconfig.e2e.json`
        );
        tsConfig.extends = `${offset}tsconfig.json`;
        tsConfig.compilerOptions = {
          ...tsConfig.compilerOptions,
          outDir: `${offset}dist/out-tsc/${project.root}`
        };
        delete tsConfig.include;
        delete tsConfig.exclude;
        createOrUpdate(
          host,

          `${project.root}/tsconfig.e2e.json`,
          serializeJson(tsConfig)
        );
        host.delete(`${project.root}/src/tsconfig.e2e.json`);
      } else {
        createDefaultE2eTsConfig(host, project);
      }
    }
  });
  return host;
}

const updateAngularJson = updateJsonInTree('angular.json', json => {
  json.newProjectRoot = '';
  json.cli = {
    ...json.cli,
    defaultCollection: '@nrwl/schematics'
  };
  delete json.projects.$workspaceRoot;
  delete json.projects['$workspaceRoot-e2e'];
  const prefix = json.schematics['@nrwl/schematics:component'].prefix;
  delete json.schematics;
  json.defaultProject = pathToName(json.defaultProject);

  const projects = {};

  Object.entries<any>(json.projects).forEach(([key, project]) => {
    const type = !project.architect.build
      ? 'e2e'
      : project.architect.build.options.main.startsWith('apps')
      ? 'application'
      : 'library';
    if (type !== 'e2e') {
      project.projectType = type;
    }

    switch (type) {
      case 'application':
        project.prefix = prefix;

        project.architect.build.options.tsConfig = `${
          project.root
        }/tsconfig.app.json`;
        project.architect.test.options.karmaConfig = `${
          project.root
        }/karma.conf.js`;
        project.architect.test.options.tsConfig = `${
          project.root
        }/tsconfig.spec.json`;

        project.architect.test.options.main = `${project.sourceRoot}/test.ts`;

        project.architect.lint.options.tsConfig = [
          `${project.root}/tsconfig.app.json`,
          `${project.root}/tsconfig.spec.json`
        ];

        project.architect.serve.options.browserTarget = serializeTarget(
          parseAndNormalizeTarget(project.architect.serve.options.browserTarget)
        );
        project.architect.serve.configurations.production.browserTarget = serializeTarget(
          parseAndNormalizeTarget(
            project.architect.serve.configurations.production.browserTarget
          )
        );
        project.architect[
          'extract-i18n'
        ].options.browserTarget = serializeTarget(
          parseAndNormalizeTarget(
            project.architect['extract-i18n'].options.browserTarget
          )
        );
        projects[pathToName(key)] = project;
        break;

      case 'library':
        project.prefix = prefix;

        project.projectType = 'library';
        project.architect.test.options.karmaConfig = `${
          project.root
        }/karma.conf.js`;
        project.architect.test.options.tsConfig = `${
          project.root
        }/tsconfig.spec.json`;
        project.architect.test.options.main = `${project.sourceRoot}/test.ts`;

        project.architect.lint.options.tsConfig = [
          `${project.root}/tsconfig.lib.json`,
          `${project.root}/tsconfig.spec.json`
        ];
        delete project.architect.build;
        delete project.architect.serve;
        delete project.architect['extract-i18n'];
        projects[pathToName(key)] = project;
        break;

      case 'e2e':
        const appProjectKey = parseTarget(
          project.architect.e2e.options.devServerTarget
        ).project;
        const appProject = json.projects[appProjectKey];
        if (appProject.projectType === 'library') {
          break;
        }
        project.root = `${appProject.root}-e2e`;
        project.sourceRoot = `${project.root}/src`;
        project.prefix = prefix;

        project.architect.e2e.options.protractorConfig = `${
          project.root
        }/protractor.conf.js`;
        project.architect.lint.options.tsConfig = [
          `${project.root}/tsconfig.e2e.json`
        ];
        project.architect.e2e.options.devServerTarget = serializeTarget(
          parseAndNormalizeTarget(project.architect.e2e.options.devServerTarget)
        );
        projects[pathToName(key)] = project;
        break;
    }
  });
  json.projects = projects;
  return json;
});

function addInstallTask(host: Tree, context: SchematicContext) {
  context.addTask(new NodePackageInstallTask());
}

function checkCli6Upgraded(host: Tree) {
  if (!host.exists('angular.json') && host.exists('.angular-cli.json')) {
    throw new Error(
      'Please install the latest version and run ng update @angular/cli first'
    );
  }
}

function parseAndNormalizeTarget(s: string) {
  const r = parseTarget(s);
  return { ...r, project: pathToName(r.project) };
}

function pathToName(s: string) {
  return s.replace(new RegExp('/', 'g'), '-');
}

export default function(): Rule {
  return chain([
    checkCli6Upgraded,
    updatePackageJson,
    updateAngularJson,
    moveE2eTests,
    updateTsConfigs,
    createAdditionalFiles,
    deleteUnneededFiles,
    patchLibIndexFiles,
    addInstallTask,
    formatFiles()
  ]);
}
