var path = require('path');
var packageJson = require('./package.json');

module.exports = function(grunt) {
  'use strict';

  var bower = grunt.file.readJSON('bower.json');
  var components = [];
  for (var i in bower.concat.app) {
    components.push('components/' + bower.concat.app[i] + '/**/*.js');
  }
  components.push('components/' + 'webaudiorecorder/lib/WebAudioRecorder.js');

  var libtextsecurecomponents = [];
  for (i in bower.concat.libtextsecure) {
    libtextsecurecomponents.push(
      'components/' + bower.concat.libtextsecure[i] + '/**/*.js'
    );
  }

  var importOnce = require('node-sass-import-once');
  grunt.loadNpmTasks('grunt-sass');

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      components: {
        src: components,
        dest: 'js/components.js',
      },
      libtextsecurecomponents: {
        src: libtextsecurecomponents,
        dest: 'libtextsecure/components.js',
      },
      test: {
        src: [
          'components/mocha/mocha.js',
          'components/chai/chai.js',
          'test/_test.js',
        ],
        dest: 'test/test.js',
      },
      //TODO: Move errors back down?
      libtextsecure: {
        options: {
          banner: ';(function() {\n',
          footer: '})();\n',
        },
        src: [
          'libtextsecure/errors.js',
          'libtextsecure/libsignal-protocol.js',
          'libtextsecure/protocol_wrapper.js',

          'libtextsecure/crypto.js',
          'libtextsecure/storage.js',
          'libtextsecure/storage/user.js',
          'libtextsecure/storage/groups.js',
          'libtextsecure/storage/unprocessed.js',
          'libtextsecure/protobufs.js',
          'libtextsecure/helpers.js',
          'libtextsecure/stringview.js',
          'libtextsecure/event_target.js',
          'libtextsecure/api.js',
          'libtextsecure/account_manager.js',
          'libtextsecure/websocket-resources.js',
          'libtextsecure/message_receiver.js',
          'libtextsecure/outgoing_message.js',
          'libtextsecure/sendmessage.js',
          'libtextsecure/sync_request.js',
          'libtextsecure/contacts_parser.js',
          'libtextsecure/ProvisioningCipher.js',
          'libtextsecure/task_with_timeout.js',
        ],
        dest: 'js/libtextsecure.js',
      },
      libtextsecuretest: {
        src: [
          'components/jquery/dist/jquery.js',
          'components/mock-socket/dist/mock-socket.js',
          'components/mocha/mocha.js',
          'components/chai/chai.js',
          'libtextsecure/test/_test.js',
        ],
        dest: 'libtextsecure/test/test.js',
      },
    },
    sass: {
      options: {
        sourceMap: true,
        importer: importOnce,
      },
      dev: {
        files: {
          'stylesheets/manifest.css': 'stylesheets/manifest.scss',
        },
      },
    },
    jshint: {
      files: [
        'Gruntfile.js',
        'js/**/*.js',
        '!js/background.js',
        '!js/backup.js',
        '!js/components.js',
        '!js/database.js',
        '!js/jquery.js',
        '!js/libsignal-protocol-worker.js',
        '!js/libtextsecure.js',
        '!js/logging.js',
        '!js/modules/**/*.js',
        '!js/Mp3LameEncoder.min.js',
        '!js/signal_protocol_store.js',
        '!js/views/conversation_search_view.js',
        '!js/views/conversation_view.js',
        '!js/views/debug_log_view.js',
        '!js/views/file_input_view.js',
        '!js/views/message_view.js',
        '!js/models/conversations.js',
        '!js/models/messages.js',
        '!js/WebAudioRecorderMp3.js',
        '!libtextsecure/message_receiver.js',
        '_locales/**/*',
      ],
      options: { jshintrc: '.jshintrc' },
    },
    dist: {
      src: [
        'background.html',
        'index.html',
        'options.html',
        '_locales/**',
        'protos/*',
        'js/**',
        'stylesheets/*.css',
        '!js/register.js',
      ],
      res: ['images/**/*', 'fonts/*'],
    },
    copy: {
      deps: {
        files: [
          {
            src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js',
            dest: 'js/Mp3LameEncoder.min.js',
          },
          {
            src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js',
            dest: 'js/WebAudioRecorderMp3.js',
          },
          {
            src: 'components/jquery/dist/jquery.js',
            dest: 'js/jquery.js',
          },
        ],
      },
      res: {
        files: [{ expand: true, dest: 'dist/', src: ['<%= dist.res %>'] }],
      },
      src: {
        files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }],
      },
    },
    jscs: {
      all: {
        src: [
          'Gruntfile',
          'js/**/*.js',
          '!js/components.js',
          '!js/libsignal-protocol-worker.js',
          '!js/libtextsecure.js',
          '!js/modules/**/*.js',
          '!js/models/conversations.js',
          '!js/models/messages.js',
          '!js/views/conversation_search_view.js',
          '!js/views/conversation_view.js',
          '!js/views/debug_log_view.js',
          '!js/views/file_input_view.js',
          '!js/views/message_view.js',
          '!js/Mp3LameEncoder.min.js',
          '!js/WebAudioRecorderMp3.js',
          'test/**/*.js',
          '!test/blanket_mocha.js',
          '!test/modules/**/*.js',
          '!test/test.js',
        ],
      },
    },
    watch: {
      sass: {
        files: ['./stylesheets/*.scss'],
        tasks: ['sass'],
      },
      libtextsecure: {
        files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
        tasks: ['concat:libtextsecure'],
      },
      dist: {
        files: ['<%= dist.src %>', '<%= dist.res %>'],
        tasks: ['copy_dist'],
      },
      scripts: {
        files: ['<%= jshint.files %>'],
        tasks: ['jshint'],
      },
      style: {
        files: ['<%= jscs.all.src %>'],
        tasks: ['jscs'],
      },
      transpile: {
        files: ['./ts/**/*.ts'],
        tasks: ['exec:transpile'],
      },
    },
    exec: {
      'tx-pull': {
        cmd: 'tx pull',
      },
      transpile: {
        cmd: 'npm run transpile',
      },
    },
    'test-release': {
      osx: {
        archive:
          'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar',
        appUpdateYML:
          'mac/' +
          packageJson.productName +
          '.app/Contents/Resources/app-update.yml',
        exe:
          'mac/' +
          packageJson.productName +
          '.app/Contents/MacOS/' +
          packageJson.productName,
      },
      mas: {
        archive: 'mas/Signal.app/Contents/Resources/app.asar',
        appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml',
        exe:
          'mas/' +
          packageJson.productName +
          '.app/Contents/MacOS/' +
          packageJson.productName,
      },
      linux: {
        archive: 'linux-unpacked/resources/app.asar',
        exe: 'linux-unpacked/' + packageJson.name,
      },
      win: {
        archive: 'win-unpacked/resources/app.asar',
        appUpdateYML: 'win-unpacked/resources/app-update.yml',
        exe: 'win-unpacked/' + packageJson.productName + '.exe',
      },
    },
    gitinfo: {}, // to be populated by grunt gitinfo
  });

  Object.keys(grunt.config.get('pkg').devDependencies).forEach(function(key) {
    if (/^grunt(?!(-cli)?$)/.test(key)) {
      // ignore grunt and grunt-cli
      grunt.loadNpmTasks(key);
    }
  });

  // Transifex does not understand placeholders, so this task patches all non-en
  // locales with missing placeholders
  grunt.registerTask('locale-patch', function() {
    var en = grunt.file.readJSON('_locales/en/messages.json');
    grunt.file.recurse('_locales', function(
      abspath,
      rootdir,
      subdir,
      filename
    ) {
      if (subdir === 'en' || filename !== 'messages.json') {
        return;
      }
      var messages = grunt.file.readJSON(abspath);

      for (var key in messages) {
        if (en[key] !== undefined && messages[key] !== undefined) {
          if (
            en[key].placeholders !== undefined &&
            messages[key].placeholders === undefined
          ) {
            messages[key].placeholders = en[key].placeholders;
          }
        }
      }

      grunt.file.write(abspath, JSON.stringify(messages, null, 4) + '\n');
    });
  });

  grunt.registerTask('getExpireTime', function() {
    grunt.task.requires('gitinfo');
    var gitinfo = grunt.config.get('gitinfo');
    var commited = gitinfo.local.branch.current.lastCommitTime;
    var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
    grunt.file.write(
      'config/local-production.json',
      JSON.stringify({ buildExpiration: time }) + '\n'
    );
  });

  grunt.registerTask('clean-release', function() {
    require('rimraf').sync('release');
    require('mkdirp').sync('release');
  });

  grunt.registerTask('fetch-release', function() {
    grunt.task.requires('gitinfo');
    require('mkdirp').sync('release');
    var fs = require('fs');
    var done = this.async();
    var gitinfo = grunt.config.get('gitinfo');
    var https = require('https');

    var urlBase = 'https://s3-us-west-1.amazonaws.com/signal-desktop-builds';
    var keyBase = 'signalapp/Signal-Desktop';
    var sha = gitinfo.local.branch.current.SHA;
    var files = [
      {
        zip: packageJson.name + '-' + packageJson.version + '.zip',
        extractedTo: 'linux',
      },
    ];

    var extract = require('extract-zip');
    var download = function(url, dest, extractedTo, cb) {
      var file = fs.createWriteStream(dest);
      var request = https
        .get(url, function(response) {
          if (response.statusCode !== 200) {
            cb(response.statusCode);
          } else {
            response.pipe(file);
            file.on('finish', function() {
              file.close(function() {
                extract(
                  dest,
                  { dir: path.join(__dirname, 'release', extractedTo) },
                  cb
                );
              });
            });
          }
        })
        .on('error', function(err) {
          // Handle errors
          fs.unlink(dest); // Delete the file async. (But we don't check the result)
          if (cb) cb(err.message);
        });
    };

    Promise.all(
      files.map(function(item) {
        var key = [keyBase, sha, 'dist', item.zip].join('/');
        var url = [urlBase, key].join('/');
        var dest = 'release/' + item.zip;
        return new Promise(function(resolve) {
          console.log(url);
          download(url, dest, item.extractedTo, function(err) {
            if (err) {
              console.log('failed', dest, err);
              resolve(err);
            } else {
              console.log('done', dest);
              resolve();
            }
          });
        });
      })
    ).then(function(results) {
      results.forEach(function(error) {
        if (error) {
          grunt.fail.warn('Failed to fetch some release artifacts');
        }
      });
      done();
    });
  });

  function runTests(environment, cb) {
    var failure;
    var Application = require('spectron').Application;
    var electronBinary =
      process.platform === 'win32' ? 'electron.cmd' : 'electron';
    var app = new Application({
      path: path.join(__dirname, 'node_modules', '.bin', electronBinary),
      args: [path.join(__dirname, 'main.js')],
      env: {
        NODE_ENV: environment,
      },
    });

    function getMochaResults() {
      return window.mochaResults;
    }

    app
      .start()
      .then(function() {
        return app.client.waitUntil(
          function() {
            return app.client.execute(getMochaResults).then(function(data) {
              return Boolean(data.value);
            });
          },
          10000,
          'Expected to find window.mochaResults set!'
        );
      })
      .then(function() {
        return app.client.execute(getMochaResults);
      })
      .then(function(data) {
        var results = data.value;
        if (results.failures > 0) {
          console.error(results.reports);
          failure = function() {
            grunt.fail.fatal(
              'Found ' + results.failures + ' failing unit tests.'
            );
          };
          return app.client.log('browser');
        } else {
          grunt.log.ok(results.passes + ' tests passed.');
        }
      })
      .then(function(logs) {
        if (logs) {
          console.error();
          console.error('Because tests failed, printing browser logs:');
          console.error(logs);
        }
      })
      .catch(function(error) {
        failure = function() {
          grunt.fail.fatal(
            'Something went wrong: ' + error.message + ' ' + error.stack
          );
        };
      })
      .then(function() {
        // We need to use the failure variable and this early stop to clean up before
        // shutting down. Grunt's fail methods are the only way to set the return value,
        // but they shut the process down immediately!
        return app.stop();
      })
      .then(function() {
        if (failure) {
          failure();
        }
        cb();
      })
      .catch(function(error) {
        console.error('Second-level error:', error.message, error.stack);
        if (failure) {
          failure();
        }
        cb();
      });
  }

  grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() {
    var environment = grunt.option('env') || 'test';
    var done = this.async();

    runTests(environment, done);
  });

  grunt.registerTask(
    'lib-unit-tests',
    'Run libtextsecure unit tests w/Electron',
    function() {
      var environment = grunt.option('env') || 'test-lib';
      var done = this.async();

      runTests(environment, done);
    }
  );

  grunt.registerMultiTask('test-release', 'Test packaged releases', function() {
    var dir = grunt.option('dir') || 'dist';
    var environment = grunt.option('env') || 'production';
    var asar = require('asar');
    var config = this.data;
    var archive = [dir, config.archive].join('/');
    var files = [
      'config/default.json',
      'config/' + environment + '.json',
      'config/local-' + environment + '.json',
    ];

    console.log(this.target, archive);
    var releaseFiles = files.concat(config.files || []);
    releaseFiles.forEach(function(fileName) {
      console.log(fileName);
      try {
        asar.statFile(archive, fileName);
        return true;
      } catch (e) {
        console.log(e);
        throw new Error('Missing file ' + fileName);
      }
    });

    if (config.appUpdateYML) {
      var appUpdateYML = [dir, config.appUpdateYML].join('/');
      if (require('fs').existsSync(appUpdateYML)) {
        console.log('auto update ok');
      } else {
        throw new Error('Missing auto update config ' + appUpdateYML);
      }
    }

    var done = this.async();
    // A simple test to verify a visible window is opened with a title
    var Application = require('spectron').Application;
    var assert = require('assert');

    var app = new Application({
      path: [dir, config.exe].join('/'),
    });

    app
      .start()
      .then(function() {
        return app.client.getWindowCount();
      })
      .then(function(count) {
        assert.equal(count, 1);
        console.log('window opened');
      })
      .then(function() {
        // Get the window's title
        return app.client.getTitle();
      })
      .then(function(title) {
        // Verify the window's title
        assert.equal(title, packageJson.productName);
        console.log('title ok');
      })
      .then(function() {
        assert(
          app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1
        );
        console.log('environment ok');
      })
      .then(
        function() {
          // Successfully completed test
          return app.stop();
        },
        function(error) {
          // Test failed!
          return app.stop().then(function() {
            grunt.fail.fatal(
              'Test failed: ' + error.message + ' ' + error.stack
            );
          });
        }
      )
      .then(done);
  });

  grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']);
  grunt.registerTask('dev', ['default', 'watch']);
  grunt.registerTask('lint', ['jshint', 'jscs']);
  grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
  grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']);
  grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
  grunt.registerTask('prep-release', [
    'gitinfo',
    'clean-release',
    'fetch-release',
  ]);
  grunt.registerTask('default', [
    'concat',
    'copy:deps',
    'sass',
    'date',
    'exec:transpile',
  ]);
};
