/*
 * @name          Model     
 * @description   The Model helper is used to define patterns which can turn a certain
 *                document or set of documents into an ActiveRecord like object which
 *                can be queried or acted on, and which can have relationships with other
 *                documents in the project.
*/
import Helper from '../helper'
import isArray from 'lodash/isArray'
import isFunction from 'lodash/isArray'
import flatten from 'lodash/flatten'
import testRule from 'skypager-util/lib/path-matcher'
import castArray from 'lodash/castArray'
import bindAll from 'lodash/bindAll'
import { mixinPropertyUtils } from 'skypager-util/lib/properties'

/**
 * A Model is a way of taking any document and treating it as a thing with attributes,
 * a thing with a status, relationships, context, etc.  These things can be derived by 
 * analyzing the contents, or through explicit annotations made by their author.
 * 
 * For example, a javascript application has many screens.  these screens have multiple components,
 * style sheets, sources of content, images and assets.  By turning the file which defines this screens
 * as a model we can use these relationships in many ways.
 */
export class Model extends Helper {
  static isCacheable = false  
  
  static dirname = __dirname
  
  // A Model returns a set of documents
  get documentInstances() {
    return this.chain
      .get('routeResults')
      .map(result => {
        const doc = result.doc
        const data = {
          ...result.meta,
          ...doc.get('data', {}),
        }

        doc.set('data', data)

        return doc
      })
      .value()
  } 

  get resultIds() {
    return this.get('routeResults').map(result => result.id)
  }

  buildId(fromDocumentId) {
    return this.idBuilder(fromDocumentId)
  }

  createContextRegistry(options = {}, filter) {
    return this.createRequireContext({
      stringify: true,
      ...options,
    }, filter)
  }

  createModuleMap(options = {}, filter) {
    return this.chain.get('documentsMap')
      .pickBy((v,k) => !filter || filter.call(this, v,k))
      .mapValues((v) => v.get(options.pathAttribute || 'baseRelativePath'))
      .mapValues(v => 
        `${options.prepend || ''}${options.resolve ? this.project.resolve(v) : v}${options.append || ''}`
      )
      .value()
  }

  createRequireContext(options = {}, filter) {
    const moduleMap = options.moduleMap || this.createModuleMap(options, filter)

    return this.project.select('require-context', {
      moduleMap, ...options,
    })      
  }

  get include() {
    return flatten([
      ...(castArray(this.result('options.include', []))),
      ...(castArray(this.result('provider.include', []))),
    ])   
  }

  get exclude() {
    return flatten([
      ...(castArray(this.result('options.exclude', []))),
      ...(castArray(this.result('provider.exclude', []))),
    ])
  }

  get matchPattern() {
    return this.tryGet('matchPattern')
  }

  get routeResults() {
    const { include, exclude } = this

    if (this.matchPattern) {
      include.unshift(this.matchPattern)
    }

    exclude.push(
      ...this.project.chain.get('paths').pick('bundles','ast','logs','temp','library','cache','output')
    )

    const filterBy = this.tryGet('filterBy')

    return this.chain
      .get('routes')
      .map(route => this.project.route(route))
      .flatten()
      .filter((item, ...args) => {
        if (!item) { return false }        
        
        if (exclude.length > 0 && exclude.find(rule => testRule(rule, item.file.path))) {
          return false
        }        

        if (include.length > 0 && !include.find(rule => testRule(rule, item.file.path))) {
          return false 
        }

        if (isFunction(filterBy)) {
          return filterBy.call(this, item, ...args)
        }

        return true
      })
      .compact()
      .value()
  }

  get resultsMappedByRoute() {
    return this.routeResultsMap
  }
  
  get routeResultsMap() {
    return this.chain
      .get('routes')
      .keyBy(i => i)
      .mapValues((route) => this.project.route(route))
      .value()
  }
  
  get idBuilder() {
    if (this.tryGet('idBuilder')) {
      return this.tryGet('idBuilder').bind(this)
    }

    return (r) => r.id || r
  }

  get idAttribute() {
    return this.tryGet('idAttribute', 'id')
  }

  get routes() {
    const cfg = this.tryResult('routes', [])
    
    if (!isArray(cfg)) {
      return flatten(Object.values(cfg))
    } 

    return cfg.map(o => typeof o === 'string' ? o : o.pattern || o.route)
  }

  get instanceMethods() {
    return this.chain.invoke('tryResult', 'instanceMethods', {})
      .pickBy(v => typeof v === 'function')
      .value()
  }

  get collectionMethods() {
    return this.chain.invoke('tryResult', 'collectionMethods', {})
      .pickBy(v => typeof v === 'function')
      .mapValues(v => v.bind(this))
      .value()
  }

  get documentsMap() {
    return this.chain.get('documentInstances').keyBy(this.idBuilder).value()
  }

  initialize() {
    this.applyMixin(this.collectionMethods, this.context, this)
    this.lazy('instances', () => this.createInstances(this.instanceMethods))
    this.hideGetter('instanceChain', () => this.chain.get('instances'))
    this.hideGetter('refreshed', () => this.refresh())
  }

  refresh() {
    delete(this.instances)
    this.lazy('instances', () => this.createInstances(this.instanceMethods))
    return this
  }  

  createInstanceObjects(...args) {
    return this.createInstances(...args)
  }

  createInstances(instanceMethods = {}, stayChained = false) {
    const instanceOptions = this.tryResult('instanceOptions', ({
      propUtils: true,
      lodash: true,
      chain: true,
    }))

    const selector = this.tryGet('selector', (this.tryGet('active') ? 'active-entity' : 'entity')) 
    const propUtils = !!instanceOptions && instanceOptions.propUtils
    const mixinLodash = !!instanceOptions && instanceOptions.lodash
    const applyChain = !!instanceOptions && instanceOptions.chain

    const chain = this.chain
      .get('documentsMap', {})
      .mapValues(doc => {
        const e = doc.select(selector, instanceMethods)
        const instance = bindAll(Object.assign(e, instanceMethods))
        return propUtils ? mixinPropertyUtils(instance, mixinLodash, applyChain) : instance
      })

    return stayChained ? chain : chain.value()      
  }

  static attach(project, options = {}) {
    return Helper.attach(project, Model, {
      registryProp: 'models',
      lookupProp: 'model',
      registry: options.registry || Helper.createContextRegistry('models', {
        context: require.context('../models', true, /\.js$/),
      }),
      ...options,
    })
  }
}

export default Model

export function validate (providerModule = {}) {
  const report = { pass: true, errors: [], warnings: [] }

  if (!has(providerModule, 'routes')) {
    report.pass = false
    report.errors.push(`Model modules must export a routes object`)
  }

  return report
}