text: const Context = require('./context')
  
  class Composer {
    constructor (...fns) {
      this.handler = Composer.compose(fns)
    }
  
    use (...fns) {
      this.handler = Composer.compose([this.handler, ...fns])
      return this
    }
  
    on (updateTypes, ...fns) {
      return this.use(Composer.mount(updateTypes, ...fns))
    }
  
    hears (triggers, ...fns) {
      return this.use(Composer.hears(triggers, ...fns))
    }
  
    command (commands, ...fns) {
      return this.use(Composer.command(commands, ...fns))
    }
  
    action (triggers, ...fns) {
      return this.use(Composer.action(triggers, ...fns))
    }
  
    inlineQuery (triggers, ...fns) {
      return this.use(Composer.inlineQuery(triggers, ...fns))
    }
  
    gameQuery (...fns) {
      return this.use(Composer.gameQuery(...fns))
    }
  
    drop (predicate) {
      return this.use(Composer.drop(predicate))
    }
  
    filter (predicate) {
      return this.use(Composer.filter(predicate))
    }
  
    entity (...args) {
      return this.use(Composer.entity(...args))
    }
  
    email (...args) {
      return this.use(Composer.email(...args))
    }
  
    url (...args) {
      return this.use(Composer.url(...args))
    }
  
    textLink (...args) {
      return this.use(Composer.textLink(...args))
    }
  
    textMention (...args) {
      return this.use(Composer.textMention(...args))
    }
  
    mention (...args) {
      return this.use(Composer.mention(...args))
    }
  
    phone (...args) {
      return this.use(Composer.phone(...args))
    }
  
    hashtag (...args) {
      return this.use(Composer.hashtag(...args))
    }
  
    cashtag (...args) {
      return this.use(Composer.cashtag(...args))
    }
  
    start (...fns) {
      return this.command('start', Composer.tap((ctx) => {
        ctx.startPayload = ctx.message.text.substring(7)
      }), ...fns)
    }
  
    help (...fns) {
      return this.command('help', ...fns)
    }
  
    settings (...fns) {
      return this.command('settings', ...fns)
    }
  
    middleware () {
      return this.handler
    }
  
    static reply (...args) {
      return (ctx) => ctx.reply(...args)
    }
  
    static catchAll (...fns) {
      return Composer.catch((err) => {
        console.error()
        console.error((err.stack || err.toString()).replace(/^/gm, '  '))
        console.error()
      }, ...fns)
    }
  
    static catch (errorHandler, ...fns) {
      const handler = Composer.compose(fns)
      return (ctx, next) => Promise.resolve(handler(ctx, next))
        .catch((err) => errorHandler(err, ctx))
    }
  
    static fork (middleware) {
      const handler = Composer.unwrap(middleware)
      return (ctx, next) => {
        setImmediate(handler, ctx, Composer.safePassThru())
        return next(ctx)
      }
    }
  
    static tap (middleware) {
      const handler = Composer.unwrap(middleware)
      return (ctx, next) => Promise.resolve(handler(ctx, Composer.safePassThru())).then(() => next(ctx))
    }
  
    static passThru () {
      return (ctx, next) => next(ctx)
    }
  
    static safePassThru () {
      return (ctx, next) => typeof next === 'function' ? next(ctx) : Promise.resolve()
    }
  
    static lazy (factoryFn) {
      if (typeof factoryFn !== 'function') {
        throw new Error('Argument must be a function')
      }
      return (ctx, next) => Promise.resolve(factoryFn(ctx))
        .then((middleware) => Composer.unwrap(middleware)(ctx, next))
    }
  
    static log (logFn = console.log) {
      return Composer.fork((ctx) => logFn(JSON.stringify(ctx.update, null, 2)))
    }
  
    static branch (predicate, trueMiddleware, falseMiddleware) {
      if (typeof predicate !== 'function') {
        return predicate ? trueMiddleware : falseMiddleware
      }
      return Composer.lazy((ctx) => Promise.resolve(predicate(ctx))
        .then((value) => value ? trueMiddleware : falseMiddleware))
    }
  
    static optional (predicate, ...fns) {
      return Composer.branch(predicate, Composer.compose(fns), Composer.safePassThru())
    }
  
    static filter (predicate) {
      return Composer.branch(predicate, Composer.safePassThru(), () => { })
    }
  
    static drop (predicate) {
      return Composer.branch(predicate, () => { }, Composer.safePassThru())
    }
  
    static dispatch (routeFn, handlers) {
      return typeof routeFn === 'function'
        ? Composer.lazy((ctx) => Promise.resolve(routeFn(ctx)).then((value) => handlers[value]))
        : handlers[routeFn]
    }
  
    static mount (updateType, ...fns) {
      const updateTypes = normalizeTextArguments(updateType)
      const predicate = (ctx) => updateTypes.includes(ctx.updateType) || updateTypes.some((type) => ctx.updateSubTypes.includes(type))
      return Composer.optional(predicate, ...fns)
    }
  
    static entity (predicate, ...fns) {
      if (typeof predicate !== 'function') {
        const entityTypes = normalizeTextArguments(predicate)
        return Composer.entity(({ type }) => entityTypes.includes(type), ...fns)
      }
      return Composer.optional((ctx) => {
        const message = ctx.message || ctx.channelPost
        const entities = message && (message.entities || message.caption_entities)
        const text = message && (message.text || message.caption)
        return entities && entities.some((entity) =>
          predicate(entity, text.substring(entity.offset, entity.offset + entity.length), ctx)
        )
      }, ...fns)
    }
  
    static entityText (entityType, predicate, ...fns) {
      if (fns.length === 0) {
        return Array.isArray(predicate)
          ? Composer.entity(entityType, ...predicate)
          : Composer.entity(entityType, predicate)
      }
      const triggers = normalizeTriggers(predicate)
      return Composer.entity(({ type }, value, ctx) => {
        if (type !== entityType) {
          return false
        }
        for (const trigger of triggers) {
          ctx.match = trigger(value, ctx)
          if (ctx.match) {
            return true
          }
        }
      }, ...fns)
    }
  
    static email (email, ...fns) {
      return Composer.entityText('email', email, ...fns)
    }
  
    static phone (number, ...fns) {
      return Composer.entityText('phone_number', number, ...fns)
    }
  
    static url (url, ...fns) {
      return Composer.entityText('url', url, ...fns)
    }
  
    static textLink (link, ...fns) {
      return Composer.entityText('text_link', link, ...fns)
    }
  
    static textMention (mention, ...fns) {
      return Composer.entityText('text_mention', mention, ...fns)
    }
  
    static mention (mention, ...fns) {
      return Composer.entityText('mention', normalizeTextArguments(mention, '@'), ...fns)
    }
  
    static hashtag (hashtag, ...fns) {
      return Composer.entityText('hashtag', normalizeTextArguments(hashtag, '#'), ...fns)
    }
  
    static cashtag (cashtag, ...fns) {
      return Composer.entityText('cashtag', normalizeTextArguments(cashtag, '$'), ...fns)
    }
  
    static match (triggers, ...fns) {
      return Composer.optional((ctx) => {
        const text = (
          (ctx.message && (ctx.message.caption || ctx.message.text)) ||
          (ctx.callbackQuery && ctx.callbackQuery.data) ||
          (ctx.inlineQuery && ctx.inlineQuery.query)
        )
        for (const trigger of triggers) {
          ctx.match = trigger(text, ctx)
          if (ctx.match) {
            return true
          }
        }
      }, ...fns)
    }
  
    static hears (triggers, ...fns) {
      return Composer.mount('text', Composer.match(normalizeTriggers(triggers), ...fns))
    }
  
    static command (command, ...fns) {
      if (fns.length === 0) {
        return Composer.entity(['bot_command'], command)
      }
      const commands = normalizeTextArguments(command, '/')
      return Composer.mount('text', Composer.lazy((ctx) => {
        const groupCommands = ctx.me && (ctx.chat.type === 'group' || ctx.chat.type === 'supergroup')
          ? commands.map((command) => `${command}@${ctx.me}`)
          : []
        return Composer.entity(({ offset, type }, value) =>
          offset === 0 &&
          type === 'bot_command' &&
          (commands.includes(value) || groupCommands.includes(value))
        , ...fns)
      }))
    }
  
    static action (triggers, ...fns) {
      return Composer.mount('callback_query', Composer.match(normalizeTriggers(triggers), ...fns))
    }
  
    static inlineQuery (triggers, ...fns) {
      return Composer.mount('inline_query', Composer.match(normalizeTriggers(triggers), ...fns))
    }
  
    static acl (userId, ...fns) {
      if (typeof userId === 'function') {
        return Composer.optional(userId, ...fns)
      }
      const allowed = Array.isArray(userId) ? userId : [userId]
      return Composer.optional((ctx) => !ctx.from || allowed.includes(ctx.from.id), ...fns)
    }
  
    static memberStatus (status, ...fns) {
      const statuses = Array.isArray(status) ? status : [status]
      return Composer.optional((ctx) => ctx.message && ctx.getChatMember(ctx.message.from.id)
        .then(member => member && statuses.includes(member.status))
      , ...fns)
    }
  
    static admin (...fns) {
      return Composer.memberStatus(['administrator', 'creator'], ...fns)
    }
  
    static creator (...fns) {
      return Composer.memberStatus('creator', ...fns)
    }
  
    static chatType (type, ...fns) {
      const types = Array.isArray(type) ? type : [type]
      return Composer.optional((ctx) => ctx.chat && types.includes(ctx.chat.type), ...fns)
    }
  
    static privateChat (...fns) {
      return Composer.chatType('private', ...fns)
    }
  
    static groupChat (...fns) {
      return Composer.chatType(['group', 'supergroup'], ...fns)
    }
  
    static gameQuery (...fns) {
      return Composer.mount('callback_query', Composer.optional((ctx) => ctx.callbackQuery.game_short_name, ...fns))
    }
  
    static unwrap (handler) {
      if (!handler) {
        throw new Error('Handler is undefined')
      }
      return typeof handler.middleware === 'function'
        ? handler.middleware()
        : handler
    }
  
    static compose (middlewares) {
      if (!Array.isArray(middlewares)) {
        throw new Error('Middlewares must be an array')
      }
      if (middlewares.length === 0) {
        return Composer.safePassThru()
      }
      if (middlewares.length === 1) {
        return Composer.unwrap(middlewares[0])
      }
      return (ctx, next) => {
        let index = -1
        return execute(0, ctx)
        function execute (i, context) {
          if (!(context instanceof Context)) {
            return Promise.reject(new Error('next(ctx) called with invalid context'))
          }
          if (i <= index) {
            return Promise.reject(new Error('next() called multiple times'))
          }
          index = i
          const handler = middlewares[i] ? Composer.unwrap(middlewares[i]) : next
          if (!handler) {
            return Promise.resolve()
          }
          try {
            return Promise.resolve(
              handler(context, (ctx = context) => execute(i + 1, ctx))
            )
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }
  }
  
  function normalizeTriggers (triggers) {
    if (!Array.isArray(triggers)) {
      triggers = [triggers]
    }
    return triggers.map((trigger) => {
      if (!trigger) {
        throw new Error('Invalid trigger')
      }
      if (typeof trigger === 'function') {
        return trigger
      }
      if (trigger instanceof RegExp) {
        return (value) => {
          trigger.lastIndex = 0
          return trigger.exec(value || '')
        }
      }
      return (value) => trigger === value ? value : null
    })
  }
  
  function normalizeTextArguments (argument, prefix) {
    const args = Array.isArray(argument) ? argument : [argument]
    return args
      .filter(Boolean)
      .map((arg) => prefix && typeof arg === 'string' && !arg.startsWith(prefix) ? `${prefix}${arg}` : arg)
  }
  
  module.exports = Composer
  