'use strict';

// Gulp
const gulp = require('gulp');

// Gulp plugins
const babel = require('gulp-babel');
const closureCompilerPackage = require('google-closure-compiler');
const closureCompiler = closureCompilerPackage.gulp();
const crisper = require('gulp-crisper');
const gulpif = require('gulp-if');
const htmlmin = require('gulp-htmlmin');
const merge = require('merge-stream');
const postcss = require('gulp-html-postcss');
const rename = require('gulp-rename');
const sass = require('gulp-sass');
const through = require('through2');
const useref = require('gulp-useref');
const vulcanize = require('gulp-vulcanize');
const watch = require('gulp-watch');
const webserver = require('gulp-webserver');

// Uglify ES6
const uglifyes = require('uglify-es');
const composer = require('gulp-uglify/composer');
const uglify = composer(uglifyes, console);

// Other helpers
const args = require('yargs').argv;
const childprocess = require('child_process');
const claat = require('./tasks/helpers/claat');
const del = require('del');
const fs = require('fs-extra');
const gcs = require('./tasks/helpers/gcs');
const glob = require('glob');
const opts = require('./tasks/helpers/opts');
const path = require('path');
const serveStatic = require('serve-static');
const spawn = childprocess.spawn;
const swig = require('swig-templates');
const url = require('url');

// DEFAULT_GA is the default Google Analytics tracker ID
const DEFAULT_GA = 'UA-49880327-14';

// DEFAULT_VIEW_META_PATH is the default path to view metadata.
const DEFAULT_VIEW_META_PATH = 'app/views/default/view.json';

// DEFAULT_VIEW_TMPL_PATH is the default path to view template.
const DEFAULT_VIEW_TMPL_PATH = 'app/views/default/index.html';

// DEFAULT_CATEGORY is the default name for uncategorized codelabs.
const DEFAULT_CATEGORY = 'Default';

// BASE_URL is the canonical base URL where the site will reside. This should
// always include the protocol (http:// or https://) and NOT including a
// trailing slash.
const BASE_URL = args.baseUrl || 'https://scott3142.uk/python-programming';

// CODELABS_DIR is the directory where the actual codelabs exist on disk.
// Despite being a constant, this can be overridden with the --codelabs-dir
// flag.
const CODELABS_DIR = args.codelabsDir || 'codelabs';

// CODELABS_ENVIRONMENT is the environment for which to build codelabs.
const CODELABS_ENVIRONMENT = args.codelabsEnv || 'web';

// CODELABS_FILTER is an inclusion filter against codelab IDs.
const CODELABS_FILTER = args.codelabsFilter || '*';

// CODELABS_FORMAT is the output format for which to build codelabs.
const CODELABS_FORMAT = args.codelabsFormat || 'html';

// CODELABS_NAMESPACE is the content namespace.
const CODELABS_NAMESPACE = (args.codelabsNamespace || 'codelabs').replace(/^\/|\/$/g, '');

// DELETE_MISSING controls whether missing files at the destination are deleted.
// The default value is true.
const DELETE_MISSING = !!args.deleteMissing || false;

// DRY_RUN indicates if dry should be used.
const DRY_RUN = !!args.dry;

// PROD_BUCKET is the default bucket for prod.
const PROD_BUCKET = gcs.bucketName(args.prodBucket || 'DEFAULT_PROD_BUCKET');

// STAGING_BUCKET is the default bucket for staging.
const STAGING_BUCKET = gcs.bucketName(args.stagingBucket || 'DEFAULT_STAGING_BUCKET');

// VIEWS_FILTER is the filter to use for view inclusion.
const VIEWS_FILTER = args.viewsFilter || '*';

// clean:build removes the build directory
gulp.task('clean:build', (callback) => {
  return del('build')
});

// clean:dist removes the dist directory
gulp.task('clean:dist', (callback) => {
  return del('dist')
});

// clean:js removes the built javascript
// NOTE: this is not included in the default 'clean' task
gulp.task('clean:js', (callback) => {
  return del('app/js/bundle')
});

// clean removes all built files
gulp.task('clean', gulp.parallel(
  'clean:build',
  'clean:dist',
));

// build:codelabs copies the codelabs from the directory into build.
gulp.task('build:codelabs', (done) => {
  copyFilteredCodelabs('build');
  done();
});

// build:scss builds all the scss files into the dist dir
gulp.task('build:scss', () => {
  return gulp.src('app/**/*.scss')
    .pipe(sass(opts.sass()))
    .pipe(gulp.dest('build'));
});

// build:css builds all the css files into the dist dir
gulp.task('build:css', () => {
  const srcs = [
    'app/elements/codelab-elements/*.css',
  ];

  return gulp.src(srcs, { base: 'app/' })
    .pipe(gulp.dest('build'));
});

// build:html builds all the HTML files
gulp.task('build:html', () => {
  const streams = [];

  streams.push(gulp.src(`app/views/${VIEWS_FILTER}/view.json`, { base: 'app/' })
    .pipe(generateView())
    .pipe(useref({ searchPath: ['app'] }))
    .pipe(gulpif('*.js', babel(opts.babel())))
    .pipe(gulp.dest('build'))
    .pipe(gulpif(['*.html', '!index.html'], generateDirectoryIndex()))
  );

  streams.push(gulp.src(`app/views/${VIEWS_FILTER}/*.{css,gif,jpeg,jpg,png,svg,tff}`, { base: 'app/views' })
    .pipe(gulp.dest('build')));

  const otherSrcs = [
    'app/404.html',
    'app/browserconfig.xml',
    'app/robots.txt',
    'app/site.webmanifest',
  ]
  streams.push(gulp.src(otherSrcs, { base: 'app/' })
    .pipe(gulp.dest('build'))
  );

  return merge(...streams);
});

// build:images builds all the images into the build directory.
gulp.task('build:images', () => {
  const srcs = [
    'app/images/**/*',
    'app/favicon.ico',
  ];

  return gulp.src(srcs, { base: 'app/' })
    .pipe(gulp.dest('build'));
});

// build:js builds all the javascript into the dest dir
gulp.task('build:js', (callback) => {
  let streams = [];

  if (!fs.existsSync('app/js/bundle/cardsorter.js')) {
    // cardSorter is compiled into app/js, not build/scripts, because it is
    // vulcanized directly into the HTML.
    const cardSorterSrcs = [
      'app/js/claat/ui/cards/cardsorter.js',
      'app/js/claat/ui/cards/cardsorter_export.js',
    ];
    streams.push(gulp.src(cardSorterSrcs, { base: 'app/' })
      .pipe(closureCompiler(opts.closureCompiler(), { platform: 'javascript' }))
      .pipe(babel(opts.babel()))
      .pipe(gulp.dest('app/js/bundle'))
    );
  }

  const bowerSrcs = [
    'app/bower_components/webcomponentsjs/webcomponents-lite.min.js',
    // Needed for async loading - remove after polymer/polymer#2380
    'app/bower_components/google-codelab-elements/shared-style.html',
    'app/bower_components/google-prettify/src/prettify.js',
  ];
  streams.push(gulp.src(bowerSrcs, { base: 'app/' })
    .pipe(gulpif('*.js', babel(opts.babel())))
    .pipe(gulp.dest('build'))
  );

  return merge(...streams);
});

gulp.task('build:elements_js', () => {
  const srcs = [
    'app/elements/codelab-elements/*.js'
  ];

  return gulp.src(srcs, { base: 'app/' })
    .pipe(gulp.dest('build'));
})

// build:vulcanize vulcanizes html, js, and css
gulp.task('build:vulcanize', () => {
  const srcs = [
    'app/elements/codelab.html',
    'app/elements/elements.html',
  ];
  return gulp.src(srcs, { base: 'app/' })
    .pipe(vulcanize(opts.vulcanize()))
    .pipe(crisper(opts.crisper()))
    .pipe(gulp.dest('build'));
});

// build builds all the assets
gulp.task('build', gulp.series(
  'clean',
  'build:css',
  'build:codelabs',
  'build:scss',
  'build:html',
  'build:images',
  'build:js',
  'build:elements_js',
  'build:vulcanize',
));

// copy copies the built artifacts in build into dist/
gulp.task('copy', (callback) => {
  // Explicitly do not use gulp here. It's too slow and messes up the symlinks
  fs.rename('build', 'dist', callback);
});

// minify:css minifies the css
gulp.task('minify:css', () => {
  const srcs = [
    'dist/**/*.css',
    '!dist/codelabs/**/*',
    '!dist/elements/codelab-elements/*.css',
  ]
  return gulp.src(srcs, { base: 'dist/' })
    .pipe(postcss(opts.postcss()))
    .pipe(gulp.dest('dist'));
});

// minify:css minifies the html
gulp.task('minify:html', () => {
  const srcs = [
    'dist/**/*.html',
    '!dist/codelabs/**/*',
  ]
  return gulp.src(srcs, { base: 'dist/' })
    .pipe(postcss(opts.postcss()))
    .pipe(htmlmin(opts.htmlmin()))
    .pipe(gulp.dest('dist'));
});

// minify:js minifies the javascript
gulp.task('minify:js', () => {
  const srcs = [
    'dist/**/*.js',
    '!dist/codelabs/**/*',
    '!dist/elements/codelab-elements/*.js',
  ]
  return gulp.src(srcs, { base: 'dist/' })
    .pipe(uglify(opts.uglify()))
    .pipe(gulp.dest('dist'));
});

// minify minifies all minifiable things in dist
gulp.task('minify', gulp.parallel(
  'minify:css',
  'minify:html',
  'minify:js',
));

// dist packages the build for distribution, compressing and condensing where
// appropriate.
gulp.task('dist', gulp.series(
  'build',
  'copy',
  'minify',
));

// watch:css watches css files for changes and re-builds them
gulp.task('watch:css', () => {
  gulp.watch('app/**/*.scss', gulp.series('build:css'));
});

// watch:html watches html files for changes and re-builds them
gulp.task('watch:html', () => {
  const srcs = [
    'app/views/**/*',
    'app/*.html',
    'app/*.txt',
    'app/*.xml',
  ]
  gulp.watch(srcs, gulp.series('build:html'));
});

// watch:css watches image files for changes and updates them
gulp.task('watch:images', () => {
  gulp.watch('app/images/**/*', gulp.series('build:images'));
});

// watch:images watches js files for changes and re-builds them
gulp.task('watch:js', () => {
  const srcs = [
    'app/js/**/*',
    '!app/js/bundle/**/*',
    'app/scripts/**/*',
  ]
  gulp.watch(srcs, gulp.series('build:js', 'build:html'));
});

// watch starts all watchers
gulp.task('watch', gulp.parallel(
  'watch:css',
  'watch:html',
  'watch:images',
  'watch:js',
));

// serve builds the website, starts the webserver, and watches for changes.
gulp.task('serve', gulp.series(
  'build',
  gulp.parallel(
    'watch',
    () => {
      return gulp.src('build')
        .pipe(webserver(opts.webserver()));
    }
  )
));

// serve:dist serves the built and minified website from dist. It does not
// support live-reloading and should be used to verify final output before
// publishing.
gulp.task('serve:dist', gulp.series('dist', () => {
  return gulp.src('dist')
    .pipe(webserver(opts.webserver()));
}));

//
// Codelabs
//
// codelabs:export exports the codelabs
gulp.task('codelabs:export', (callback) => {
  const source = args.source;

  if (source !== undefined) {
    const sources = Array.isArray(source) ? source : [source];
    claat.run(CODELABS_DIR, 'export', CODELABS_ENVIRONMENT, CODELABS_FORMAT, DEFAULT_GA, sources, callback);
  } else {
    const codelabIds = collectCodelabs().map((c) => { return c.id });
    claat.run(CODELABS_DIR, 'update', CODELABS_ENVIRONMENT, CODELABS_FORMAT, DEFAULT_GA, codelabIds, callback);
  }
});



//
// Helpers
//

// gulpdebug is a helper for debugging gulp pipeline transforms. It logs the
// file path and then invokes the callback.
const gulpdebug = () => {
  return through.obj((file, enc, callback) => {
    console.log(file.path);
    callback(null, file)
  });
}

// run executes the given command with the specified arguments.
const run = (cmd, args, callback) => {
  const proc = spawn(cmd, args, { stdio: 'inherit' });
  proc.on('close', callback);
}

// parseViewMetadata parses the metadata of a single view and returns that metadata
// as JSON.
const parseViewMetadata = (filepath) => {
  if (path.basename(filepath) !== 'view.json') {
    throw new Error(`can only be called on view.json, got: ${filepath}`);
  }

  const dirname = path.dirname(filepath);
  const meta = JSON.parse(fs.readFileSync(filepath));

  meta.id = path.basename(dirname);
  meta.url = viewFilename(meta.id).replace(/\.html$/, '');

  if (meta.sort === undefined) {
    meta.sort = meta.id === 'default' ? 'mainCategory' : 'title';
  }

  if (fs.existsSync(path.resolve(dirname, 'style.css'))) {
    meta.customStyle = `/${meta.id}/style.css`;
  }

  const defMeta = defaultViewMetadata();
  for (let key in defMeta) {
    if (meta[key] === undefined) {
      meta[key] = defMeta[key];
    }
  }

  return meta;
}

// parseCodelabMetadata parses the codelab metadata at the given path.
const parseCodelabMetadata = (filepath) => {
  var meta = JSON.parse(fs.readFileSync(filepath));

  meta.category = meta.category || [];
  if (!Array.isArray(meta.category)) {
    meta.category = [meta.category];
  }

  meta.mainCategory = meta.category[0] || DEFAULT_CATEGORY;
  meta.categoryClass = categoryClass(meta);
  meta.url = path.join(CODELABS_NAMESPACE, meta.id, 'index.html');

  return meta;
}

// defaultViewMetadata returns the default view metadata. This is cached for
// performance.
let _defaultViewMetadata;
const defaultViewMetadata = () => {
  if (!_defaultViewMetadata) {
    _defaultViewMetadata = JSON.parse(fs.readFileSync(DEFAULT_VIEW_META_PATH));
    _defaultViewMetadata.id = 'default';
    _defaultViewMetadata.url = viewFilename('default');
    _defaultViewMetadata.catLevel = 0;
    delete _defaultViewMetadata.ga;
  }
  return _defaultViewMetadata;
}


// Parse view.json and codelab.json files in all directories. Value returned in
// the callback is an object:
//
//     {
//       views: Object.<String, ViewObj>,
//       codelabs: Array.<CodelabObj>,
//       categories: Array.<String>
//     }
//
// Both codelabs and categories are sorted alphabetically. Codelabs are sorted
// by main category.
let _allMetadata;
const collectMetadata = () => {
  if (_allMetadata === undefined) {
    let codelabs = [];
    let categories = {};
    let views = {};

    const viewFiles = glob.sync('app/views/*/view.json');
    for (let i = 0; i < viewFiles.length; i++) {
      const view = parseViewMetadata(viewFiles[i]);
      views[view.id] = view;
    }

    const codelabFiles = glob.sync(`${CODELABS_DIR}/*/codelab.json`);
    for (let i = 0; i < codelabFiles.length; i++) {
      const codelab = parseCodelabMetadata(codelabFiles[i]);
      codelabs.push(codelab);
      categories[codelab.mainCategory] = true;
    }

    _allMetadata = {
      categories: Object.keys(categories).sort(),
      codelabs: codelabs,
      views: views,
    }
  }

  return _allMetadata;
}

// generateDirectoryIndex accepts a stream of HTML files and converts them to
// directory indexes for use on a webserver. For example:
//
//    foo.html => foo/index.html
//
// Then the server can be configured to serve indexes and /foo will render
// properly.
const generateDirectoryIndex = () => {
  return through.obj((file, enc, callback) => {
    const srcPath = file.path;
    if (srcPath.match(/\.html$/) && fs.existsSync(srcPath)) {
      const dstPath = srcPath.replace(/\.html$/, '') + '/index.html';
      const dstPathDir = path.dirname(dstPath);
      const srcPathRel = path.relative(dstPathDir, srcPath);

      // Ensure directory exists
      fs.mkdirpSync(dstPathDir);

      // Change into the directory and create a relative symlink
      chdir(dstPathDir, () => {
        fs.ensureSymlinkSync(srcPathRel, 'index.html');
      });
    }
    callback(null, file);
  });
}

const chdir = (dir, callback) => {
  const cwd = process.cwd();
  process.chdir(dir);
  callback()
  process.chdir(cwd);
}

// generateView creates an index for the given view and codelabs data.
const generateView = () => {
  return through.obj((file, enc, callback) => {
    // viewId is the basename of the dirname of the event (e.g.
    // app/views/my-event/view.json -> my-event).
    const viewId = path.basename(path.dirname(file.path));

    // Read the HTML template for this view (or the default template if no
    // view-specific one exists).
    let templatePath = `app/views/${viewId}/index.html`;
    if (!fs.existsSync(templatePath)) {
      templatePath = DEFAULT_VIEW_TMPL_PATH;
    }
    const template = fs.readFileSync(templatePath);

    // Get the metadata about this view.
    const view = parseViewMetadata(file.path);

    // Aanalytics information.
    const ga = DEFAULT_GA;

    // Full list of views
    const all = collectMetadata();

    // Calculate URL parameters to append.
    let codelabUrlParams = 'index=' + encodeURIComponent('../..' + view.url);
    if (view.ga || args.indexGa) {
      let viewGa = args.indexGa ? args.indexGa : view.ga;
      codelabUrlParams += '&viewga=' + viewGa;
    }

    // Get the list of codelabs and categories for this view
    const filtered = filterCodelabs(view, all.codelabs);
    const codelabs = filtered.codelabs;
    const categories = filtered.categories;

    let locals = {
      baseUrl: BASE_URL,
      categories: categories,
      codelabs: codelabs,
      ga: ga,
      showcats: categories.length > 1,
      view: view,
      views: all.views,

      canonicalViewUrl: viewFuncs.canonicalViewUrl(),
      categoryClass: viewFuncs.categoryClass(),
      categoryHasShowableCodelabs: viewFuncs.categoryHasShowableCodelabs(viewId),
      codePrettyDate: viewFuncs.codelabPrettyDate(),
      codelabPin: viewFuncs.codelabPin(),
      codelabUrl: viewFuncs.codelabUrl(codelabUrlParams),
      levelledCategory: viewFuncs.levelledCategory(),
    };

    const html = swig.render(template.toString(), { locals: locals });
    file.contents = new Buffer.from(html);
    file.base = __dirname;
    file.path = viewFilename(viewId);

    callback(null, file);
  });
}

// viewFuncs is the list of functions shared by views (usually for rendering).
// This is a constant for performance instead of defining the functions inline.
const viewFuncs = {
  // categoryClass returns the top-level categoryClass function.
  categoryClass: () => {
    return categoryClass;
  },

  // canonicalViewUrl returns the canonical URL to the given view.
  canonicalViewUrl: () => {
    return (view) => {
      if (view.id === 'default' || view.id === '') {
        return `${BASE_URL}/`;
      }
      return `${BASE_URL}/${view.id}/`;
    };
  },

  // categoryHasShowableCodelabs returns true if the given category is present
  // in any of the given codelabs, or false otherwise.
  categoryHasShowableCodelabs: (viewId) => {
    return (category, codelabs) => {
      const codelabsUsingCategory = codelabs
        .filter((codelab) => {
          return codelab.category.indexOf(category) !== -1;
        })
        .filter((codelab) => {
          // Rilter hidden codelabs from the default view. All other views are
          // explictly opt-in via metadata.
          return viewId !== 'default' || codelab.status.indexOf('hidden') === -1;
        });

      return codelabsUsingCategory.length > 0;
    }
  },

  // codelabPin returns the index of the pinned item, if it exists.
  codelabPin: () => {
    return (view, codelab) => {
      const i = view.pins.indexOf(codelab.id);
      if (i >= 0) {
        return i + 1;
      }
      return '';
    }
  },

  // codelabUrl returns the url for this codelab, combined with any additional
  // parameters if given.
  codelabUrl: (params) => {
    return (view, codelab) => {
      let url = codelab.url;
      if (params !== undefined) {
        url = `${url}?${params}`;
      }
      if (url.length > 0 && url[0] !== '/') {
        url = `/${url}`;
      }
      return url;
    }
  },

  // codelabPrettyDate returns a pretty-formatted date for the codelab view
  // page.
  codelabPrettyDate: () => {
    return (ts) => {
      const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
      const d = new Date(ts);
      const month = monthNames[d.getMonth()];
      const date = d.getUTCDate();
      const year = d.getFullYear();

      return `${month} ${date}, ${year}`;
    }
  },

  // levelledCategory returns the levelledCategory top-level function.
  levelledCategory: () => {
    return levelledCategory;
  },
};

// viewFilename returns the view index file name based on provided view.
const viewFilename = (view) => {
  if (view === 'default') {
    return 'index.html';
  }
  return `${view}.html`;
}

// levelledCategory returns the codelab category level and name. If no category
// exists at that level, the nearest is returned. If the codelab has no
// categories, the default category and level 0 are returned.
const levelledCategory = (codelab, level) => {
  let i;
  for (i = level; i >= 0; i--) {
    var name = codelab.category[i];
    if (name) {
      break;
    }
  }
  if (!name) {
    name = DEFAULT_CATEGORY;
  }
  return { name: name, level: i };
}

// categoryClass converts the codelab to its corresponding CSS class.
const categoryClass = (codelab, level) => {
  var name = codelab.mainCategory;
  if (level > 0) {
    name += ' ' + codelab.category[level];
  }
  return name.toLowerCase().replace(/\s/g, '-');
}

// Filters out codelabs which do not match view spec. Currently, the matching is
// done by intersecting view.tags with each codelab.tags, and view.categories
// with codelab.category. All codelabs are matched if the view.tags and
// view.categories are empty.
//
// Additionally, each codelab is tested against view.exclude exclusion filters,
// matching codelab ID, status, tags and categories.
const filterCodelabs = (view, codelabs) => {
  var vtags = cleanStringList(view.tags);
  var vcats = cleanStringList(view.categories);

  // Filter out codelabs with tags that don't intersect with view.tags
  // unless view.tags is empty - equivalent to any tag.
  // Same for categories.
  codelabs = codelabs.filter(function(codelab) {
    // Matches by default if both tags and cats are empty.
    var match = !vtags.length && !vcats.length;
    var ctags = cleanStringList(codelab.tags);
    var ccats = cleanStringList(codelab.category);
    // Match by tags.
    if (vtags.length) {
      for (var i = 0; i < ctags.length; i++) {
        if (vtags.indexOf(ctags[i]) > -1) {
          match = true;
          break;
        }
      }
    }
    // Match by category.
    if (!match && vcats.length) {
      for (var i = 0; i < ccats.length; i++) {
        if (vcats.indexOf(ccats[i]) > -1) {
          match = true;
          break;
        }
      }
    }
    if (!match || !view.exclude.length) {
      return match;
    }
    // Tag or category matches. Test the exclusion filter.
    var cstatus = cleanStringList(codelab.status);
    for (var i = 0; i < view.exclude.length; i++) {
      var rex = view.exclude[i];
      if (codelab.id.match(rex)) {
        return false;
      }
      for (var j = 0; j < cstatus.length; j++) {
        if (cstatus[j].match(rex)) {
          return false;
        }
      }
      for (var j = 0; j < ctags.length; j++) {
        if (ctags[j].match(rex)) {
          return false;
        }
      }
      for (var j = 0; j < ccats.length; j++) {
        if (ccats[j].match(rex)) {
          return false;
        }
      }
    }
    return true;
  });

  // Compute distinct categories.
  var categories = {};
  for (var i in codelabs) {
    var cat = levelledCategory(codelabs[i], view.catLevel);
    categories[cat.name] = true;
  }

  // sort the codelabs.
  sortCodelabs(codelabs, view);

  return {
    codelabs: codelabs,
    categories: Object.keys(categories).sort(),
  };
}


// cleanStringList removes empty elements and converts the rest to lower case.
// The a argument is an array of strings or null/undefined. Alternatively, the
// argument can be a string of JSON-serialized array form.
const cleanStringList = (a) => {
  if (typeof a === 'string') {
    // Legacy codelab.json have array field as a string, for instance:
    // "status": "[u'ready for review']".
    a = JSON.parse(a.replace(/u'/, "'").replace(/'/g, '"'));
  }
  a = (a || []).filter((v) => { return (v || '').length > 0; });
  return a.map((v) => { return v.toLowerCase(); });
}

// sortCodelabs reorders codelabs array in-place for the given view. Pinned
// codelabs always come first, sorted by their pin index.
const sortCodelabs = (codelabs, view) => {
  let attr = function(codelab) {
    return levelledCategory(codelab, view.catLevel).name;
  }

  if (view.sort !== "mainCategory") {
    attr = function(codelab) { return codelab[view.sort] };
  }

  codelabs.sort(function(a, b) {
    // Move pinned codelabs to the top.
    var ia = view.pins.indexOf(a.id);
    var ib = view.pins.indexOf(b.id);
    if (ia >= 0 && ib < 0) {
      return -1;
    }
    if (ia < 0 && ib >= 0) {
      return 1;
    }
    if (ia >= 0 && ib >= 0) {
      return ia - ib;
    }
    // Regular sorting.
    var aa = attr(a);
    var ba = attr(b);
    if (aa < ba) { return -1; }
    if (aa > ba) { return 1; }
    return 0;
  });
}

// copyFilteredCodelabs copies all codelabs that match the given id regular
// expression or view regular expression into the build/ folder. If no filters
// are specified (i.e. if codelabRe and viewRe are both undefined), then this
// function returns all codelabs in the codelabs directory.
var ncp = require('ncp').ncp;
const copyFilteredCodelabs = (dest) =>  {
  // No filters were specified, symlink the codelabs folder directly and save
  // processing.
  if (CODELABS_FILTER === '*' && VIEWS_FILTER === '*') {
    const source = path.join(CODELABS_DIR);
    const target = path.join(dest, CODELABS_NAMESPACE);
    //fs.ensureSymlinkSync(source, target, 'dir');
    ncp(source, target, function (err) {
      if (err) {
        return console.error(err);
      }
    });
    return
  }

  const codelabs = collectCodelabs();

  for(let i = 0; i < codelabs.length; i++) {
    const codelab = codelabs[i];
    const source = path.join(CODELABS_DIR, codelab.id);
    const target = path.join(dest, CODELABS_NAMESPACE, codelab.id);
    //fs.ensureSymlinkSync(source, target, 'dir');
    ncp(source, target, function (err) {
      if (err) {
        return console.error(err);
      }
    });
  }
};

// collectCodelabs collects the list of codelabs that match the given view or
// codelab filter.
const collectCodelabs = () => {
  const meta = collectMetadata();
  let codelabs = meta.codelabs;

  // Only select codelabs that match the given codelab ID.
  if (CODELABS_FILTER !== '*') {
    codelabs = meta.codelabs.filter((codelab) => {
      return codelab.id.match(CODELABS_FILTER);
    });

    if (codelabs.length === 0) {
      throw new Error(`no codelabs matched: ${CODELABS_FILTER}`);
    }
  }

  // Only select codelabs that match the given view ID.
  if (VIEWS_FILTER !== '*') {
    let views = [];
    Object.keys(meta.views).forEach((key) => {
      if (key.match(VIEWS_FILTER)) {
        views.push(meta.views[key]);
      }
    });

    if (views.length === 0) {
      throw new Error(`no views matched: ${VIEWS_FILTER}`);
    }

    // Iterate over each view and include codelabs for that view.
    let s = new Set();
    for(let i = 0; i < views.length; i++) {
      let filtered = filterCodelabs(views[i], codelabs).codelabs;
      s.add(...filtered);
    }
    codelabs = s.values();
  }

  // Check if we have any codelabs
  if (codelabs.length === 0) {
    throw new Error('no codelabs matched given filters');
  }

  return codelabs;
}

// publish:staging:codelabs uploads the dist folder codelabs to a staging
// bucket. This only uploads the codelabs, the views remain unchanged.
gulp.task('publish:staging:codelabs', (callback) => {
  const opts = { dry: DRY_RUN, deleteMissing: DELETE_MISSING };
  const src = path.join('dist', CODELABS_NAMESPACE, '/');
  const dest = gcs.bucketFolderPath(STAGING_BUCKET, CODELABS_NAMESPACE);
  gcs.rsync(src, dest, opts, callback);
});

// publish:staging:views uploads the dist folder views and associated assets to
// a staging bucket. This does not upload any of the codelabs.
gulp.task('publish:staging:views', (callback) => {
  const opts = { exclude: CODELABS_NAMESPACE, dry: DRY_RUN, deleteMissing: DELETE_MISSING };
  gcs.rsync('dist', STAGING_BUCKET, opts, callback);
});

// publish:prod:codelabs syncs codelabs from the staging to the production
// bucket.
gulp.task('publish:prod:codelabs', (callback) => {
  const opts = { dry: DRY_RUN, deleteMissing: DELETE_MISSING };
  const src = gcs.bucketFolderPath(STAGING_BUCKET, CODELABS_NAMESPACE);
  const dest = gcs.bucketFolderPath(PROD_BUCKET, CODELABS_NAMESPACE);
  gcs.rsync(src, dest, opts, callback);
});

// publish:prod:views syncs views and associated assets from the staging to the
// production bucket.
gulp.task('publish:prod:views', (callback) => {
  const opts = { exclude: CODELABS_NAMESPACE, dry: DRY_RUN, deleteMissing: DELETE_MISSING };
  gcs.rsync(STAGING_BUCKET, PROD_BUCKET, opts, callback);
});


/*
Deploy site to Github pages
*/

// Deploys production website to gh-pages branch
var deploy = require('gulp-gh-pages-gift');
gulp.task('deploy:stage', gulp.series(
  'build',
  function () {
    return gulp.src("./build/**/*")
     .pipe(deploy({
        remoteUrl: "git@github.com:Scott3142/python-programming.git",
        branch: "gh-pages"
      }))
  }
));

gulp.task('deploy:prod', gulp.series(
  'dist',
  function () {
    return gulp.src("./dist/**/*")
     .pipe(deploy({
        remoteUrl: "git@github.com:Scott3142/python-programming.git",
        branch: "gh-pages"
      }))
  }
));
