'use strict'

const autoprefixer = require('autoprefixer')
const browserify = require('browserify')
const concat = require('gulp-concat')
const cssnano = require('cssnano')
const fs = require('fs-extra')
const imagemin = require('gulp-imagemin')
const merge = require('merge-stream')
const ospath = require('path')
const path = ospath.posix
const postcss = require('gulp-postcss')
const postcssCalc = require('postcss-calc')
const postcssImport = require('postcss-import')
const postcssUrl = require('postcss-url')
const postcssVar = require('postcss-custom-properties')
const { Transform } = require('stream')
const map = (transform) => new Transform({ objectMode: true, transform })
const tailwind = require('tailwindcss')
const through = () => map((file, enc, next) => next(null, file))
const uglify = require('gulp-uglify')
const vfs = require('vinyl-fs')
const axios = require('axios')
const log = require('fancy-log')

/** Bundles JavaScript files using Browserify.

    This function takes an object as a parameter with the base directory and
    the bundle extension. It returns a map function that processes each file.
    If the file ends with the bundle extension, it uses Browserify to bundle the file.
    It also updates the modification time of the file if any of its dependencies
    have a newer modification time.

    @param {Object} options - The options for bundling.
    @param {string} options.base - The base directory.
    @param {string} options.ext - The bundle extension.
    @returns {Function} A function that processes each file.
 */
function bundleJsFiles ({ base: basedir, ext: bundleExt = '.bundle.js' }) {
  // Return a map function that processes each file
  return map((file, enc, next) => {
    // If the file ends with the bundle extension, bundle it
    if (bundleExt && file.relative.endsWith(bundleExt)) {
      const mtimePromises = []
      const bundlePath = file.path

      // Use Browserify to bundle the file
      browserify(file.relative, { basedir, detectGlobals: false })
        .plugin('browser-pack-flat/plugin')
        .on('file', (bundledPath) => {
          // If the bundled file is not the original file, add its modification time to the promises
          if (bundledPath !== bundlePath) mtimePromises.push(fs.stat(bundledPath).then(({ mtime }) => mtime))
        })
        .bundle((bundleError, bundleBuffer) =>
          // When all modification times are available, update the file's modification time if necessary
          Promise.all(mtimePromises).then((mtimes) => {
            const newestMtime = mtimes.reduce((max, curr) => (curr > max ? curr : max), file.stat.mtime)
            if (newestMtime > file.stat.mtime) file.stat.mtimeMs = +(file.stat.mtime = newestMtime)
            if (bundleBuffer !== undefined) file.contents = bundleBuffer
            next(bundleError, Object.assign(file, { path: file.path.slice(0, file.path.length - 10) + '.js' }))
          })
        )
      return
    }
    // If the file does not end with the bundle extension, just read its contents
    fs.readFile(file.path, 'UTF-8').then((contents) => {
      next(null, Object.assign(file, { contents: Buffer.from(contents) }))
    })
  })
}

/** Fixes pseudo elements in CSS.

    This function walks through the CSS rules and replaces single colons with double colons
    for pseudo elements. It uses a regular expression to match the pseudo elements and then
    replaces the single colon with a double colon.

    @param {Object} css - The PostCSS CSS object.
    @param {Object} result - The result object.
 */
function postcssPseudoElementFixer (css, result) {
  // Walk through the CSS rules
  css.walkRules(/(?:^|[^:]):(?:before|after)/, (rule) => {
    // For each rule, replace single colon with double colon for pseudo elements
    rule.selector = rule.selectors.map((it) => it.replace(/(^|[^:]):(before|after)$/, '$1::$2')).join(',')
  })
}
/** Returns an array of PostCSS plugins.

    This function takes the destination directory and a boolean
    indicating whether to generate a preview as parameters.

    It returns an array of PostCSS plugins to be used for
    processing CSS files.

    @param {string} dest - The destination directory.
    @param {boolean} preview - Whether to generate a preview.
    @returns {Array} An array of PostCSS plugins.
 */
function getPostCssPlugins (dest, preview) {
  // Return an array of PostCSS plugins
  return [
    // Import CSS files
    postcssImport,
    // Use Tailwind CSS
    tailwind,
    // Custom plugin to update the modification time of the file
    (css, { messages, opts: { file } }) =>
      Promise.all(
        messages
          .reduce((accum, { file: depPath, type }) => (type === 'dependency' ? accum.concat(depPath) : accum), [])
          .map((importedPath) => fs.stat(importedPath).then(({ mtime }) => mtime))
      ).then((mtimes) => {
        const newestMtime = mtimes.reduce((max, curr) => (!max || curr > max ? curr : max), file.stat.mtime)
        if (newestMtime > file.stat.mtime) file.stat.mtimeMs = +(file.stat.mtime = newestMtime)
      }),
    // Resolve URLs in CSS files
    postcssUrl([
      {
        filter: new RegExp('^src/css/[~][^/]*(?:font|face)[^/]*/.*/files/.+[.](?:ttf|woff2?)$'),
        url: (asset) => {
          const relpath = asset.pathname.substr(1)
          const abspath = require.resolve(relpath)
          const basename = ospath.basename(abspath)
          const destpath = ospath.join(dest, 'font', basename)
          if (!fs.pathExistsSync(destpath)) fs.copySync(abspath, destpath)
          return path.join('..', 'font', basename)
        },
      },
    ]),
    // Process CSS custom properties
    postcssVar({ preserve: preview }),
    // Calculate CSS values (only in preview mode)
    preview ? postcssCalc : () => {}, // cssnano already applies postcssCalc
    // Add vendor prefixes to CSS rules
    autoprefixer,
    // Minify CSS (only in non-preview mode)
    preview
      ? () => {}
      : (css, result) => cssnano({ preset: 'default' })(css, result).then(() => postcssPseudoElementFixer(css, result)),
  ]
}

/** Returns an array of tasks for building the UI.

    This function takes several parameters including options for the vfs source,
    whether to generate source maps, PostCSS plugins to use, whether to generate a preview,
    and the source directory. It returns an array of tasks that are used to build the UI.

    @param {Object} opts - The options for the vfs source.
    @param {boolean} sourcemaps - Whether to generate source maps.
    @param {Array} postcssPlugins - The PostCSS plugins to use.
    @param {boolean} preview - Whether to generate a preview.
    @param {string} src - The source directory.
    @returns {Array} An array of tasks for building the UI.
 */
function getAllTasks (opts, sourcemaps, postcssPlugins, preview, src) {
  // Return an array of tasks
  return [
    // Task for getting the 'ui.yml' file
    vfs.src('ui.yml', { ...opts, allowEmpty: true }),
    // Task for bundling JavaScript files
    vfs
      .src('js/+([0-9])-*.js', { ...opts, read: false, sourcemaps })
      .pipe(bundleJsFiles(opts))
      .pipe(uglify({ output: { comments: /^! / } }))
      // NOTE concat already uses stat from newest combined file
      .pipe(concat('js/site.js')),
    // Task for bundling vendor JavaScript files
    vfs
      .src('js/vendor/*([^.])?(.bundle).js', { ...opts, read: false })
      .pipe(bundleJsFiles(opts))
      .pipe(uglify({ output: { comments: /^! / } })),
    // Task for getting vendor minified JavaScript files
    vfs
      .src('js/vendor/*.min.js', opts)
      .pipe(map((file, enc, next) => next(null, Object.assign(file, { extname: '' }, { extname: '.js' })))),
    // Task for processing CSS files
    // NOTE use the next line to bundle a JavaScript library that cannot be browserified, like jQuery
    //vfs.src(require.resolve('<package-name-or-require-path>'), opts).pipe(concat('js/vendor/<library-name>.js')),
    vfs.src('css/boostlook.css', opts).pipe(
      postcss([
        autoprefixer,
        preview
          ? () => {}
          : cssnano({
            preset: 'default',
          }),
      ])
    ),
    vfs
      .src(['css/site.css', 'css/vendor/*.css'], { ...opts, sourcemaps })
      .pipe(postcss((file) => ({ plugins: postcssPlugins, options: { file } }))),
    // Task for getting font files
    vfs.src('font/*.{ttf,woff*(2)}', opts),
    // Task for getting image files
    vfs.src('img/**/*.{gif,ico,jpg,png,svg}', opts).pipe(
      preview
        ? through()
        : imagemin(
          [
            imagemin.gifsicle(),
            imagemin.jpegtran(),
            // imagemin.optipng(),
            imagemin.svgo({
              plugins: [
                { cleanupIDs: { preservePrefixes: ['icon-', 'view-'] } },
                { removeViewBox: false },
                { removeDesc: false },
              ],
            }),
          ].reduce((accum, it) => (it ? accum.concat(it) : accum), [])
        )
    ),
    // Task for getting helper JavaScript files
    vfs.src('helpers/*.js', opts),
    // Task for getting layout handlebars files
    vfs.src('layouts/*.hbs', opts),
    // Task for getting partial handlebars files
    vfs.src('partials/*.hbs', opts),
    // Task for getting static files
    vfs.src('static/**/*[!~]', { ...opts, base: ospath.join(src, 'static'), dot: true }),
  ]
}

/** Fetches the boostlook.css file.

    This function uses axios to download the boostlook.css file.

    It then writes the file to the `../../src/css` directory.

    However, it first checks if the --skip-boostlook flag was set in the
    command line.
    If it was, it skips the download process.
    This can be used to work locally on the boostlook.css file
    without this process overwriting the file.

    It also checks if the file already exists and was updated in
    the last hour.
    If it was, it also skips the download process.
    This ensures contributors can work on the content with
    a recent version of the boostlook.css file while still
    avoiding unnecessary downloads.

    In CI, the file won't be available so it always
    uses the most recent version.
 */
async function fetchBoostlookCss () {
  log('Fetching boostlook.css file...')
  const skipBoostlook = process.argv.includes('--skip-boostlook')
  const cssDir = ospath.join(__dirname, '..', '..', 'src', 'css')
  const cssFilePath = ospath.join(cssDir, 'boostlook.css')
  const boostlookBranch = process.env.BOOSTLOOK_BRANCH || 'master'
  const url = `https://raw.githubusercontent.com/boostorg/boostlook/${boostlookBranch}/boostlook.css`

  if (skipBoostlook) {
    log('Skipping boostlook.css download due to --skip-boostlook flag.')
    return
  }

  try {
    const fileExists = await fs.pathExists(cssFilePath)
    if (fileExists) {
      const stats = await fs.stat(cssFilePath)
      const oneHourAgo = Date.now() - 3600000 // 3600000 milliseconds = 1 hour
      if (stats.mtimeMs > oneHourAgo) {
        log('boostlook.css file already exists and was updated in the last hour. Skipping download.')
        log(`boostlook.css file path: ${cssFilePath}`)
        return
      }
    }

    log('Downloading boostlook.css file...')
    const response = await axios.get(url)
    log('Writing boostlook.css file to ' + cssFilePath)
    await fs.outputFile(cssFilePath, response.data)
    log(`boostlook.css file written to ${cssFilePath} successfully.`)
  } catch (error) {
    console.error('Error fetching boostlook.css file:', error)
  }
}

/**
    Builds UI assets.

    This function takes the source directory, destination directory, and a boolean
    indicating whether to generate a preview as parameters.

    @param {string} src - The source directory.
    @param {string} dest - The destination directory.
    @param {boolean} preview - Whether to generate a preview.
    @returns {Function} The `UiAssetsBuilder` function.
 */
function buildUiAssets (src, dest, preview) {
  // Define an async function `UiAssetsBuilder`
  async function UiAssetsBuilder () {
    // Prepare the options
    const opts = { base: src, cwd: src }
    const sourcemaps = preview || process.env.SOURCEMAPS === 'true'

    // Fetch the boostlook.css file
    await fetchBoostlookCss()

    // Get the PostCSS plugins
    const postcssPlugins = getPostCssPlugins(dest, preview)

    // Get all tasks
    const tasks = getAllTasks(opts, sourcemaps, postcssPlugins, preview, src)

    // Merge all tasks and pipe them to the destination directory
    // Return a Promise that resolves when the stream ends and
    // rejects if an error occurs in the stream
    return new Promise((resolve, reject) => {
      merge(...tasks)
        .pipe(vfs.dest(dest, { sourcemaps: sourcemaps && '.' }))
        .on('end', resolve) // Resolve the Promise when the stream ends
        .on('error', reject) // Reject the Promise if an error occurs in the stream
    })
  }

  // Return the `UiAssetsBuilder` function
  return UiAssetsBuilder
}
module.exports = buildUiAssets
