import { AddLoadersMethod, PathWithLoaders, RequireData, RequireDataBase } from '../typings/definitions'
import * as path from 'path'
import * as loaderUtils from 'loader-utils'
import * as SourceMap from 'source-map'
import { getFilesInDir, concatPromiseResults, cacheInvalidationDebounce } from './index'
import ModuleDependency = require('webpack/lib/dependencies/ModuleDependency')
import escapeStringForRegex = require('escape-string-regexp')
import {memoize, uniqBy} from 'lodash'
import * as debug from 'debug'
const log = debug('utils')

export function appendCodeAndCallback(loader: Webpack.Core.LoaderContext, source: string, inject: string, sourceMap?: SourceMap.RawSourceMap, synchronousIfPossible = false) {
  inject += (!source.trim().endsWith(';')) ? ';\n' : '\n'

  // support existing SourceMap
  // https://github.com/mozilla/source-map#sourcenode
  // https://github.com/webpack/imports-loader/blob/master/index.js#L34-L44
  // https://webpack.github.io/docs/loaders.html#writing-a-loader
  if (sourceMap) {
    const currentRequest = loaderUtils.getCurrentRequest(loader)
    const SourceNode = SourceMap.SourceNode
    const SourceMapConsumer = SourceMap.SourceMapConsumer
    const sourceMapConsumer = new SourceMapConsumer(sourceMap)
    const node = SourceNode.fromStringWithSourceMap(source, sourceMapConsumer)

    node.add(inject)

    const result = node.toStringWithSourceMap({
      file: currentRequest
    })

    loader.callback(null, result.code, result.map.toJSON())
  } else {
    if (synchronousIfPossible) {
      return inject ? source + inject : source
    } else {
      loader.callback(null, source + inject)
    }
  }
}

export async function splitRequest(literal: string, loaderInstance?: Webpack.Core.LoaderContext) {
  // log(`Split Request: ${literal}`)
  let pathBits = literal.split(`/`)
  let remainingRequestBits = pathBits.slice()
  const literalIsRelative = literal[0] === '.'
  if (!literalIsRelative) {
    const fullPathNdIdx = pathBits.lastIndexOf('node_modules')
    if (fullPathNdIdx >= 0) {
      // conform full hard disk path /.../node_modules/MODULE_NAME/... to just MODULE_NAME/...
      pathBits = pathBits.slice(fullPathNdIdx + 1)
    }
    const moduleNameLength = pathBits[0].startsWith(`@`) ? 2 : 1
    const moduleName = pathBits.slice(0, moduleNameLength).join(`/`)
    // remainingRequest may be globbed:
    let ifModuleRemainingRequestBits = pathBits.slice(moduleNameLength)
    const remainingRequest = ifModuleRemainingRequestBits.join(`/`)
    let moduleRoot = ''
    let tryModule: {
      resolve: EnhancedResolve.ResolveResult | undefined;
    } = { resolve: undefined }
    if (loaderInstance && !moduleName.includes(`*`)) {
      // TODO: test this
      tryModule = await resolveLiteral({ literal: `${moduleName}` }, loaderInstance, undefined, false)
      if (tryModule.resolve && tryModule.resolve.descriptionFileRoot) {
        moduleRoot = tryModule.resolve.descriptionFileRoot
      }
      log(`does module '${moduleName}' exist?: ${tryModule.resolve && 'true' || 'false'}`)
    }
    if (!loaderInstance || tryModule.resolve) {
      return {
        moduleName, moduleRoot, remainingRequest, pathBits, remainingRequestBits: ifModuleRemainingRequestBits
      }
    }
  }
  return { remainingRequest: literal, remainingRequestBits, pathBits, moduleName: '', moduleRoot: '' }
}

export async function expandGlobBase(literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath)) {
  const { pathBits, remainingRequest, remainingRequestBits, moduleName, moduleRoot } = await splitRequest(literal, loaderInstance)
  let possibleRoots = loaderInstance.options.resolve.modules.filter((m: string) => path.isAbsolute(m)) as Array<string>

  const nextGlobAtIndex = remainingRequestBits.findIndex(pb => pb.includes(`*`))
  const relativePathUntilFirstGlob = remainingRequestBits.slice(0, nextGlobAtIndex).join(`/`)
  const relativePathFromFirstGlob = remainingRequestBits.slice(nextGlobAtIndex).join(`/`)

  if (moduleName && moduleRoot) {
    // TODO: add support for aliases when they point to a subdirectory
    // Or maybe the resolve will already include it?
    possibleRoots = [moduleRoot]
  } else if (rootForRelativeResolving) {
    possibleRoots = [rootForRelativeResolving, ...possibleRoots]
  }

  let possiblePaths = await concatPromiseResults(
    possibleRoots.map(async directory => await getFilesInDir(path.join(directory, relativePathUntilFirstGlob), {
      recursive: true, emitWarning: loaderInstance.emitWarning, emitError: loaderInstance.emitError,
      fileSystem: loaderInstance.fs, skipHidden: true
    }))
  )

  possiblePaths = uniqBy(possiblePaths, 'filePath')

  // test case: escape('werwer/**/werwer/*.html').replace(/\//g, '[\\/]+').replace(/\\\*\\\*/g, '\.*?').replace(/\\\*/g, '[^/\\\\]*?')
  const globRegexString = escapeStringForRegex(relativePathFromFirstGlob)
    .replace(/\//g, '[\\/]+') // accept Windows and Unix slashes
    .replace(/\\\*\\\*/g, '\.*?') // multi glob ** => any number of subdirectories
    .replace(/\\\*/g, '[^/\\\\]*?') // single glob * => one directory (stops at first slash/backslash)
  const globRegex = new RegExp(`^${globRegexString}$`) // (?:\.\w+)
  const correctPaths = possiblePaths.filter(p => p.stat.isFile() && globRegex.test(p.relativePath))

  return correctPaths.map(p => p.filePath)
}

const expandGlob = memoize(expandGlobBase, (literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving = path.dirname(loaderInstance.resourcePath)) => {
  /** valid for 10 seconds for the same literal and resoucePath */
  const cacheKey = `${literal}::${path.dirname(loaderInstance.resourcePath)}::${rootForRelativeResolving}`
  // invalidate every 10 seconds based on each unique Webpack compilation
  cacheInvalidationDebounce(cacheKey, expandGlob.cache, loaderInstance._compilation)
  return cacheKey
})

function fixWindowsPath(windowsPath: string) {
  return windowsPath.replace(/\\/g, '/')
}

export async function expandAllRequiresForGlob<T extends { literal: string }>(requires: Array<T>, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath), returnRelativeLiteral = false) {
  const needDeglobbing = requires.filter(r => r.literal.includes(`*`))
  const deglobbed = requires.filter(r => !r.literal.includes(`*`))
  const allDeglobbed = deglobbed.concat(await concatPromiseResults(needDeglobbing.map(async r =>
    (await expandGlob(r.literal, loaderInstance, rootForRelativeResolving))
      .map(correctPath => Object.assign({}, r, {
        literal: returnRelativeLiteral ?
          `./${fixWindowsPath(
            path.relative(path.dirname(loaderInstance.resourcePath), correctPath)
          )}` : correctPath
      }))
  )))
  return uniqBy(allDeglobbed, 'literal')
}

// TODO: function cleanUpPath
// this func does: makes a relative path from absolute
// OR strips all node_modules and makes a 'module' request path instead
// USE IT in the above glob expansion or better yet, in the below getRequireString, so we have nice requests instead of full paths!

export async function getRequireStrings(maybeResolvedRequires: Array<RequireData | { literal: string, resolve?: undefined }>, addLoadersMethod: AddLoadersMethod | undefined, loaderInstance: Webpack.Core.LoaderContext, forceFallbackLoaders = false): Promise<Array<string>> {
  const requires = (await Promise.all(maybeResolvedRequires.map(
    async r => !r.resolve ? await resolveLiteral(r, loaderInstance) : r
  )) as Array<RequireData>).filter(r => !!r.resolve)

  type PathsAndLoadersWithLiterals = PathWithLoaders & {removed?: boolean, literal: string}
  let pathsAndLoaders: Array<PathsAndLoadersWithLiterals>

  if (typeof addLoadersMethod === 'function') {
    const maybePromise = addLoadersMethod(requires, loaderInstance)
    const pathsAndLoadersReturnValue = (maybePromise as Promise<Array<PathWithLoaders>>).then ? await maybePromise : maybePromise as Array<PathWithLoaders>
    pathsAndLoaders = pathsAndLoadersReturnValue.map(p => {
      const rq = requires.find(r => r.resolve.path === p.path)
      if (!rq) return Object.assign(p, {removed: true, literal: undefined})
      return Object.assign(p, { loaders: (p.loaders && !forceFallbackLoaders) ? p.loaders : (rq.loaders || rq.fallbackLoaders || []), literal: rq.literal, removed: false })
    }).filter(r => !r.removed) as Array<PathsAndLoadersWithLiterals>
  } else {
    pathsAndLoaders = requires.map(r => ({ literal: r.literal, loaders: r.loaders || r.fallbackLoaders || [], path: r.resolve.path }))
  }

  return pathsAndLoaders.map(p =>
    (p.loaders && p.loaders.length) ?
      `!${p.loaders.join('!')}!${p.literal}` :
      p.literal
  )
}

export function wrapInRequireInclude(toRequire: string) {
  return `require.include('${toRequire}');`
}

// TODO: memoize:
export function resolveLiteral<T extends { literal: string }>(toRequire: T, loaderInstance: Webpack.Core.LoaderContext, contextPath = path.dirname(loaderInstance.resourcePath) /* TODO: could this simply be loaderInstance.context ? */, sendWarning = true) {
  debug('resolve')(`Resolving: ${toRequire.literal}`)
  return new Promise<{resolve: EnhancedResolve.ResolveResult | undefined} & T>((resolve, reject) =>
    loaderInstance.resolve(contextPath, toRequire.literal,
      (err, result, value) => err ? resolve(Object.assign({resolve: value}, toRequire)) || (sendWarning && loaderInstance.emitWarning(err.message)) :
      resolve(Object.assign({resolve: value}, toRequire))
    )
  )
}

export function addBundleLoader<T extends RequireDataBase>(resources: Array<T>, property = 'fallbackLoaders') {
  return resources.map(toRequire => {
    const lazy = toRequire.lazy && 'lazy' || ''
    const chunkName = (toRequire.chunk && `name=${toRequire.chunk}`) || ''
    const and = lazy && chunkName && '&' || ''
    const bundleLoaderPrefix = (lazy || chunkName) ? 'bundle?' : ''
    const bundleLoaderQuery = `${bundleLoaderPrefix}${lazy}${and}${chunkName}`

    return bundleLoaderQuery ? Object.assign({ [property]: [bundleLoaderQuery] }, toRequire) : toRequire
  }) as Array<T & { loaders?: Array<string>, fallbackLoaders?: Array<string> }>
}

// TODO: use custom ModuleDependency instead of injecting code
export class SimpleDependencyClass extends ModuleDependency {
  module: Webpack.Core.NormalModule
  type = 'simple-dependency'
  constructor(request: string) {
    super(request)
    debugger
  }
}

export class SimpleDependencyTemplate {
  apply(parentDependency: SimpleDependencyClass, source: Webpack.WebpackSources.ReplaceSource, outputOptions: { pathinfo }, requestShortener: { shorten: (request: string) => string }) {
    debugger
    if (outputOptions.pathinfo && parentDependency.module) {
      const comment = ("/*! simple-dependency " + requestShortener.shorten(parentDependency.request) + " */")
      source.insert(source.size(), comment)
    }
  }
}

export const SimpleDependency = Object.assign(SimpleDependencyClass, { Template: SimpleDependencyTemplate })
