/**
 * @file mixins/evented.js
 * @모듈 이벤트
 */
'글로벌/창'에서 창 가져오기;
import * as Dom from '../utils/dom';
import * as Events from '../utils/events';
import * as Fn from '../utils/fn';
import * as Obj from '../utils/obj';
'../event-target'에서 EventTarget 가져오기;
'../utils/dom-data'에서 DomData 가져오기;
'../utils/log'에서 로그 가져오기;

const objName = (obj) => {
  if (typeof obj.name === '함수') {
    return obj.name();
  }

  if (typeof obj.name === '문자열') {
    obj.name 반환;
  }

  if (obj.name_) {
    반환 obj.name_;
  }

  if (객체 생성자 && obj.constructor.name) {
    반환 obj.constructor.name;
  }

  반환 유형of obj;
};

/**
 * 객체에 이벤트 혼합이 적용되었는지 여부를 반환합니다.
 *
 * @param {객체} 객체
 * 테스트할 개체입니다.
 *
 * @return {부울}
 * 개체가 이벤트로 나타나는지 여부.
 */
const isEvented = (개체) =>
  EventTarget의 개체 인스턴스 ||
  !!object.eventBusEl_ &&
  ['켜짐', '하나', '꺼짐', '트리거'].every(k => typeof object[k] === '함수');

/**
 * 이벤트 혼합이 적용된 후 실행할 콜백을 추가합니다.
 *
 * @param {객체} 객체
 * 추가할 객체
 * @param {함수} 콜백
 * 실행할 콜백입니다.
 */
const addEventedCallback = (대상, 콜백) => {
  if (isEvented(대상)) {
    콜백();
  } else {
    if (!target.eventedCallbacks) {
      target.eventedCallbacks = [];
    }
    target.eventedCallbacks.push(콜백);
  }
};

/**
 * 값이 유효한 이벤트 유형인지 여부 - 비어 있지 않은 문자열 또는 배열.
 *
 * @사적인
 * @param {문자열|배열} 유형
 * 테스트할 유형 값.
 *
 * @return {부울}
 * 유형이 유효한 이벤트 유형인지 여부.
 */
const isValidEventType = (유형) =>
  // 여기서 정규식은 `type`에 적어도 하나의 비
  // 공백 문자.
  (유형의 유형 === '문자열' && (/\S/).테스트(유형)) ||
  (Array.isArray(유형) && !!유형.길이);

/**
 * 유효한 이벤트 대상인지 확인하기 위해 값을 검증합니다. 그렇지 않은 경우 던집니다.
 *
 * @사적인
 * @throws {오류}
 * 대상이 유효한 이벤트 대상으로 나타나지 않는 경우.
 *
 * @param {객체} 대상
 * 테스트할 개체입니다.
 *
 * @param {객체} 객체
 * 유효성을 검사하는 이벤트 객체
 *
 * @param {문자열} fn이름
 * 이것을 호출한 이벤트 믹스인 함수의 이름.
 */
const validationTarget = (대상, obj, fnName) => {
  if (!target || (!target.nodeName) && !isEvented(대상))) {
    throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
  }
};

/**
 * 유효한 이벤트 대상인지 확인하기 위해 값을 검증합니다. 그렇지 않은 경우 던집니다.
 *
 * @사적인
 * @throws {오류}
 * 유형이 유효한 이벤트 유형으로 나타나지 않는 경우.
 *
 * @param {문자열|배열} 유형
 * 테스트할 유형입니다.
 *
 * @param {객체} 객체
* 유효성을 검사하는 이벤트 객체
 *
 * @param {문자열} fn이름
 * 이것을 호출한 이벤트 믹스인 함수의 이름.
 */
const validateEventType = (유형, obj, fnName) => {
  if (!isValidEventType(유형)) {
    throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
  }
};

/**
 * 유효한 리스너인지 확인하기 위해 값의 유효성을 검사합니다. 그렇지 않은 경우 던집니다.
 *
 * @사적인
 * @throws {오류}
 * 리스너가 함수가 아닌 경우.
 *
 * @param {함수} 리스너
 * 테스트할 리스너입니다.
 *
 * @param {객체} 객체
 * 유효성을 검사하는 이벤트 객체
 *
 * @param {문자열} fn이름
 * 이것을 호출한 이벤트 믹스인 함수의 이름.
 */
const validateListener = (리스너, obj, fnName) => {
  if (리스너 유형 !== '함수') {
    throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
  }
};

/**
 * `on()` 또는 `one()`에 주어진 인수 배열을 가져와 유효성을 검사하고
 * 객체로 정규화합니다.
 *
 * @사적인
 * @param {객체} 자기 자신
 * `on()` 또는 `one()`이 호출된 이벤트 개체입니다. 이
 * 객체는 리스너에 대한 `this` 값으로 바인딩됩니다.
 *
 * @param {배열} 인수
 * `on()` 또는 `one()`에 전달되는 인수 배열.
 *
 * @param {문자열} fn이름
 * 이것을 호출한 이벤트 믹스인 함수의 이름.
 *
 * @return {객체}
 * `on()` 또는 `one()` 호출에 유용한 값을 포함하는 객체입니다.
 */
const normalizeListenArgs = (self, args, fnName) => {

  // 인수의 수가 3개 미만이면 대상은 항상
  // 이벤트 객체 자체.
  const isTargetingSelf = args.length < 3 || 인수[0] === 자기 || args[0] === self.eventBusEl_;
  목표를 보자;
  유형을 보자;
  리스너하자;

  if (isTargetingSelf) {
    대상 = self.eventBusEl_;

    // 3개의 인수가 있지만 여전히 듣고 있는 경우를 처리합니다.
    // 이벤트 객체 자체.
    if (인수 길이 > = 3) {
      args.shift();
    }

    [유형, 리스너] = 인수;
  } else {
    [대상, 유형, 청취자] = 인수;
  }

  validationTarget(대상, 자기, fnName);
  validateEventType(유형, 자체, fnName);
  validateListener(listener, self, fnName);

  listener = Fn.bind(self, listener);

  {isTargetingSelf, 대상, 유형, 리스너}를 반환합니다.
};

/**
 * 대상의 이벤트 유형에 리스너를 추가하여 다음을 위해 정규화합니다.
 * 대상 유형.
 *
 * @사적인
 * @param {요소|객체} 대상
 * DOM 노드 또는 이벤트 객체.
 *
 * @param {문자열} 메서드
 * 사용할 이벤트 바인딩 방법("on" 또는 "one").
 *
 * @param {문자열|배열} 유형
 * 하나 이상의 이벤트 유형.
 *
 * @param {함수} 리스너
 * 리스너 기능.
 */
const listen = (타겟, 메소드, 타입, 리스너) => {
  validationTarget(대상, 대상, 방법);

  if (타겟.노드이름) {
    Events[method](target, type, listener);
  } else {
    target[method](유형, 리스너);
  }
};

/**
 * 전달되는 개체에 이벤트 기능을 제공하는 메서드를 포함합니다.
 * {@link module:evented|이벤트}로.
 *
 * @mixin EventedMixin
 */
const EventedMixin = {

  /**
   * 이 개체 또는 다른 이벤트의 이벤트(또는 이벤트)에 리스너 추가
   * 물체.
   *
   * @param {문자열|배열|요소|객체} targetOrType
   * 문자열 또는 배열인 경우 이벤트 유형을 나타냅니다.
   * 리스너를 트리거합니다.
   *
   * 대신 다른 이벤트 개체를 여기에 전달할 수 있습니다.
   * 리스너가 _that_ 객체에서 이벤트를 수신하도록 합니다.
   *
   * 두 경우 모두 리스너의 `this` 값은 다음에 바인딩됩니다.
   * 이 객체.
   *
   * @param {문자열|배열|함수} typeOrListener
   * 첫 번째 인수가 문자열 또는 배열인 경우 이것은
   * 리스너 기능. 그렇지 않으면 문자열 또는 이벤트 배열입니다.
   * 유형.
   *
   * @param {함수} [청취자]
   * 첫 번째 인수가 다른 이벤트 객체인 경우 이것은
   * 리스너 기능.
   */
  on(...인수) {
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'on');

    listen(target, 'on', type, listener);

    // 이 개체가 다른 이벤트 개체를 수신하는 경우.
    if (!isTargetingSelf) {

      // 이 객체가 삭제되면 리스너를 제거합니다.
      const removeListenerOnDispose = () => this.off(타겟, 타입, 리스너);

      // 나중에 제거할 수 있도록 리스너와 동일한 함수 ID를 사용합니다.
      // 원래 리스너의 ID를 사용합니다.
      removeListenerOnDispose.guid = 리스너.guid;

      // 타겟의 dispose 이벤트에도 리스너를 추가합니다. 이렇게 하면
      // 타겟이 이 객체보다 먼저 배치되면 제거합니다.
      // 방금 추가된 리스너를 제거합니다. 그렇지 않으면 메모리 누수가 발생합니다.
      const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);

      // 나중에 제거할 수 있도록 리스너와 동일한 함수 ID를 사용합니다.
      // 원래 리스너의 ID를 사용합니다.
      removeRemoverOnTargetDispose.guid = 리스너.guid;

      listen(this, 'on', 'dispose', removeListenerOnDispose);
      listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
    }
  },

  /**
   * 이 개체 또는 다른 이벤트의 이벤트(또는 이벤트)에 리스너 추가
   * 물체. 수신기는 이벤트당 한 번 호출된 다음 제거됩니다.
   *
   * @param {문자열|배열|요소|객체} targetOrType
   * 문자열 또는 배열인 경우 이벤트 유형을 나타냅니다.
   * 리스너를 트리거합니다.
   *
   * 대신 다른 이벤트 개체를 여기에 전달할 수 있습니다.
   * 리스너가 _that_ 객체에서 이벤트를 수신하도록 합니다.
   *
   * 두 경우 모두 리스너의 `this` 값은 다음에 바인딩됩니다.
   * 이 객체.
   *
   * @param {문자열|배열|함수} typeOrListener
   * 첫 번째 인수가 문자열 또는 배열인 경우 이것은
   * 리스너 기능. 그렇지 않으면 문자열 또는 이벤트 배열입니다.
   * 유형.
   *
   * @param {함수} [청취자]
   * 첫 번째 인수가 다른 이벤트 객체인 경우 이것은
   * 리스너 기능.
   */
  하나(...인수) {
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'one');

    // 이 이벤트 개체를 대상으로 합니다.
    if (isTargetingSelf) {
      listen(대상, '하나', 유형, 청취자);

    // 다른 이벤트 개체를 대상으로 합니다.
    } else {
      // 할 것: 이 래퍼는 올바르지 않습니다! 그것은 단지
      // 호출한 이벤트 유형의 래퍼를 제거합니다.
      // 대신 첫 번째 트리거에서 모든 리스너가 제거됩니다!
      // https://github.com/videojs/video.js/issues/5962 참조
      const 래퍼 = (...큰) => {
        this.off(대상, 유형, 래퍼);
        listener.apply(null, largs);
      };

      // 나중에 제거할 수 있도록 리스너와 동일한 함수 ID를 사용합니다.
      // 원래 리스너의 ID를 사용합니다.
      wrapper.guid = 리스너.guid;
      listen(target, 'one', type, wrapper);
    }
  },

  /**
   * 이 개체 또는 다른 이벤트의 이벤트(또는 이벤트)에 리스너 추가
   * 물체. 리스너는 트리거된 첫 번째 이벤트에 대해 한 번만 호출됩니다.
   * 그런 다음 제거됩니다.
   *
   * @param {문자열|배열|요소|객체} targetOrType
   * 문자열 또는 배열인 경우 이벤트 유형을 나타냅니다.
   * 리스너를 트리거합니다.
   *
   * 대신 다른 이벤트 개체를 여기에 전달할 수 있습니다.
   * 리스너가 _that_ 객체에서 이벤트를 수신하도록 합니다.
   *
   * 두 경우 모두 리스너의 `this` 값은 다음에 바인딩됩니다.
   * 이 객체.
   *
   * @param {문자열|배열|함수} typeOrListener
   * 첫 번째 인수가 문자열 또는 배열인 경우 이것은
   * 리스너 기능. 그렇지 않으면 문자열 또는 이벤트 배열입니다.
   * 유형.
   *
   * @param {함수} [청취자]
   * 첫 번째 인수가 다른 이벤트 객체인 경우 이것은
   * 리스너 기능.
   */
  모든(...인수) {
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'any');

    // 이 이벤트 개체를 대상으로 합니다.
    if (isTargetingSelf) {
      listen(target, 'any', type, listener);

    // 다른 이벤트 개체를 대상으로 합니다.
    } else {
      const 래퍼 = (...큰) => {
        this.off(대상, 유형, 래퍼);
        listener.apply(null, largs);
      };

      // 나중에 제거할 수 있도록 리스너와 동일한 함수 ID를 사용합니다.
      // 원래 리스너의 ID를 사용합니다.
      wrapper.guid = 리스너.guid;
      listen(target, 'any', type, wrapper);
    }
  },

  /**
   * 이벤트 객체의 이벤트에서 리스너를 제거합니다.
   *
   * @param {문자열|배열|요소|객체} [targetOrType]
   * 문자열 또는 배열인 경우 이벤트 유형을 나타냅니다.
   *
   * 다른 이벤트 객체가 여기에 대신 전달될 수 있습니다. 이 경우
   * 3개의 인수는 모두 _필수_입니다.
   *
   * @param {문자열|배열|함수} [typeOrListener]
   * 첫 번째 인수가 문자열 또는 배열인 경우 이는
   * 리스너 기능. 그렇지 않으면 문자열 또는 이벤트 배열입니다.
   * 유형.
   *
   * @param {함수} [청취자]
   * 첫 번째 인수가 다른 이벤트 객체인 경우 이것은
   * 리스너 기능; 그렇지 않으면 _all_ 리스너가
   * 이벤트 유형이 제거됩니다.
   */
  off(targetOrType, typeOrListener, 리스너) {

    // 이 이벤트 개체를 대상으로 합니다.
    if (!targetOrType || isValidEventType(targetOrType)) {
      Events.off(this.eventBusEl_, targetOrType, typeOrListener);

    // 다른 이벤트 개체를 대상으로 합니다.
    } else {
      const target = targetOrType;
      const type = typeOrListener;

      // 빠르고 의미 있는 방식으로 실패합니다!
      validationTarget(target, this, 'off');
      validateEventType(type, this, 'off');
      validateListener(listener, this, 'off');

      // 함수가 사용되지 않았더라도 최소한 guid가 있는지 확인합니다.
      listener = Fn.bind(this, listener);

      // 주어진 이 이벤트 객체에서 폐기 리스너를 제거합니다.
      // on()의 이벤트 리스너와 동일한 guid입니다.
      this.off('dispose', listener);

      if (타겟.노드이름) {
        Events.off(타겟, 타입, 리스너);
        Events.off(target, 'dispose', listener);
      } 그렇지 않으면 (isEvented(대상)) {
        target.off(타입, 리스너);
        target.off('dispose', listener);
      }
    }
  },

  /**
   * 이 이벤트 객체에서 이벤트를 발생시켜 리스너를 호출합니다.
   *
   * @param {문자열|객체} 이벤트
   * type 속성이 있는 이벤트 유형 또는 객체.
   *
   * @param {객체} [해시]
   * 청취자에게 전달할 추가 객체입니다.
   *
   * @return {부울}
   * 기본 동작이 방지되었는지 여부.
   */
  트리거(이벤트, 해시) {
    validateTarget(this.eventBusEl_, this, '트리거');

    const 유형 = 이벤트 && typeof 이벤트 !== '문자열' ? 이벤트 유형 : 이벤트;

    if (!isValidEventType(유형)) {
      const 오류 = `${objName(this)}#trigger에 대한 잘못된 이벤트 유형; ` +
        '비어 있지 않은 값을 가진 유형 키가 있는 비어 있지 않은 문자열 또는 객체여야 합니다.';

      경우 (이벤트) {
        (this.log || 로그).오류(오류);
      } else {
        새 오류 발생(오류);
      }
    }
    return Events.trigger(this.eventBusEl_, 이벤트, 해시);
  }
};

/**
 * 대상 개체에 {@link module:evented~EventedMixin|EventedMixin}을 적용합니다.
 *
 * @param {객체} 대상
 * 이벤트 메서드를 추가할 개체입니다.
 *
 * @param {객체} [옵션={}]
 * 믹스인 동작을 사용자 정의하기 위한 옵션.
 *
 * @param {문자열} [options.eventBusKey]
 * 기본적으로 대상 객체에 `eventBusEl_` DOM 요소를 추가하고,
 * 이벤트 버스로 사용됩니다. 대상 개체에 이미
 * 사용해야 하는 DOM 요소는 여기에 키를 전달합니다.
 *
 * @return {객체}
 * 대상 개체입니다.
 */
함수 이벤트(대상, 옵션 = {}) {
  const {eventBusKey} = 옵션;

  // eventBusEl_을 설정하거나 생성합니다.
  if (이벤트버스키) {
    if (!target[eventBusKey].nodeName) {
      throw new Error(`the eventBusKey "${eventBusKey}"는 요소를 참조하지 않습니다.`);
    }
    target.eventBusEl_ = 대상[eventBusKey];
  } else {
    target.eventBusEl_ = Dom.createEl('span', {className: 'vjs-event-bus'});
  }

  Obj.assign(대상, EventedMixin);

  if (target.eventedCallbacks) {
    target.eventedCallbacks.forEach((콜백) => {
      콜백();
    });
  }

  // 이벤트 객체가 삭제되면 모든 리스너가 제거됩니다.
  target.on('처리', () => {
    target.off();
    [대상, target.el_, target.eventBusEl_].forEach(function(val) {
      만약 (값 && DomData.has(발)) {
        DomData.delete(val);
      }
    });
    window.setTimeout(() => {
      target.eventBusEl_ = null;
    }, 0);
  });

  반환 대상;
}

내보내기 기본 이벤트;
내보내기 {isEvented};
내보내기 {addEventedCallback};