import lodash from 'lodash'
import { basename, sep, relative, join, resolve } from 'path'
import { mixinPropertyUtils } from 'skypager-util/lib/properties'
import query from 'skypager-util/lib/query'
import router from 'skypager-util/lib/router'
import Document from '../document'
import { toVFile as vfile } from '../importers/node-vinyl'
const { compact, set, groupBy, snakeCase, uniq, chain, mapValues } = lodash
const { values } = Object

import { isVinyl } from 'vinyl'

export class Collection {

  static mount (cwd, options = {}) {
    return create({
      cwd, ...options,
    }).imported.withDocuments
  }

  /**
  * Create a collection which wraps a directory and creates a queryable index
  * of all of the files inside of it.  Files exist as VFile (@see https://github.com/wooorm/vfile) and are
  * wrapped by Document classes which provide the interface for metadata, transformations, and things of that nature.
  * A collection also provides a lodash wrapper interface for building extraction functions from the collections
  * files and data.
  *
  * @param {Object} options - the options hash
  * @param {String} options.type - the type of document - will determine which document model wrapper is used
  * @param {String} options.cwd - the root of the collection, defaults to process.cwd()
  * @param {String} options.cwd - the cwd, if different from the base
  * @param {Array} options.include - an array of glob patterns to include
  * @param {Array} options.exclude - an array of glob patterns to exclude
  * @param {Object} options.documentOptions - an object that will be passed to the document wrapper when created
  * @param {Object} options.importerOptions - an object that will be passed to the document importer when created
  */
  constructor (options = {}) {
    mixinPropertyUtils(this)

    this.hide('cwd', options.cwd || options.root || options.base || options.basePath)
    this.hide('options', normalize(options))

    this.pathId = this.cwd.split('/').reverse().slice(0,2).join('-')
    this.instanceId = `${this.pathId}-${Math.floor(Date.now() / 1000)}`

    this.hideGetter('collection', options.collection)
    this.hide('filesIndex', {})
    this.hide('documentsIndex', {})
    this.hide('project', options.project)

  }

  get gitInfo() {
    try {
      return require('../git')(this.cwd)
    } catch(error) {
      return {}
    }
  }

  get importer() {
    return new this.Importer(this.cwd, {
      ...this.importerOptions, patterns: this.patterns
    })
  }

  get type() {
    return snakeCase(this.options.type || 'Project').replace(/s$/i, '')
  }

  get files() {
    return lodash.chain(this.filesIndex)
    .values()
    .compact()
    .sortBy(d => d.path)
    .value()
  }

  get documents() {
    return lodash.chain(this.documentsIndex)
    .values()
    .compact()
    .sortBy(d => d.file.path)
    .value()
  }

  get documentClass() {
    return this.options.documentClass || Document
  }

  get importerOptions() {
    const c = this

    return {
      ...c.options.importerOptions,
      patterns: lodash.uniq([
        ...c.patterns || [],
        ...c.options.importerOptions.patterns || []
      ]),
    }
  }

  get documentOptions() {
    const collection = this

    return {
      collectionType: collection.type,
      ...collection.options.documentOptions || {},
      cwd: collection.cwd,
    }
  }

  get Document() {
    return this.documentClass
  }

  get withDocuments() {
    this.createDocuments()
    return this
  }

  shouldRejectFile(file) {
    if (typeof this.options.rejectFile === 'function') {
      return this.options.rejectFile.call(this, file)
    }

    if (file.path.indexOf('/ios/') >= 0) {
      return true
    }

    if (file.path.indexOf('/android/') >= 0) {
      return true
    }

    return !file.path.startsWith(this.cwd)
  }

  createDocuments(documentOptions = {}) {
    const Document = this.Document

    lodash.mapValues(this.filesIndex, (file, path) => {
      path = relative(this.cwd, path)

      this.documentsIndex[path] = this.documentsIndex[path]
      || new Document(file, {
        ...this.documentOptions,
        ...documentOptions,
        project: this.project,
        collection: this,
      })
    })

    return this
  }

  isDirectory(file) {
    const path = file.path ? file.path : file

    return typeof file.isDirectory === 'function'
      ? file.isDirectory()
      : !path.match(/\.\w+$/)
  }

  add (file) {
    if (file.path && !this.isDirectory(file)) {
      lodash.assign(this.filesIndex, {
        [relative(this.cwd, file.path)]: file
      })
    }
  }

  prepareImportedFilesForIndexing (files = []) {
    const cwd = this.cwd

    return lodash.chain(files)
      .reject(f => this.isDirectory(f))
      .reject(f => this.shouldRejectFile(f))
      .compact()
      .sortBy(f => f.path.length)
      .keyBy(f => relative(cwd, f.path))
      .value()
  }
    
  indexFiles(files = []) {
    files.forEach(file => this.loadFile(file))
    return this
  }

  loadFile(file = {}, cwd = this.cwd) {
    lodash.assign(this.filesIndex, {
      [this.resolve(cwd, file.path)]: this.prepareFileObject(file, cwd)
    })

    return this
  }

  prepareFileObject(file, cwd = this.cwd) {
    return this.options.prepareFile ? this.options.prepareFile.call(this, file) : ensureVFile(file, cwd)
  } 

  loadFiles(files = []) {
    lodash.compact(files)
      .filter(f => !this.isDirectory(f))
      .forEach(f => this.loadFile(f))
      
    return this    
  }

  loadFilesFromImporter() {
    return this.importer.fileWrappers
    .then(files = this.prepareImportedFilesForIndexing(files))
    .then(files => this.indexFiles(files.map(file => file.toVFile ? file.toVFile() : file)))
    .then(collection => collection.withDocuments)
    .catch(error => error)
  }

  get loaderPrefix() {
    return this.get('options.loaderPrefix', '')
  }

  get documentEntryPoints() {
    return this.chain
      .get('documents')
      .keyBy('id')
      .mapValues((doc) => ([
        `${this.loaderPrefix}${doc.file.path}`
      ]))
      .mapValues(array => compact(array))
      .value()
  }

  get documentsMappedById() {
    return this.chain
      .get('documents')
      .keyBy('id')
      .value()
  }

  get documentsMap () {
    return this.documentsIndex
  }

  get Importer() {
    return this.options.importer
    ? require(`../importers/` + this.options.importer + '.js')
    : require('../importers/node-vinyl').DocumentImporter
  }

  get patterns() {
    return lodash.uniq(
      this.includePatterns.concat(this.excludePatterns)
    )
  }

  get includePatterns() {
    return lodash.uniq(
      this.options.include.concat(this.typePatterns)
    )
  }

  get typePatterns () {
    switch (this.type.toUpperCase()) {
      case PACKAGE:
      return PatternMapping.Package
      break
      case SCRIPT:
      return PatternMapping.Script
      break
      case SETTINGS_FILE, COPY_FILE:
      return PatternMapping.SettingsFile
      break
      case DOCUMENT:
      return PatternMapping.Document
      break
      case STYLESHEET:
      return PatternMapping.Stylesheet
      break
      case VECTOR:
      return PatternMapping.Vector
      break
      case PROJECT:
      default:
      return PatternMapping.Project
    }
  }

  get router() {
    return this.routers.documents
  }

  get routers() {
    if (!this._routers) {
      this.hide('_routers', {})

      this.createRouter('files', {
        pathProperty: 'relative'
      })

      this.createRouter('documents', {
        pathProperty: 'baseRelativePath',
        pathsGetter: 'documents',
      })

      this.createRouter('folders', {
        pathProperty: 'toString',
        pathsGetter: 'subFolderNames'
      })
    }

    return mapValues(this._routers,
      (router) => router.get.bind(router)
    )
  }

  createRouter(name, options = {}) {
    if (options.getter) { options.pathsGetter = options.getter}
    if (options.property) { options.pathProperty = options.property}

    router(this._routers, {
      routerProperty: name,
      pathsGetter: name,
      pathProperty: 'id',
      ...options,
      host: this,
      matchesOnly: true,
    })
  }

  createDelegators(receiver, ...propNames) {
    propNames.forEach(propName => {
      receiver.hideGetter(propName, () => this[propName])
    })
  }

  getFileByRelativePath (relativeFilePath) {
    const {index} = this.filesIndex[relativeFilePath]
    return this.files[index]
  }

  join(...args) {
    return join(this.cwd, ...args)
  }

  relative(...args) {
    return relative(this.cwd, ...args)
  }

  resolve(...args) {
    return resolve(this.cwd, ...args)
  }

  get chain() {
    return chain(this)
  }

  select(...paths) {
    return this.selectChain(...paths).value()
  }

  selectChain(...paths) {
    const set = (obj,path,val) => {
      lodash.set(obj,path,val)
      return obj
    }

    return this.chain.at(...lodash.sortBy(paths, (p) => p.length))
    .map((value,index) => [paths[index], value])
    .reduce(((memo, pair) => set(memo, ...pair)), {})
  }

  selectByObject (pathObject) {
    const keys = keys(pathObject)
    const paths = values(pathObject)

    return this.chain.at(...paths).reduce(
      ((memo,val,index) => set(memo, keys[index], val)),
      {}
    ).value()
  }

  get relativeObjectPaths() {
    return this.chain.get('files')
    .sortBy(f => f.dirname.length)
    .map((f) => [
      this.relative(f.dirname).replace(/\//g, '.'),
      f.basename,
      f.relative
    ])
    .groupBy(i => i.shift())
    .value()
  }

  get subFolderObjectPaths() {
    return this.chain.get('subFolderNames')
    .sortBy(v => v.length)
    .map(p => p.replace(/\//g,'.'))
    .value()
  }

  get excludePatterns () {
    if (this.options.includeAll || this.options.exclude === false) {
      return []
    }

    return this.options.exclude
    // HACK tmp
      .concat(this.project ? this.project.skypagerIgnoreContents || [] : [])
      .map(p => p.startsWith('!') ? p : `!${p}`)
      .concat([
        '!**/node_modules/**',
        '*.xcodeproj',
        '!_*',
      ])
  }

  get imported() {
    if(this.files.length === 0) {
      this.indexFiles(this.importer.readFilesSync(false))
    }

    return this
  }

  get rootFiles() {
    return this.chains.files.filter(f => f.dirname === project.root).value()
  }

  get groupedByChildFolder() {
    return this.chains.files
    .groupBy(f => this.relative(f.dirname).split('/').shift())
    .pickBy((v,k) => k.length > 0)
    .value()
  }

  get groupedByChildFolderAndExtension() {
    return this.chains.files
    .groupBy(f => this.relative(f.dirname).split('/').shift())
    .mapValues(v => groupBy(v, (v) => v.extname))
    .pickBy((v,k) => k.length > 0)
    .value()
  }

  get chains() {
    return {
      documents: chain(this.documents),
      files: chain(this.files),
    }
  }

  get query() {
    const collection = this

    return {
      get files() {
        return query.bind(query, collection.files)
      },
      get documents() {
        return query.bind(query, collection.documents)
      }
    }
  }

  findDocumentById (documentId) {
    return this.documents.find(doc => doc.id === documentId)
  }

  applyFilesRoute(pattern) {
    const path = route()(pattern)
    const results = this.files.map(f => ({path: f.relative, pathData: path(f.relative)})).filter(f => f.pathData)

    return {
      results,
      pattern
    }
  }

  get depths() {
    return this.chains
    .files.map(f => this.relative(f.dirname))
    .uniq()
    .groupBy(f => f.split(sep).length - 1)
    .omit('')
    .value()
  }

  get relativeFilePaths() {
    return this.files.map(file => relative(this.cwd, file.path))
  }

  get relativeSubFolderPaths() {
    return this.files.map(file => relative(this.cwd, file.dirname))
  }

  get subFolderPaths() {
    return uniq(this.files.map(f => f.dirname))
  }

  get subFolderNames() {
    return this.subFolderPaths
    .map(f => relative(this.cwd, f))
    .filter(f => f.length > 0)
  }

  get subFolderBasenames () {
    return this.subFolderPaths
    .map(f => relative(this.cwd, f))
    .map(f => basename(f))
  }

  get childFolderNames() {
    return uniq(
      this.subFolderNames.map(p => p.split('/').shift())
    )
  }

  get childFolderPaths() {
    return uniq(
      this.childFolderNames.map(
        p => this.join(p)
      )
    )
  }

  get ready() {
    return this.importer.status === 'LOADED'
    ? Promise.resolve(this)
    : this.loadFilesFromImporter().then(() => this)
  }

  static create (options = {}, ready) {
    const collection = new Collection(options)

    return typeof ready === 'function'
      ? ready(collection, Collection)
      : collection
  }

  static get Document() {
    return Document
  }
}

export default Collection

const PACKAGE = 'PACKAGE'
const SCRIPT = 'SCRIPT'
const DOCUMENT = 'DOCUMENT'
const SETTINGS_FILE = 'SETTINGS_FILE'
const COPY_FILE = 'COPY_FILE'
const PROJECT = 'PROJECT'
const VECTOR = 'VECTOR'
const STYLESHEET = 'STYLESHEET'

export const PatternMapping = {
  'Package': ['**/package.json'],
  'Document': ['**/*.md'],
  'Script': ['**/*.js','**/*.jsx'],
  'Stylesheet': ['**/*.{css,less,sass,scss,styl'],
  'SettingsFile': ['**/*.yml'],
  'CopyFile': ['**/*.yml'],
  'Vector': ['**/*.svg'],
  'DataSource': ['**/*.json','**/*.yml', '**/*.data.js'],
  'Project': [
    '**/*'
  ]
}

export function create(options = {}) {
  return Collection.create(options)
}

function normalize (options = {}) {
  return {
    ...options,
    importerOptions: options.importerOptions || {},
    documentOptions: options.documentOptions || {},
    include: [...(options.include || []), ...(options.patterns || [])],
    exclude: [...(options.exclude || []), ...(options.ignore || [])]
  }
}

function ensureVFile(object = {}, cwd) {
  if (cwd) {
    object.cwd = cwd
  }

  if (typeof object.toVFile === 'function') {
    return object.toVFile()
  } else if (isVinyl(object)) {
    return vfile(object)
  } else if (typeof object === 'object' && object.contents && object.path) {
    return vfile({
      contents: new Buffer(object.contents.toString()),
      path: object.path,
      cwd: object.cwd,
    })
  } else {
    return vfile(object)
  }
}