import url from 'url'
import { parse as parsePath, sep, basename, resolve, relative } from 'path'
import { mixinPropertyUtils } from 'skypager-util/lib/properties'
import * as stringUtils from 'skypager-util/lib/string'
import isAbsolutePath from 'path-is-absolute'
import Collection from '../collection'
import ContextRegistry from 'skypager-registry/lib/context'
import Cache from '../cache'
import mapValues from 'lodash/mapValues'
import isFunction from 'lodash/isFunction'
import vfile from 'vfile'
import set from 'lodash/set'
import omit from 'lodash/omit'
import pick from 'lodash/pick'
import uniq from 'lodash/uniq'
import isEmpty from 'lodash/isEmpty'
import castArray from 'lodash/castArray'
import hashObject from 'object-hash'

export const PATH_SEPARATOR = sep

export const DefaultPatterns = [
  '**/*.{js,md,yml,json,svg,html,css,scss,less,json}',
  '!node_modules/**'
]

export const patterns = DefaultPatterns

export class Document {
  static patterns = DefaultPatterns;
 
  static mountCollection(cwd, options = {}) {
    if (!cwd) {
      throw new Error('Must pass a folder to Document.mountCollection')
    }

    return Collection.mount(cwd, options)
  }

  static cache = new Cache()

  static selectors = new ContextRegistry('documentSelectors', {
    context: require.context('../selectors/document', true, /\.js$/),
    wrapper: function(fn, id) {
      return (doc, ...args) =>
        Document.cache.fetch(`selectors:${id}:${hashObject({args})}:${doc.cacheKey}`, () => fn.call(doc, doc.exporter, ...args).value())
    }
  })

  constructor (vFileOrPath, options = {}) {
    mixinPropertyUtils(this)

    if (options.project) {
      this.hide('project', options.project)
      delete(options.project)
    }

    if (options.collection) {
      this.hide('collection', options.collection)
      delete(options.collection)
    }
    
    this.hide('options', options)
    this.hide('cwd', options.cwd)

    const originalURI = normalizeUri(
      vFileOrPath.path ? vFileOrPath.path : vFileOrPath,
      options.cwd
    )

    this.hide('originalURI', originalURI)
    this.hide('parsedURI', url.parse(this.originalURI))
    this.hide('url', this.parsedURI.href)
    this.hide('uri', this.url)
    this.hide('path', unescape(this.parsedURI.path))
    this.hide('_appliedDocTypes', {})

    if (vFileOrPath.path) {
      this.options.file = vFileOrPath
    }

    this.lazy('file', () => this.toVFile({
      cwd: this.cwd,
      ...(this.options.file || {})
    }))

    this.hide('data', {})
    this.lazy('id', this.buildId.bind(this))

    this.lazy('fileStat', () => this.get('file.stat', {}))
    this.lazy('createdAt', () => this.result('fileStat.ctime', () => Math.floor(new Date() / 1000)))

    this.lazy('pathInfo', () => parsePath(this.path)) 

    this.lazy('vmSandbox', () => this.project.vm.createContext({
      ...this.project.sandbox,
      doc: this,
      global: {},
    }))
  }

  get appliedDocTypes() {
    return Object.keys(this._appliedDocTypes)
  }
  
  get updatedAt() {
    return Math.floor(this.get('data.updatedAt', this.createdAt) / 1000)
  }

  touch() {
    try {
      this.set('data.updatedAt', Math.floor(new Date() / 1000))
    } catch(e) {

    }

    return this
  }

  get decorated() {
    this.applyDoctypes()
    return this
  } 

  createContext(sandbox = {}) {
    return {
      ...sandbox,
      doc: this,
      project: this.project,
    }    
  }

  relativeDoc(relativeDocId) {
    return this.project.documentsMap[ 
      this.project.relative(
        this.pathToRelative( relativeDocId )
      )
    ]
  }

  pathToRelative(relativePath) {
    return resolve(this.fileDirname, relativePath)        
  }

  getTransformPresets() {
    return this.getTransforms()
  }
 
  getTransforms() {
    return this.matchingDocTypes
      .reduce((memo, docType) => ({
        ...memo,
        [docType.name]: docType.getTransforms ? docType.getTransforms() : {},
      }), {})
  }  
  
  getUtils() {
    return this.matchingDocTypes
      .reduce((memo, docType) => ({
        ...memo,
        ...docType.getUtils ? docType.getUtils() : {},
      }))
  }

  buildId (usingPath = this.path, cwd = this.cwd) {
    if (isFunction(this.options.buildId)) {
      return this.options.buildId(
        relative(cwd, usingPath)
      )
    } else if (this.collection && isFunction(this.collection.buildId)) {
      return this.collection.buildId(
        relative(cwd, usingPath)
      )
    } else {
      return relative(cwd, usingPath)
        .replace(/\.\w+$/g, '')
        .replace(/\.js$/g,'')
    }
  }

  get selectors() {
    return this.constructor.selectors
  }
  
  select(selector, ...args) {
    if (isFunction(selector)) {
      const result = selector.call(this, this.exporter, ...args)
      return result && typeof result.value === 'function' ? result.value() : result
    } else if (typeof selector === 'string') {
      return this.constructor.selectors.lookup(selector)(this, ...args)
    }
  }

  selectChain(selector, ...args) {
    return this.chain.invoke('select', selector, ...args)
  }

  selectorMap (config = {}, resolveChain = true) {
    const c = this.chain
      .plant(config)
      .mapValues((selector) => this.select(...castArray(selector)))

    return resolveChain ? c.value() : c
  }

  get chain() {
    return this.project.chain.plant(this)
  }

  get exporter() {
    return this.project.chain.plant(this.ready)
  }

  get ready() {
    return this.readFile(true).decorated
  }

  get doctypes() {
    return this.docTypes
  }

  get docTypes() {
    const { project } = this

    return project.chain
      .get('documentTypes.available', [])
      .thru(list => list.map(docType => project.documentType(docType)))  
      .value()
  }

  get matchingDocTypes() {
    const { project } = this

    return project.chain
      .get('documentTypes.available', [])
      .thru(list => list.map(docType => project.documentType(docType)))  
      .flatten()
      .filter(docType => docType.testDocument(this))
      .value()  
  }

  sliceId(removeFromBeginning = 1, removeFromEnd = 1) {
    if (removeFromEnd < 0) { removeFromEnd = removeFromEnd * -1 }

    return this.idParts.slice(removeFromBeginning, removeFromEnd < 0 ? removeFromEnd * -1 : this.idParts.length)
  }

  get idParts() {
    return this.id.split(PATH_SEPARATOR)
  }

  get baseFolder() {
    return this.idParts[0]
  }

  get categoryFolderName() {
    const length = this.idParts.length
    return this.isIndex 
      ? this.idParts[length - 3] || this.idParts[1]
      : this.idParts[length - 2] || this.idParts[1]
  }

  get categoryFolder() {
    return (this.isIndex
      ? this.idParts.slice(0, this.idparts.length - 2) 
      : this.idParts.slice(0, this.idparts.length - 1)).join(PATH_SEPARATOR)
  }

  applyDoctypes() {
    if (isEmpty(this.appliedDocTypes)) {
      this.matchingDocTypes.forEach(docType => {
        docType.decorate(this)
        this.appliedDocTypes[docType.name] = true
      })    
    }

    return this
  }

  toJson (options = {}) {
    return {
      id: this.id,
      file: omit(this.file, 'contents', '_contents'),
      metadata: this.metadata,
      idParts: this.idParts,
      categoryFolder: this.categoryFolder,
      categoryFolderName: this.categoryFolderName,
      baseFolder: this.baseFolder,
      variations: this.variations,
      ...(options.pick ? pick(this, options.pick) : {}),
      ...(options.pickBy ? pickBy(this, options.pickBy) : {}),
      ...(options.includeContents ? {contents: this.file.contents} : {contents: null}),
    }
  }

  toVFile(fileOptions = {}) {
    return vfile({
      path: this.path,
      cwd: this.cwd,
      ...fileOptions,
    })
  }

  readFile(cache = true) {
    if (cache && this.contents && this.contents.length > 0) {
      this.file.contents = this.contents      
      return this
    }

    this.file.contents = this.contents = this.project.readFileSync(this.file.path).toString()

    return this.touch() 
  }

  readFileAsync() {
    return this.project.readFileAsync(this.file.path)
      .then((contents) => this.contents = contents.toString())
  }

  get metadata() {
    return {
      ...this.fileData,
      ...this.data,
      ...this.get('exportables.frontmatter', {}),
    }
  }

  get idPair() {
    return [this.id, this]
  }

  get isIndex() {
    return !!( 
      this.id.match(/index$/) || 
      this.fileStem === 'index' || 
      this.fileBaseName.startsWith('index')
    )
  }

  get isPackageManifest() {
    return this.fileBaseName === 'package.json'
  }

  get variationsBase() {
    return this.isIndex
      ? this.fileDirnameBasename
      : this.fileStem
  }

  get fileExtname() {
    return this.file.extname
  }

  get isMarkdown() {
    return this.fileExtname && this.fileExtname.match(/.md$/)
  }
  
  get isJavascript() {
    return this.fileExtname && this.fileExtname.match(/.js$/)
  }
 
  /**
   * Create a lodash chain by picking some properties from the project.
   * @param  {Array[String]} properties List of properties you want to be a part of the chain
   * @return {Chain}           The lodash chain whose initial value consists of the properties given
   */
  slice (...properties) {
    return properties.reduce((memo, prop) => {
      set(memo, prop, this.result(prop))
      return memo
    }, {})
  }
  
  createEntityFrom(...properties) {
    const src = this.ready.slice(...properties)    
    return require('../entity').createEntity(src)
  }

  asyncRunner(code, options = {}, context = {}) {
    return this.project.createSandboxedScriptRunner(code, {
      sandbox: this.vmSandbox
    })
  }
 
  createStemVariations (base = this.variationsBase, options = {}) {
    const { startCase, camelCase, kebabCase, snakeCase, humanize, titleize } = stringUtils

    return mapValues({
      classified: startCase(camelCase(base)).replace(/\s+/g,''),
      camel: camelCase(base),
      kebab: kebabCase(base),
      snake: snakeCase(base),
      underscored: kebabCase(base).replace(/\-/g,'_'),
      humanized: humanize(kebabCase(base).replace(/\-/g,'_')),
      titleized: titleize(humanize(kebabCase(base).replace(/\-/g,'_'))),
    }, (v) => {
      const value = `${options.prefix || ''}${v}${options.suffix || ''}`
      const modifier = options.pluralize
        ? stringUtils.pluralize
        : options.singularize ? stringUtils.singularize : ((v) => v)

      return modifier(value)
    })
  }

  get fileData () {
    return isFunction(this.options.getFileData)
      ? this.options.getFileData.call(this, this.file)
      : this.result('file.data', () => Object.assign(this.file, {
        data: {},
      }))
  }

  get fileMessages () {
    return this.get('file.messages', {})
  }

  get variationsWithDirname () {
    const base = `${this.fileDirnameBasename}_${this.fileStem}`

    return {
      ...this.createStemVariations(base),
      singularized: this.createStemVariations(base, {
        singularize: true,
      }),
      pluralized: this.createStemVariations(base, {
        pluralize: true,
      }),
    }
  }

  get variations () {
    return {
      ...this.createStemVariations(this.variationsBase),
      singularized: this.createStemVariations(this.variationsBase, {
        singularize: true,
      }),
      pluralized: this.createStemVariations(this.variationsBase, {
        pluralize: true,
      }),
    }
  }

  get nameVariations() {
    const variations = this.variations

    return uniq(Object.values(omit(variations, 'pluralized', 'singularized')).concat(
      Object.values(variations.pluralized),
      Object.values(variations.singularized)
    ))
  }

  get indexVariations() {
    return this.createStemVariations(this.variationsBase, {
      suffix: `/index${this.fileExtname}`,
    })
  }

  get filenameVariations() {
    return {
      ...this.createStemVariations(this.variationsBase, {
        suffix: this.fileExtname,
      })
    }
  }

  get fileStem() {
    return this.file.stem
  }

  get fileBaseName() {
    return this.file.basename
  }

  get fileDirname () {
    return this.file.dirname 
  }

  get fileDirnameBasename () {
    return basename(this.fileDirname)
  }

  get baseRelativePath() {
    return relative(
      this.cwd,
      this.path
    )
  }

  get contentLength() {
    const statSize = this.result('stat.size')

    if (typeof statSize === 'undefined') {
      return 0
    }  else {
      return (this.get('contents', this.get('file.contents', '')) || '').length
    }
  }

  get stat() {
    return this.get('file.stat', {})
  }

  get cacheKey() {
    return [
      this.baseRelativePath, 
      this.updatedAt.toString(), 
      this.contentLength.toString()
    ].join(':')
  }

  get isRemote() {
    return this.uri.protocol.indexOf('http') !== -1
  }
  
  reloadFileContents() {
    return this.contents = this.file.contents = this.readFileSync(false)
  }

  getFileContents() {
    return this.result('file.contents', '').toString()
  }
}

export default Document

export function normalizeUri (input, cwd = process.cwd()) {
  if (typeof input !== 'string') {
    throw('Must pass the URI as a string')
  }

  const parsed = url.parse(input)

  const isFilePath = parsed.protocol === null || parsed.protocol.indexOf('file') !== -1
  const isRemoteUrl = !isFilePath && parsed.protocol.indexOf('http') !== -1
  const isAbsoluteFilePath = isFilePath && isAbsolutePath(parsed.path)

  if (isRemoteUrl) {
    return url.format(parsed)
  }

  if (isFilePath && isAbsoluteFilePath) {
    return url.format({
      protocol: 'file:',
      slashes: true,
      pathname: parsed.path,
    })
  } else {
    return url.format({
      protocol: 'file:',
      slashes: true,
      pathname: resolve(cwd, parsed.path),
    })
  }

  return input
}
