const path = require('path')
const Context = require('./Context')
const { fileExists } = require('./utils')

class Component {
  constructor(id, context) {
    this.id = id || this.constructor.name

    if (this.id === 'Context') {
      throw Error('You cannot use "Context" as a component name. It is reserved.')
    }

    // todo change this
    if (!context || !context.readState) {
      context = new Context(context)
    }

    // we need to keep the entire instance in memory to pass it to child components
    this.context = {
      instance: context
    }

    // Set state
    this.state = {}

    // Define default function
    // Adds the instance context to it
    const that = this

    // make sure author defined at least a default function
    if (typeof that.default !== 'function') {
      throw Error(`default function is missing for component "${this.id}"`)
    }

    const defaultFunction = function(inputs) {
      return that.default.call(that, inputs)
    }

    // Add Component class properties like context and state
    Object.keys(this).forEach((prop) => {
      defaultFunction[prop] = this[prop]
    })

    // Add Component class methods like the save() method
    const classMethods = Object.getOwnPropertyNames(Component.prototype)
    classMethods.forEach((classMethod) => {
      defaultFunction[classMethod] = (...classMethodInputs) =>
        this[classMethod].call(that, ...classMethodInputs) // apply instance context
    })

    // Add instance methods
    // those are the methods of the class that extends Component
    // if user added his own save() method for example,
    // this would overwrite the Component class save() method
    const instanceMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
    instanceMethods.forEach((instanceMethod) => {
      defaultFunction[instanceMethod] = (instanceMethodInputs) =>
        this[instanceMethod].call(that, instanceMethodInputs) // apply instance context
    })

    return defaultFunction
  }

  // populating state is an async operation in most contexts
  // and we can't run async operations in the constructor
  // so we can't auto populate state on instance construction
  async init() {
    await this.context.instance.init()

    this.state = await this.context.instance.readState(this.id)

    // the context object in the componnet instance is a subset
    // of the actual context instance, to keep the component
    // api clean, and be able to turn it off for child components
    this.context.credentials = this.context.instance.credentials
    this.context.resourceId = () => this.context.instance.resourceId()
    this.context.log = (msg) => this.context.instance.log(msg)
    this.context.debug = (msg) => this.context.instance.debug(msg)
    this.context.status = (msg, entity) => this.context.instance.status(msg, entity || this.id)

    // keeping this function for a while
    this.context.output = (key, val) => {
      if (typeof this.context.instance.output === 'function') {
        return this.context.instance.output(key, val)
      }
      return
    }
  }

  async save() {
    await this.context.instance.writeState(this.id, this.state)
  }

  async load(nameOrPath, componentAlias) {
    let externalComponentPath
    let childComponent

    if (this.context.instance.root) {
      externalComponentPath = path.resolve(this.context.instance.root, nameOrPath, 'serverless.js')
    } else {
      externalComponentPath = path.resolve(nameOrPath, 'serverless.js')
    }

    if (await fileExists(externalComponentPath)) {
      delete require.cache[require.resolve(externalComponentPath)]
      childComponent = require(externalComponentPath)
    } else {
      childComponent = require(nameOrPath)
    }

    const childComponentId = `${this.id}.${componentAlias || childComponent.name}`

    const childComponentInstance = new childComponent(childComponentId, this.context.instance)

    // populate state based on the component id
    await childComponentInstance.init()

    // disable context methods for child components
    childComponentInstance.context.log = () => {}
    childComponentInstance.context.status = () => {}
    childComponentInstance.context.output = () => {}

    return childComponentInstance
  }
}

module.exports = Component
